【C#学习笔记】委托与事件 (从观察者模式看C#的委托与事件)

从观察者模式看C#的委托与事件

    • [一 委托](#一 委托)
      • [1.1 委托的使用](#1.1 委托的使用)
      • [1.2 异步调用委托](#1.2 异步调用委托)
      • [1.3 委托多播](#1.3 委托多播)
        • [1.3.1 多播委托的执行顺序](#1.3.1 多播委托的执行顺序)
        • [1.3.2 多播执行中断与返回值](#1.3.2 多播执行中断与返回值)
      • [1.4 Delegate类、MulticastDelegate类](#1.4 Delegate类、MulticastDelegate类)
        • [1.4.1 运行时添加的四个成员方法](#1.4.1 运行时添加的四个成员方法)
        • [1.4.2 ildasm.exe查看文件](#1.4.2 ildasm.exe查看文件)
      • [1.5 常用的委托类型Action、Func、Predicate](#1.5 常用的委托类型Action、Func、Predicate)
    • [二 事件](#二 事件)
      • [2.1 事件的使用](#2.1 事件的使用)
      • [2.2 事件访问器](#2.2 事件访问器)
      • [2.3 事件的本质、事件与委托的区别](#2.3 事件的本质、事件与委托的区别)
      • [2.4 观察者风格的事件案例](#2.4 观察者风格的事件案例)
      • [2.5 标准 .NET 模式的事件](#2.5 标准 .NET 模式的事件)
    • [三 观察者模式](#三 观察者模式)
      • [3.1 委托 vs IObserable](#3.1 委托 vs IObserable)
      • [3.2 观察者模式模型](#3.2 观察者模式模型)
    • [四 总结](#四 总结)

转载请注明出处:🔗https://blog.csdn.net/weixin_44013533/article/details/134655722

作者:CSDN@|Ringleader|

主要参考:

一 委托

委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。以下示例声明名为 Callback 的委托,表示该委托可以封装以字符串作为参数并返回 void 的方法:

csharp 复制代码
public delegate void Callback(string message);
  • 委托是一个类型,意味着与class interface enum 等类型同级别,可以定义在namespace下,也可以在class内部。
  • 委托类似于 C++ 函数指针,但是是面向对象且是类型安全的。
    • 因为委托是与特定方法签名相关联的类型,这意味着编译器会在编译时检查委托与方法之间的匹配性,确保委托只能引用与其声明的签名相匹配的方法。这有助于在编译阶段捕获一些类型不匹配的错误。
    • 与函数指针不同,委托面向对象且类型安全。而且委托可以表示静态方法或实例方法, 当委托表示实例方法时,委托不仅存储对方法入口点的引用,还存储对类实例的引用。
  • 委托允许将方法作为参数进行传递,因此便于实现方法回调。
  • 多个相同签名的委托实例可以链接在一起,invoke时实现多个方法的顺序调用,也就是委托的多播。

1.1 委托的使用

一个完整示例:

csharp 复制代码
// Declare a delegate.
delegate void NotifyCallback(string str);

// Declare a method with the same signature as the delegate.
static void Notify(string name)
{
    Console.WriteLine($"Notification received for: {name}");
}

public static void Main(string[] args)
{
    // Create an instance of the delegate.
    NotifyCallback del= new NotifyCallback(Notify);
    // Call the delegate.
    del("Hello World");
}

其中委托实例化除了使用NotifyCallback callback= new NotifyCallback(Notify);也可以使用下面方法:

csharp 复制代码
// 1. 将方法组分配给委托类型:
// C# 2.0 provides a simpler way to declare an instance of NotifyCallback.
NotifyCallback del2 = Notify;

// 2. 声明匿名方法:
// Instantiate NotifyCallback by using an anonymous method.
NotifyCallback del3 = delegate(string name)
    { Console.WriteLine($"Notification received for: {name}"); };
    
// 3. 使用 lambda 表达式:
// Instantiate NotifyCallback by using a lambda expression.
NotifyCallback del4 = name =>  { Console.WriteLine($"Notification received for: {name}"); };

其中委托的调用除了使用 del("Hello World");也可以使用del.Invoke("Hello World").

需要注意的是,如果委托实例没有绑定任何方法,调用委托会报'System.NullReferenceException'异常,Invoke写法可以使用Null 条件运算符 ? 判空,如:del?.Invoke("Hello World"),而括号写法不行。

参见: null 条件运算符 ?. 和 ?[]

1.2 异步调用委托

上面的括号或者Invoke调用委托形式都是同步调用,除此之外,还有BeginInvoke的异步调用形式。

BeginInvoke 方法启动异步调用。 该方法具有与你要异步执行的方法相同的参数,另加两个可选参数。 第一个参数是一个 AsyncCallback 委托,此委托引用在异步调用完成时要调用的方法。 第二个参数是一个用户定义的对象,该对象将信息传递到回调方法。 BeginInvoke 将立即返回,而不会等待异步调用完成。 BeginInvoke 返回可用于监视异步调用的进度的 IAsyncResult。

EndInvoke 方法用于检索异步调用的结果。 它可以在调用 BeginInvoke之后的任意时间调用。 如果异步调用尚未完成,那么 EndInvoke 将阻止调用线程,直到完成异步调用。 EndInvoke 的参数包括要异步执行的方法的 out 和 ref 参数以及 BeginInvoke 返回的 IAsyncResult。

下面的代码示例演示了异步调用同一个长时间运行的方法 TestMethod的各种方式。 TestMethod 方法会显示一条控制台消息,说明该方法已开始处理,休眠了几秒钟,然后结束。 TestMethod 有一个 out 参数,该参数用于演示将此类参数添加到 BeginInvoke 和 EndInvoke的签名中的方式。

  1. 下面的代码示例显示了 TestMethod 的定义和可用于异步调用 AsyncMethodCaller 的名称为 TestMethod 的委托。 要编译此代码示例,必须包括 TestMethod 和 AsyncMethodCaller 委托的定义。
csharp 复制代码
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
    public class AsyncDemo
    {
        // The method to be executed asynchronously.
        public string TestMethod(int callDuration, out int threadId)
        {
            Console.WriteLine("Test method begins.");
            Thread.Sleep(callDuration);
            threadId = Thread.CurrentThread.ManagedThreadId;
            return String.Format("My call time was {0}.", callDuration.ToString());
        }
    }
    // The delegate must have the same signature as the method
    // it will call asynchronously.
    public delegate string AsyncMethodCaller(int callDuration, out int threadId);
}
  1. 异步执行方法的最简单方式是通过调用委托的 BeginInvoke 方法开始执行此方法,在主线程上执行一些操作,然后调用委托的 EndInvoke 方法。 EndInvoke 可能会阻止调用线程,因为该方法直到异步调用完成后才返回。 这种方式非常适合执行文件或网络操作。
csharp 复制代码
using System;
using System.Threading;

namespace Examples.AdvancedProgramming.AsynchronousOperations
{
    public class AsyncMain
    {
        public static void Main()
        {
            // The asynchronous method puts the thread id here.
            int threadId;

            // Create an instance of the test class.
            AsyncDemo ad = new AsyncDemo();

            // Create the delegate.
            AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);

            // Initiate the asynchronous call.
            IAsyncResult result = caller.BeginInvoke(3000,
                out threadId, null, null);

            Thread.Sleep(0);
            Console.WriteLine("Main thread {0} does some work.",
                Thread.CurrentThread.ManagedThreadId);

            // Call EndInvoke to wait for the asynchronous call to complete,
            // and to retrieve the results.
            string returnValue = caller.EndInvoke(out threadId, result);

            Console.WriteLine("The call executed on thread {0}, with return value \"{1}\".",
                threadId, returnValue);
        }
    }
}

/* This example produces output similar to the following:

Main thread 1 does some work.
Test method begins.
The call executed on thread 3, with return value "My call time was 3000.".
 */

异步委托其他内容参见:使用异步方式调用同步方法

1.3 委托多播

调用时,委托可以调用多个方法。 这被称为多播。 若要向委托的方法列表(调用列表)添加其他方法,只需使用加法运算符或加法赋值运算符("+"或"+=")添加两个委托。 例如:

csharp 复制代码
var obj = new MethodClass();
Callback d1 = obj.Method1;
Callback d2 = obj.Method2;
Callback d3 = DelegateMethod;

//Both types of assignment are valid.
Callback allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;

若要删除调用列表中的方法,请使用减法运算符或减法赋值运算符(- 或 -=)。 例如:

csharp 复制代码
//remove Method1
allMethodsDelegate -= d1;

// copy AllMethodsDelegate while removing d2
Callback oneMethodDelegate = allMethodsDelegate - d2;
1.3.1 多播委托的执行顺序

委托的调用列表是一个有序 的委托集合,其列表中的每个元素都会调用委托所代表的一个方法。调用列表可以包含重复的方法 。在调用过程中,方法按照它们在调用列表中出现的顺序进行调用 。委托尝试调用其调用列表中的每个方法;重复方法每次出现在调用列表中都会被调用一次。委托是不可变的;一旦创建,委托的调用列表就不会更改。

1.3.2 多播执行中断与返回值

如果调用的方法引发异常,该方法将停止执行,该异常将传递回委托的调用方,并且不会调用调用列表中的其余方法。 捕获调用方中的异常不会改变此行为。

当委托调用的方法的签名包含返回值时,委托将返回调用列表中最后一个元素的返回值。 当签名包含通过引用传递的参数时,参数的最终值是调用列表中按顺序执行并更新参数值的每个方法的结果。

1.4 Delegate类、MulticastDelegate类

类 Delegate 是委托类型的基类。 但是,只有系统和编译器才能从 Delegate 类或 MulticastDelegate 类显式派生。 此外,不允许从委托类型派生新类型。 类 Delegate 不被视为委托类型;它是用于派生委托类型的类。

Delegate有几个重要的属性和方法:

  • public object Target属性:获取当前委托调用实例方法时的类实例。如果委托表示实例方法,则为当前委托调用实例方法的对象;如果委托表示静态方法,则为null。

  • public MethodInfo Method属性:获取由委托表示的方法。

  • Combine(Delegate, Delegate)方法:连接两个委托的调用列表。

    csharp 复制代码
    public static Delegate? Combine (Delegate? a, Delegate? b);

    返回一个新的委托,其调用列表按照a和b的顺序连接在一起。如果b为null,则返回a;如果a为null引用,则返回b;如果a和b都为null引用,则返回null引用。

  • Delegate.Remove(Delegate, Delegate) 方法:从前一个委托的调用列表移除后一个委托的调用列表。

    csharp 复制代码
    public static Delegate? Remove (Delegate? source, Delegate? value);

    如果在 source 的调用列表中找到 value 的调用列表,则将 source 的调用列表移除 value 的调用列表,并形成一个新的带有调用列表的委托;如果source调用列表匹配多个value中的调用列表,则移除最后一个匹配项。如果 value 为空或在 source 的调用列表中找不到 value 的调用列表,则返回 source。如果 value 的调用列表等于 source 的调用列表,或 source 为空引用,则返回空引用null。

    用代码辅助理解"最后一个匹配项/出现项"(the last occurrence):

    csharp 复制代码
    public class Ringleader
        {
            public delegate void StateChangeHandler();
            private static void Method1()
            {
                Console.Write("1 ");
            }
            private static void Method2()
            {
                Console.Write("2 ");
            }
            private static void Method3()
            {
                Console.Write("3 ");
            }
            private static void Method4()
            {
                Console.Write("4 ");
            }
    
            public static void Main(string[] args)
            {
                StateChangeHandler stateChangeHandler1 = Method1;
                StateChangeHandler stateChangeHandler2 = Method2;
                StateChangeHandler stateChangeHandler3 = Method3;
                StateChangeHandler stateChangeHandler4 = Method4;
                var stateChangeHandler12 = stateChangeHandler1 + stateChangeHandler2;
                var stateChangeHandler34 = stateChangeHandler3 + stateChangeHandler4;
                var stateChangeHandler24 = stateChangeHandler2 + stateChangeHandler4;
                var stateChangeHandler13 = stateChangeHandler1 + stateChangeHandler3;
                var stateChangeHandler32 = stateChangeHandler3 + stateChangeHandler2;
                
                var stateChangeHandler1234 = stateChangeHandler12 + stateChangeHandler34;
                stateChangeHandler1234();Console.WriteLine("");//1 2 3 4
                var stateChangeHandler1324 = stateChangeHandler13 + stateChangeHandler24;
                stateChangeHandler1324();Console.WriteLine("");//1 3 2 4
                stateChangeHandler1324 -= stateChangeHandler12;
                stateChangeHandler1324();Console.WriteLine("");//1 3 2 4  (1324-12=1324)
                
                stateChangeHandler1324 -= stateChangeHandler32;
                stateChangeHandler1324();Console.WriteLine("");//1 4  (1324-32=14)
    
    
                var stateChangeHandler13244321312 = stateChangeHandler1 + stateChangeHandler3 + stateChangeHandler2 + stateChangeHandler4 +
                                                 stateChangeHandler4 + stateChangeHandler3 + stateChangeHandler2 + stateChangeHandler1 + 
                                                 stateChangeHandler3 + stateChangeHandler1 + stateChangeHandler2;
                stateChangeHandler13244321312();Console.WriteLine("");//1 3 2 4 4 3 2 1 3 1 2
                stateChangeHandler13244321312 -= stateChangeHandler3 + stateChangeHandler2;
                stateChangeHandler13244321312();Console.WriteLine("");//1 3 2 4 4 1 3 1 2 (13244321312-(3+2)= 132441312)
            }
    
        }

    因为委托的+/-/-=/+=运算本质就是调用Combine或Remove方法,所以上面用运算符验证结果是相同的。

MulticastDelegate继承自Delegate,且包含invocationList参数,代表委托调用列表,可以通过Delegate[] GetInvocationList()方法获取这个列表。

1.4.1 运行时添加的四个成员方法

如果查看Delegate和MulticastDelegate类源码的话,你可能会奇怪,前面提到的Invoke、BeginInvoke、EndInvoke为什么会没有,其实这是运行时交由底层声明并管理的:

一个委托声明会被转换为一个继承自 MulticastDelegate(而不是 CLI 规范中指定的 Delegate)的类。该类始终具有确切的 4 个成员,它们的运行时实现由 CLR 提供:

  1. 一个构造函数.ctor(object,native int),接受一个对象object和一个 IntPtr。对象是 Delegate.Target,IntPtr 是目标方法的地址,也即Delegate.Method。这些成员在调用委托时稍后使用,Target 属性在委托绑定的方法是实例方法时提供 this 引用,对于静态方法则为 null。Method 属性决定要调用的方法。

  2. 一个 Invoke() 方法。该方法的参数是动态生成的,并与委托声明匹配。调用 Invoke() 方法在同一线程上运行委托目标方法,即同步调用。通常只需使用括号语法糖即可,通过对象名称后跟括号来调用委托对象。

  3. 一个 BeginInvoke() 方法,提供了一种进行异步调用的方式。该方法在目标方法正在忙于执行时迅速完成,类似于 ThreadPool.QueueUserWorkItem,但具有类型安全的参数。返回类型始终为 System.IAsyncResult,用于查找异步调用何时完成,并提供给 EndInvoke() 方法。第一个参数是一个可选的 System.AsyncCallback 委托对象,当异步调用完成时,其目标将自动被调用。第二个参数是一个可选的对象,它将原样传递给回调,对于跟踪状态很有用。附加参数是动态生成的,并与委托声明匹配。

  4. 一个 EndInvoke() 方法。它接受一个 IAsyncResult 类型的单一参数,您必须传递从 BeginInvoke() 得到的参数。它完成异步调用并释放资源。

参见:

1.4.2 ildasm.exe查看文件

可以通过反汇编代码来查看编译后的委托被运行时底层添加了什么。

Rider创建新solution时使用Console Application,运行项目时,根目录会多出一个bin>debug>*.exe一个执行文件,将这个文件拖入系统自带的C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.1 Tools\ildasm.exe 反汇编工具中。

如下示代码,声明了一个委托,实际上是创建了一个继承自MulticastDelegate的类,这个类包含一个构造器,和Invoke/BeginInvoke/EndInvoke三个方法。

csharp 复制代码
namespace AsyncDelegate
{
    public class DecompiledTest2
    {
        public delegate void StateChangeHandler();
    }
}

上面提到的"委托的+/-/-=/+=运算本质就是调用Combine或Remove方法"也可以用反汇编验证这个说法。

参考:

1.5 常用的委托类型Action、Func、Predicate

.NET Core 框架包含几个在需要委托类型时可重用的类型。 这些是泛型定义,因此需要新的方法声明时可以声明自定义。

  1. 第一个类型是 Action 类型和一些变体:

    csharp 复制代码
    public delegate void Action();
    public delegate void Action<in T>(T arg);
    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
    // Other variations removed for brevity.
  2. 几种可用于返回值的委托类型的泛型委托类型:

    csharp 复制代码
    public delegate TResult Func<out TResult>();
    public delegate TResult Func<in T1, out TResult>(T1 arg);
    public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
    // Other variations removed for brevity
  3. 一种专门的委托类型 Predicate,此类型返回单个值的测试结果:

    csharp 复制代码
    public delegate bool Predicate<in T>(T obj);

更多内容参见:强类型委托

二 事件

事件是对象用于(向系统中的所有相关组件)广播已发生事情的一种方式。 任何其他组件都可以订阅事件,并在事件引发时得到通知。比如这些事件会报告鼠标移动、按钮点击和类似的交互。

事件的语言设计针对这些目标:

  • 在事件源和事件接收器之间启用非常小的耦合。 这两个组件可能不会由同一个组织编写,甚至可能会通过完全不同的计划进行更新。
  • 订阅事件并从同一事件取消订阅应该非常简单。
  • 事件源应支持多个事件订阅服务器。 它还应支持不附加任何事件订阅服务器。

你会发现事件的目标与委托的目标非常相似。 因此,事件语言支持基于委托语言支持构建

用于定义事件以及订阅或取消订阅事件的语法是对委托语法的扩展。

如下定义了StateChangeHandler委托类型的事件,事件用event关键字修饰:

csharp 复制代码
public delegate void StateChangeHandler();
public event StateChangeHandler StatechangeEvent;

事件的调用、注册和注销方法和委托相同:

csharp 复制代码
StatechangeEvent += ()=>{ Console.WriteLine("事件触发~");};
StatechangeEvent?.Invoke();

2.1 事件的使用

下面是完整实例:

csharp 复制代码
using System;

namespace AsyncDelegate
{
    public delegate void StateChangeHandler();
    public class EventTest
    {
        public event StateChangeHandler StatechangeEvent;
        public static void Method()
        {
            Console.WriteLine("事件触发~");
        }
        public static void Main(string[] args)
        {
            var eventTest = new EventTest();
            eventTest.StatechangeEvent += Method;
            eventTest.StatechangeEvent?.Invoke();
        }
    }
}

为了弄清event字段的作用,利用Ildasm.exe反汇编代码。


默认事件访问器的反汇编结果

发现系统自动添加了一个StateChangeHandler委托类型的私有变量StatechangeEvent,同时添加了两个方法add_StatechangeEvent、remove_StatechangeEvent.

这等同于下面的写法:

csharp 复制代码
namespace AsyncDelegate
{
    public delegate void StateChangeHandler();
    public class EventTest
    {
        public event StateChangeHandler StatechangeEvent
        {
            add => _stateChangeHandler += value;
            remove => _stateChangeHandler -= value;
        }
        private StateChangeHandler _stateChangeHandler;
        public static void Method()
        {
            Console.WriteLine("事件触发~");
        }
        public static void Main(string[] args)
        {
            var eventTest = new EventTest();
            eventTest.StatechangeEvent += Method;
            eventTest._stateChangeHandler?.Invoke();
        }
    }
}

多了一个私有StateChangeHandler变量,以及一个事件访问器,其中add、remove方法里的value代指使用"+/-/+=/-="委托运算符时对应的方法名。

可以对比一下上面代码反汇编后的结果:

自定义事件访问器的反汇编结果

当在其他类使用上面事件并试图调用其Invoke方法时,编辑器报错:The event 'StatechangeEvent' can only appear on the left hand side of += or -= (except when used from within the class 'AsyncDelegate.EventTest'),表明事件只向外暴露add和remove访问器,Invoke并没有暴露,所以无法访问。

2.2 事件访问器

事件访问器可以自定义,除了上面的例子,还可以如下所示:

csharp 复制代码
public event StateChangeHandler StatechangeEvent
 {
     add
     {
         _stateChangeHandler += value;
         Console.WriteLine("已添加{0}方法",value.GetMethodInfo().Name);
     }
     remove
     {
         _stateChangeHandler -= value;
         Console.WriteLine("已移除{0}方法",value.GetMethodInfo().Name);
     }
 }

这样可以打印所添加的方法。

还可以在多线程环境下对事件所在的对象进行加锁,比如:

csharp 复制代码
 public event StateChangeHandler StatechangeEvent
  {
      add
      {
          lock (this)
          {
              _stateChangeHandler += value;
          }
      }
      remove
      {
          lock (this)
          {
              _stateChangeHandler -= value;
          }
      }
  }

经测试,只要有自定义事件访问器,系统就不会自动为你添加对应私有委托变量。


当存在自定义事件访问器时,不会自动添加对应私有委托变量

2.3 事件的本质、事件与委托的区别

当你删除上面代码的event字段,你将会发现,代码依然可以运行。

csharp 复制代码
namespace AsyncDelegate
{
    public delegate void StateChangeHandler();
    public class EventTest
    {
        public StateChangeHandler StatechangeEvent;
    }
    class OtherClass
    {
        public static void Method()
        {
            Console.WriteLine("事件触发~");
        }
        public static void Main(string[] args)
        {
            var eventTest = new EventTest();
            eventTest.StatechangeEvent += Method;
            eventTest.StatechangeEvent?.Invoke();
        }
    }
}


去除event字段的反汇编结果

唯一区别就是现在事件可以在外部Invoke了。

通过之前的介绍和分析,不难明白,event字段本质就是对委托进行私有访问限制,事件的本质就是委托,只不过系统会对用event字段修饰的委托进行了特殊处理,比如自动生成一个私有的委托变量,添加两个事件访问器,同时禁止外部类对事件的Invoke等方法调用。

2.4 观察者风格的事件案例

前面的案例还是把事件定义、事件触发、事件订阅都放在一个类中。现在对其进行改写,使其符合事件使用时的解耦场景:

csharp 复制代码
using System;
namespace AsyncDelegate
{
    public delegate void StateChangeHandler();
    // 事件定义,在观察者模式中称作subject主题
    public class EventTest
    {
        public event StateChangeHandler StatechangeEvent;
        public void OnStateChange()
        {
            Console.WriteLine("事件触发~");
            StatechangeEvent?.Invoke();
        }
    }
    // 观察者定义,在观察者模式中称作Observer观察者
    class Observer
    {
        public void Method()
        {
            Console.WriteLine("观察者接收到事件触发");
        }
    }
    // 主程序,注册观察者、触发事件等
    class Ringleader{
        public static void Main(string[] args)
        {
            var eventTest = new EventTest();
            var observer = new Observer();
            eventTest.StatechangeEvent += observer.Method;
            eventTest.OnStateChange();
        }
    }
}

2.5 标准 .NET 模式的事件

如果要遵循标准 .NET 模式的事件,可以利用.NET 类库中的EventHandler 委托(当然你也可以自定义遵循这种风格的委托),其定义如下:

csharp 复制代码
public delegate void EventHandler(object sender, EventArgs e);

其中

  • object sender:事件源
  • EventArgs e:不包含事件数据的对象。

将上面的例子改写成标准 .NET 模式的事件:

csharp 复制代码
using System;

namespace AsyncDelegate
{
    public class EventHandlerTest
    {

        public event EventHandler StatechangeEvent;

        public void OnStateChange()
        {
            StatechangeEvent?.Invoke(this,new MyEventArgs("事件触发啦~"));
            // StatechangeEvent?.Invoke(this,EventArgs.Empty);//不带参数的写法
        }
    }

    class MyEventArgs : EventArgs
    {
        public string Msg { get; }

        public MyEventArgs(){}
        public MyEventArgs(string msg)
        {
            this.Msg = msg;
        }
        
    }
    class Observer
    {
        public void Method(Object sender, EventArgs e)
        {
            Console.WriteLine($"观察者接收到事件发出的消息:{((MyEventArgs)e).Msg}");
            Console.WriteLine($"事件来源:{sender.GetType()}");
        }
    }
    class Ringleader{

        public static void Main(string[] args)
        {
            var eventTest = new EventHandlerTest();
            var observer = new Observer();

            eventTest.StatechangeEvent += observer.Method;
            eventTest.OnStateChange();
        }
    }
    // 打印:
    // 观察者接收到事件发出的消息:事件触发啦~
    // 事件来源:AsyncDelegate.EventHandlerTest
}

三 观察者模式

在讲事件的时候,反复提到观察者模式。但奇怪的是,很多人包括官方文档都使用 " 订阅、发布 " 这种发布订阅模式常用的词汇。那委托和事件到底是观察者模式还是发布订阅模式呢?

我个人理解,在24种基本设计模式是没有发布订阅模式的,发布订阅模式是观察者模式的一种变体,通常会多一层Topic/event管理中心,订阅和发布者解耦程度会比观察者模式种主题和观察者更大,所以我还是采用 " C#委托和事件是观察者模式 " 这一说法。

参考:

我在学习委托和事件的时候一直有个疑问,即C#为什么要费这个劲搞出委托这一套东西?它比用接口形式实现观察者模式有什么优势?

3.1 委托 vs IObserable

以下是我个人的一些思考,不一定对,请选择性参考。

C#的委托和事件本质是一种消息通知模式,即一种以观察者模式进行消息通知的形式。

比如程序中会有各种突发事件,像鼠标点击、鼠标移动、键盘敲击等,每个事件都会引发某些行为,这个可以由用户自定义,比如在游戏中,点击鼠标左键将引发开枪的行为,那么对于"点击鼠标左键"这个事件的触发(trigger)如何引发(raise)"开枪"这个行为呢,有三种,我们将这些事件和行为统一称作event和action,那么就有:

  1. 直接触发,event触发--直接引发->action
  2. 轮询,action端轮询(event是否触发),当event触发,则执行action
  3. 观察者模式,将event的定义和触发端叫做主题(Subject),将action的执行端称作观察者(Observer),Subject定义了一个针对某个event的容器,让对这个event感兴趣的的观察者将自身注册进这个容器,当Subject触发event时,就会依次调用这个容器中所有观察者对应的方法(当然也有异步调用)。

C#中处理方式和3类似,只是进一步将这个容器抽象出来,委托给Delegate处理(也许这就是为啥它叫做 " 委托 "),让Delegate处理事件定义和观察者注册,并且观察者的注册不再是自身实例,只需要是处理方法本身即可,可以是静态方法、实例方法、匿名方法或者是λ表达式,提高了使用灵活性。

尽管C#拥有委托这个机制,它依然加入了观察者模式的接口实现模式,即IObservable / IObserver,有人认为是委托的调用列表这个集合有不足之处,不如Hashset这种优化了的容器:

同时上面那个作者认为委托机制会产生一些垃圾

虽然尽管这些垃圾生命周期很短

从性能方面来说,另一位网友认为接口形式的效率会比委托快,但如果性能和垃圾不成问题时,他更倾向使用简洁灵活且优雅的委托。

来源:Delegates vs Observer Pattern

但直到2010.4月才首次在.NET Framework 4.0 引入 IObservable/IObserver,可能是为以后跨平台跨语言作准备的,不是很懂。

来源:Implement Observer Pattern in .NET (3 Techniques)

来源:what are the differences between .net observer pattern variations (IObservable and event delegation)?

这里就不再纠结IObservable提出的历史了。对委托以及.net与Java的纠葛感兴趣的可以参考下面链接:

3.2 观察者模式模型

此节内容主要翻译自微软的技术文档,限于篇幅,我这里就不摘录了,感兴趣的可以访问我翻译的这篇文章
Exploring the Observer Design Pattern微软技术文章翻译

这篇文章详述了观察者模式的模型,并用四种方式(Hashset、IObesever、Delegate、Event patern)进行实现。


观察者模式模型

四 总结

本文针对C#的委托和事件,详述了委托的使用、异步委托、多播委托、事件的使用、事件访问器等基本知识,并利用ildasm工具查看编译后代码,探索委托和事件的本质和区别。同时研究了委托和事件背后的观察者模式,辨析了接口形式的实现方式和委托的区别。

相关推荐
冷眼看人间恩怨4 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
向宇it6 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
向宇it7 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
东风吹柳8 小时前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
Hejjon9 小时前
SpringBoot 整合 SQLite 数据库
笔记
坐井观老天12 小时前
在C#中使用资源保存图像和文本和其他数据并在运行时加载
开发语言·c#
西洼工作室12 小时前
【java 正则表达式 笔记】
java·笔记·正则表达式
初学者7.12 小时前
Webpack学习笔记(2)
笔记·学习·webpack
T1an-113 小时前
设计模式之【观察者模式】
观察者模式·设计模式
新手上路狂踩坑13 小时前
Android Studio的笔记--BusyBox相关
android·linux·笔记·android studio·busybox