更新记录
转载请注明出处:https://www.cnblogs.com/cqpanda/p/16690892.html
2022年9月16日 发布。
2022年9月10日 从笔记迁移到博客。
Delegate实例是一个对象,是一种可调用的对象
Delegate类型定义委托实例可以调用的方法类型
通过调用委托类型的实例,委托类型实例再调用对应的目标方法
委托调用相当于间接(indirection)调用
委托类型和类类型一样是一种用户自定义的类型
可以认为委托是持有一个或多个方法的对象
但委托对象是可以执行的,即执行委托对象持有的方法
委托类似C/C++中的函数指针(Function Pointers)的概念,但更加安全更好调试
委托类似JavaScript中回调函数(Callback)的概念
委托类似是一种可以指向方法的引用类型,可以理解为一种函数指针,是类型安全的
委托可以是泛型的
Delegates are especially used for implementing events and the call-back methods.
Delegate会导致紧耦合
Delegate使用不当会导致可读性下降、Debug难度增加
Delegate、异步调用、多线程混合后,导致代码难以维护
Delegate使用不当会导致内存泄漏和性能下降
实例:委托与插件方法(Delegates with Plug-in Methods)
可以把方法做成像变量一样调用
public delegate int Transformer (int x); class Util { public static void Transform (int[] values, Transformer t) { for (int i = 0; i < values.Length; i++) values[i] = t (values[i]); } } class Test { static void Main() { int[] values = { 1, 2, 3 }; Util.Transform (values, Square); // Hook in the Square method foreach (int i in values) Console.Write (i + " "); // 1 4 9 } static int Square (int x) => x * x; }
可以把委托看作一个包含有序方法列表的对象
方法的列表成为调用列表
委托保存的方法来自各种类或结构,方法需要与委托类型的签名和返回值匹配
调用列表中的方法可以是实例方法也可以是静态方法
在调用委托对象时,委托对象会执行调用列表中所有方法
声明一个委托类型
声明一个委托类型的变量
实例化该委托类型的变量,然后给它赋值
继续赋值
调用执行委托
声明委托类型
提示:声明委托类型类似声明一个方法,即:delegate + 方法签名
注意:不可以在方法内声明委托,只可以在全局声明或声明为类成员
委托对象是引用类型,所以有引用和对象
实例化委托变量,方法一:使用new
实例化委托变量,方法二:使用直接快捷赋值
示意图:
委托实例引用的实例方法和静态方法(Instance Versus Static Method Targets)
委托实例使用Target属性 指向 其引用的实例方法的实例
注意:如果是静态方法,则Target为null
委托实例使用Method属性 指向 其引用的方法
实例:
public delegate void ProgressReporter (int percentComplete); class X { public void InstanceProgress (int percentComplete) => Console.WriteLine (percentComplete); } class Test { static void Main() { X x = new X(); ProgressReporter p = x.InstanceProgress; p(99); // 99 Console.WriteLine (p.Target == x); // True Console.WriteLine (p.Method); // Void InstanceProgress(Int32) } }
委托类型是引用类型,重新给委托赋值会改变委托变量中的引用
新委托值赋值给委托变量后,旧的委托值会被垃圾回收器回收
示意图:
可以将多个委托组成一个委托成为组合委托
组合委托可以执行多个方法
所有委托实例都具有多播功能,所以可以引用多个方法
注意:
组合委托并不修改原有的委托
实际上委托是恒定的,委托对象创建后不可以被修改
组合委托只是将多个委托进行组合在一起
为委托添加方法使用+=运算符
注意:委托必须先初始化再使用+=或-=
示意图:
实例:更新进度
//进度报告委托 public delegate void ProgressReporter (int percentComplete); public class Util { //具体进行的任务 public static void HardWork (ProgressReporter p) { for (int i = 0; i < 10; i++) { p (i * 10); // 调用委托,报告进度 // 模拟需要花时间的任务 System.Threading.Thread.Sleep (100); } } } class Test { static void Main() { //新建委托变量 ProgressReporter p = WriteProgressToConsole; p += WriteProgressToFile; //进行具体的任务 Util.HardWork (p); } //报告进度-输出到Console static void WriteProgressToConsole (int percentComplete) => Console.WriteLine (percentComplete); //报告进度-输出到文件 static void WriteProgressToFile (int percentComplete) => System.IO.File.WriteAllText ("progress.txt",percentComplete.ToString()); }
从委托移除方法使用-=运算符
示意图:
注意:
试图移除空委托变量的方法会抛出异常,使用前记得和null比较
试图删除委托中不存在的方法是没有效果的
删除方法从委托的调用列表最后开始搜索,移除第一个匹配到的方法
说明:
调用委托变量类似于调用方法
用于委托的参数将传递给调用列表中的每一个方法
注意:
调用空委托会跳出异常,使用前应检查null
参数是值类型,将返回最后一个函数的返回值.其他返回值将被丢弃
调用组合委托变量时,方法是无序的
参数是引用类时,传递的是引用,会连续改变引用参数
委托除了使用DelegateName()方式调用,还可以使用DelegateName.Invoke()调用
using System; namespace ConsoleApp3 { //定义委托类型 delegate void Panda(); class Program { static void Main(string[] args) { //定义委托变量 Panda panda = delegate () { //使用局部变量 Console.WriteLine("Panda666"); }; //调用委托 panda.Invoke(); //wait Console.ReadKey(); } } }
如果委托有返回值并且调用列表有一个以上的方法,那么:
调用列表的最后一个方法的返回值将作为委托调用的返回值
调用列表中的其他方法的返回值将被忽略
注意:因为调用顺序是不确定的,所以不要依赖委托返回值
如果委托有引用参数,在调用委托列表中的下一个方法时,参数的新值(不是初始值)会传给下一个方法
示意图:
委托变量内有多个方法,其中一个方法抛出异常,委托方法列表将不会继续执行
为了处理这个方法,可以使用GetInvocationList()方法进行自己来遍历调用方法
实例:
using System; namespace Test { class TestClass { //定义一个委托类型 delegate void Panda(); public static void Main() { //新建并实例化委托变量 Panda panda = new Panda(TestClass.Method1); panda += TestClass.Method2; //自定义遍历委托方法列表 foreach (Panda item in panda.GetInvocationList()) { try { item(); } catch(Exception e) { Console.WriteLine(e.Message); } } Console.ReadKey(); } /// <summary> /// 测试用的方法一 /// </summary> public static void Method1() { throw new Exception("Panda Exception"); } /// <summary> /// 测试用的方法二 /// </summary> public static void Method2() { Console.WriteLine("Method2"); } } }
using System; using System.Threading; using System.Threading.Tasks; namespace PandaTestDemo { //定义委托类型 delegate void PandaDelegate(); class Program { /// <summary> /// 做某事 /// </summary> static void DoSomething() { Console.WriteLine("DoSomething"); } static void Main(string[] args) { //定义委托实例 PandaDelegate pandaDelInstance = DoSomething; //调用委托 pandaDelInstance.Invoke(); //wait Console.WriteLine("Success"); Console.ReadKey(); } } }
BeginInvoke会在底层使用多线程实现异步调用
注意:.NET 5已不支持该方法
using System; using System.Threading; namespace PandaTestDemo { //定义委托类型 delegate void PandaDelegate(); class Program { /// <summary> /// 做某事1 /// </summary> static void DoSomething1() { Console.WriteLine("DoSomething1-Begin"); Thread.Sleep(TimeSpan.FromSeconds(3)); Console.WriteLine("DoSomething1-End"); } /// <summary> /// 做某事2 /// </summary> static void DoSomething2() { Console.WriteLine("DoSomething2-Begin"); Thread.Sleep(TimeSpan.FromSeconds(3)); Console.WriteLine("DoSomething2-End"); } static void Main(string[] args) { //定义委托实例 PandaDelegate pandaDelInstance1 = new PandaDelegate(DoSomething1); PandaDelegate pandaDelInstance2 = new PandaDelegate(DoSomething2); //异步调用委托 pandaDelInstance1.BeginInvoke(null, null); pandaDelInstance2.BeginInvoke(null, null); //wait Console.WriteLine("Success"); Console.ReadKey(); } } }
委托使用了外部作用域的变量称为捕获变量
当代码离开了作用域后,变量仍然存在,委托仍然可以使用,类似闭包
using System; namespace ConsoleApp3 { //定义委托类型 delegate void Panda(); class Program { static void Main(string[] args) { //局部变量 int localValue = 666; //定义委托变量 Panda panda = delegate () { //使用局部变量 Console.WriteLine(localValue); }; //wait Console.ReadKey(); } } }
如果委托对象存储的是实例的方法,则可以使用Target属性获得实例的引用
使用委托变量的Method属性获得关联的方法
使用委托变量的GetInvocationList ()方法获得关联的多个方法
using System; namespace ConsoleApp1 { //测试使用的委托 delegate void PandaDelegate(); //测试类 class PandaClass { public void DoSomething() { Console.WriteLine("PandaClass DoSomething"); } } class Program { static void Main(string[] args) { //新建实例 PandaClass pandaClass = new PandaClass(); //委托变量赋值 PandaDelegate pandaDelegate = pandaClass.DoSomething; pandaDelegate += pandaClass.DoSomething; //输出目标 Console.WriteLine(pandaDelegate.Target); //输出方法 Console.WriteLine(pandaDelegate.Method); //输出所有方法 foreach (PandaDelegate item in pandaDelegate.GetInvocationList()) { Console.WriteLine(item.Method); } //wait Console.ReadKey(); } } }
委托实例本质是一个包含其他对象方法地址的对象
委托本质是一种特别的类
委托类型派生自System.MulticastDelegate(这个类型又派生自System.Delegate)
但自定义委托不允许直接显式派生自这两个类型
这2个类不能直接在代码中使用
委托对象包含方法:
Invoke() //调用方法列表
BeginInvoke() //异步调用方法列表
EndInvoke() //异步调用方法列表
GetInvocationList() //获得委托对象中的方法列表
Target
Method
当给一个委托变量赋值的时候,本质是实例化对象
所以本质上委托变量的赋值是发生在运行时
注意:A delegate variable is assigned a method at runtime
实例:
Transformer t = Square; //as same Transformer t = new Transformer (Square);
当调用一个委托变量的时候,本质是调用其的Invoke方法
实例:
t(3) //as same t.Invoke(3)
编译器最终会将+, -, +=,-=运算符编译为System.Delegate类型的Static的Combine方法和Remove方法
Handler 作为普通委托的后缀
EventHandler 作为事件委托的后缀
实例:
public delegate void BeeHandler(string str) public delegate void ClickEventhandler(object sender, EventArg args)
委托类型的实例是不可变的(immutable)
当对委托类型的实例调用+=或-=,本质是创建新的委托类型实例
不同的委托类型不相互兼容
但可以在创建委托时,进行赋值
如果相同类型的委托实例具有相同的方法目标,则认为它们相等
实例:委托赋值兼容性
delegate void D1(); delegate void D2(); D1 d1 = Method1; D2 d2 = d1; // 不可以,编译错误Compile-time error //这是允许的 D2 d2 = new D2 (d1);
实例:委托类型声明的签名相同,但不相互兼容
delegate void D1(); delegate void D2(); D1 d1 = Method1; D2 d2 = d1; // Compile-time error
实例:指向相同方法的委托是相等的
delegate void D(); D d1 = Method1; D d2 = Method1; Console.WriteLine (d1 == d2); // True
实例:
delegate object ObjectRetriever(); static void Main() { ObjectRetriever o = new ObjectRetriever (GetString); object result = o(); Console.WriteLine (result); // hello } static string GetString() => "hello";
如果相同类型的委托变量指向相同的方法并且顺序相同,则委托变量相同
实例:
using System; using System.Threading; namespace ConsoleApp1 { //测试使用的委托 delegate void PandaDelegate(); class Program { static void Main(string[] args) { PandaDelegate a1 = Test; PandaDelegate a2 = Test; Console.WriteLine(a1 == a2); //true //wait Console.ReadKey(); } static void Test() { // } } }
委托类型可以定义泛型参数
泛型委托最佳实践:
将只用于返回类型的类型参数标记为协变(out)
将只用于参数的任意类型参数标记为逆变(in)
实例:
public delegate T Transformer<T> (T arg);
实例:
using System; namespace ConsoleApp3 { //定义泛型委托类型 delegate void PandaDelegate<in T>(T arg1); class Program { public static void DoSomething(int arg1) { } static void Main(string[] args) { PandaDelegate<int> pandaDelegate = new PandaDelegate<int>(DoSomething); //wait Console.ReadKey(); } } }
使用.NET预定义的泛型委托可以减少自己定义委托的麻烦
Action:表示不带参数,返回类型是void。的委托
Action<>:表示带参数(最多8个),返回类型是void,的委托
Func<>:表示带参数(最多16个),返回类型不是void,的委托
Predicate<>:表示带1个泛型参数,返回类型是bool,的委托
The Action Generic Delegate in C# is also present in the System namespace. It takes one or more input parameters and returns nothing. This delegate can take a maximum of 16 input parameters of the different or same type
Note: Whenever your delegate does not return any value, whether by taking any input parameter or not, then you need to use the Action Generic delegate in C#.
Action Action<>
实例:
Action test1 = () => { }; Action<int> test2 = (int i) => { }; Action<int, string> test3 = (int i, string s) => { }; Action<bool, int> test4 = (bool b, int i) => { };
The Func Generic Delegate in C# is present in the System namespace. This delegate takes one or more input parameters and returns one out parameter. The last parameter is considered as the return value. The Func Generic Delegate in C# can take up to 16 input parameters of different types. It must have one return type. The return type is mandatory but the input parameter is not.
Note: Whenever your delegate returns some value, whether by taking any input parameter or not, you need to use the Func Generic delegate in C#.
注意:最后一个类型参数是返回类型
Func<>
实例:
Func<int> test1 = () => { return 1; }; Func<int, string> test2 = (int i) => { return i.ToString(); }; Func<bool, int, int> test3 = (bool b, int i) => { return 1; };
The Predicate Generic Delegate in C# is also present in the System namespace. This delegate is used to verify certain criteria of the method and returns the output as Boolean, either True or False. It takes one input parameter and always returns a Boolean value which is mandatory. This delegate can take a maximum of 1 input parameter and always return the value of the Boolean type.
public delegate bool Predicate<in T>(T obj)
实例:
Predicate<int> pp = (int i) => i > 0;
委托能实现的功能一般用接口一样能实现
以下情况优先选择委托:
The interface defines only a single method
Multicast capability is needed
The subscriber needs to implement the interface multiple times
实例:
public interface ITransformer { int Transform (int x); } public class Util { public static void TransformAll (int[] values, ITransformer t) { for (int i = 0; i < values.Length; i++) values[i] = t.Transform (values[i]); } } class Squarer : ITransformer { public int Transform (int x) => x * x; } static void Main() { int[] values = { 1, 2, 3 }; Util.TransformAll (values, new Squarer()); foreach (int i in values) Console.WriteLine (i); }
using System; namespace PandaNamespace { class PandaClass { //定义委托 public delegate void test(); static void Main(string[] args) { //声明并初始化委托 test test = someMethod; //执行委托 test(); Console.WriteLine("Task Process Over"); Console.ReadKey(); } public static void someMethod() { Console.WriteLine("test"); } } }
using System; namespace PandaNamespace { class TestClass { //声明委托类型 public delegate void TestDelegate(); //定义委托成员变量 public TestDelegate someAction; //添加方法到委托变量中 public void AddAction(TestDelegate someAction) { if(this.someAction == null) { this.someAction = someAction; } else { this.someAction += someAction; } } //删除委托中的方法 public void DeleteAction(TestDelegate someAction) { if(this.someAction != null) { this.someAction -= someAction; } } //执行委托 public void RunAction() { if(this.someAction != null) { this.someAction(); } } public void SomeMethod1() { Console.WriteLine("SomeMethod1"); } public void SomeMethod2() { Console.WriteLine("SomeMethod2"); } public void SomeMethod3() { Console.WriteLine("SomeMethod3"); } } class PandaClass { static void Main(string[] args) { TestClass testObj = new TestClass(); testObj.AddAction(testObj.SomeMethod1); testObj.AddAction(testObj.SomeMethod2); testObj.AddAction(testObj.SomeMethod3); testObj.RunAction(); testObj.DeleteAction(testObj.SomeMethod2); testObj.RunAction(); Console.WriteLine("Task Process Over"); Console.ReadKey(); } } }
using System; namespace PandaNamespace { class TestClass { //声明委托类型 public delegate void TestDelegate(int arg1, int arg2); //执行委托 public void RunAction(TestDelegate method, int arg1, int arg2) { method(arg1,arg2); } public void SomeMethod1(int arg1, int arg2) { Console.WriteLine($"SomeMethod1 arg1 = {arg1}, arg2 = {arg2}"); } public void SomeMethod2(int arg1, int arg2) { Console.WriteLine($"SomeMethod2 arg1 = {arg1}, arg2 = {arg2}"); } } class PandaClass { static void Main(string[] args) { TestClass testObj = new TestClass(); testObj.RunAction(testObj.SomeMethod1, 123, 456); testObj.RunAction(testObj.SomeMethod2, 666, 888); Console.WriteLine("Task Process Over"); Console.ReadKey(); } } }
using System; using System.Threading; namespace ConsoleApp1 { //测试使用的委托 delegate void ProgressDelegate(int percent); class Program { static void Main(string[] args) { //定义委托 ProgressDelegate progressDelegate = PrintProgressToConsole; //模拟的任务 for (int i = 0; i <= 100; i++) { progressDelegate(i); Thread.Sleep(100); } //wait Console.ReadKey(); } static void PrintProgressToConsole(int percent) { Console.WriteLine($"Action {percent}%"); } } }
一些方法只会使用一次,如果从零开始创建委托并引用该方法,会很麻烦
使用匿名方法(anonymous method)在初始化委托时就声明方法
可以在以下地方使用匿名方法:
声明委托变量时直接作为赋值
组合委托时赋值
为委托增加事件时赋值
注意:
不需要在匿名方法中声明返回值,因为委托已经声明了返回值
如果委托声明中包含params参数,在匿名方法中不需要params修饰
如果委托声明中包含ref和out参数,在匿名方法中必须带上修饰
如果没有参数,参数列表可以省略
在匿名方法中可以访问外部变量,甚至可以形成闭包
匿名方法中的变量作用域只在方法内
An anonymous method in C# cannot contain any jump statement like goto, break or continue.
It cannot access the ref or out parameter of an outer method.
The Anonymous methods cannot have or access the unsafe code.