<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());
}
}
<NET CLR via c# 第4版>笔记 第17章 委托的更多相关文章
- <NET CLR via c# 第4版>笔记 第19章 可空值类型
System.Nullable<T> 是结构. 19.1 C# 对可空值类型的支持 C# 允许用问号表示法来声明可空值类型,如: Int32? x = 5; Int32? y = null ...
- <NET CLR via c# 第4版>笔记 第18章 定制特性
18.1 使用定制特性 FCL 中的几个常用定制特性. DllImport 特性应用于方法,告诉 CLR 该方法的实现位于指定 DLL 的非托管代码中. Serializable 特性应用于类型,告诉 ...
- <NET CLR via c# 第4版>笔记 第16章 数组
//创建一个一维数组 int[] myIntegers; //声明一个数组引用 myIntegers = new int[100]; //创建含有100个int的数组 //创建一个二维数组 doubl ...
- <NET CLR via c# 第4版>笔记 第13章 接口
13.1 类和接口继承 13.2 定义接口 C#用 interface 关键字定义接口.接口中可定义方法,事件,无参属性和有参属性(C#的索引器),但不能定义任何构造器方法,也不能定义任何实例字段. ...
- <NET CLR via c# 第4版>笔记 第12章 泛型
泛型优势: 源代码保护 使用泛型算法的开发人员不需要访问算法的源代码.(使用c++模板的泛型技术,算法的源代码必须提供给使用算法的用户) 类型安全 向List<DateTime>实例添加一 ...
- <NET CLR via c# 第4版>笔记 第5章 基元类型、引用类型和值类型
5.1 编程语言的基元类型 c#不管在什么操作系统上运行,int始终映射到System.Int32; long始终映射到System.Int64 可以通过checked/unchecked操作符/语句 ...
- <NET CLR via c# 第4版>笔记 第6章 类型和成员基础
6.1 类型的各种成员 6.2 类型的可见性 public 全部可见 internal 程序集内可见(如忽略,默认为internal) 可通过设定友元程序集,允许其它程序集访问该程序集中的所有inte ...
- <NET CLR via c# 第4版>笔记 第7章 常量和字段
7.1 常量 常量 是值从不变化的符号.定义常量符号时,它的值必须能够在编译时确定. 只能定义编译器识别的基元类型的常量,如果是非基元类型,需把值设为null. 常量的值直接嵌入代码,所以不能获取常量 ...
- <NET CLR via c# 第4版>笔记 第8章 方法
8.1 实例构造器和类(引用类型) 构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零 .没有被构造器显式重写的所有字段都保证获得 0 或 null 值. 构造器不能被继承 ...
随机推荐
- 51NOD 1069 Nim游戏
1069 Nim游戏 有N堆石子.A B两个人轮流拿,A先拿.每次只能从一堆中取若干个,可将一堆全取走,但不可不取,拿到最后1颗石子的人获胜.假设A B都非常聪明,拿石子的过程中不会出现失误.给出 ...
- 树莓派GPIO
- FPGA 概述
概述 verilog HDL Verilog HDL基本结构 1 Verilog HDL程序是由模块构成的.每个模块嵌套在module和endmodule声明语句中. 2 每个Verilog HDL源 ...
- C# 实现简单的 Heap 堆(二叉堆)
如题,C# 实现简单的二叉堆的 Push() 和 Pop(), 如有不足欢迎指正. 另外,在C#中使用 Heap 的相似功能可以考虑使用:Priority Queues,SortedDictiona ...
- cartographer安装--Ubuntu14.04--indigo
0.安装所有依赖项 sudo apt-get install -y google-mock libboost-all-dev libeigen3-dev libgflags-dev libgoogl ...
- ubuntu 14.04 (desktop amd 64) 安装和配置ROS Indigo
安装ROS 配置Ubuntu的软件源 配置Ubuntu要求允许接受restricted.universe和multiverse的软件源,可以根据下面的链接配置: https://help.ubuntu ...
- 使用自定义RadioButton和ViewPager实现TabHost效果和带滑动的页卡效果
在工作中又很多需求都不是android系统自带的控件可以达到效果的,内置的TabHost就是,只能达到简单的效果 ,所以这个时候就要自定义控件来达到效果:这个效果就是: 使用自定义RadioButto ...
- Codeforces D - Ithea Plays With Chtholly
D - Ithea Plays With Chtholly 思路:考虑每个位置最多被替换c/2次 那么折半考虑,如果小于c/2,从左往右替换,大于c/2总右往左替换,只有小于这个数(从左往右)或者大于 ...
- canvas和图片之间的互相装换
canvas和图片之间的互相装换 一.总结 一句话总结:一个是canvas的drawImage方法,一个是canvas的toDataURL方法 canvas drawImage() Image对象 c ...
- python面试题(转自https://www.cnblogs.com/wupeiqi/p/9078770.html)
第一部分 Python基础篇(80题) 为什么学习Python? 通过什么途径学习的Python? Python和Java.PHP.C.C#.C++等其他语言的对比? 简述解释型和编译型编程语言? P ...