码迷,mamicode.com
首页 > 其他好文 > 详细

说说委托那些事儿

时间:2016-08-12 18:00:07      阅读:165      评论:0      收藏:0      [点我收藏+]

标签:

委托基础

委托是个啥?

很多人第一反映可能是"函数指针",个人觉得"函数指针"是委托实例

委托的定义类似interface,是一种方法的"规范"或者说"模版",用来规范方法的"行为",以便将方法作为参数传递

public delegate void MyDelegate();

这样便定义了一个无参无返回值的委托,要求此委托的实例必须是无参无返回值的方法

public class MyClass
{
  public static void MyMethod1() }
  public static void MyMethod2() } }
MyDelegate myDelegate new MyDelegate(MyClass.MyMethod1);//定义了委托实例,并添加了相应的操作方法 //MyDelegate myDelegate = MyClass.MyMethod;//<--简写就是这样 myDelegate += MyClass.MyMethod2;//多播委托

上面的代码展示了委托的基本用法,多播委托也可以用Delegate.Combin()方法来实现

多播委托可以美化成下面的代码

MyDelegate myDelegate null;
myDelegate += MyClass.MyMethod1;
myDelegate += MyClass.MyMethod2;

是不是漂亮多了!

在C#3以后常用委托都可以用Action跟Func来替代了(C#3还是2忘记了- -)

委托存在的意义:方法传递

真实案例:

在controller的自定义基类中有一个protected void CreateCookie(string name, string value) 方法

在获取到微信openid后,进行一些数据库处理,同时保存此openid的登录信息到cookies


public static void SetOpenId(string openId, Action<string, string> setCookie)
WeixinTool.SetOpenId(openid, CreateCookie);

这样便将CreateCookie传递给了SetOpenId方法

匿名委托

不需要定义方法名,直接书写方法体赋值给委托

在lambda表达式出来后用的不多了, 实际上lambda表达式就是匿名委托

MyDelegate anonymous1 delegate() Console.WriteLine("this is a test 1"); };//匿名委托
MyDelegate anonymous2 () => Console.WriteLine("this is a test 2"); };//lambda表达式
 
anonymous1();
anonymous2();

上面的代码编译后使用IlSpy查看直接就是俩匿名委托

技术分享

使用ildasm查看il也是一致的

技术分享

说了委托,是不是该说事件了

大家应该都写过winform啦,点击按钮触发click事件,相关事件处理程序影响该事件

很同学都知道有事件,但并不能准确描述事件是什么 (前文的多播委托的优化版是不是看着像事件)

public event MyDelegate ChangeSomething;

首先事件是"属性",是类的一个"属性",所以只能定义在一个类里面(或者结构体里面)

但是event关键字让你不能直接对这个属性赋值,所以只能用"+="或者"-="来操作这个"属性"

事件存在的目的是为了实现"发布/订阅模式",也就是大家常说的pub/sub

为啥不能让你直接给这个属性赋值呢,因为"订阅者"并不知道有多少人订阅了这个事件,如果大家都用"="来操作,后面的"订阅者"就会覆盖前面的"订阅者",容易造成bug,故而event关键字封装了委托,关闭了直接赋值通道

 

委托的逆变与协变

用过泛型的很多同学都知道,泛型有逆变跟协变,其实委托也有逆变跟协变(接口,数组也有此特性)

那么啥是逆变与协变呢

简单来说

逆变:

基类变子类 -> 逆了天了,这都可以,所以叫逆变

逆变实际是编译器根据执行上下文推断类型是可以转换,才编译通过的

看似逆天实际也属于"is-a"关系正常转换

 

协变:

子类变基类->CLR协助变形,所以叫协变

大家在编程中常用到,"is-a"关系,所以可以正常转换

 

对于委托,逆变与协变可以是返回值变化,也可以是参数变化,亦可以是二者同时变化

 

来来来,我们来看一些具体的栗子:

定义类型与继承

class Person {}
 
class Employee : Person {}

定义委托

delegate Person EmployeeInPersonOut(Employee employee);

定义一些适合委托的方法

class Methods
{
    public static Person EmployeeInPersonOut(Employee employee)
    {
        return new Person();
    }
 
    public static Employee EmployeeInEmployeeOut(Employee employee)
    {
        return new Employee();
    }
 
    public static Person PersonInPersonOout(Person person)
    {
        return new Person();
    }
 
    public static Employee PersonInEmployeeOut(Person person)
    {
        return new Employee();
    }
}

常规使用

//常规使用
EmployeeInPersonOut employeeInPersonOut Methods.EmployeeInPersonOut;
Person person = employeeInPersonOut(new Employee());

协变

//协变使用
/*
 * 返回值Employee跟Person属于"is-a"关系,所以是常规转换
 */
EmployeeInPersonOut employeeInPersonOut Methods.EmployeeInEmployeeOut;
Person person = employeeInPersonOut(new Employee());

逆变

//逆变使用
/*
* 对于委托声明:委托方法的参数Person竟然可以变成Employee!
* 实际是编译器根据上下文推断,对象可以成功转换
* 在执行的时候, 委托声明EmployeeInPersonOut只能输入Employee
* Employee对于Methods.PersonInPersonOout的参数peron是"is-a关系",所以可以正常转换成方法参数
*/
EmployeeInPersonOut employeeInPersonOut Methods.PersonInPersonOout;
Person person = employeeInPersonOut(new Employee());

协变与逆变一起使用

//这段就不解释了,仔细看前两段就能明白其中原理
EmployeeInPersonOut employeeInPersonOut Methods.PersonInEmployeeOut;
Person person = employeeInPersonOut(new Employee());

协变在winform中的应用

class Program
{
    static void Main(string[] args)
    {
        var button =  new Button(){Text "click me!"};
        button.Click += HandleEvent;
        button.KeyPress += HandleEvent;
 
        var form new Form();
        form.Controls.Add(button);
 
        Application.Run(form);
    }
 
    static void HandleEvent(object sender, EventArgs args)
    {
        MessageBox.Show(args.GetType().FullName);
    }
}

用匿名无参委托忽略事件参数也是可以的

button.Click += delegate {/*do something.*/};

 

委托与闭包

什么是闭包

class Program
{
    static void Main(string[] args)
    {
        var action = ClosureMethod();
 
        action();
        action();
        action();
 
        Console.ReadKey();
 
    }
 
    static Action ClosureMethod()
    {
        int localCounter 0;
 
        Action x delegate
        {
            localCounter++;
            Console.WriteLine(localCounter);
        };
 
        return x;
    }
}

这段代码依次输出1,2,3

这就是闭包

可以参考javascript中的闭包,猜测一下:匿名方法使用了局部变量"localCounter",使得在方法执行完后无法释放变量,从而形成了一个"范围内的全局变量"

下面我们来验证一下这个猜测

祭出神器:IL DASM

为了看着简单点,我把代码稍微做了点修改

static Action ClosureMethod()
{
    string local "零";
 
    Action x delegate
    {
        local += "壹";
        Console.WriteLine(local);
    };
 
    return x;
}

汉字在il中更容易找到位置

技术分享

 

从il中可以看出

C#闭包并不是与js一样是由于垃圾回收机制的原因

由于匿名方法捕获了一个"外部方法"的局部变量"local"

使得编译器生成了一个"内部类"(<>c_DisplayClass1)

而"外部方法"直接使用了这个"内部类"的实例中的变量(il中的<>c_DisplayClass1::local)

委托"Aciton x"也使用了该实例

这样变完成了"闭包", 所以C#中的闭包完全是编译器的功劳

 

闭包的作用

1.局部变量实例化,使得外部可以使用该变量

static  IList<string> StringFilter(List<string> list, int length)
        {
            return list.FindAll(delegate(string str)
            {
                return str.Length > length;
            });
 
        }

当然也可以使用lambda表达式

static IList<string> StringFilter(List<string> list, int length)
        {
            return list.FindAll(str => str.Length > length);
 
        }

前面说过lambda表达式实际就是匿名委托

上面的代码都捕获了外部变量length

 

2.延长变量生命周期,委托不死,变量不亡(var action = ClosureMethod();这有在action释放后,"ClosureMethod"的变量"local"才会被释放)

就像闭包部分第一段代码的计数器,在"ClosureMethod"方法执行完毕后,变量"localCounter"的生命周期延长了

 

说一说闭包中的坑

在for中使用闭包

坑1:

static void Main(string[] args)
{
    var actions = LoopClosure();
 
    actions[0]();
    actions[0]();
    actions[0]();
 
    actions[1]();
    actions[2]();
 
    Console.ReadKey();
 
}
 
static IList<Action> LoopClosure()
{
    var list new List<Action>();
 
    for (int i 0; i 3; i++)
    {
        int val = i*10;
 
        list.Add(delegate
        {
            val++;
            Console.WriteLine(val);
        });   
    }
 
    return list;
}

输出结果是1,2,3,11,21

技术分享

此循环虽然只有生成了一个"内部类",但是每次循环都产生了一个"内部类"的实例,所以会有上述结果

坑2:

var actions new List<Action>();
for (int i 0; i 3; i++)
    actions.Add(() => Console.WriteLine(i));//access to modified closure ‘i‘
foreach (var action in actions)
    action();

输出结果是3,3,3

因为使用了变化/修改过的闭包变量

但是在foreach中是没有这个坑的

var actions Enumerable.Range(0, 3).Select(i => (Action)(() => Console.WriteLine(i))).ToList();

这样的在foreach中的闭包就能正常输出0,1,2

 

趣味编程:

能不能在C#中像javascript一样写一个自执行方法 ^_^

 

参考资料:

https://msdn.microsoft.com/zh-cn/library/ee207183.aspx
https://msdn.microsoft.com/zh-cn/library/dd233060.aspx
https://msdn.microsoft.com/zh-cn/library/dd465122.aspx
http://csharpindepth.com/articles/chapter5/closures.aspx

 

欢迎以任何形式的转载本文,转载请注明出处,尊重他人劳动成果
转载请注明:文章转载自:博客园[http://www.cnblogs.com]
本文标题:说说委托那些事儿
本文地址:http://www.cnblogs.com/eyu/p/all_those_delegate_things.html

 

说说委托那些事儿

标签:

原文地址:http://www.cnblogs.com/eyu/p/all_those_delegate_things.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!