委托

作者:追风剑情 发布于:2021-1-22 9:20 分类:C#

委托: 官方文档

本章要讨论回调函数。回调函数是一种非常有用的编程机制,它的存在已经有很多年了。Microsoft.NET Framework 通过委托来提供回调函数机制。不同于其他平台(比如非托管C++)的回调机制,委托的功能要多得多。例如,委托确保回调方法是类型安全的(这是 CLR 最重要的目标之一)。委托还允许顺序调用多个方法,并支持调用静态方法和实例方法。

一、初识委托

C“运行时”的 qsort函数获取指向一个回调函数的指针,以便对数组中的元素进行排序。在Microsoft Windows 中,窗口过程、钩子过程和异步过程调用等都需要回调函数。在.NETFramework 中,回调方法的应用更是广泛。例如,可以登记回调方法来获得各种各样的通知,例如未处理的异常、窗口状态变化、菜单项选择、文件系统变化、窗体控件事件和异步操作已完成等。

在非托管C/C++中,非成员函数的地址只是一个内存地址。这个地址不携带任何额外的信息,比如函数期望收到的参数个数、参数类型、函数返回值类型以及函数的调用协定。简单地说,非托管C/C++回调函数不是类型安全的(不过它们确实是一种非常轻量级的机制)。

.NET Framework 的回调函数和非托管 Windows 编程环境的回调函数一样有用,一样普遍。但是,.NET Framework 提供了称为委托的类型安全机制。为了理解委托,先来看看如何使用它。以下代码演示了如何声明、创建和使用委托:

  1. using System;
  2. using System.IO;
  3.  
  4. namespace ConsoleApp22
  5. {
  6. //声明一个委托类型,它的实例引用一个方法,
  7. //该方法获取一个Int32参数,返回void
  8. internal delegate void Feedback(Int32 value);
  9.  
  10. class Program
  11. {
  12. static void Main(string[] args)
  13. {
  14. StaticDelegateDemo();
  15. InstanceDelegateDemo();
  16. //Demo1与Demo2写法不同,生成的IL代码是相同的
  17. ChainDelegateDemo1(new Program());
  18. ChainDelegateDemo2(new Program());
  19.  
  20. Console.ReadLine();
  21. }
  22.  
  23. private static void StaticDelegateDemo()
  24. {
  25. Console.WriteLine("----- Static Delegate Demo 2 -----");
  26. Counter(1, 3, null);
  27. Counter(1, 3, new Feedback(Program.FeedbackToConsole));
  28. Counter(1, 3, new Feedback(FeedbackToMsgBox));//前缀"Program."可选
  29. Console.WriteLine();
  30. }
  31.  
  32. private static void InstanceDelegateDemo()
  33. {
  34. Console.WriteLine("----- Instance Delegate Demo 2 -----");
  35. Program p = new Program();
  36. Counter(1, 3, new Feedback(p.FeedbackToFile));
  37. Console.WriteLine();
  38. }
  39.  
  40. private static void ChainDelegateDemo1(Program p)
  41. {
  42. Console.WriteLine("----- Chain Delegate Demo 2 -----");
  43. Feedback fb1 = new Feedback(FeedbackToConsole);
  44. Feedback fb2 = new Feedback(FeedbackToMsgBox);
  45. Feedback fb3 = new Feedback(p.FeedbackToFile);
  46.  
  47. Feedback fbChain = null;
  48. fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
  49. fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
  50. fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
  51. Counter(1, 2, fbChain);
  52.  
  53. Console.WriteLine();
  54. fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
  55. Counter(1, 2, fbChain);
  56. }
  57.  
  58. private static void ChainDelegateDemo2(Program p)
  59. {
  60. Console.WriteLine("----- Chain Delegate Demo 2 -----");
  61. Feedback fb1 = new Feedback(FeedbackToConsole);
  62. Feedback fb2 = new Feedback(FeedbackToMsgBox);
  63. Feedback fb3 = new Feedback(p.FeedbackToFile);
  64. Feedback fbChain = null;
  65. fbChain += fb1;
  66. fbChain += fb2;
  67. fbChain += fb3;
  68. Counter(1, 2, fbChain);
  69.  
  70. Console.WriteLine();
  71. fbChain -= new Feedback(FeedbackToMsgBox);
  72. Counter(1, 2, fbChain);
  73. }
  74.  
  75. private static void Counter(Int32 from, Int32 to, Feedback fb)
  76. {
  77. for (Int32 val = from; val <= to; val++)
  78. {
  79. //如果指定了任何回调,就调用它们
  80. if (fb != null)
  81. fb(val);//会被编译器改写成fb.Invoke(val);
  82. }
  83. }
  84.  
  85. private static void FeedbackToConsole(Int32 value)
  86. {
  87. Console.WriteLine("FeedbackToConsole: Item=" + value);
  88. }
  89.  
  90. private static void FeedbackToMsgBox(Int32 value)
  91. {
  92. Console.WriteLine("FeedbackToMsgBox: Item=" + value);
  93. }
  94.  
  95. private void FeedbackToFile(Int32 value)
  96. {
  97. Console.WriteLine("FeedbackToFile: Item=" + value);
  98. }
  99. }
  100. }

运行测试
111111.png

将方法绑定到委托时,C#和 CLR 都允许引用类型的协变性(covariance)和逆变性(contravariance)。协变性是指方法能返回从委托的返回类型派生的一个类型。逆变性是指方法获取的参数可以是委托的参数类型的基类。注意,只有引用类型才支持协变性与逆变性,值类型或void不支持。显然,值类型和void之所以不支持,是因为它们的存储结构是变化的,而引用类型的存储结构始终是一个指针。幸好,试图执行不支持的操作,C#编译器会报错。

委托类既可嵌套在一个类型中定义,也可在全局范围中定义。简单地说,由于委托是类,所以凡是能够定义类的地方,都能定义委托

二、委托揭秘

首先重新审视这一行代码:
internal delegate void Feedback(Int32 value);

看到这行代码后,编译器实际会像下面这样定义一个完整的类:

internal class Feedback : System.MulticastDelegate {
	//构造器
	public Feedback(Object @object, IntPtr method);
	//这个方法的原型和源代码指定的一样
	public virtual void Invoke(Int32 value);
	//以下方法实现对回调方法的异步回调
	public virtual IAsyncResult BeginInvoke(Int32 value,
		AsyncCallback callback, Object @object);
	public virtual void EndInvoke(IAsyncResult result);
}
//伪代码形式
public Int32 Invoke(Int32 value) {
	Int32 result;
	Delegate[] delegateSet = _invocationList as Delegate[];
	if (delegateSet != null) {
		//这个委托数组指定了应该调用的委托
		foreach (Feedback d in delegateSet)
			result = d(value); //调用每个委托
	} else { //否则就不是委托链
		//该委托标识了要回调的单个方法,
		//在指定的目标对象上调用这个回调方法
		result = _methodPtr.Invoke(_target, value);
		//上面这行代码接近实际代码
		//实际发生的事情用C#是表示不出来的
	}
	return result;
}

MulticastDelegate的三个重要的非公共字段
字段 类型 说明
_target System.Object 当委托对象包装一个静态方法时,这个字段为null,当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要传给实例方法的隐式参数this的值。
_methodPtr System.IntPtr 一个内部的整数值,CLR用它标识要回调的方法
_invocationList System.Object 该字段通常为null。构造委托链时它引用一个委托数组

可以用下面这句代码获取委托列表,然后自己实现算法调用每个委托:
Delegate[] arrayOfDelegates = fbChain.GetInvocationList();

.NET Framework中定义的泛型委托:(在System命名空间中)
public delegate void Action<T1,...,T16>(T1 arg1,...,T16 arg16);
public delegate TResult Func<T1,...,T16, TResult>(T1 arg1,...T16 arg16);

以下情况需要自定义委托:
1、需要使用ref或out关键字以传递引用的方式传递参数,例如
delegate void Bar(ref Int32 z);
2、如果委托要通过C#的params关键字获取数量可变的参数
3、要为委托的任何参数指定默认值
4、要对委托的泛型参数进行约束

获取泛型实参并返回值的委托支持逆变和协变,而且建议总是利用这些功能,因为它们没有副作用,而且使你的委托用于更多情形。

简化语法1:不需要构造委托对象

//下面两句代码等价
button1.Click += new EventHandler(button1_Click);
button1.Click += button1_Click;//编译器帮我们创建了EventHandler包装器

简化语法2:不需要定义回调方法(lambda表达式)

//编译器会将lambda表达式转成匿名函数(总是private)
ThreadPool.QueueUserWorkItem( obj => Console.WriteLine(obj), 5 );

简化语法是编译器提供的"语法糖"(一般而言,越是高级的语言,提供的简化语法越多,以方便写程序,这就是所谓的“语法糖”——译注)

=>操作符左侧供指定传给lambda表达式的参数名称。下列总结了一些规则:

//如果委托不获取任何参数,就使用()
Func<String> f = () => "Jeff";
//如果委托获取1个或更多参数,可显式指定类型
Func<Int32, String> f2 = (Int32 n) => n.ToString();
Func<Int32, Int32, String> f3 = (Int32 n1, Int32 n2) => (n1 + n2).ToString();
//如果委托获取1个或更多参数,编译器可推断类型
Func<Int32, String> f4 = (n) => n.ToString();
Func<Int32, Int32, String> f5 = (n1, n2) => (n1 + n2).ToString();
//如果委托获取1个参数,可省略()
Func<Int32, String> f6 = n => n.ToString();
//如果委托有ref/out参数,必须显式指定ref/out和类型
Bar b = (out Int32 n) => n = 5;
//如果主体由两个或多个语句构成,必须用大括号将语句封闭。在用了大括号的情况下,
//如果委托期待返回值,还必须在主体中添加return语句。
//只有一句主体时,才可以省略大括号和return语句 Func<Int32, Int32, String> f7 = (n1, n2) => { Int32 sum = n1 + n2; return sum.ToString(); };

简化语法3:局部变量不需要手动包装到类中即可传给回调方法

internal sealed class AClass {
	public static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo) {
		// 一些局部变量
		Int32[] squares = new Int32[numToDo];
		AutoResetEvent done = new AutoResetEvent(false);
		
		// 在其他线程上执行一系列任务
		for (Int32 n = 0; n < squares.Length; n++) {
			ThreadPool.QueueUserWorkItem(
				obj => {
					Int32 num = (Int32) obj;
					// 该任务通常更耗时
					squares[num] = num * num;
					// 如果是最后一个任务,就让主线程继续运行
					if (Interlocked.Decrement(ref numToDo) == 0)
						done.Set();
				}, n);
		}
		// 等待其他所有线程结束运行
		done.WaitOne();
		
		// 显示结果
		for (Int32 n = 0; n < squares.Length; n++) {
			Console.WriteLine("Index {0}, squeare={1}", n, squares[n]);
		}
	}
}

C#编译器实际是像下面这样重写了代码

internal sealed class AClass {
	public static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo) {
		// 一些局部变量
		WaitCallback callback1 = null;
		
		// 构造辅助类的实例
		<>c__DisplayClass2 class1 = new <>c__DisplayClass2();
		
		// 初始化辅助类的字段
		class1.numToDo = numToDo;
		class1.squares = new Int32[class1.numToDo];
		class1.done = new AutoResetEvent(false);
		
		// 在其他线程上执行一系列任务
		for (Int32 n = 0; n < class1.squares.Length; n++) {
			if (callback1 == null) {
				// 新建的委托对象绑定到辅助对象及其匿名实例方法
				callback1 = new WaitCallback(
				// <UsingLocalVariablesInTheCallbackCode>b_0
				// 是编译器生成的方法名,这里的<>是方法名的一部分,不是泛型
				class1.<UsingLocalVariablesInTheCallbackCode>b_0);
			}
			ThreadPool.QueueUserWorkItem(callback1, n);
		}
		
		// 等待其他所有线程结束运行
		done.WaitOne();
		
		// 显示结果
		for (Int32 n = 0; n < squares.Length; n++) {
			Console.WriteLine("Index {0}, squeare={1}", n, squares[n]);
		}
		
		// 为避免冲突,辅助类被指定了一个奇怪的名称,
		// 而且被指定为私有的,禁止从AClass类外部访问
		[CompilerGenerated] //此特性表明这个类由编译器生成
		private sealed class <>c_DisplayClass2 : Object {
			// 回调代码要使用的每个局部变量都有一个对应的公共字段
			public Int32[] squares;
			public Int32 numToDo;
			public AutoResetEvent done;
			
			// 公共无参构造器
			public <>c_DisplayClass2 { }
			
			// 包含回调代码的公共实例方法
			public void <UsingLocalVariablesInTheCallbackCode>b_0(Object obj) {
				Int32 num = (Int32) obj;
				squares[num] = num * num;
				if (Interlocked.Decrement(ref numToDo) == 0)
					done.Set();
			}
		}
	}
}

lambda表达式用于简化代码

// 创建并初始化一个String数组
String[] names = {"Jeff", "Kristin", "Aidan", "Grant"};
// 只获取含小写字母'a'的名字
Char charToFind = 'a';
names = Array.FindAll(names, name => name.IndexOf(charToFind) >=0);
// 将每个字符串的字符转换为大写
names = Array.ConvertAll(names, name => name => name.ToUpper());
// 显示结果
Array.ForEach(names, Console.WriteLine);
三、委托和反射
  1. using System;
  2. using System.Reflection;
  3. using System.IO;
  4. using System.Linq;
  5.  
  6. namespace ConsoleApp23
  7. {
  8. // 下面是一些不同的委托定义
  9. internal delegate Object TowInt32s(Int32 n1, Int32 n2);
  10. internal delegate Object OneString(String s1);
  11.  
  12. class DelegateReflection
  13. {
  14. static void Main(string[] args)
  15. {
  16. if (args.Length < 2)
  17. {
  18. String usage =
  19. @"Usage:" +
  20. "{0} delType methodName [Arg1] [Arg2]" +
  21. "{0} where delType must be TweInt32s or OneString" +
  22. "{0} if delType is TwoInt32s, methodName must be Add or Subtract" +
  23. "{0} if delType is OneString, methodName must be NumChars or Reverse" +
  24. "{0}" +
  25. "{0}Examples:" +
  26. "{0} TwoInt32s Add 123 321" +
  27. "{0} TwoInt32s Subtract 123 321" +
  28. "{0} OneString NumChars \"Hello there\"" +
  29. "{0} OneString Reverse \"Hello there\"";
  30. Console.WriteLine(usage, Environment.NewLine);
  31. Console.ReadLine();
  32. return;
  33. }
  34.  
  35. // 将delType实参转换为委托类型
  36. Type delType = Type.GetType("ConsoleApp23." + args[0]);
  37. if (delType == null)
  38. {
  39. Console.WriteLine("Invalid delType argument: " + args[0]);
  40. return;
  41. }
  42.  
  43. Delegate d;
  44. try
  45. {
  46. // 将Arg1实参转换为方法
  47. MethodInfo mi =
  48. typeof(DelegateReflection).GetTypeInfo().GetDeclaredMethod(args[1]);
  49. // 创建包装了静态方法的委托对象
  50. d = mi.CreateDelegate(delType);
  51. }
  52. catch (ArgumentException)
  53. {
  54. Console.WriteLine("Invalid methodName argument: " + args[1]);
  55. return;
  56. }
  57.  
  58. // 创建一个数组,其中只包含要通过委托对象传给方法的参数
  59. Object[] callbackArgs = new Object[args.Length - 2];
  60.  
  61. if (d.GetType() == typeof(TowInt32s))
  62. {
  63. try
  64. {
  65. // 将String类型的参数转换为Int32类型的参数
  66. for (Int32 a = 2; a < args.Length; a++)
  67. callbackArgs[a - 2] = Int32.Parse(args[a]);
  68. }
  69. catch (FormatException)
  70. {
  71. Console.WriteLine("Parameters must be integers.");
  72. return;
  73. }
  74. }
  75.  
  76. if (d.GetType() == typeof(OneString))
  77. {
  78. // 只复制String参数
  79. Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
  80. }
  81.  
  82. try
  83. {
  84. // 调用委托并显示结果
  85. Object result = d.DynamicInvoke(callbackArgs);
  86. Console.WriteLine("Result = " + result);
  87. }
  88. catch (TargetParameterCountException)
  89. {
  90. Console.WriteLine("Incorrect number of parameters specified.");
  91. }
  92. }
  93.  
  94. private static Object Add(Int32 n1, Int32 n2)
  95. {
  96. return n1 + n2;
  97. }
  98.  
  99. private static Object Subtract(Int32 n1, Int32 n2)
  100. {
  101. return n1 - n2;
  102. }
  103.  
  104. private static Object NumChars(String s1)
  105. {
  106. return s1.Length;
  107. }
  108.  
  109. private static Object Reverse(String s1)
  110. {
  111. return new String(s1.Reverse().ToArray());
  112. }
  113. }
  114. }

运行测试
111111.png

四、异步调用委托
  1. using System;
  2. using System.Threading;
  3.  
  4. namespace ConsoleApp24
  5. {
  6. public delegate string AsyncMethodCaller(int callDuration, out int threadId);
  7.  
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. int threadId;
  13. AsyncMethodCaller caller = new AsyncMethodCaller(TestMethod);
  14. //异步调用委托
  15. IAsyncResult result = caller.BeginInvoke(3000, out threadId, new AsyncCallback(
  16. (ar) =>
  17. {
  18. IAsyncResult ret = (IAsyncResult)ar;
  19. Console.WriteLine();
  20. Console.WriteLine("AsyncCallback");
  21. Console.WriteLine("AsyncState: {0}", ret.AsyncState);
  22. Console.WriteLine("CompletedSynchronously: {0}", ret.CompletedSynchronously);
  23. Console.WriteLine("IsCompleted: {0}", ret.IsCompleted);
  24. }
  25. ), "custom args");
  26.  
  27. Thread.Sleep(0);
  28. Console.WriteLine("Main thread {0} does some work.",
  29. Thread.CurrentThread.ManagedThreadId);
  30.  
  31.  
  32. //阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号
  33. result.AsyncWaitHandle.WaitOne();
  34. string returnValue = caller.EndInvoke(out threadId, result);
  35. //释放由当前 System.Threading.WaitHandle 占用的所有资源。
  36. result.AsyncWaitHandle.Close();
  37.  
  38. Console.WriteLine("The call executed on thread {0}, with return value \"{1}\".",
  39. threadId, returnValue);
  40.  
  41. Console.ReadLine();
  42. }
  43.  
  44. public static string TestMethod(int callDuration, out int threadId)
  45. {
  46. Console.WriteLine("Test method begins.");
  47. Thread.Sleep(callDuration);
  48. threadId = Thread.CurrentThread.ManagedThreadId;
  49. return String.Format("My call time was {0}.", callDuration.ToString());
  50. }
  51. }
  52. }

运行测试
111111.png

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号