委托基础
委托是个啥?
很多人第一反映可能是"函数指针",个人觉得"函数指针"是委托实例
委托的定义类似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