5

C# 委托原理刨析、事件原理刨析,外加两者对比 - 极客Bob

 1 year ago
source link: https://www.cnblogs.com/Bob-luo/p/17129790.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

什么是委托

委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容参数和返回类型的方法进行绑定。 你可以通过委托实例调用方法。

简单的理解,委托是方法的抽象类,它定义了方法的类型,可以实例化。和普通的类一样,可以申明变量进行赋值,可以当作参数传递,可以定义成属性。

委托具有以下属性:

  • 委托类似于 C++ 函数指针,但委托完全面向对象,不像 C++ 指针会记住函数,委托会同时封装对象实例和方法。
  • 委托允许将方法作为参数进行传递。
  • 委托可用于定义回调方法。
  • 委托可以链接在一起;具备单播、多播功能。
  • 方法不必与委托类型完全匹配。 有关详细信息,请参阅使用委托中的变体
  • 使用 Lambda 表达式可以更简练地编写内联代码块。 Lambda 表达式(在某些上下文中)可编译为委托类型。

1.委托基础介绍

1.1 delegate委托的声明

使用 delegate 关键字,定义具体的委托类型,Delegate至少0个参数,至多32个参数,可以无返回值,也可以指定返回值类型。

查看代码

namespace ConsoleApp.DelegateTest
{
    //例:表示无参数,无返回。
    public delegate void MethodtDelegate();
    //例:表示有两个参数,并返回int型。
    public delegate int MethodtDelegate(int x, int y);
}

方法绑定,进行调用

查看代码

static void Main(string[] args)
        {
            MethodtDelegate methodt = Test;
            //例1:直接调用
            methodt(1,2);
            //例2:假设作为参数传递,进行调用。比如回调函数场景
            InvokeTest(methodt);
        }
        public static int Test(int a, int b)
        {
            return a + b;
        }
        public static void InvokeTest(MethodtDelegate methodt)
        {
            //以下两种方式都可以调用
            var sum = methodt(1, 2);
            var sum = methodt.Invoke(1, 2);
        }

1.2 Action 和 Func 背景

抽象的 Delegate 类提供用于松散耦合和调用的基础结构,但是这样看来,引发一个问题,无论何时需要不同的方法参数,这都会创建新的委托类型。 一段时间后此操作可能变得繁琐。 每个新功能都需要新的委托类型,幸运的是,没有必要这样做,框架已经帮我们定义Action 和 Func 类,我们可以直接申明进行使用

1.3 Action<T> 类

Action是无返回值的泛型委托。Action 委托的变体可包含多达 16 个参数,如 Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>。 重要的是这些定义对每个委托参数使用不同的泛型参数,这样可以具有最大的灵活性。框架源码,如图:

2696180-20220303202514462-1624097925.png

使用就很方便了,我们只需要直接申明委托类型进行使用,例:

查看代码


//例:表示有传入参数int,string,bool无返回值的委托
Action<int,string,bool> 

1.4 Func<T> 类

Func 委托的变体可包含多达 16 个输入参数,如 Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>。 按照约定,返回结果的类型始终是所有 Func 声明中最后一个参数的类型,利用out类型参数实现。

Func是有返回值的泛型委托,func至少0个参数,至多16个参数,根据返回值泛型返回。必须有返回值,不可void。框架源码,如下:

2696180-20220307192304672-6829825.png

使用就很方便了,我们只需要直接申明委托类型进行使用,例:

查看代码


//表示无参,返回值为int的委托,
Func<int> 
//表示传入参数为object, string 返回值为int的委托
Func<object,string,int> 

2. 委托实战案例

我这里就做一个多播案例,帮助大家理解,其实.NET core 日志框架和其他第三方日志框架,差不多就是这种套路

2.1 定义Logger类

这个类我们的定义好委托和调用委托的方法。

查看代码


    public static class Logger
    {
        public static Action<string> WriteMessage;

        public static void LogMessage(string msg)
        {
            WriteMessage(msg);
        }
    }

2.2 定义文件记录器

一个写入文件的,文件记录器

查看代码


    public class FileLogger
    {
        public FileLogger()
        {
            Logger.WriteMessage += LogMessage;
        }

        public void DetachLog() => Logger.WriteMessage -= LogMessage;
        // make sure this can't throw.
        private void LogMessage(string msg)
        {
            try
            {
                Console.WriteLine($"FileLogger\t{msg}");
            }
            catch (Exception)
            {
                // Hmm. We caught an exception while
                // logging. We can't really log the
                // problem (since it's the log that's failing).
                // So, while normally, catching an exception
                // and doing nothing isn't wise, it's really the
                // only reasonable option here.
            }
        }
    }

2.3 定义数据库记录器

一个写入不同数据库的,数据库记录器

查看代码


   public class DBLogger
    {
        private readonly string name;
        public DBLogger(string name)
        {
            this.name = name;
            Logger.WriteMessage += LogMessage;
        }

        public void DetachLog() => Logger.WriteMessage -= LogMessage;
        // make sure this can't throw.
        private void LogMessage(string msg)
        {
            try
            {
                Console.WriteLine($"DBLogger{name}\t{msg}");
            }
            catch (Exception)
            {
                // Hmm. We caught an exception while
                // logging. We can't really log the
                // problem (since it's the log that's failing).
                // So, while normally, catching an exception
                // and doing nothing isn't wise, it's really the
                // only reasonable option here.
            }
        }
    }

以上两个代码逻辑,博主就不介绍了,就用一个控制台输出,代表业务代码了

2.4 测试

测试一下,广播和委托删除效果

查看代码

static void Main(string[] args)
        {
            //添加一个文件记录器和两个数据库记录器
            new FileLogger();
            new DBLogger("DB1");
            var a = new DBLogger("DB2");
            //调用委托
            Logger.LogMessage("add失败");
    
            //删除此数据库记录器
            a.DetachLog();
            Console.WriteLine("======DetachLogDB2========");
            //调用委托
            Logger.LogMessage("add失败");
        }

运行效果:

2696180-20220307192304676-73525444.png

在实际项目中,大家就自行发挥

3. 委托变量捕获

3.1效果演示

说到委托,博主也把这个重要的知识点讲解一下,这个知识点很多人可能不知道或者踩过坑,但掌握了这个知识点其实可以实现一些比较花哨功能。

这里博主就用一个案例进行体现变量捕获,这里代码博主就用 lambda 表达式 进行简写,不太熟悉的可以通过链接跳转进行学习。

逻辑就是,简单的累计一下数量,通过最终的值体现。这里博主分别申明两个整数型变量,通过两个委托分别累计,然后看各自的值。两个委托区别就是传值方式的不同。

查看代码

        static void Main(string[] args)
        {
            int count1 = 0;//委托1的参数
            int count2 = 0;//委托2的参数
            //实例化委托1
            Action<int> action1 = (p) =>
            {
                p++;
                Console.WriteLine("action1:" + p);
            };
            //实例化委托2
            Action action2 = () =>
            {
                count2++;
                Console.WriteLine("action2:" + count2);
            };
            //循环5此
            for (int i = 0; i < 5; i++)
            {
                action1(count1);//调用委托1
                action2();//调用委托2
                Console.WriteLine("---------------------------分割线");
            }
            Console.WriteLine("count1 最终值:" + count1);
            Console.WriteLine("count2 最终值:" + count2);
        }

测试效果:

2696180-20220308193902088-241538307.png

大家发现没?逻辑代码一下,只是参数传递方式不一样,结果截然不同:

委托1的方式:不改变变量的值,方法之间是不共享这个参数的。这种很容易理解,就和我们调用普通方法一样,变量是值类型,是拷贝了一个副本传给了方法进行使用

委托2的方式:改变变量的值,方法之间是共享这个参数的。这种就像引用类型参数一样,是不是很神奇,难道是利用了ref关键字实现的?

3.2原理刨析

其实没有大家想学的那么神秘,委托之所以使用方式和类无异,是因为它本身就是一个类,只是这个过程的定义由编译器帮我们做了,我们只需要使用C#的语法糖。接下来博主就带大家揭开委托的神秘面纱。

我也给大家画一个简单的编译=》执行的过程

2696180-20230217144149057-950794363.png
3.2.1 委托真实面貌

博主就简单写了一个委托,然后通过IL DASM工具查看IL代码

查看代码


    internal class Program
    {
        static void Main(string[] args)
        {
            int b = 888888888;
            Func<int> action = () =>
            {
                return b++;
            };
            var a = action.Invoke();
        }
    }
2696180-20220311210235358-743537772.png
3.2.2模拟委托调用过程

查看代码


    internal class Program
    {
        public class DisplayClass
        {
            public int b;
            public int Invoke()
            {
                return b++;
            }
        }
        public class _Func<T>
        {
            private readonly DisplayClass displayClass;
            public _Func(DisplayClass display)
            {
                displayClass = display;
            }
            public T Invoke()
            {
                object b = displayClass.Invoke();
                return (T)b;
            }
        }
        static void Main(string[] args)
        {
            var display = new DisplayClass();
            display.b = 888888888;
            var actionTest = new _Func<int>(display);
            var a = actionTest.Invoke();
        }
    }
2696180-20220311210235351-748564411.png

大家发现没,最终的IL代码一模一样。也就说,委托就是编译器帮我们把func编译成一个带invoke函数的func类和生成一个装捕获的变量和函数体的类,然后通过构造函数将对象引用和函数指针(获取指针就是大家所说的把非托管指针压入当前)传给func类的实例化。然后最终调用的时候,委托类的invoke函数会去调用真正的函数。就这样完成了对函数的抽象。

3.2.3 委托变量生命周期

现在大家是不是对委托有了一定的理解了,而委托涉及到的捕获变量和参数变量,生命周期就说得通了,也知道为啥委托改变了变量,能通知到原本的变量,因为对变量就行了类的装箱,打包成了一个一个引用类型,那方法外部当然知道变量的值被改变了,因为大家都是拿着引用对象的地址呀。下面做个生命周期小总结:

  • p变量是普通变量,当方法被销毁时,它就会被销毁。
  • count2变量是捕获变量,当委托实例被销毁时,它才会被销毁。

其实讲完委托,事件就很容易理解了, 博主就简单讲解一下,如果大家有需要,博主就再写一篇详细的讲解。

事件:实际上,事件是建立在对委托的语言支持之上的一种设计而已。

4.1 事件定义语法

/定义一个委托
4     public delegate void delegateRun();
5     //定义一个事件
6     public event delegateRun eventRun;

简单的说,事件可以看作是一个委托类型的变量

4.2委托和事件共性:

它们都提供了一个后期绑定方案:在该方案中,组件通过调用仅在运行时识别的方法进行通信。 它们都支持单个和多个订阅服务器方法。 也就是单播和多播支持。 二者均支持用于添加和删除处理程序的类似语法。 最后,引发事件和调用委托使用完全相同的方法调用语法。 它们甚至都支持与 ?. 运算符一起使用的相同的 Invoke() 方法语法。

4.3 事件原理刨析

public event EventHandler<NewMailEventArgs> NewMail;  

1216351-20171101112334732-941480457.png

可以看到当我们定义一个NewEvent时,编译器帮我们生成了:1. 一个private NewMail 字段,类型为 EventHandler<NewMailEventArgs>。 2.一个 add_NewMail 方法,用于将委托添加到委托链(内部调用了Delegate.Combine方法)。3.一个 remove_NewMail 方法,用于将委托从委托链移除(内部调用了Delegate.Remove方法)。对事件的操作,就是是对NewMail字段的操作。

4.4 如何选择

主要区别就是:

    1.事件处理程序通过修改事件参数对象的属性将信息传回到事件源。 虽然这些惯用语可发挥作用,但它们不像从方法返回值那样自然。

    2.包含事件的类以外的类只能添加和删除事件侦听器;只有包含事件的类才能调用事件。 事件通常是公共类成员。 相比之下,委托通常作为参数传递,并存储为私有类成员(如果它们全部存储)

    3.当事件源将在很长一段时间内引发事件时,基于事件的设计会更加自然。比如基于事件的 UI 控件设计案例

(1)事件:事件时属于类的成员,所以要放在类的内部。

(2)委托:属于一个定义,是和类、接口类似的,通常放在外部。

所以事件这种架构设计思想还是很值得大家去学习的。

所以说,如果你的代码在不调用任何订阅服务器的情况下可完成其所有工作,使用基于事件的设计会更好点。

大家在项目中,怎么进行选择,就看实际需求了。

看到这里的朋友,肯定对委托和事件还是有了一定的了解了,毕竟博主很用心的在写,尽量讲细一点。如果大家觉得博主讲解的比较全面,且透彻。大家可以点点赞,给予鼓励。也可以关注博主后续的更新,每一篇都会尽心讲解


Recommend

  • 49

    推广 - @artoostark - 送一本新书(自己写的,年底出版,适合非常白的小白看),外加自己书库里的几本书,比如《重构》,很新。是这样的,我在做微信公众号,搞的时间不长,对内容规划没感觉,想听一听大家的意见。微信公

  • 31

    “我的手机号码是1xxxxxx,尾号8888……”曾经,我们一听到有人这样报出自己的联系方式,就会“肃然起敬”,知道对方是个有“身份”的人。拥有这样一个“靓号”也是多少“成功人士”的标配。不仅仅方便业务往来,关键在于“有面子”。对“面子”的

  • 40

    最近好多朋友反馈,大厂面试中提到ZooKeeper的概率越来越高,看书刷题恐怕不能攻克知识边界的疑难杂症,难以跟面试官周旋,想拿一个好的Offer比登天还难。 如果有大佬能手把手指导 分布式一致性算法...

  • 24
    • segmentfault.com 3 years ago
    • Cache

    .NET委托,事件和Lambda表达式

    委托 委托是什么? 委托是一种引用类型(其实就是一个类,继承MulticastDelegate特殊的类。),表示对具有特定参数列表和返回类型的方法的引用。 每个委托提供Invoke方法, BeginInvoke和EndInvoke异步方法...

  • 12

    可视化大屏快速开发教程,外加28张精美模板,超赞李启方数据分析不是个事儿年底了, 各...

  • 11

    by zhangxinxu from http://www.zhangxinxu.com/wordpress/?p=6614 本文可全文转载,但需得到原作者书面许可,同时保留原作者和出处,摘要引流则随...

  • 11
    • 微信 mp.weixin.qq.com 3 years ago
    • Cache

    Dotnet的局部函数和委托的对比

    上一篇说了一下委托,这篇来说说局部函数和委托的对比。 把委托和局部函数放成前后篇,是因为这两个内容很像,用起来容易混。 需要了解委托相关内容,可以看这一篇 【传送门】 使用委托表达式(Lambda)...

  • 6

    沉浸式游戏体验外加高达buff,黑鲨4S系列新机开箱 2021-10-15 16:30:17   [  中关村在线 原创  ]   作者:戴夫

  • 13

    人生总是忙完这一阵就可以继续忙下一阵了,尤其是我这种处理事件的 capacity 本来就低的人来说,每天至少1-2小时躺平 decompress 为最优先。过去的半年(其实中间隔了一个月没有进行,所以实际上是5个月)里我终于试着把锻炼这件事断断续续地做了起来,以 Shawn...

  • 2

    Kubernetes 由 Google 孵化并得到持续的社区支持,实际上是容器编排的理想之选。然而,Kubernetes 并不是适用...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK