Delegate 委托细说
目录
委托实例方法的使用C#Invoke\BeginInvoke\Endinoke
系统自带的委托Action、Action<T>、Func<T>、Predicate<T>
委托提供了一种机制,可实现涉及组件间最小耦合度的软件设计。委托(delegate) 它是一种引用类型,是对方法的引用。如果说int,string等是对数据类型的定义,那么委托就类似于对“方法类型”的定义 作 。委托声明方式类似于类、接口。但是意义上 和数据类型(in float等)更相近
委托的申明
委托是用来收集方法的,相当同一类型方法的容器,因此我们可以用定义方法的方式来理解委托 委托的申明=delegate+方法签名
//没有返回值 有一个参数类型的委托 delegate void MyDelegate(int x, int y);//普通委托 public delegate void MyGenericDelegate<T>(T x, T y);//泛型委托
委托也是类,我们通过ILSPy工具就可以查看 。委托在IL 中式定义为一个MyDelegate的类 并且继承MulticastDelegate。该类中有3个方法 Invoke、BeginInvoke、EndInvoke。这个三个方法编辑器自动生成的,作为委托声明的一部分,这三个方法不属于类库方法。
委托的赋值
委托是方法的容器,所以给委托赋值只能是方法和匿名函数,委托也是类 是引用类型,委托里面装的是指向函数的指针。
delegate void MyDelegate(int x, int y);//普通委托 public delegate void MyGenericDelegate<T>(T x, T y);//泛型委托 //有返回值 有一个参数类型的委托 delegate int MyDelegatereturnint(int x, int y);//普通委托,给方法定制一个容器用来收集方法 public static int MyAdd(int x,int y) { return x + y; } static void Main() { MyDelegatereturnint Add = MyAdd;//委托是方法的容器,所以给委托只能给委托赋值方法
// MyDelegatereturnint Add = new MyDelegatereturnint(MyAdd);另外一种赋值方式
Add += (xx, yy) => xx + yy+8;//或者直接给给委托赋值,匿名委托和lambda表达式 Console.WriteLine(Add(1,2)); }
委托实例成员方法的使用
用户定义的委托类型上的 BeginInvoke 和 EndInvoke 方法实际上并未在 .NET Framework 库中定义。相反,这些方法由编译器发出(请参阅使用委托进行异步编程中的“重要”说明)作为声明委托类型的构建代码的一部分。
因此在.netcore 已经不支持BeginInvoke 和 EndInvoke 的情况下,vs2022不会发出提醒,直到调试是出错。
1、 Invoke() (委托同步调用)
a、委托的Invoke方法,在当前线程中执行委托。
b、委托执行时阻塞当前线程,知道委托执行完毕,当前线程才继续向下执行。
c、委托的Invoke方法,类似方法的常规调用。
使用案例:
b、Delegate.Invoke
2、 BeginInvoke()(委托异步调用)
a、委托的Delegate.BeginInvoke方法也是.net提供的异步调用机制之一,在ThreadPool分配的子线程中执行委托, 以获得异步执行效果的。
b、委托执行时不会阻塞主线程(调用委托的BeginInvoke线程),主线程继续向下执行。
c、委托结束时,如果有返回值,子线程讲返回值传递给主线程;如果有回调函数,子线程将继续执行回调函数。
- BeginInvoke()方法用来启动异步调用,它与委托类型具有相同的参数;该例子中为int32 a和int32 b
-
BeginInvoke()方法还有两个可选参数:
- AsyncCallback委托,通过这个参数可以指定异步调用完成时要调用的方法(回调函数)
- 用户定义的对象,该对象可向回调方法传递信息
- BeginInvoke()方法调用后立即返回,不等待委托执行完成;也就是说调用线程不会阻塞,可以继续执行
- BeginInvoke()将返回实现IAsyncResult接口的对象,这个对象可用于监视异步调用的进度;在准备获取方法调用的结果时,可以把它传给EndInvoke()方法
- .method public hidebysig newslot virtual
instance int32 EndInvoke (
class [mscorlib]System.IAsyncResult result
) runtime managed
{
} // end of method NumberAdd::EndInvoke
3、EndInvoke()方法异步结果处理
- EndInvoke方法返回类型就是委托类型的返回类型,该例子中为int32
- EndInvoke只有一个实现IAsyncResult接口的参数(BeginInvoke()方法的返回结果),结合这个参数EndInvoke()方法可以获取异步调用的结果
- 在调用BeginInvoke()方法后,可以随时调用EndInvoke()方法,如果异步调用尚未完成,则EndInvoke()方法将一直阻止调用线程,直到异步调用完成后才允许调用线程执行
使用案例:
案例一:异步委托使用了一个新的线程来执行numberAdd实例,这样主线程就不会在BeginInvoke后阻塞,可以继续执行;但是当主线程执行到EndInvoke时,由于异步调用还没有完成,主线程将在EndInvoke处阻塞。
介绍过BeginInvoke()和EndInvoke()方法后,看一个异步调用方法的例子。
Console.WriteLine("main thread (id: {0}) invoke numberAdd function", Thread.CurrentThread.ManagedThreadId); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, null, null); Console.WriteLine("main thread (id: {0}) can do something after BeginInvoke", Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 5; i++) Console.WriteLine("......"); Console.WriteLine("main thread (id: {0}) wait at EndInvoke", Thread.CurrentThread.ManagedThreadId);//主线程将在EndInvoke调用处阻塞,知道异步调用执行完成为止 int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result);
其实这个例子中还是有很大的问题,主线程还是会被阻塞。下面进行一点点改进,通过轮询的方式查看异步调用状态。
案例二:轮询异步调用状态
在IAsyncResult接口中,通过实现这个接口的实例的IsCompleted属性,可以检测异步操作是否已完成的指示,如果操作完成则为True,否则为False
简单看看IAsyncResult 的成员:
- AsyncState:获取用户定义的对象,可以作为回调函数的参数,从调用线程想回调函数传递信息
- AsyncWaitHandle:获取用于等待异步操作完成的 WaitHandle
- CompletedSynchronously:获取一个值,该值指示异步操作是否同步完成
- IsCompleted:获取一个布尔值,该值指示异步操作是否已完成
所以,代码中就可以利用IsCompleted来获取异步操作的状态:
Console.WriteLine("main invoke numberAdd function"); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, null, null); while (iAsyncResult.IsCompleted != true) { Console.WriteLine("main thread can do something after BeginInvoke"); Console.WriteLine("......"); Thread.Sleep(500); } int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result);
案例三:IAsyncResult.AsyncWaitHandle的使用
Console.WriteLine("main invoke numberAdd function"); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, null, null); while (!iAsyncResult.AsyncWaitHandle.WaitOne(500)) { Console.WriteLine("main thread can do something after BeginInvoke"); Console.WriteLine("......"); } int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result);
案例四:回调函数(callback) AsyncCallback
这种不会阻塞主线程,而是用回调函数去执行结果,这种就是回调函数。参考代码如下:
使用IAsyncResult
和BeginInvoke属于异步编程模型(APM)
不再是进行异步调用的首选方法。从 .NET Framework 4.5 开始,基于任务的异步模式 (TAP) 是推荐的异步模型。并且因为异步委托的实现依赖于 .NET Core 中不存在的远程处理功能,.NET Core 中不支持 BeginInvoke 和 EndInvoke 委托调用。。这在GitHub问题dotnet/corefx #5940中进行了讨论。
但是vs2022 编辑的时候无法发现 这个错误,直到调试的时候报错。根本问题是因为:用户定义的委托类型上的方法实际上不是在 .NET Framework 库中定义的。相反,这些方法由编译器生成的(请参阅使用委托进行异步编程中的"重要"注释),作为声明委托类型的生成代码的一部分。
当然,现有的 .NET Framework 代码可以继续使用异步模式,但在 .NET Core 上运行该代码将导致在运行时出现类似于以下内容的异常:
IAsyncResult
Unhandled Exception: System.PlatformNotSupportedException: Operation is not supported on this platform.
at BeginInvokeExploration.Program.dele_method.BeginInvoke(Int32 arg, AsyncCallback callback, Object object
内容来源:https://devblogs.microsoft.com/dotnet/migrating-delegate-begininvoke-calls-for-net-core/
class Program { public delegate int dele_async(int a); static void Main(string[] args) { dele_async dele_method = Executefunc; //委托方法实例化以后有Invoke和BeginInvoke方法,Invoke是同步调用,BeginInvoke是异步等待。 // int result1 = dele_method.Invoke(10); ////.net core 已经不支持异步编程模型APM,因此BeginInvoke、EnkInvoke 运行时候会报错 // IAsyncResult result = dele_method.BeginInvoke(10, Asynccallback,"sfsdfsdf"); ///要重构成 基于任务的异步模式TAP var workTask = Task.Run(() => dele_method.Invoke(11)); var followUpTask = workTask.ContinueWith(Asynccallback); Console.WriteLine("主函数执行完毕!"); Console.ReadLine(); } private static int Executefunc(int m) { Console.WriteLine("正在执行invoke函数..."); Thread.Sleep(2000); Console.WriteLine("执行invoke函数完毕"); return m * m; } private static void Asynccallback(IAsyncResult iar) { if (iar is null) { throw new ArgumentNullException(nameof(iar)); } Console.WriteLine("正在执行callback函数..."); dele_async dele_method = (dele_async)iar.AsyncState; int result = dele_method.EndInvoke(iar); Console.WriteLine(result); Console.WriteLine("执行callback函数完毕"); } }
系统自带的委托
系统自带的3个委托,可以满足大部分的工作需要,说我们可以在工作不用自己在声明泛型委托了,直接使用系统的。
C#委托Action、Action<T>、Func<T>、Predicate<T>
Delegate成员函数的使用
更多多函数使用方法请移步:https://docs.microsoft.com/zh-cn/dotnet/api/system.delegate.combine?view=net-5.0
Delegate.Combine:可以用+替代 从而简化了操作
Delegate.Remove:可以用- 替代 从而简化了操作
Delegate.GetInvocationList 方法 返回委托的调用列表。
数组中的每个委托只表示一个方法。数组中的委托顺序与当前委托调用这些委托所表示的方法的顺序相同。
public virtual Delegate[] GetInvocationList ();
下面的示例将三个方法分配给一个委托。 然后,它会调用 GetInvocationList 方法来获取分配给该委托的方法的总计数,并按相反顺序执行这些委托,并执行其名称不包括子字符串 "File" 的方法。
Action<String> outputMessage = null; outputMessage += Console.WriteLine; outputMessage += OutputToFile; outputMessage += ShowMessageBox; outputMessage = [Delegate].Combine( { output1, output2, output3 } ) Console.WriteLine("Invocation list has {0} methods.", outputMessage.GetInvocationList().Length); for (int ctr = outputMessage.GetInvocationList().Length - 1; ctr >= 0; ctr--) { var outputMsg = outputMessage.GetInvocationList()[ctr]; outputMsg.DynamicInvoke("Greetings and salutations!"); } Console.WriteLine("Press <Enter> to continue..."); Console.ReadLine(); // Invoke each delegate that doesn't write to a file. for (int ctr = 0; ctr < outputMessage.GetInvocationList().Length; ctr++) { var outputMsg = outputMessage.GetInvocationList()[ctr]; if (! outputMsg.GetMethodInfo().Name.Contains("File")) outputMsg.DynamicInvoke( new String[] { "Hi!" } ); }
案例来源:https://docs.microsoft.com/zh-cn/dotnet/api/system.delegate.getinvocationlist?view=net-5.0
多播委托MulticastDelegate 类
表示多路广播委托;即,其调用列表中可以拥有多个元素的委托。
public abstract class MulticastDelegate : Delegate
组合的委托必须是同一个类型,其相当于创建了一个按照组合的顺序依次调用的新委托对象。委托的组合一般是给事件用的,用普通委托的时候很少用。
通过+来实现将方法添加到委托实例中,-来从委托实例中进行方法的移除。
+和-纯粹是为了简化代码而生的,实际上其调用的分别是Delegate.Combine方法和Delegate.Remove。
如果委托中存在多个带返回值的方法,那么调用委托的返回值是最后一个方法的返回值。
public static void MultipleShow()
{
//多播委托
NoParaWithReturnEventHandler _NoParaWithReturnEventHandler = new NoParaWithReturnEventHandler(GetDateTime);
_NoParaWithReturnEventHandler += GetDateTime;
Console.WriteLine(_NoParaWithReturnEventHandler());
}
public static string GetDateTime()
{
return string.Format("今天是{0}号。", DateTime.Now.Day.ToString());
}
委托总结:
- 委托封装了包含特殊返回类型和一组参数的行为,类似包含单一方法的接口;
- 委托类型声明中所描述的类型签名决定了哪个方法可用于创建委托实例,同时决定了调用的签名;
- 为了创建委托实例,需要一个方法以及(对于实例方法来说)调用方法的目标;
- 委托实例是不易变的,就像String一样;
- 每个委托实例都包含一个调用列表——一个操作列表;
- 事件不是委托实例——只是成对的add/remove方法(类似于属性的取值方法/赋值方法)。
常见使用场景:窗体传值、线程启动时绑定方法、lambda表达式、异步等等。
生活中的例子:现在不是大家都在抢火车票吗,使用云抢票就相当于使用委托,你可以直接自己买票,也可以托管于云抢票,自己抢票的话,在快要开枪的时候,你必须时刻刷新,下单输验证码等等,使用云抢票的话,你只要放票前,提前输入抢票信息,就再也不需要你管了,自动出票,你根本不需要知道云抢票那边是怎么帮你实现抢票的。相同时间和车次可以做成一个委托实例,有很多人都通过这个委托实例来进行抢票操作。
为什么要使用委托
我们完全可以直接调用方法,为什么还需要通过一个委托来调用呢?委托有什么意义?
解耦,对修改关闭,对扩展开放。逻辑分离。
你可以把委托理解为函数的父类,或者是一个方法的占位符。
我们来看下代码,假设有2个方法,一个说英语,一个说汉语,而这2个方法的函数签名是一样的。
public static void SayChinese(string name) { Console.WriteLine("你好," + name); } public static void SayEnglish(string name) { Console.WriteLine("hello," + name); }
那么我们在外部调用的时候,
MyDelegate.SayChinese("张三"); MyDelegate.SayEnglish("zhangsan");
如果要调用这两个不同的方法,是不是要写不同的调用代码
我们能不能只一个方法调用呢?修改代码如下:
public static void Say(string name,WithParaNoReturnEventHandler handler) { handler(name); } public static void SayChinese(string name) { Console.WriteLine("你好," + name); } public static void SayEnglish(string name) { Console.WriteLine("hello," + name); }
这样,只通过一个方法Say来进行调用。
如何调用呢?如下三种调用方式:
WithParaNoReturnEventHandler _WithParaNoReturnEventHandler = new WithParaNoReturnEventHandler(MyDelegate.SayChinese); MyDelegate.Say("张三",_WithParaNoReturnEventHandler); MyDelegate.Say("张三", delegate(string name) { Console.WriteLine("你好," + name); }); //匿名方法 MyDelegate.Say("张三", (name) => { Console.WriteLine("你好," + name); }); //lambda表达式
以上代码使用了几种调用方式,这些调用方式都是随着C#的升级而不断优化的。第一种是C#1.0中就存在的传统调用方式,第二种是C#2.0中的匿名方法调用方式,所谓匿名方法,就是没有名字的方法,当方法只调用一次时使用匿名方法最合适不过了。C#3中的lambda表达式。其实泛型委托同样是被支持的,而.NET 3.5则更进一步,引入了一组名为Func的泛型委托类型,它能获取多个指定类型的参数,并返回另一个指定类型的值。
lambda表达式
lambda表达式的本质就是一个方法,一个匿名方法。
如果方法体只有一行,无返回值,还可以去掉大括号和分号。
MyDelegate.Say("张三", (name) => Console.WriteLine("你好," + name));
如果方法体只有一行,有返回值,可以去掉大括号和return。
WithParaWithReturnEventHandler _WithParaWithReturnEventHandler = (name)=>name+",你好";
从.NET3.5开始,基本上不需要我们自己来申明委托了,因为系统有许多内置的委托。
Action和Func委托,分别有16个和17个重载。int表示输入参数,out代表返回值,out参数放置在最后。
Action表示无返回值的委托,Func表示有返回值的委托。因为方法从大的角度来分类,也分为有返回值的方法和无返回值的方法。
也就是说具体调用什么样的方法,完全由调用方决定了,就有了更大的灵活性和扩展性。为什么这么说,如果我有些时候要先说英语再说汉语,有些事时候要先说汉语再说英语,如果没有委托,我们会怎么样实现?请看如下代码:
public static void SayEnglishAndChinese(string name) { SayEnglish(name); SayChinese(name); } public static void SayChineseAndEnglish(string name) { SayChinese(name); SayEnglish(name); }
如果又突然要添加一种俄语呢?被调用方的代码又要修改,如此循环下去,是不是要抓狂了?随着不断添加新语种,代码会变得越来越复杂,越来越难以维护。这样的代码耦合性非常高,是不合理的,也就是出现了所谓的代码的坏味道,你可以通过设计模式(如观察者模式等),在不使用委托的情况下来重构代码,但是实现起来是非常麻烦的,要写很多更多的代码...
委托可以传递方法,而这些方法可以代表一系列的操作,这些操作都由调用方来决定,就很好扩展了,而且十分灵活。我们不会对已有的方法进行修改,而是只以添加方法的形式去进行扩展。
可能有人又会说,我直接在调用方那里来一个一个调用我要执行哪些方法一样可以实现这样的效果啊?
可你有没有想过,你要调用的是一系列方法,你根本无法复用这一系列的方法。使用委托就不一样了,它好比一个方法集合的容器,你可以往里面增减方法,可以复用的。而且使用委托,你可以延时方法列表的调用,还可以随时对方法列表进行增减。委托对方法进行了再一次的封装。
总结:也就是当你只能确定方法的函数签名,无法确定方法的具体执行时,为了能够更好的扩展,以类似于注入方法的形式来实现新增的功能,就能体现出委托的价值。
委托和直接调用函数的区别:用委托就可以指向任意的函数,哪怕是之前没定义的都可以,而不用受限于哪几种。
执行委托
public object? DynamicInvoke(params object?[]? args);
Delegate类里面有一个 DynamicInvoke
的方法,可以在不清楚委托实际类型的情况下执行委托方法,但是用 DynamicInvoke
去执行的话会比直接用 Invoke
的方法会慢上很多,差了两个数量级,所以在知道委托类型的情况下尽可能使用 Invoke
执行,但有时候我们并不知道委托的实际类型,比如在很多类库项目中可能并不是强类型的委托。
实例委托 执行Invoke()方法。减少不必要的装箱和拆箱操作
delegate int seee(int a ,int b); static void Main(string[] args) { seee se = null; se,invoke(2,3); }