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. IntelliJ IDEA 设置代码提示或自动补全的快捷键(Alt+/)

    点击 文件菜单(File) –> 点击 设置(Settings… Ctrl+Alt+S), –> 打开设置对话框.在左侧的导航框中点击 KeyMap. 接着在右边的树型框中选择 Main ...

  2. 【Android实验】 UI设计-ListView

    目录 实验目的 实验要求 实验内容 实现效果 实验代码 实验总结 实验目的 学习使用ListView 学习使用menu 实验要求 实现一个列表,其中显示班级学号姓名,提供添加功能,如需要删去某一项,长 ...

  3. 更换主机后SSH无法登录的问题

    之前通过SSH远程一台机器(起个名字:cc),某一天把cc重装了一下系统,再SSH时显示密钥验证失败: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ...

  4. maven 下载不到jar包时候,更改阿里源

      maven 源 下载太慢,更改国内的阿里源会快一些 <repositories> <repository> <id>alimaven</id> &l ...

  5. StringBuffer中的sBuffer.delete(0,4);

    只删除第0-3位的字符,第4位是不删的

  6. Windows系统下在Eclipse中集成Python

    我现在偶尔开发代码,已经不用Eclipse了,主要原因是查看Jar包中的代码反编译十分不便,项目加载的时候卡,偶尔还会崩溃 用Intellij IDEA和PyCharm 原来的笔记如何在Eclipse ...

  7. 深度排序与alpha混合

    原文: https://blogs.msdn.microsoft.com/shawnhar/2009/02/18/depth-sorting-alpha-blended-objects/ 翻译:李现民 ...

  8. 【Golang】 可以自动生成测试用例的库--gotests

    简介 gotests是一个Golang命令行工具,它可以使编写Go的测试代码变得容易.它能基于目标源文件的函数和方法生成数据驱动测试用例,并且在此过程会自动导入任何依赖. 下面是gotests在使用S ...

  9. Java知识集锦

    Java知识集锦 一.Java程序基础 1.1 开发和运行环境 1.2 Java语言概述 二.Java语法基础 2.1 基础类型和语法 2.2 对象和类型 2.3 包和访问控制 三.数据类型及类型转换 ...

  10. Ubuntu 14.04 的 VNC Server

    首先,如果是Desktop 版本的 Ubuntu,不需要另外安装vnc server. 网上也不知怎么搞的,一堆奇怪的方法,要安装TightVNCServer,然后一堆sb设置 然后,主要有两个配置 ...