你的位置:首页 > ASP.net教程

[ASP.net教程]说说委托那些事儿


委托基础

委托是个啥?

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

委托的定义类似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