<NET CLR via c# 第4版>笔记 第17章 委托
17.1 初识委托
- .net 通过委托来提供回调函数机制.
- 委托确保回调方法是类型安全的.
- 委托允许顺序调用多个方法.
17.2 用委托回调静态方法
- 将方法绑定到委托时,C# 和 CLR 都允许引用类型的 协变性(covariance) 和 逆变性(contravariance).
- 协变性是指方法能返回从委托的返回类型派生的一个类型.
- 逆变性是方法获取的参数可以是委托的参数类型的基类.
- 只有引用类型才支持协变性与逆变性,值类型或 void 不支持
delegate object MyCallback(FileStream s);
//可以应用于委托 MyCallback
string SomeMethod(Stream s);
//返回值是值类型,不能应用于委托 MyCallback
int SomeOtherMethod(Stream s)
17.3 用委托回调实例方法
17.4 委托揭秘
- 编译器看到委托后,会生成一个同名的类,并继承于
System.MulticastDelegate
System.MulticastDelegate
派生自System.Delegate
System.MulticastDelegate
的三个重要的非公共字段:- target System.Object 当委托对象包装一个静态方法时,这个字段为 null .当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象.
- methodPtr System.IntPtr 一个内部的整数值, CLR 用它标识要回调的方法.
- invocationList System.Object 该字段通常为 null, 构造委托链时它引用一个委托数组.
17.5 用委托回调多个方法( 委托链 )
- 使用 Delegate 类的公共静态方法 Combine 将委托添加到链中:
fbChain=(Feedback) Delegate.Combine(fbChain, fb1);
- 使用 Delegate 类的公共静态方法 Remove 从链中删除委托. 每次 Remove 方法调用只能从链中删除一个委托.
fbChain=(Feedback) Delegate.Combine(fbChain, fb1);
- 委托
public delegate int Feedback(int value)
的 Invoke 的伪代码:
public int Invoke(int value) {
int 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;
}
17.5.1 C# 对委托链的支持
- c#编译器自动为委托类型的实例重载了+=和-=操作符.这些操作符分别调用 Delegate.Combine 和 Delegate.Remove.
fbChain += fb1;
fbChain += fb2;
fbChain -= fb1;
17.5.2 取得对委托链调用的更多控制
- 可以通过实例方法 GetInvocationList 获取委托链中委托的集合,然后通过自定义算法,显式调用这些委托.
17.6 委托定义不要太多( 泛型委托 )
- 尽量使用
Action<T>
和Func<T>
委托.需要使用 ref ,out ,params 关键字的地方除外.
17.7 C# 为委托提供的简化语法
- 后面描述的这些只是 C# 的语法糖.
17.7.1 简化语法1: 不需要构造委托对象
- 由于c#编译器能自己进行推断,所以可以省略构造 ThreadPool.QueueUserWorkItem 方法中 WaitCallback 委托对象的代码.
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(SomeAsyncTask, 5);
}
private static void SomeAsyncTask(object o) {
Console.WriteLine(o);
}
17.7.2 简化语法2: 不需要定义回调方法( lambda表达式 )
- C# 允许使用 Lambda 表达式写回调代码,如:
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(obj =>Console.WriteLine(obj), 5);
}
- C# 编译器看到这个 lambda 表达式之后,会在类中自动定义一个新的私有方法,称为匿名函数,如下:
class Program
{
//创建该私有字段是为了缓存委托对象,
//优点: 不会每次调用都新建一个对象
//缺点: 缓存的对象永远不会被垃圾回收
[CompilerGenerated]
private static WaitCallback <>9_xxxDelegate1;
static void Main(string[] args)
{
if(<>9_xxxDelegate1==null)
<>9_xxxDelegate1 = new WaitCallback(<Main>.b_0);
ThreadPool.QueueUserWorkItem(<>9_xxxDelegate1,5);
}
[CompilerGenerated]
private static void <Main>.b_0(object obj) {
Console.WriteLine(obj);
}
}
- 如果调用方法不是静态的,但其内部的 匿名函数 不包含实例成员引用,编译器仍会生成静态匿名函数,因为它的效率比实例方法高; 但如果 匿名函数 的代码确实引用了实例成员,编译器就会生成非静态匿名函数.
- 下面是一些 lambda 表达式的使用范例:
//如果委托不获取任何参数,就使用()
Func<string> f = () => "Jeff";
//如果委托获取一个或更多参数,可显式指定类型
Func<int, string> f2 = (int n) => n.ToString();
Func<int, int, string> f3 = (int n1, int n2) => (n1 + n2).ToString();
//如果委托获取一个或更多参数,编译器可推断类型
Func<int, string> f4 = (n) => n.ToString();
Func<int, int, string> f5 = (n1, n2) => (n1 + n2).ToString();
//如果委托获取一个参数,可省略 ( 和 )
Func<int, string> f6 = n => n.ToString();
//如果委托有 ref/out 参数,必须显式指定 ref/out 和类型
Bar b = (out int n) => n = 5;
//Bar的定义
delegate void Bar(out int z);
- 如果主体由两个或多个语句构成,必须用大括号将语句封闭.在用了大括号的情况下,如果委托期待返回值,还必须在主体中添加 return 语句.
17.7.3 简化语法3: 局部变量不需要手动包装到类中即可传给回调方法
示例代码:
static void Main(string[] args)
{
//一些局部变量
int numToDo = 20;
int[] squares = new int[numToDo];
AutoResetEvent done = new AutoResetEvent(false);
//在其他线程上执行一系列任务
for (int n = 0; n < squares.Length; n++)
{
ThreadPool.QueueUserWorkItem(obj =>
{
int num = (int)obj;
//该任务通常更耗时
squares[num] = num * num;
//如果这是最后一个任务,就让主线程继续运行
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
}, n);
}
done.WaitOne();
for (int n = 0; n < squares.Length; n++)
{
Console.WriteLine("Index {0} ,Square={1}", n, squares[n]);
}
}
- 对于上面代码,C#编译器会定义一个新的辅助类,这个类要为打算传给 回调代码 的每个值都定义一个字段.
- 此外,回调代码 还必须定义成辅助类中的实例方法.
C#编译器会像下面这样重写代码:
class Program
{
static void Main(string[] args)
{
//一些局部变量
int numToDo = 20;
WaitCallback callback1 = null;
//构造辅助类的实例
<>c_DisplayClass2 class1 = new <>c_DisplayClass2();
//初始化辅助类的字段
class1.numToDo = numToDo;
class1.squares = new int[class1.numToDo];
class1.done = new AutoResetEvent(false);
//在其他线程上执行一系列任务
for (int n = 0; n < class1.squares.Length; n++) {
if (callback1 == null) {
//新建的委托对象绑定到辅助对象及其匿名实例方法
callback1 = new WaitCallback(class1.<Main>b_0);
}
ThreadPool.QueueUserWorkItem(callback1,n);
}
//等待其他所有线程结束运行
class1.done.WaitOne();
//显示结果
for (int n = 0; n < class1.squares.Length; n++) {
Console.WriteLine("Index {0} ,Square={1}", n, class1.squares[n]);
}
}
//为避免冲突,辅助类被指定了一个奇怪的名称.
//而且被指定为私有的,禁止从Program类外部访问
[CompilerGenerated]
private sealed class <>c_DisplayClass2:Object{
//回调代码要使用的每个局部变量都有一个对应的公共字段
public int[] squares;
public int numToDo;
public AutoResetEvent done;
//公共无参构造器
public <>c_DisplayClass2{}
//包含回调代码的公共实例方法
public void <Main>b_0(object obj) {
int num = (int)obj;
squares[num] = num * num;
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
}
}
}
- 作者给自己定了个规则:如果需要在回调方法中包含3行以上的代码,就不使用 lambda 表达式,而是手写一个方法,并为其分配自己的名称.
17.8 委托和反射
可以通过 System.Reflection.MethodInfo 提供的 CreateDelegate 方法,创建一个 Delegate 对象,然后调用该对象的 DynamicInvoke 方法,以实现在运行时动态调用委托.
//下面是一些不同的委托定义
internal delegate object TwoInt32s(int n1, int n2);
internal delegate object OneString(String s1);
static class Program
{
static void Main(string[] args)
{
//如果委托在命名空间下,第一个参数一定要是带命名空间的完全限定名
//args = new[] { "TwoInt32s", "Add", "1", "11" };
if (args.Length < 2)
{
Console.WriteLine("Usage: TwoInt32s Add 123 321");
return;
}
//将delType实参转换为委托类型
Type delType = Type.GetType(args[0]);
if (delType == null)
{
Console.WriteLine("Invalid delType argument: " + args[0]);
return;
}
Delegate d;
try
{
//将Arg1实参转换为方法
MethodInfo mi = typeof(Program).GetTypeInfo().GetDeclaredMethod(args[1]);
//创建包装了静态方法的委托对象
d = mi.CreateDelegate(delType);
}
catch (ArgumentException)
{
Console.WriteLine("Invalid methodName argument: " + args[1]);
return;
}
//创建一个数组,其中只包含要通过委托对象传给方法的参数
object[] callbackArgs = new object[args.Length - 2];
if (d.GetType() == typeof(TwoInt32s))
{
try
{
//将String类型的参数转换为Int32类型的参数
for (int a = 2; a < args.Length; a++)
callbackArgs[a - 2] = int.Parse(args[a]);
}
catch (FormatException)
{
Console.WriteLine("Parameters must be integers.");
return;
}
}
if (d.GetType() == typeof(OneString))
{
//只复制String参数
Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
}
try
{
//调用委托并显示结果
object result = d.DynamicInvoke(callbackArgs);
Console.WriteLine("Result = " + result);
}
catch (TargetParameterCountException)
{
Console.WriteLine("Incorrect number of parameters specified.");
}
}
private static object Add(int n1, int n2)
{
return n1 + n2;
}
private static object Subtract(int n1, int n2)
{
return n1 - n2;
}
private static object NumChars(string s1)
{
return s1.Length;
}
private static object Reverse(string s1)
{
return new String(s1.Reverse().ToArray());
}
}