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.CombineDelegate.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());
}
}

<NET CLR via c# 第4版>笔记 第17章 委托的更多相关文章

  1. <NET CLR via c# 第4版>笔记 第19章 可空值类型

    System.Nullable<T> 是结构. 19.1 C# 对可空值类型的支持 C# 允许用问号表示法来声明可空值类型,如: Int32? x = 5; Int32? y = null ...

  2. <NET CLR via c# 第4版>笔记 第18章 定制特性

    18.1 使用定制特性 FCL 中的几个常用定制特性. DllImport 特性应用于方法,告诉 CLR 该方法的实现位于指定 DLL 的非托管代码中. Serializable 特性应用于类型,告诉 ...

  3. <NET CLR via c# 第4版>笔记 第16章 数组

    //创建一个一维数组 int[] myIntegers; //声明一个数组引用 myIntegers = new int[100]; //创建含有100个int的数组 //创建一个二维数组 doubl ...

  4. <NET CLR via c# 第4版>笔记 第13章 接口

    13.1 类和接口继承 13.2 定义接口 C#用 interface 关键字定义接口.接口中可定义方法,事件,无参属性和有参属性(C#的索引器),但不能定义任何构造器方法,也不能定义任何实例字段. ...

  5. <NET CLR via c# 第4版>笔记 第12章 泛型

    泛型优势: 源代码保护 使用泛型算法的开发人员不需要访问算法的源代码.(使用c++模板的泛型技术,算法的源代码必须提供给使用算法的用户) 类型安全 向List<DateTime>实例添加一 ...

  6. <NET CLR via c# 第4版>笔记 第5章 基元类型、引用类型和值类型

    5.1 编程语言的基元类型 c#不管在什么操作系统上运行,int始终映射到System.Int32; long始终映射到System.Int64 可以通过checked/unchecked操作符/语句 ...

  7. <NET CLR via c# 第4版>笔记 第6章 类型和成员基础

    6.1 类型的各种成员 6.2 类型的可见性 public 全部可见 internal 程序集内可见(如忽略,默认为internal) 可通过设定友元程序集,允许其它程序集访问该程序集中的所有inte ...

  8. <NET CLR via c# 第4版>笔记 第7章 常量和字段

    7.1 常量 常量 是值从不变化的符号.定义常量符号时,它的值必须能够在编译时确定. 只能定义编译器识别的基元类型的常量,如果是非基元类型,需把值设为null. 常量的值直接嵌入代码,所以不能获取常量 ...

  9. <NET CLR via c# 第4版>笔记 第8章 方法

    8.1 实例构造器和类(引用类型) 构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零 .没有被构造器显式重写的所有字段都保证获得 0 或 null 值. 构造器不能被继承 ...

随机推荐

  1. Is it bad to rely on foreign key cascading? 外键 级联操作

    Is it bad to rely on foreign key cascading? I'll preface前言 this by saying that I rarely delete rows ...

  2. 常用字符与ASCII代码对照表

    常用字符与ASCII代码对照表 为了便于查询,以下列出ASCII码表:第128-255号为扩展字符(不常用) ASCII码 键盘 ASCII 码 键盘 ASCII 码 键盘 ASCII 码 键盘 27 ...

  3. 2017年P4中国峰会北京站 会议小结

    2017 P4 中国峰会 北京 本次会议依然侧重介绍P4,并highlight P4的benifit,大致分为以下几类: 1.学术界 - 未来网络的发展,为何提出P4技术? 未来网络和实体经济.其他学 ...

  4. HDU 3065 病毒侵袭持续中(AC自动机(每个模式串出现次数))

    http://acm.hdu.edu.cn/showproblem.php?pid=3065 题意:求每个模式串出现的次数. 思路: 不难,把模板修改一下即可. #include<iostrea ...

  5. SqlParameter 参数化模糊查询

    sql += " and a.f_fullName like N'%'+@fullName+'%'";

  6. ros 编译包含脚本文件以及launch文件

    目录结构如下: 修改CMakeLists.txt文件 install(PROGRAMS scripts/initial_pos.py DESTINATION ${CATKIN_PACKAGE_BIN_ ...

  7. Springboot 学习笔记 之 Day 2

    “约定大于配置”这样一句话,就是说系统,类库,框架应该假定合理的默认值,而非要求提供不必要的配置,可是使用Spring或者SpringMVC的话依然有许多这样的东西需要我们进行配置,这样不仅徒增工作量 ...

  8. [链接]最短路径的几种算法[迪杰斯特拉算法][Floyd算法]

    最短路径—Dijkstra算法和Floyd算法 http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html Dijkstra算 ...

  9. 团队作业7—团队项目设计完善&编码测试

    一.根据OOD详细设计工作要点,修改完善团队项目系统设计说明书和详细设计说明. <软件设计方案说明书>Github仓库地址:https://github.com/RNTF6/web 完善内 ...

  10. 时间常用api

    1.常用api 创建 Date 对象  -  年  -  月  -  日   -  小时  -  分  -  秒 -  星期 var now=new Date() var year = now.get ...