CSharp 委托(delegate)与事件(event)


CSharp 委托(delegate)与事件(event)

前话

面向群体

  • C#初学者
  • 对委托或者事件分辨不清的同学

目标

  • 了解C#语言中的委托与事件
  • 分辨C#语言中的委托与事件
  • 了解C#语言中的委托与事件部分应用场景
  • 了解事件的实现原理

委托(delegate)

委托是什么

A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type . You can invoke (or call) the method through the delegate instance. ——Delegates (C# Programming Guide)
翻译过来的大致意思(本人英语水平不佳)如下:
委托是一种表示对具有特定参数列表和返回类型的方法的 引用类型 。实例化委托时,可以将其实例与具有 兼容签名返回类型 的任何方法相关联。您可以通过委托实例调用(或调用)方法。
从官网咱们可以看到委托的定义,明确了下面这几点:

  1. 委托是一种引用类型,这意味着,委托是和类(class)、枚举类型(enum)、结构体(struct)同等级别的存在,其作用都是定义了一种类型。
  2. 委托实例可以和具有相同签名的 兼容签名返回类型任何方法 相关联。
    这句话可能对于各位来说,包含的信息会多一些,我举一段代码进行说明:
/// 在此我们定义了一个名叫MyDelegate的委托类型
/// 其返回值为int类型,而参数列表中有两个分别名为a和b的int类型的参数
delegate int MyDelegate(int a,int b);

class Test{
    /// 在Test类中包含了一个名为mDelelgate的MyDelegate类型实例
    MyDelegate mDelegate;
    public Test(MyDelegate myDelegate){
        /// 在Test类中的构造方法中赋予mDelegate值
        mDelegate = myDelegate;
    }
    
    /// 通过Test类中的Invoke方法里像调用方法一样使用mDelegate
    public int Invoke(int a,int b){
        return mDelegate(a,b);
    }
    
    public static int Add(int a,int b){
        return a+b;
    }
    
    /// 为了说明某个要点,构建了一个名为InnerClass的内部类
    /// 该类只有一个方法
    public class InnerClass{
        public int Plus(int a,int b){
            return a*b;
        }
    }
    
    public static void Main(){
        /// 使用静态方法Add作为参数构造一个Test实例
        Test test1 = new Test(new MyDelegate(Add));
        /// 使用InnerClass类的实例方法Plus作为参数构造一个Test实例
        Test test2 = new Test(new InnerClass().Plus);
        /// 分别输出两个实例Invoke方法的结构
        Console.WriteLine(test1.Invoke(1,2));
        Console.WriteLine(test2.Invoke(1,2));
        Console.ReadKey();
    }
}

阅读了这段代码后,咱们再回过头来看那第二个要点:

  • 委托实例可以和具有相同签名的 兼容签名返回类型任何方法 相关联。
    首先是关键词 兼容签名 这里的签名主要指的是方法的参数列表的参数个数、参数类型要兼容,这里的兼容目前理解为一致即可。(若要深入理解的话,可以了解C#的协变性与逆变性)
    再是关键词 返回类型 其实与签名一致,也需要是兼容的返回类型。
    最后是关键词 任何方法 ,这里的任何指的意思就是说包括静态方法和实例方法,在上述代码中我们举了Add静态方法和Plus实例方法为例说明了这一要点。
    也就是说,当方法满足了这三个条件后,我们便可以将这个方法来当成一个委托实例来使用(事实上是进行了类型转换),或者使用这个方法来生成一个委托实例。
    虽然说,委托的定义和一些规矩咱们已经清楚了,但是大家肯定还会有疑问,比如下面:
  • 我到底为什么要定义一个委托,直接调用一个方法不好吗?
    接下来我会对这个疑问来进行说明。

委托的用处

  • 现在给你一个任务,实现冒泡排序让一个int数组从小到大排序。
for (int i = 0; i < arr.Length - 1; i++){
    for (int j = 0; j < arr.Length - 1 - i; j++) {
        if (arr[j] - arr[j + 1]>0){
            var temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
            }
        }
    }
}
  • 再给你一个任务,实现冒泡排序让一个int数组从大到小排序。
for (int i = 0; i < arr.Length - 1; i++){
    for (int j = 0; j < arr.Length - 1 - i; j++) {
        if (arr[j] - arr[j + 1]<0){
            var temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
            }
        }
    }
}
  • 再再再给你一个任务,实现冒泡排序让一个int数组按绝对值从小到大排序,你是不是已经有一点厌烦了?
for (int i = 0; i < arr.Length - 1; i++){
    for (int j = 0; j < arr.Length - 1 - i; j++) {
        if (Math.Abs(arr[j]) - Math.Abs(arr[j + 1])>0){
            var temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
            }
        }
    }
}
  • 如果接着让你实现不同要求的冒泡排序,你是不是会有点想打我?
    不过为了我的生命安全,暂且就实现到这里。
    你可以发现,其实这三段代码大同小异,三个要求的改变之处无非就是if后面的条件语句发生了改变。
    并且,如果往后还要提其他要求的话,事实上你也只需要改变一下条件语句,而其他的代码都是可以复用的。
    于是我们希望有一种方式能够达到这一要求,此时委托便是解决这个问题的好办法。
    首先我们定义一个方法的规则,传入的参数为两个int值a、b。
    1. 若a在逻辑上大于b,方法返回一个大于0的数字。
    2. 若a在逻辑上小于b,方法则返回一个小于0的数字。
    3. 若a在逻辑上等于b,则返回零。
    然后请阅读以下代码:
delegate int CMP(int a, int b);

class Test
{
    /// 冒泡排序主体
    static void Sort(int[] arr, CMP cmp)
    {
        for (int i = 0; i < arr.Length - 1; i++)
        {
            for (int j = 0; j < arr.Length - 1 - i; j++)
            {
                if (cmp(arr[j], arr[j + 1]) > 0)
                {
                    var temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }
    /// 比较函数1,若a>b则返回大于0的数字
    static int CMP1(int a,int b)
    {
        return a - b;
    }

    /// 比较函数2,若a<b则返回大于0的数字
    static int CMP2(int a, int b)
    {
        return b-a;
    }
    
    /// 比较函数2,若a绝对值>b绝对值则返回大于0的数字
    static int CMP3(int a, int b)
    {
        return Math.Abs(a) - Math.Abs(b);
    }
    
    /// 辅助函数,遍历arr并输出显示
    static void PrintArr(int[] arr) {
        foreach(var v in arr)
        {
            Console.Write($"{v} ");
        }
        Console.WriteLine();
    }
    
    public static void Main() {
        var arr = new[] { 1, -2, 9, 8, 10 };
        Sort(arr, CMP1);
        PrintArr(arr);
        
        Sort(arr, CMP2);
        PrintArr(arr);
        
        Sort(arr, CMP3);
        PrintArr(arr);
        
        Console.ReadKey();
    }
}

我们发现,定义了一种委托以后,我们在需求发生改变的时候,只需要新写一个比较方法,然后再调用Sort方法的时候将其当成委托实例传入即可。
而如果我们直接调用一个方法的话,我们就要写多个不同的排序方法,从而有大量的重复逻辑。
这样,我们节省了很多的重复代码,而让自己的代码得到了复用。
事实上,我们也可以从面向对象的角度来理解:

  • 委托实例即是将一个方法当作一个对象来看待,我们可以通过传递委托实例,来动态的改变某个地方的逻辑,而使得其他的代码复用。
    有些同学可能会发现,这和面向对象中的多态会有些类似。
    事实上,上述代码完全可以使用多态来实现,但是多态需要继承,也就是说,如果使用多态来实现上述内容的话,那么很明显我们需要再写三个类,很明显这是一个比较麻烦的事情,甚至按照上个例子来说,这三个类中只需要实现各自的CMP方法,而不存在其他的内容。这样很明显是一种很不方便的方式。
    而使用委托的话,就不需要再弄几个新的类,而只需要写几个新的方法即可,甚至还有更简单的方式,也就是接下来我们会提到的lambda表达式。

lambda表达式

上文提到,如果要使用委托的话,往往都会使用一个方法来转换成一个委托实例。然而,如果每弄一个别的委托实例都需要新建一个方法的话,存在以下问题:

  1. 起名字困难,比如上述的CMP1,CMP2,CMP3,很难以起一个形象的好名字。
  2. 要转换成委托实例的方法不一定会被作为委托实例以外别处调用,一个类会因此多一个无用的方法。
    为了解决这些问题,简化编程,有一种语法叫做lambda表达式。
/// 使用lambda表达式的话,就可以使用下面这段代码来代替上述的CMP1方法构造CMP委托的实例
CMP cmp = (a,b)=>a-b;

关于lambda表达式的粗略了解,可以阅读 Lambda 表达式(C# 编程指南) 的表达式lambda与语句lambda部分进行了解。

事件(event)

Delegate.Combine & Delegate.Remove

在了解事件前我们先来了解一下这两个Delegate类的静态方法。
我通过下面的伪代码来进行讲解:

delegate void MyDelegate();
var delegate1 = ()=>{ Console.WriteLine("delegate1!"); };
var delegate2 = ()=>{ Console.WriteLine("delegate2!"); };
/// 使用Combine方法将两个delegate实例结合返回一个新的实例
delegate1 = Delegate.Combine(delegate1,delegate2);
/// 调用delegate3会分别调用delegate1和delegate2
delegate1();
/// 使用Remove方法把delegate3中对delegate2的调用移除
delegate1 = Delegate.Remove(delegate1,delegate2);
delegate1();

/// 另外,C#中重载了委托类型的 +、-、+=、-=操作符
/// +会调用Delegate.Combine方法
/// -会调用Delegate.Remove方法
/// +=、-=依次类推

/// 所以上述代码也可以这样写
var delegate1 = ()=>{ Console.WriteLine("delegate1!"); };
var delegate2 = ()=>{ Console.WriteLine("delegate2!"); };
delegate1 += delegate2;
delegate1();
delegate1 -= delegate2;
delegate1();

事件是什么

Events are a special kind of multicast delegate that can only be invoked from within the class or struct where they are declared (the publisher class). ——event (C# reference)
翻译过来的意思即是:
事件是一种特殊的多播委托仅可以从声明事件的类或结构(发布服务器类)中对其进行调用
在上述描述中有两处比较指的关注的地方:

  • 多播委托:多播委托暂时可以这么理解,即之前使用Delegate.Combine方法,将两个委托合成一个委托,合成的这个委托即是多播委托。多播的意思也就是说,一处调用,就会造成多处影响。如想了解更多可以阅读:C# 委托(二)—— 多播委托与事件
  • 仅可以从声明事件的类或结构(发布服务器类)中对其进行调用:这句话会稍难解释一些,我们先来看一下事件是怎么用的:
delegate void MyDelegate();
class Test{
    /// 使用event关键词修饰一个委托实例,我们称这个为一个事件
    public static event MyDelegate Delegate;
    
    public static void Invoke(){
        Delegate?.Invoke();
    }
}

class Solution{
    public static void Main(){
        MyDelegate delegate1 = ()=>Console.WirteLine(1);
        MyDelegate delegate2 = ()=>Console.WirteLine(2);
        Test.Delegate += delegate1;
        Test.Delegate += delegate2;
        
        Test.Invoke();
        Console.ReadKey();
    }
}

大家可以看到,在Test类里有一个名为Delegate的MyDelegate类型的事件,在Solution类中的Main方法内对其进行了+=操作(称之为注册),在后面通过Test类中的Invoke方法触发了这个事件(称之为通知)。
然鹅,我们发现,这个用法和之前的多播委托来说大同小异。我们再回过头来看那句话:

  • 事件是一种特殊的多播委托仅可以从声明事件的类或结构(发布服务器类)中对其进行调用
    首先,事件是一种特殊的委托,特殊在哪呢?他只可以再声明事件的类或者结构中对其调用。我们发现,在上面例子中,我特意用了两个类,使用Test类去调用Invoke方法去触发这个事件。
    各位可以试一试,虽然Test类中的Delegate事件是用public修饰的,但是在Solution类中却不能直接触发这个事件。
    而如果使用public的多播委托的话,我们同样可以使用+=、-=进行注册或者取消注册,但是很明显,在能够使用+=、-=注册或者取消注册的同时,我们也开放了调用这个多播委托的权限。
    因此我们可以总结:
  • 使用event修饰一个委托形成事件是给委托加一个调用的限制。
    可是,可能很多同学会有这样的疑问,如果直接使用多播委托,能否达到相同的效果呢?事实上这是完全可以的,下面我们来描述下委托的实质。

事件的实质

首先我们使用多播委托来实现一下事件:

class Test{
    private MyDelegate mDelegate = null;
    private Invoke(){ mDelegate?.Invoke(); }
    public Add(MyDelegate d){ mDelegate += d; }
    public Remove(MyDelegate d){ mDelegate -= d; }
    #region Other Members
    ///
    #endregion
}

利用上述代码,我们便可以使用Add方法来注册事件而使用Remove方法来取消注册事件。
并且,我们将mDelegate的调用权限给了这个类而不公开。
事实上,事件的默认实现就是类似上述的代码实现,当你使用event关键词定义一个事件时,会为他创建一个私有的委托字段,还有两个与该事件的访问权限一致的方法(即类似上述的Add与Remove方法)。
而之所以刚刚强调是事件的默认实现,是因为我们其实可以改变事件的add方法和remove方法的行为,这个形式其实与C#中的属性会很相似,下面通过一段代码来演示一下如何使用自定义的事件。

delegate void MyDelegate();
class Test{
    private MyDelegate mDelegate=null;
    public event MyDelegate Event{
        add{ mDelegate += value; }
        remove{ mDelegate -= value; }
    }
}

是不是非常类似于属性呢?当然了,其实他们两个的设计思维我觉得很类似,属性的实质其实也类似,其本质都是简化了很多操作的语法糖。
另外,这里还给出一个自定义事件的例子来帮助大家的理解。
默认的事件实现有一个特性,如果一个委托实例被多次注册,事实上在事件发送通知的时候,这个委托会被执行多次。
那么,怎样来让每个委托实例只被执行一次呢?
请阅读下面的代码:

delegate void MyDelegate();
class Test
{
    /// 使用一个HashSet容器来保存委托实例,以在容器中,每个委托实例只存在对他的一个引用
    static HashSet<MyDelegate> delegates = new HashSet<MyDelegate>();
    public static event MyDelegate Delegate
    {
        add { delegates.Add(value); }
        remove { delegates.Remove(value); }
    }
    public static void Invoke()
    {
        foreach(var dele in delegates)
        {
            dele();
        }
    }
}
class Test2
{
    public static void Main()
    {
        MyDelegate delegate1 = () => Console.WriteLine(1);
        Test.Delegate += delegate1;
        Test.Delegate += delegate1;
        Test.Delegate += delegate1;

        Test.Invoke();
        Console.ReadKey();
    }
}

后话

总结

委托:

  • 委托是一种引用类型,其目的是将方法作为对象来对待。
  • 委托可以很方便的去替换方法中的部分逻辑。
  • 通过lambda表达式可以很方便的创建委托。

事件:

  • 事件是类似于属性的存在,其作用是开放委托实例的+=、-=权限但是保留类的调用权限。
  • 可以重写事件的add、remove方法来自定义事件的行为。

其他资料

Func与Action

Func与Action是两种官方提供的泛型委托类型,按我的理解来说的话,主要目是简化常去创建新的委托类型的操作。
应用较为简单,各位可以参考这篇博客进行简要了解:C# 之 Action 和 Func 的用法

观察者模式

观察者模式是一种可以广泛应用事件机制的设计模式,如果想对这种设计模式进行了解的话建议各位去观看这个视频:23个设计模式——观察者模式
或者阅读《Head First 设计模式》/《设计模式》的相关章节。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM