转载请注明出处:https://blog.csdn.net/weixin_44013533/article/details/134655722
作者:CSDN@|Ringleader|
主要参考:
- 委托(C# 编程指南)
- 事件介绍
- C# 中的委托和事件简介
- Delegate 类
- Exploring the Observer Design Pattern微软技术文章翻译
委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。以下示例声明名为 Callback 的委托,表示该委托可以封装以字符串作为参数并返回 void 的方法:
public delegate void Callback(string message);
一个完整示例:
// 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);
也可以使用下面方法:
// 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 条件运算符 ?. 和 ?[]
上面的括号或者Invoke调用委托形式都是同步调用,除此之外,还有BeginInvoke
的异步调用形式。
BeginInvoke
方法启动异步调用。 该方法具有与你要异步执行的方法相同的参数,另加两个可选参数。 第一个参数是一个 AsyncCallback 委托,此委托引用在异步调用完成时要调用的方法。 第二个参数是一个用户定义的对象,该对象将信息传递到回调方法。 BeginInvoke 将立即返回,而不会等待异步调用完成。 BeginInvoke 返回可用于监视异步调用的进度的 IAsyncResult。
EndInvoke
方法用于检索异步调用的结果。 它可以在调用 BeginInvoke之后的任意时间调用。 如果异步调用尚未完成,那么 EndInvoke 将阻止调用线程,直到完成异步调用。 EndInvoke 的参数包括要异步执行的方法的 out 和 ref 参数以及 BeginInvoke 返回的 IAsyncResult。
下面的代码示例演示了异步调用同一个长时间运行的方法 TestMethod的各种方式。 TestMethod 方法会显示一条控制台消息,说明该方法已开始处理,休眠了几秒钟,然后结束。 TestMethod 有一个 out 参数,该参数用于演示将此类参数添加到 BeginInvoke 和 EndInvoke的签名中的方式。
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);
}
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.".
*/
异步委托其他内容参见:使用异步方式调用同步方法
调用时,委托可以调用多个方法。 这被称为多播。 若要向委托的方法列表(调用列表)添加其他方法,只需使用加法运算符或加法赋值运算符(“+”或“+=”)添加两个委托。 例如:
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;
若要删除调用列表中的方法,请使用减法运算符或减法赋值运算符(- 或 -=)。 例如:
//remove Method1
allMethodsDelegate -= d1;
// copy AllMethodsDelegate while removing d2
Callback oneMethodDelegate = allMethodsDelegate - d2;
委托的调用列表是一个有序的委托集合,其列表中的每个元素都会调用委托所代表的一个方法。调用列表可以包含重复的方法。在调用过程中,方法按照它们在调用列表中出现的顺序进行调用。委托尝试调用其调用列表中的每个方法;重复方法每次出现在调用列表中都会被调用一次。委托是不可变的;一旦创建,委托的调用列表就不会更改。
如果调用的方法引发异常,该方法将停止执行,该异常将传递回委托的调用方,并且不会调用调用列表中的其余方法。 捕获调用方中的异常不会改变此行为。
当委托调用的方法的签名包含返回值时,委托将返回调用列表中最后一个元素的返回值。 当签名包含通过引用传递的参数时,参数的最终值是调用列表中按顺序执行并更新参数值的每个方法的结果。
类 Delegate 是委托类型的基类。 但是,只有系统和编译器才能从 Delegate 类或 MulticastDelegate 类显式派生。 此外,不允许从委托类型派生新类型。 类 Delegate 不被视为委托类型;它是用于派生委托类型的类。
Delegate有几个重要的属性和方法:
public object Target
属性:获取当前委托调用实例方法时的类实例。如果委托表示实例方法,则为当前委托调用实例方法的对象;如果委托表示静态方法,则为null。
public MethodInfo Method
属性:获取由委托表示的方法。
Combine(Delegate, Delegate)
方法:连接两个委托的调用列表。
public static Delegate? Combine (Delegate? a, Delegate? b);
返回一个新的委托,其调用列表按照a和b的顺序连接在一起。如果b为null,则返回a;如果a为null引用,则返回b;如果a和b都为null引用,则返回null引用。
Delegate.Remove(Delegate, Delegate)
方法:从前一个委托的调用列表移除后一个委托的调用列表。
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):
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()
方法获取这个列表。
如果查看Delegate和MulticastDelegate类源码的话,你可能会奇怪,前面提到的Invoke、BeginInvoke、EndInvoke为什么会没有,其实这是运行时交由底层声明并管理的:
一个委托声明会被转换为一个继承自 MulticastDelegate(而不是 CLI 规范中指定的 Delegate)的类。该类始终具有确切的 4 个成员,它们的运行时实现由 CLR 提供:
一个构造函数.ctor(object,native int)
,接受一个对象object和一个 IntPtr。对象是 Delegate.Target,IntPtr 是目标方法的地址,也即Delegate.Method。这些成员在调用委托时稍后使用,Target 属性在委托绑定的方法是实例方法时提供 this 引用,对于静态方法则为 null。Method 属性决定要调用的方法。
一个 Invoke()
方法。该方法的参数是动态生成的,并与委托声明匹配。调用 Invoke() 方法在同一线程上运行委托目标方法,即同步调用。通常只需使用括号语法糖即可,通过对象名称后跟括号来调用委托对象。
一个 BeginInvoke()
方法,提供了一种进行异步调用的方式。该方法在目标方法正在忙于执行时迅速完成,类似于 ThreadPool.QueueUserWorkItem,但具有类型安全的参数。返回类型始终为 System.IAsyncResult,用于查找异步调用何时完成,并提供给 EndInvoke() 方法。第一个参数是一个可选的 System.AsyncCallback 委托对象,当异步调用完成时,其目标将自动被调用。第二个参数是一个可选的对象,它将原样传递给回调,对于跟踪状态很有用。附加参数是动态生成的,并与委托声明匹配。
一个 EndInvoke()
方法。它接受一个 IAsyncResult 类型的单一参数,您必须传递从 BeginInvoke() 得到的参数。它完成异步调用并释放资源。
参见:
- Where are CLR-defined methods like [delegate].BeginInvoke documented?
- ECMA-335, 6th edition, June 2012
可以通过反汇编代码来查看编译后的委托被运行时底层添加了什么。
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三个方法。
namespace AsyncDelegate
{
public class DecompiledTest2
{
public delegate void StateChangeHandler();
}
}
上面提到的“委托的+/-/-=/+=运算本质就是调用Combine或Remove方法”也可以用反汇编验证这个说法。
参考:
- C#数据结构-委托(Delegate)和事件(Event)
- c#中委托和事件? - 小约翰的回答 - 知乎
- 初识Ildasm.exe–IL反编译的实用工具
- ildasm.exe(IL反汇编程序)
.NET Core 框架包含几个在需要委托类型时可重用的类型。 这些是泛型定义,因此需要新的方法声明时可以声明自定义。
第一个类型是 Action 类型和一些变体:
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.
几种可用于返回值的委托类型的泛型委托类型:
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
一种专门的委托类型 Predicate,此类型返回单个值的测试结果:
public delegate bool Predicate<in T>(T obj);
更多内容参见:强类型委托
事件是对象用于(向系统中的所有相关组件)广播已发生事情的一种方式。 任何其他组件都可以订阅事件,并在事件引发时得到通知。比如这些事件会报告鼠标移动、按钮点击和类似的交互。
事件的语言设计针对这些目标:
你会发现事件的目标与委托的目标非常相似。 因此,事件语言支持基于委托语言支持构建。
用于定义事件以及订阅或取消订阅事件的语法是对委托语法的扩展。
如下定义了StateChangeHandler委托类型的事件,事件用event
关键字修饰:
public delegate void StateChangeHandler();
public event StateChangeHandler StatechangeEvent;
事件的调用、注册和注销方法和委托相同:
StatechangeEvent += ()=>{ Console.WriteLine("事件触发~");};
StatechangeEvent?.Invoke();
下面是完整实例:
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.
这等同于下面的写法:
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并没有暴露,所以无法访问。
事件访问器可以自定义,除了上面的例子,还可以如下所示:
public event StateChangeHandler StatechangeEvent
{
add
{
_stateChangeHandler += value;
Console.WriteLine("已添加{0}方法",value.GetMethodInfo().Name);
}
remove
{
_stateChangeHandler -= value;
Console.WriteLine("已移除{0}方法",value.GetMethodInfo().Name);
}
}
这样可以打印所添加的方法。
还可以在多线程环境下对事件所在的对象进行加锁,比如:
public event StateChangeHandler StatechangeEvent
{
add
{
lock (this)
{
_stateChangeHandler += value;
}
}
remove
{
lock (this)
{
_stateChangeHandler -= value;
}
}
}
经测试,只要有自定义事件访问器,系统就不会自动为你添加对应私有委托变量。
当你删除上面代码的event字段,你将会发现,代码依然可以运行。
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();
}
}
}
唯一区别就是现在事件可以在外部Invoke了。
通过之前的介绍和分析,不难明白,event字段本质就是对委托进行私有访问限制,事件的本质就是委托,只不过系统会对用event字段修饰的委托进行了特殊处理,比如自动生成一个私有的委托变量,添加两个事件访问器,同时禁止外部类对事件的Invoke等方法调用。
前面的案例还是把事件定义、事件触发、事件订阅都放在一个类中。现在对其进行改写,使其符合事件使用时的解耦场景:
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();
}
}
}
如果要遵循标准 .NET 模式的事件,可以利用.NET 类库中的EventHandler 委托(当然你也可以自定义遵循这种风格的委托),其定义如下:
public delegate void EventHandler(object sender, EventArgs e);
其中
将上面的例子改写成标准 .NET 模式的事件:
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#为什么要费这个劲搞出委托这一套东西?它比用接口形式实现观察者模式有什么优势?
以下是我个人的一些思考,不一定对,请选择性参考。
C#的委托和事件本质是一种消息通知模式,即一种以观察者模式进行消息通知的形式。
比如程序中会有各种突发事件,像鼠标点击、鼠标移动、键盘敲击等,每个事件都会引发某些行为,这个可以由用户自定义,比如在游戏中,点击鼠标左键将引发开枪的行为,那么对于“点击鼠标左键”这个事件的触发(trigger)如何引发(raise)“开枪”这个行为呢,有三种,我们将这些事件和行为统一称作event和action,那么就有:
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的纠葛感兴趣的可以参考下面链接:
此节内容主要翻译自微软的技术文档,限于篇幅,我这里就不摘录了,感兴趣的可以访问我翻译的这篇文章
Exploring the Observer Design Pattern微软技术文章翻译
这篇文章详述了观察者模式的模型,并用四种方式(Hashset、IObesever、Delegate、Event patern)进行实现。
本文针对C#的委托和事件,详述了委托的使用、异步委托、多播委托、事件的使用、事件访问器等基本知识,并利用ildasm工具查看编译后代码,探索委托和事件的本质和区别。同时研究了委托和事件背后的观察者模式,辨析了接口形式的实现方式和委托的区别。