郭子鹏 發表於 2024-8-5 08:00:00

C#.Net筑基-解密委托与事件

<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338359-1430770466.png" alt="image.png" loading="lazy"></p>
<p>委托与事件是C#中历史比较悠久的技术,从<code>C#1.0</code>开始就有了,核心作用就是将方法作为参数(变量)来传递和使用。其中委托是基础,需要熟练掌握,编程中常用的Lambda表达式、Action、Func都是委托,包括事件也是基于委托实现的。</p>
<hr>
<h1 id="01认识委托delegate">01、认识委托delegate</h1>
<h2 id="11什么是委托">1.1、什么是委托?</h2>
<p>委托是一种用来包装方法的特殊类型,可以将方法包装为对象进行传递、调用,类似函数指针。delegate 关键字用来定义一个委托类型,语法类似方法申明,可以看做是一个“方法签名模板”,和方法一样定义了方法的返回值、参数。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338415-428311511.jpg" alt="" loading="lazy"></p>
<ul>
<li>用 <code>delegate</code> 定义的委托是<strong>一个类</strong>,继承自 System.MulticastDelegate、System.Delegate,“方法名”就是委托类型的名称。</li>
<li>委托的使用同其他普通类型,实例指向一个方法的引用,该方法的申明和委托定义的“方法签名模板”须匹配(支持协变逆变)。</li>
<li>委托支持连接多个委托(方法),称为多播委托(MulticastDelegate),执行时都会调用。</li>
</ul>
<pre><code class="language-csharp">public delegate void Foo(string name); //申明一个委托类型
void Main()
{
    Foo faction;   //申明一个Foo委托(实例)变量
        faction = DoFoo; //赋值一个方法
        faction += str =&gt; { Console.WriteLine($"gun {str}"); };//添加多个"方法实例"
    faction += DoFoo; //继续添加,可重复
        faction("sam");          //执行委托,多个方法会依次执行
    faction.Invoke("zhang"); //同上,上面调用方式实际上还是执行的Invoke方法。
}
private void DoFoo(string name){
        Console.WriteLine($"hello {name}");
}
</code></pre>
<p>委托的主要使用场景:核心就是把方法作为参数来传递,分离方法申明和方法实现。</p>
<ul>
<li><strong>回调方法</strong>,包装方法为委托,作为参数进行传递,解耦了方法的申明、实现和调用,可以在不同的地方进行。</li>
<li><strong>Lambda表达式</strong>,这是委托的简化语法形式,更简洁,比较常用。</li>
<li><strong>事件</strong>,事件是一种特殊的委托,是基于委托实现的,可以看做是对委托的封装。</li>
</ul>
<h2 id="12delegate-api"><strong>1.2、Delegate</strong> API</h2>
<table>
<thead>
<tr>
<th><strong>🔸Delegate属性</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Method</td>
<td>获取委托所表示的方法信息,多个值返回最后一个</td>
</tr>
<tr>
<td>Target</td>
<td>获取委托方法所属的对象实例,多个值返回最后一个,静态方法则为<code>null</code>。<br><strong>所以要注意</strong>:委托、事件不用时要移除,避免GC无法释放资源。</td>
</tr>
<tr>
<td><strong>🔸Delegate静态成员</strong></td>
<td><strong>-</strong></td>
</tr>
<tr>
<td>CreateDelegate</td>
<td>用代码创建指定类型的委托,包括多个重载方法</td>
</tr>
<tr>
<td>Combine(Delegate, Delegate)</td>
<td>将多个委托组合为一个新委托(链),简化语法<code>+</code>、<code>+=</code>:<code>Foo d = d1 + d2;</code></td>
</tr>
<tr>
<td>Remove(source, value)</td>
<td>移除指定委托的调用列表,返回新的委托。简化语法<code>-</code>、<code>-=</code>:<code>d -= d1</code></td>
</tr>
<tr>
<td>RemoveAll(source, value)</td>
<td>同上,区别是<code>Remove</code>值移除找到的最后一个,<code>RemoveAll</code> 移除所有找到的</td>
</tr>
<tr>
<td><strong>🔸MulticastDelegate成员</strong></td>
<td><strong>-</strong></td>
</tr>
<tr>
<td>GetInvocationList()</td>
<td>按照调用顺序返回此多路广播委托的委托列表</td>
</tr>
</tbody>
</table>
<h2 id="13解密委托类型">1.3、解密委托“类型”</h2>
<p>用 <code>delegate</code> 定义的委托,编译器会自动生成一个密封类,so,委托本质上就是一个类。该委托类继承自 System.MulticastDelegate,<code>MulticastDelegate</code>又继承自 System.Delegate,Delegate是委托的基类,她们都是抽象类( abstract class)。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338483-2055001451.jpg" alt="" loading="lazy"></p>
<p><code>delegate</code>定义的委托编译后的IL代码如下(已简化),可查看在线sharplab。</p>
<pre><code class="language-csharp">public delegate void Foo(string name,int age); //申明一个委托类型

//编译器生成的Foo委托类(简化代码)
class public auto ansi sealed Foo extends System.MulticastDelegate]
{
    void Foo(object obj, IntPtr method) { ... }
    public virtual void Invoke (string name,int32 age) { ... }
    public virtualBeginInvoke (string name,int32 age,System.AsyncCallback callback, object 'object') { ... }
    public virtual void EndInvoke (class System.IAsyncResult result){ ... }
}
</code></pre>
<ul>
<li>委托的构造函数有两个参数,<code>obj</code>为方法所在的对象,<code>method</code>为方法指针。该构造函数由编译器调用,了解即可。</li>
<li>执行委托的三个方法<code>Invoke</code> 、 <code>BeginInvoke</code> 和 <code>EndInvoke</code> 签名和委托申明一致。</li>
<li>执行一个委托(方法)就是调用<code>foo.Invoke()</code>,其简化语法为<code>foo()</code>。<code>BeginInvoke</code> 和 <code>EndInvoke</code>用于异步调用。</li>
<li>因为委托本质上就是一个类,所以委托的定义通常在类外部(和类平级)。</li>
</ul>
<blockquote>
<p>📢 委托、事件的执行,推荐使用<code>?.Invoke</code>,判断是否为<code>null</code>:<code>foo?.Invoke()</code></p>
</blockquote>
<p>测试一下委托的继承层次:</p>
<pre><code class="language-csharp">public delegate void Foo(string name); //申明一个委托类型
void Main()
{
    Foo faction; //申明一个Foo委托变量
        faction = DoFoo; //赋值
       
        var ftype = faction.GetType();
        while (ftype != null)
        {
                Console.WriteLine(ftype.FullName);
                ftype = ftype.BaseType;
        }
        //输出:
        //Foo
        //System.MulticastDelegate
        //System.Delegate
        //System.Object
}
private void DoFoo(string name){
        Console.WriteLine($"hello {name}");
}
</code></pre>
<h2 id="14多播委托multicastdelegate">1.4、多播委托MulticastDelegate</h2>
<p>我们编码中使用的委托、事件其实都是<strong>多播委托 MulticastDelegate</strong>,可包含多个(单一)委托。<code>MulticastDelegate</code> 中有一个委托链表<code>_invocationList</code>,可存放多个(单一)委托(可重复添加),当执行委托时,委托链表中的委托方法会依次执行。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338552-109415353.jpg" alt="" loading="lazy"></p>
<p><strong>🔸添加移除</strong>:推荐用<code>+</code>、<code>-</code>操作符添加、移除委托,其本质是调用<code>Delegate</code>的静态方法<code>Delegate.Combine</code>、<code>Delegate.Remove</code>。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338413-1975174202.png" alt="image.png" loading="lazy"></p>
<blockquote>
<p>📢<strong>注意</strong>:委托方法的<code>+</code>、<code>-</code>是线程不安全的,事件的<code>add</code>、<code>remove</code>是线程安全的。</p>
</blockquote>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338433-1086291196.png" alt="image.png" loading="lazy"></p>
<p><strong>🔸执行委托</strong> <strong><code>A.Invoke()/A()</code></strong>,:所有(委托)方法都会执行。可通过 GetInvocationList() 获取委托(方法)列表,手动控制执行。</p>
<ul>
<li>如果其中一个方法执行报错,链表后面的就不会执行了。</li>
<li>如果委托方法有返回值,则只能获取最后一个结果。</li>
</ul>
<blockquote>
<p>📢<strong>注意</strong>:添加、移除操作都会返回一个新的委托,原有委托并不受影响,<strong>委托是恒定的</strong>!</p>
</blockquote>
<pre><code class="language-csharp">public delegate void Foo(string name); //申明一个委托类型
void Main()
{
        Foo f1 = default; //申明一个Foo委托变量
        f1 += DoFoo; //添加一个方法
        f1 += DoFoo; //再添加一个方法
        f1 += str =&gt; { Console.WriteLine($"gun {str}"); };//继续添加
        f1("sam");//执行了3次方法
        f1 -= DoFoo;//移除
        f1("sam");//执行了2次方法
       
        Foo f2 = DoFoo;
        Foo f3 = f1+f2;//组合委托
        Foo f4 = (Foo)Delegate.Combine(f1,f2); //同上
        Console.WriteLine(f3==f4); //True,内部方法列表中的元素相同,则委托相同
        Console.WriteLine(f3-f2 == f1); //True,移除委托
}
private void DoFoo(string name)
{
        Console.WriteLine($"hello {name}");
}
</code></pre>
<h2 id="15匿名方法和lambda表达式">1.5、匿名方法和Lambda表达式</h2>
<ul>
<li><strong>匿名方法</strong>是一种没有名分(名字)的方法,用 <code>delegate</code>关键字申明,可传递给委托或Lambda表达式。</li>
<li><strong>Lambda表达式</strong>和匿名方法一样,本质上都是委托,生成的IL代码是类似的。Lambda表达式更简洁,支持类型推断,所以现代的编程中基本都是用Lambda表达式了。</li>
</ul>
<pre><code class="language-csharp">public delegate void Foo(string name); //申明一个委托类型
void Main()
{
        //匿名方法
        Foo f1 = delegate(string name){
                Console.WriteLine(name);
        };
        Action a1 = delegate() { Console.WriteLine("hello");};
        f1("sam");
        a1();
   
        //Lambda表达式
        Foo f2 = name=&gt;Console.WriteLine(name);
        f2("king");
}
</code></pre>
<blockquote>
<p>匿名方法、Lambda方法 会被编译为一个私有方法,在一个私有的类中。</p>
</blockquote>
<hr>
<h1 id="02内置委托类型actionfunc">02、内置委托类型Action、Func</h1>
<p>由上文可知委托在编译时会创建一个类型,为提高性能、效率,避免大量不必要重复的委托定义,<code>.Net</code>内置了一些泛型委托 Action、Func,基本上可以满足大多数常用场景。</p>
<ul>
<li><strong>Action</strong>:支持0到16个泛型参数的委托,无返回值。</li>
<li><strong>Func</strong>:支持0到16个输入泛型参数,及一个返回值的泛型委托。</li>
<li><strong>Predicate</strong>:<code>bool Predicate&lt;in T&gt;(T obj)</code>,用于测试判断的委托,返回测试结果<code>bool</code>。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338408-1025325143.png" alt="image.png" loading="lazy"></p>
<p>源代码:</p>
<pre><code class="language-csharp">public delegate void Action();
public delegate void Action&lt;in T&gt;(T obj);
public delegate void Action&lt;in T1, in T2&gt;(T1 arg1, T2 arg2);
...
public delegate TResult Func&lt;out TResult&gt;();
public delegate TResult Func&lt;in T, out TResult&gt;(T arg);
public delegate TResult Func&lt;in T1, in T2, out TResult&gt;(T1 arg1, T2 arg2);
...
public delegate bool Predicate&lt;in T&gt;(T obj);
</code></pre>
<p>上面委托参数<code>in</code>、<code>out</code>是标记可变性(协变、逆变)的修饰符,详见后文《泛型T &amp; 协变逆变》</p>
<hr>
<h1 id="03认识事件event">03、认识事件Event</h1>
<h2 id="31什么是事件event">3.1、什么是事件<code>event</code>?</h2>
<p><strong>事件</strong>是一种特殊类型的委托,他是基于委托实现的,是对委托的进一步封装,因此使用上和委托相似。事件使用 <code>event</code>关键字进行申明,任何其他组件都可以订阅事件,当事件被触发时,它会调用所有已经订阅它的委托(方法)。</p>
<p>事件是基于委托的一种(<strong>事件驱动</strong>)编程模型,用于在对象之间实现基于发布-订阅模式的通知机制,是实现观察者模式的方式之一。常用在GUI编程、异步编程以及其他需要基于消息的系统。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338506-1316954226.jpg" alt="" loading="lazy"></p>
<pre><code class="language-csharp">void Main()
{
        var u = new User();
        //订阅事件
        u.ScoreChanged += (sender, e) =&gt; { Console.WriteLine(sender); };
        u.AddScore(100);
        u.AddScore(200);
}
public class User
{
        public int Score { get; private set; }

        public event EventHandler ScoreChanged;   //定义事件,使用内置的“事件”委托 EventHandler

        public void AddScore(int score)
        {
                this.Score += score;
                this.ScoreChanged?.Invoke(this, null); //触发事件
        }
}
</code></pre>
<p><strong>🔸事件的关键角色</strong>:</p>
<ul>
<li>
<p><strong>①事件的发布者</strong>,发布事件的所有者,在合适的时候触发事件,并通过事件参数传递信息:</p>
<ul>
<li><strong><code>sender</code></strong>:事件源,就是引发事件的发布者。</li>
<li><strong><code>EventArgs</code></strong>:事件参数,一般是继承<code>System.EventArgs</code>的对象,当然这不是必须的,在<code>.NET Core</code>中事件参数可以是任意类型。<code>System.EventArgs</code> 只是一个空的<code>class</code>,啥也没有。</li>
</ul>
</li>
<li>
<p><strong>②事件的订阅者</strong>:订阅发布的事件,事件发生后执行的具体操作。</p>
</li>
</ul>
<blockquote>
<p>📢 <strong>EventHandler</strong>(object? sender, EventArgs e)、<strong>EventArgs</strong><code>&lt;T&gt;</code>、<strong><code>Button.Click</code></strong>算是微软的标准事件模式,是一种习惯约定。</p>
</blockquote>
<p><strong>🔸事件使用实践</strong>:</p>
<ul>
<li>使用<code>+=</code> 订阅事件,支持任意多个订阅。<code>-=</code>移除不用的事件订阅,避免内存溢出,注意<code>-=</code>对匿名方法、Lambda无效,因为每次都是新的委托。</li>
<li>事件的触发需判断<code>null</code>,避免没有订阅时触发报错:<code>Progress?.Invoke()</code>。</li>
<li>事件委托类型以“EventHandler”结尾,大多数场景下使用<code>EventHandler&lt;TEventArgs&gt;</code>即可,当然也可以自定义,或使用<code>Action</code>。</li>
</ul>
<p><strong>🔸事件命名</strong>:名词+动词(被动)</p>
<ul>
<li>事件已发生用<strong>过去式</strong>:Closed、PropertyChanged。</li>
<li>事件将要发生用<strong>现在式</strong>,Closing、ToolTipOpening。</li>
<li>订阅的方法前缀通常加“<code>On</code>”、“<code>Raise</code>”,<code>fileLister.Progress += OnProgress;</code></li>
</ul>
<h2 id="32解密事件-封装委托">3.2、解密事件-“封装委托”</h2>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338384-875912515.png" alt="image.png" loading="lazy"></p>
<p>事件的定义:<code>public event EventHandler MyEvent;</code>,其中<code>EventHandler</code>就是一个委托,下面为其源码:</p>
<pre><code class="language-csharp">public delegate void EventHandler(object? sender, EventArgs e);
public delegate void EventHandler&lt;TEventArgs&gt;(object? sender, TEventArgs e);
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338418-2106822159.png" alt="" loading="lazy"></p>
<p>当定义个事件时,C#编译器会生成对委托的事件包装,类似属性对字段的包装,在线sharplab源码。</p>
<pre><code class="language-csharp">//定义一个事件
public event EventHandler MyEvent;
//用其他委托定义事件
public event Action&lt;string&gt; MyEvent2;

//编译后的IL代码(简化)**********

//委托字段
private EventHandler m_MyEvent;
//类似属性的get、set访问器,通过+ - 来订阅、取消事件订阅。
public event EventHandler MyEvent
{
    add { m_MyEvent += value; }    //Delegate.Combine
    remove { m_MyEvent -= value; } //Delegate.Remove
}
</code></pre>
<ul>
<li>定义事件的“EventHandler”为一个委托,可以是任意委托类型,C#中大多使用内置泛型委托<code>EventHandler&lt;TEventArgs&gt;</code>。</li>
<li>编译后生成了一个私有委托字段<code>m_MyEvent</code>,这是事件的核心。</li>
<li>生成了<code>add</code>订阅、<code>remove</code>取消订阅的方法,控制委托的新增和移除,使用时用<code>+=</code>、<code>-=</code>语法。上面代码是简化过的,实际代码要稍复杂一点点,主要是加了线程安全处理。</li>
<li>自定义事件也可以直接使用上面示例中的<code>add</code>、<code>remove</code>的方式封装。</li>
</ul>
<blockquote>
<p>📢 由上可以看出事件是基于委托封装的,类似属性封装字段。外部只能<code>add</code>订阅、<code>remove</code>取消订阅,事件(委托)的执行(触发)只能在内部进行。</p>
</blockquote>
<h2 id="33标准事件模型">3.3、标准事件模型</h2>
<p>C#内部有大量的事件应用,形成了一个默认的事件(标准的)模式,主要定义了用于创建事件的委托、事件参数。</p>
<ul>
<li><strong>System.EventArgs</strong> :事件参数,这是标准事件模型的核心,作为事件参数的基类,用来继承自定义实现一些事件要传递的字段(属性)。</li>
<li>委托返回值为<code>void</code>。</li>
<li>委托两个参数<code>sender</code>、<code>EventArgs</code>,<code>sender</code>为触发事件的对象,也是事件的广播者;<code>EventArgs</code>为事件的参数。</li>
<li>委托以“EventHandler”命名结尾。</li>
<li>内置的泛型版本<code>EventHandler&lt;TEventArgs&gt;</code> 可以满足上述条件,是一个比较通用的标准事件委托。</li>
</ul>
<pre><code class="language-csharp">public class EventArgs
{
        public static readonly EventArgs Empty = new EventArgs();
}
public delegate void EventHandler(object? sender, EventArgs e);
//通用泛型版本
public delegate void EventHandler&lt;TEventArgs&gt;(object? sender, TEventArgs e);
</code></pre>
<p>当然这个这个模式并不是必须的,只是一种编程习惯或规范。</p>
<h2 id="34该用委托还是事件">3.4、该用委托还是事件?</h2>
<p>事件是基于委托的,事件的功能委托大都能支持,两者功能和使用都比较相似,都支持单播、多播,后期绑定,那两者该如何选择呢?</p>
<ul>
<li>事件一般没有返回值,当然你想要也是可以的。</li>
<li>事件提供更好的封装,类似属性对字段的封装,符合开闭原则。事件的执行只能在内部,外部只能<code>+=</code>订阅、<code>-=</code>取消订阅。</li>
</ul>
<p>✅<strong>所以结论</strong>:</p>
<ul>
<li>简单场景用委托:一对一通讯、传递方法。</li>
<li>复杂场景用事件:一对多通讯、需要安全权限封装。</li>
</ul>
<hr>
<h1 id="04其他-委托的性能问题">04、其他-委托的性能问题?</h1>
<p>由前文我们知道委托实际上都是一个多播委托类型,执行委托时实际是执行<code>Invoke()</code>方法,内部会迭代执行方法列表,这要比直接方法调用要慢不少。</p>
<pre><code class="language-csharp">public static int Sum(int x, int y) =&gt; x + y;   //方法
public static Func&lt;int, int, int&gt; SumFunc = Sum;//委托

public void Sum_MethodCall() //直接调用方法
{
        int sum = 0;
        for (int i = 0; i &lt; 10; i++)
        {
                sum += Sum(i, i + 1);
        }
}
public void Sum_FuncCall()//调用委托
{
        int sum = 0;
        for (int i = 0; i &lt; 10; i++)
        {
                sum += SumFunc(i, i + 1);
        }
}
</code></pre>
<p>在<code>.Net6</code>中运行<code>Benchmark</code>测试对比如下,直接调用的效率要高4-5倍。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338417-1281911813.png" alt="image.png" loading="lazy"></p>
<p>在<code>.Net7</code>、<code>.Net8</code>中作了大量性能优化,委托调用达到了类似直接调用的性能,因此再也不用担心委托的性能缺陷了。下图为<code>.Net8</code>中<code>Benchmark</code>测试。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202406/151257-20240603210338380-517793614.png" alt="image.png" loading="lazy"></p>
<hr>
<h1 id="参考资料">参考资料</h1>
<ul>
<li>System.Delegate 和 delegate 关键字</li>
<li>标准 .NET 事件模式</li>
<li>还弄不明白【委托和事件】么?,适合入门。</li>
<li>由浅入深理解C#中的事件,比较细致,适合入门</li>
<li>C# 的委托与事件大致是怎么一回事,B站视频</li>
<li>.NET中委托性能的演变</li>
</ul>
<hr>
<blockquote>
<p><strong>©️版权申明</strong>:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!<em>原文编辑地址-语雀</em></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/anding/p/18229672
頁: [1]
查看完整版本: C#.Net筑基-解密委托与事件