草庐IT

C# 委托原理、事件原理刨析和对比

极客Bob 2023-03-28 原文

引言:什么是委托

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

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

委托具有以下属性:

  • 委托类似于 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 ActionFunc 背景

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

1.3 Action<T> 类

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

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

查看代码

//例:表示有传入参数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。框架源码,如下:

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

查看代码

//表示无参,返回值为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失败");
        }

运行效果:

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

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);
        }

测试效果:

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

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

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

3.2原理刨析

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

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

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();
        }
    }

3.2.2模拟委托调用过程
查看代码
    internal class Program
    {
        public class DisplayClass
        {
            public int b;
            public int b__0()
            {
                return b++;
            }
        }
        public class _Func<T>
        {
            private readonly DisplayClass displayClass;
            public _Func(DisplayClass display)
            {
                displayClass = display;
            }
            public T Invoke()
            {
                object b = displayClass.b__0();
                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();
        }
    }

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

3.2.3 委托变量生命周期

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

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

4. 事件

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

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

4.1 事件定义语法

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

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

4.2委托和事件共性:

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

4.3 事件原理刨析

public event EventHandler<NewMailEventArgs> NewMail;  

可以看到当我们定义一个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)委托:属于一个定义,是和类、接口类似的,通常放在外部。

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

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

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

彩蛋

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

 

有关C# 委托原理、事件原理刨析和对比的更多相关文章

  1. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  2. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  3. ruby-on-rails - 事件管理员日期过滤器日期格式自定义 - 2

    是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. ruby-on-rails - 事件记录 : Select max of limit - 2

    我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).

  6. ruby-on-rails - 事件管理员和自定义方法 - 2

    这是我在ActiveAdmin中的自定义页面ActiveAdmin.register_page"Settings"doaction_itemdolink_to('Importprojects','settings/importprojects')endcontentdopara"Text"endcontrollerdodefimportprojectssystem"rakedataspider:import_projects_ninja"para"OK"endendend我想做的是,当我单击“导入项目”按钮时,我想在Controller中执行rake任务。但是我无法访问该方法。可能是什

  7. ruby-on-rails - 在不重新查询数据库的情况下重新排序 Rails 中的事件记录? - 2

    例如,假设我有一个名为Products的模型,并且在ProductsController中,我有以下代码用于product_listView以显示已排序的产品。@products=Product.order(params[:order_by])让我们想象一下,在product_listView中,用户可以使用下拉菜单按价格、评级、重量等进行排序。数据库中的产品不会经常更改。我很难理解的是,每次用户选择新的order_by过滤器时,rails是否必须查询,或者rails是否能够以某种方式缓存事件记录以在服务器端重新排序?有没有一种方法可以编写它,以便在用户排序时rails不会重新查询结果

  8. ruby-on-rails - Ruby 长时间运行的进程对队列事件使用react - 2

    我有一个将某些事件写入队列的Rails3应用。现在我想在服务器上创建一个服务,每x秒轮询一次队列,并按计划执行其他任务。除了创建ruby​​脚本并通过cron作业运行它之外,还有其他稳定的替代方案吗? 最佳答案 尽管启动基于Rails的持久任务是一种选择,但您可能希望查看更有序的系统,例如delayed_job或Starling管理您的工作量。我建议不要在cron中运行某些东西,因为启动整个Rails堆栈的开销可能很大。每隔几秒运行一次它是不切实际的,因为Rails上的启动时间通常为5-15秒,具体取决于您的硬件。不过,每天这样做几

  9. ruby - 将属性方法委托(delegate)给父对象 - 2

    我有以下类(class):classAlphabetattr_reader:letter_freqs,:statistic_letterdefinitialize(lang)@lang=langcaselangwhen:en@alphabet=('A'..'Z').to_a@letter_freqs={...}when:ru@alphabet=('А'..'Я').to_a.insert(6,'Ё')@letter_freqs={...}...end@statistic_letter=@letter_freqs.max_by{|k,v|v}[0]endendfoo=Alphabet.n

  10. ruby-on-rails - 使用 Rails 事件记录获取二级模型 - 2

    我有一个帖子属于城市的关系,城市又属于一个州,例如:classPost现在我想找到所有帖子及其所属的城市和州。我编写了以下查询来获取带有城市的帖子,但不知道如何在同一查找器中获取带有城市的相应州:@post=Post.find:all,:include=>[:city]感谢任何帮助。谢谢。 最佳答案 Post.all(:include=>{:city=>:state}) 关于ruby-on-rails-使用Rails事件记录获取二级模型,我们在StackOverflow上找到一个类似的问

随机推荐