委托,事件学习进阶
本文不是入门文章,不会从最简单的什么是委托和事件开始.只是稍稍深入学习下委托和事件,作为记录.
目录:
先说两个蛮经典的C#面试题.
1.定义一种过滤器,比如在一个整形集合找到满足定义的子集
要求:
a.定义可以扩展,比如取出偶数,或者取出奇数,或者取出除3余1的数.
b.最好可以满足泛型需要,集合也可以变为其他类型
2.喵叫老鼠跑,主人醒.
要求:
a.要有联动性,老鼠和人的行为是被动的
b.考虑可扩展行,猫叫声可能会引起其他联动效应
问题1可以拿模板方法来实现,问题2可以拿观察者来实现,不过那都已经OUT了,用C#的完全可以不去记什么是观察者模式[delegate],C#原生支持.
顺便吐槽下在C#中学习设计模式.
C#原生支持的设计模式还有迭代器模式[foreach].
模板方法[每种OO语言都支持,C#当然支持].
像解释器这种基本上用不上的设计模式,也不用去学.
单例模式基本上就是个类模板,新建选择一下就OK了,基本上密封静态初始化就行,密封使用初始化,单锁,双锁,延迟初始化什么的基本都是浮云.
单例在C#里面有6中实现(还有一种是使用内置的线程锁),都差不多,只不过适用场合不一样,但是密封静态初始化这种可以满足98%的需求.
共享下模板文件Singlelet.cs和Singlelet.vstemplate
1 using System; 2 3 namespace $rootnamespace$ 4 { 5 public sealed class $safeitemrootname$ 6 { 7 /// <summary> 8 /// 构造函数私有化,防止外部初始化 9 /// </summary> 10 private $safeitemrootname$(){} 11 /// <summary> 12 /// 内部唯一实例 13 /// </summary> 14 private static $safeitemrootname$ instance=new $safeitemrootname$(); 15 /// <summary> 16 /// 获取单例 17 /// </summary> 18 public static $safeitemrootname$ Instance 19 { 20 get 21 { 22 return instance; 23 } 24 } 25 } 26 }
<?xml version="1.0" encoding="utf-8"?> <VSTemplate Version="3.0.0" Type="Item" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005"> <TemplateData> <Name Package="{FAE04EC1-301F-11d3-BF4B-00C04F79EFBR}" ID="546" /> <Description Package="{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}" ID="547" /> <Icon Package="{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}" ID="548" /> <TemplateID>Microsoft.CSharp.Class</TemplateID> <ProjectType>CSharp</ProjectType> <SortOrder>90</SortOrder> <RequiredFrameworkVersion>2.0</RequiredFrameworkVersion> <NumberOfParentCategoriesToRollUp>1</NumberOfParentCategoriesToRollUp> <DefaultName>Singlelet.cs</DefaultName> </TemplateData> <TemplateContent> <References> <Reference> <Assembly>System</Assembly> </Reference> </References> <ProjectItem ReplaceParameters="true">Singlelet.cs</ProjectItem> </TemplateContent> </VSTemplate>
添加新项如图:
好了,开始处理上述的那个问题.
先看问题1的标准实现:
/// <summary> /// 自定义过滤器 /// </summary> public abstract class CustomFilter<T> { /// <summary> /// 过滤器 /// </summary> /// <param name="item">传入对象</param> /// <returns>看对象是否能否过滤</returns> public abstract bool Filtrator(T item); /// <summary> /// 执行过滤 /// </summary> /// <param name="source">集合源</param> /// <returns>结果</returns> public IEnumerable<T> Filtering(IEnumerable<T> source) { foreach (var element in source) { if(Filtrator(element)) { yield return element; } } } }
/// <summary> /// 奇数过滤器 /// </summary> public class OddFilter:CustomFilter<int> { public override bool Filtrator(int item) { return item%2==1; } }
/// <summary> /// 偶数过滤器 /// </summary> public class EvenFilter:CustomFilter<int> { public override bool Filtrator(int item) { return item%2==0; } }
/// <summary> /// 除3余1 /// </summary> public class SpecialFilter:CustomFilter<int> { public override bool Filtrator(int item) { return item%3==1; } }
class Program { public static void Main(string[] args) { //源集合 var array=new []{1,2,3,4,5,6,7,8,9,10,11,12,13}; //奇数 Print(new OddFilter().Filtering(array)); //偶数 Print(new EvenFilter().Filtering(array)); //除3余1 Print(new SpecialFilter().Filtering(array)); Console.ReadKey(); } public static void Print<T>(IEnumerable<T> source) { Console.WriteLine(string.Join(",",source)); } }
你看这实现的多么的OO,扩展性多么的强;}.要啥过滤器,来新建一个class,实现一下方法,噢耶,搞定收工.
看起来确实很OO,也确实有很强悍的扩展性,不过美中不足的是,貌似像奇偶数这么相似的过滤器,却要定义两个类,未免有些过于破坏气氛,而且第三个实现和前两个也差不多,竟然也需呀加一个类,太破坏气氛了.
说好的OO是为了带来代码重用的好处,但是每个类里面就一行代码不一样,感觉总是不是那么和谐,OO的不一定就是美的啊.
其实第一个问题不太适合拿OO的方式来解决,因为是数学问题,最好拿F#来做.你会感觉好短好精悍,代码如下:
//初始化一个数组 let nums=[1..20];; //过滤方法 let filtering filtrator source= source |>Seq.filter filtrator;; //奇数 filtering (fun i->i%2=1) nums;; //偶数 filtering (fun i->i%2=0) nums;; //除3余1 filtering (fun i->i%3=1) nums;;
但是C#和F#是同一套IL,拿C#换一种方式,也很不错,比如:
public class Filter<T> { public Func<T,bool> filtrator{get;set;} public IEnumerable<T> Filtering(IEnumerable<T> source) { foreach (var element in source) { if(filtrator(element)) { yield return element; } } } }
class Program { public static void Main(string[] args) { //源集合 var array=new []{1,2,3,4,5,6,7,8,9,10,11,12,13}; var filter=new Filter<int>(); filter.filtrator=i=>i%2==1; //奇数 Print(filter.Filtering(array)); filter.filtrator=i=>i%2==0; //偶数 Print(filter.Filtering(array)); filter.filtrator=i=>i%3==1; //除3余1 Print(filter.Filtering(array)); Console.ReadKey(); } public static void Print<T>(IEnumerable<T> source) { Console.WriteLine(string.Join(",",source)); } }
如你所见,具体的判断算法暴露了出来,使用的时候按照具体的要求来这个算法,就能求出结果.
这当是委托的一个非常有用的地方:你不需要使用模板方法的方式来动态构建算法,你只需要将变化的一部分抽象出一个委托签名,在客户端具体使用的时候再定义这个委托的具体实现.
LINQ中很多优雅的语法(如where,如select,如XXX,除了那些聚合函数,基本上都是)就是依靠委托这个很有意思的特性.如果没有委托,世界上要么没有LINQ,要么没有LINQ.:)
在此吐槽下LINQ中让人很无语的Distinct,Distinct两种.
第一种是 通过使用默认的相等比较器对值进行比较返回序列中的非重复元素。
第二种是 通过使用指定的 System.Collections.Generic.IEqualityComparer<T> 对值进行比较返回序列中的非重复元素。
第一种你没得选,因为默认的系统决定,除非你的类实现这个默认的比较接口.
第二种呢,你需要继承那个接口然后实例化一个实例传到Distinct里面,突然想起有人吐槽OO的一句名言:我想要一只猴子,你却要把整个森林搬给我.
为啥不能提供第三种,提供Comparison<T>委托参数的调用方式呢.当年List.Sort是多么美的调用,为啥LINQ就不支持下.
上述问题还有一种写法,使用C#中扩展方法如下:
public static class NewFilter { public static IEnumerable<T> Filtering<T>(this IEnumerable<T> source,Func<T,bool> filtrator) { foreach (var element in source) { if(filtrator(element)) { yield return element; } } } }
class Program { public static void Main(string[] args) { //源集合 var array=new []{1,2,3,4,5,6,7,8,9,10,11,12,13}; //奇数 Print(array.Filtering(i => i % 2 == 1)); //偶数 Print(array.Filtering(i => i % 2 == 0)); //除3余1 Print(array.Filtering(i => i % 3 == 1)); Console.ReadKey(); } public static void Print<T>(IEnumerable<T> source) { Console.WriteLine(string.Join(",",source)); } }
看到了么,其实可以这么简洁,委托可以做参数,传递个另外一个方法(有木有想起高阶函数);
这是委托的第二个很有用的地方:你可以将委托作为参数传递给一个方法.
为了让这一点加上一些说服力,再说一个基本上任何一门语言都可能会问到的经典送分题--斐波那契数列.
class Program { public static void Main(string[] args) { Console.WriteLine(Fibonacci(30)); Console.ReadKey(); } public static int Fibonacci(int n) { if (n <= 0) throw new ArgumentException("n小于0"); if (n == 1 || n == 2) return 1; return Fibonacci(n - 1) + Fibonacci(n - 2); } }
还有种写法,十分的NB,但是写出来的东西未必每个人都会喜欢,比如我这么写.(注:写法仅供玩赏,切勿作为正式代码出现)
class Program { public static void Main(string[] args) { //斐波那契数 Console.WriteLine(RFunc<int, int>((f, n) => n <= 2 ? 1 : f(n - 1) + f(n - 2))(30)); //求和 Console.WriteLine(RFunc<int, int>((f, n) => n <= 1 ? 1 : f(n - 1) + n)(100)); //阶乘 Console.WriteLine(RFunc<int, int>((f, n) => n <= 1 ? 1 : f(n - 1) * n)(5)); Console.ReadKey(); } static Func<T, TResult> RFunc<T, TResult>(Func<Func<T, TResult>, T, TResult> func) { return (x) => func(RFunc(func), x); } }
不过这个时候你一定会发现委托原来可以这么NB.附:分享个递归帮助类喜欢F#的各位可以拿F#实现看看,非常简洁.在此不写了.
再回过头来,看看问题1.如果现在需要再判断之前,输出一下参数,你会觉得很简单.
class Program { public static void Main(string[] args) { //源集合 var array = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; //奇数 Print(array.Filtering(i => { Console.WriteLine(i); return i % 2 == 1; })); Console.ReadKey(); } public static void Print<T>(IEnumerable<T> source) { Console.WriteLine(string.Join(",", source)); } }
如果你不觉得很简单,证明你还不太明白啥是委托,建议从头再看一遍.
不过新问题来,不许你的输出方法和判断方法为同一个方法,怎么办.
放心,我们有委托,可以这么写.
class Program { public static void Main(string[] args) { //源集合 var array = new[] { -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; Func<int, bool> f = null; f += i => { Console.WriteLine(i); return true; }; f += i => { if (i < 0) throw new ArgumentException("i为复数"); return true; }; f += i => i % 2 == 1; //奇数 Print(array.Filtering(f)); Console.ReadKey(); } public static void Print<T>(IEnumerable<T> source) { Console.WriteLine(string.Join(",", source)); } }
结果正常打印了,也输出了.
看到了么f的起始值为null,但是这并不影响后面的+=,未报空引用错误.因为委托的+=在编译后会变成System.Delegate.Combine来合并委托链.
所以使用委托的+=操作符前,委托的引用是可以为空的.
但是,有木有发现委托在+=之前,它的返回值是true,+=之后返回值才是动态算的.
如果你看到这一点,你就知道一个关于带返回值委托的一个事实:当一条链上的带返回值委托执行时,总是返回链尾委托的结果.
那么你可以推理知道,委托是按照委托链的顺序依次执行,如果一条委托链上的一个委托发生异常,后面的委托就不会执行.
比如有如下代码,你能看到输出一个数,但是却无法得到结果,因为委托链无法按照顺序执行.
class Program { public static void Main(string[] args) { //源集合 var array = new[] { -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }; Func<int, bool> f = i => { Console.WriteLine(i); return true; }; f += i => { if (i < 0) throw new ArgumentException("i为复数"); return true; }; f += i => i % 2 == 1; //奇数 Print(array.Filtering(f)); Console.ReadKey(); } public static void Print<T>(IEnumerable<T> source) { Console.WriteLine(string.Join(",", source)); } }
因为委托天生具备的+=操作符(-=也是有的),所以你无法得知一个委托能正常执行,内部是否发生了异常.怎么避免这种未能预知问题.
建议:1.尽量少的使用委托的+=操作符,确保每个委托上只有一个方法执行.
2.如果必须使用+=,在使用委托的+=操作符时,最好确保你添加的委托内部不发生异常.
3.如果你不能确保是否会发生异常,当使用委托作为函数参数的时候,在执行委托时最好加上异常处理.
附,建议1的代码示例
/// <summary> /// 单一方法委托 /// </summary> public class SingleDelegate { private Action<string> print; public void Register(Action<string> print) { this.print = print; } }
建议3的代码示例
public static class NewFilter { public static IEnumerable<T> Filtering<T>(this IEnumerable<T> source, Func<T, bool> filtrator) { foreach (var element in source) { bool result = false; try { result = filtrator(element); } catch { } if (result) yield return element; } } }
以上为委托.
----------------我是华丽的分割线-----------------我俩是华丽的分割线---------------
再看问题2的标准实现,说好的不用观察者模式:
/// <summary> /// 喵的实体 /// </summary> public class Cat { /// <summary> /// 猫大叫 /// </summary> public void Miao() { System.Console.WriteLine("猫:喵~~~~"); if(OnMiaowed!=null) OnMiaowed(this,System.EventArgs.Empty); } /// <summary> /// 猫大叫完成事件 /// </summary> public event System.EventHandler OnMiaowed; }
/// <summary> /// 老鼠 /// </summary> public class Mouse { /// <summary> /// 老鼠跑 /// </summary> public void Run() { System.Console.WriteLine("老鼠:快跑......."); } }
/// <summary> /// 主人 /// </summary> public class Master { /// <summary> /// 美丽的主人苏醒了 /// </summary> public void Wake() { System.Console.WriteLine("主人:i am awake,i am awake"); } }
class Program { public static void Main(string[] args) { //先需要搭建场景,喵,老鼠,主人 Cat cat=new Cat(); Mouse mouse=new Mouse(); Master master=new Master(); //注册各自的事件 cat.OnMiaowed+= delegate { mouse.Run(); }; cat.OnMiaowed+= delegate { master.Wake(); }; //喵大叫一声 cat.Miao(); Console.ReadKey(); } }
看到以上代码.我不禁会问委托可以+=,事件也可以+=,为啥用事件不用委托?
为啥呢?
因为委托是赤裸裸的方法指针,所以在类内部定义public或protected委托都可以公共调用或在子类中调用.就是说原本应有类环境的方法,脱离了类环境运行.严重破坏了类的封装性.而事件相当于是封装了一个委托变量,只不过这个委托变量永远是private的,所以永远无法在在类外部或子类中直接调用这个委托.保护了类内部的封装性.
所以除了可以调用执行的场景不同之外,委托和事件并没有太多区别,希望以后不要再问这么让人觉得很高深但其实答案只有一个问题了.
不过微软为事件提供了很特殊的属性语法,普通的属性是get;set;而事件这种特殊的"属性",是add,remove用来注册和销毁委托上订阅者.这也算一个.
1.尽量使用系统内部定义的委托,其实我觉得系统内部的委托已经足够使用了.除非是觉得系统命名的委托不太符合习惯的命名.
2.在使用委托链时获取多个结果时,因为是按照链的顺序来执行的.如果对结果的顺序没有要求,建议使用并行(Parallel)算法来求结果.
3..NET没有真正的异步,所有的异步操作都是注册到线程池来做的,所以异步执行委托和并行执行委托的效率并不相同.(并行!=异步)
4.如果不是非常必要,不要使用有返回值的事件(见委托使用时的注意事项).
5.最好使用系统 EventHandler<TEventArgs>来定义事件,因为每一个事件的第一个参数都应该是事件的发起者.不建议使用无法溯源的事件.
6.由于系统会根据对象是否可达来决定是否销毁对象,所以在为对象注册时,请记得在适当的时候销毁注册的委托或事件.
(非常感谢一路转圈的雪人,指明了我错误的理解,系统是按照对象是否可达来决定是否销毁对象的,非常感谢)
非常感谢大侠能读到这里,你懂得!:)