委托,事件学习进阶

  本文不是入门文章,不会从最简单的什么是委托和事件开始.只是稍稍深入学习下委托和事件,作为记录.

目录:

   单例模板文件

     委托

     事件

     个人使用习惯

    先说两个蛮经典的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 }
Singlelet.cs
<?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>
Singlelet.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;
                }
            }
        }
    }
CustomFilter
    /// <summary>
    /// 奇数过滤器
    /// </summary>
    public class OddFilter:CustomFilter<int>
    {
        public override bool Filtrator(int item)
        {
            return item%2==1;
        }
    }
OddFilter
    /// <summary>
    /// 偶数过滤器
    /// </summary>
    public class EvenFilter:CustomFilter<int>
    {
        public override bool Filtrator(int item)
        {
            return item%2==0;
        }
    }
EvenFilter
    /// <summary>
    /// 除3余1
    /// </summary>
    public class SpecialFilter:CustomFilter<int>
    {
        public override bool Filtrator(int item)
        {
            return item%3==1;
        }
    }
SpecialFilter
    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;;
F#过滤器

 但是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;
                }
            }
        }
    }
原生版Filter
    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;
                }
            }
        }
    }
NewFilter
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);
        }

    }
View Code

不过这个时候你一定会发现委托原来可以这么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;
            }
        }
    }
安全版NewFilter

 以上为委托.

 ----------------我是华丽的分割线-----------------我俩是华丽的分割线---------------

事件

再看问题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;
    }
Cat
    /// <summary>
    /// 老鼠
    /// </summary>
    public class Mouse
    {
        /// <summary>
        /// 老鼠跑
        /// </summary>
        public void Run()
        {
            System.Console.WriteLine("老鼠:快跑.......");
        }
    }
Mouse
    /// <summary>
    /// 主人
    /// </summary>
    public class Master
    {
        /// <summary>
        /// 美丽的主人苏醒了
        /// </summary>
        public void  Wake()
        {
            System.Console.WriteLine("主人:i am awake,i am awake");
        }
    }
Master
    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.由于系统会根据对象是否可达来决定是否销毁对象,所以在为对象注册时,请记得在适当的时候销毁注册的委托或事件.

     (非常感谢一路转圈的雪人,指明了我错误的理解,系统是按照对象是否可达来决定是否销毁对象的,非常感谢)

     非常感谢大侠能读到这里,你懂得!:)

  

posted @ 2013-05-27 00:48  方外老和尚  阅读(1796)  评论(25编辑  收藏