委托是一个知道如何调用方法的对象。

委托类型(delegate type)定义委托实例(delegate instances)可以调用的方法类型。具体来说,它定义了方法的返回类型参数类型。下面定义了一个名为Transformer的委托类型:

delegate int Transformer (int x);

Transformer兼容任何返回类型为int且只有一个int形参的方法,例如:

static int Square (int x) { return x * x; }

或者:

static int Square (int x) => x * x;

将一个方法赋值给一个委托变量会创建一个委托实例,可以像调用方法一样调用这个委托实例。

delegate int Transformer (int x);
class Test
{static void Main(){Transformer t = Square; // Create delegate instanceint result = t(3); // Invoke delegateConsole.WriteLine (result); // 9}static int Square (int x) => x * x;
}

委托实例实际上充当调用方的委托:调用方调用委托,然后委托调用目标方法。这种间接方法将调用者与目标方法解耦。

语句:

Transformer t = Square;

是下面语句的缩写:

Transformer t = new Transformer (Square);

从技术上讲,当引用不带括号和参数的Square时,指定了一个方法组 (method group)。如果该方法是重载的,C# 将根据赋值给它的委托的签名选择正确的重载。

表达式:

t(3)

是下面语句的缩写:

t.Invoke(3)

委托类似于回调(callback),这是一个捕获构造(如C函数指针)的通用术语。


用委托编写插件方法

委托变量在运行时被分配一个方法。这对于编写插件方法非常有用。在本例中,有一个名为Transform的实用方法,该方法将转换应用于整数数组中的每个元素。Transform方法有一个委托参数,用于指定插件转换。

public delegate int Transformer (int x);class Util
{public static void Transform (int[] values, Transformer t){for (int i = 0; i < values.Length; i++)values[i] = t (values[i]);}
}class Test
{static void Main(){int[] values = { 1, 2, 3 };Util.Transform (values, Square); // Hook in the Square methodforeach (int i in values)Console.Write (i + " "); // 1 4 9}static int Square (int x) => x * x;
}

上面的Transform方法是一个高阶函数(higher-order function),因为它是一个以函数为参数的函数。(返回一个委托的方法也是高阶函数。)


多播委托

所有委托实例都具有多播功能。这意味着一个委托实例不仅可以引用单个目标方法,还可以引用一系列目标方法。++= 操作符组合了委托实例。例如:

SomeDelegate d = SomeMethod1;
d += SomeMethod2;
// d = d + SomeMethod2;   // 与上一语句功能相同

调用 d 现在将同时调用 SomeMethod1 和 SomeMethod2。调用委托的顺序与添加委托的顺序相同。

--= 操作符从左委托操作数中移除右委托操作数。例如:

d -= SomeMethod1;

调用 d 现在只会调用 SomeMethod2。

在一个具有 null 值的委托变量上调用 ++= 是有效的,相当于将该变量赋值为一个新值:

SomeDelegate d = null;
d += SomeMethod1; // Equivalent (when d is null) to d = SomeMethod1;

类似地,在具有单个目标的委托变量上调用 -= 相当于将 null 赋给该变量。

委托是不可变的(immutable),所以当调用 +=-= 时,实际上是在创建一个新的委托实例,并将其赋值给现有的变量。

如果多播委托具有非空返回类型,则调用方将从最后一个要调用的方法接收返回值。前面的方法仍然会被调用,但是它们的返回值会被丢弃。在使用多播委托的大多数场景中,它们都有 void 返回类型,所以不会出现这种微妙的情况。

所有委托类型隐式派生自 System.MulticastDelegate,它继承自 System.Delegate。C# 将在委托上的 +-+=-= 操作编译为 System.Delegate 类的静态 CombineRemove 方法。

多播委托示例

假设编写了一个执行时间很长的方法。该方法可以通过调用委托定期向其调用方报告进度。在这个例子中,HardWork 方法有一个ProgressReporter 委托参数,它调用该参数来指示进度:

public delegate void ProgressReporter (int percentComplete);public class Util
{public static void HardWork (ProgressReporter p){for (int i = 0; i < 10; i++){p (i * 10); // Invoke delegateSystem.Threading.Thread.Sleep (100); // Simulate hard work}}
}

为了监控进程,Main 方法创建了一个多播委托实例 p,这样进程就由两个独立的方法来监控:

class Test
{static void Main(){ProgressReporter p = WriteProgressToConsole;p += WriteProgressToFile;Util.HardWork (p);}static void WriteProgressToConsole (int percentComplete)=> Console.WriteLine (percentComplete);static void WriteProgressToFile (int percentComplete)=> System.IO.File.WriteAllText ("progress.txt", percentComplete.ToString());
}

实例&静态方法目标

当将实例(instance)方法赋值给委托对象时,委托对象不仅必须维护对该方法的引用,还必须维护对该方法所属实例的引用。System.Delegate 类的 Target 属性表示此实例 (对于引用静态方法的委托,这个属性将为空)。例如:

public delegate void ProgressReporter (int percentComplete);class Test
{static void Main(){X x = new X();ProgressReporter p = x.InstanceProgress;p(99); // 99Console.WriteLine (p.Target == x); // TrueConsole.WriteLine (p.Method); // Void InstanceProgress(Int32)}
}class X
{public void InstanceProgress (int percentComplete)=> Console.WriteLine (percentComplete);
}

泛型委托类型

委托类型可以包含泛型类型参数。例如:

public delegate T Transformer<T> (T arg);

根据这个定义,可以编写一个适用于任何类型的通用 Transform 实用方法:

public class Util
{public static void Transform<T> (T[] values, Transformer<T> t){for (int i = 0; i < values.Length; i++)values[i] = t (values[i]);}
}class Test
{static void Main(){int[] values = { 1, 2, 3 };Util.Transform (values, Square); // Hook in Squareforeach (int i in values)Console.Write (i + " "); // 1 4 9}static int Square (int x) => x * x;
}

Func 和 Action 委托

使用泛型委托,可以编写一组非常通用的委托类型,它们可以用于任何返回类型和任何(合理的)数量的参数的方法。这些委托是定义在 System 命名空间中的 FuncAction 委托 (inout 注释表示变体(variance)):

delegate TResult Func <out TResult> ();
delegate TResult Func <in T, out TResult> (T arg);
delegate TResult Func <in T1, in T2, out TResult> (T1 arg1, T2 arg2);
... and so on, up to T16delegate void Action ();
delegate void Action <in T> (T arg);
delegate void Action <in T1, in T2> (T1 arg1, T2 arg2);
... and so on, up to T16

这些委托非常通用。前面例子中的 Transformer 委托可以被替换为 Func 委托,Func 委托接受一个 T 类型的参数,并返回一个相同类型的值:

public static void Transform<T> (T[] values, Func<T,T> transformer)
{for (int i = 0; i < values.Length; i++)values[i] = transformer (values[i]);
}

委托 vs. 接口

可以用委托解决的问题也可以用接口解决。例如,可以用一个叫做 ITransformer 的接口而不是委托来重写最初的例子:

public interface ITransformer
{int Transform (int x);
}public class Util
{public static void TransformAll (int[] values, ITransformer t){for (int i = 0; i < values.Length; i++)values[i] = t.Transform (values[i]);}
}class Squarer : ITransformer
{public int Transform (int x) => x * x;
}
...static void Main()
{int[] values = { 1, 2, 3 };Util.TransformAll (values, new Squarer());foreach (int i in values)Console.WriteLine (i);
}

如果下列条件中的一个或多个为真,委托设计可能是比接口设计更好的选择:

  • 该接口只定义了一个方法。
  • 需要多播能力。
  • 用户需要多次实现该接口。

在 ITransformer 示例中,不需要多播。但是,该接口只定义了一个方法。此外,用户可能需要多次实现 ITransformer,以支持不同的转换,例如平方或立方计算。对于接口,不得不为每个转换编写单独的类型,因为 Test 只能实现 ITransformer 一次。这相当麻烦:

class Squarer : ITransformer
{public int Transform (int x) => x * x;
}class Cuber : ITransformer
{public int Transform (int x) => x * x * x;
}
...static void Main()
{int[] values = { 1, 2, 3 };Util.TransformAll (values, new Cuber());foreach (int i in values)Console.WriteLine (i);
}

委托的兼容性

类型的兼容性

委托类型彼此不兼容,即使它们的签名相同。

delegate void D1();
delegate void D2();
...D1 d1 = Method1;
D2 d2 = d1; // Compile-time error

但是,下面语句是允许的:

D2 d2 = new D2 (d1);

如果委托实例具有相同的方法目标,则认为它们是相等的。

delegate void D();
...D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True

如果多播委托以相同的顺序引用相同的方法,则认为它们是相等的。

参数的兼容性

当调用一个方法时,可以提供比该方法的参数具有更特定类型的参数。这是普通的多态行为。出于完全相同的原因,委托可以拥有比它的方法目标更具体的参数类型。这就是所谓的逆变 (contravariance)。

下面是一个示例:

delegate void StringAction (string s);class Test
{static void Main(){StringAction sa = new StringAction (ActOnObject);sa ("hello");}static void ActOnObject (object o) => Console.WriteLine (o); // hello
}

与类型参数变体(variance)一样,委托仅对于引用转换(reference conversions)是协变的(variant)。

委托只是代表别人调用一个方法。在这种情况下,StringAction 被调用时,实参是 string 类型。当该实参随后被传递给目标方法时,该实参将隐式地向上转换为一个 object。

标准事件模式旨在通过使用公共 EventArgs 基类来帮助你利用逆变。例如,你可以让两个不同的委托调用一个方法,一个传递 MouseEventArgs,另一个传递 KeyEventArgs。

返回类型的兼容性

如果调用一个方法,则可能会得到一个比所要求的更具体的类型。这是普通的多态行为。出于完全相同的原因,委托的目标方法可能返回比委托描述的更具体的类型。这称为协变 (covariance)。例如:

delegate object ObjectRetriever();class Test
{static void Main(){ObjectRetriever o = new ObjectRetriever (RetrieveString);object result = o();Console.WriteLine (result); // hello}static string RetrieveString() => "hello";
}

ObjectRetriever 期望返回一个 object,但 object 子类也可以:委托返回类型是协变的 (covariant)。

泛型委托类型形参变体

在第3章中介绍了泛型接口如何支持协变和逆变类型形参。委托也存在相同的功能 (从C# 4.0开始)。

如果要定义泛型委托类型,最好是:

  • 将仅用于返回值的类型形参标记为协变 (out)。
  • 将任何仅用于形参的类型形参标记为逆变 (in)。

这样做允许转换通过遵循类型之间的继承关系而自然工作。

下面的委托 (定义在 System 命名空间中) 有一个协变的 TResult:

delegate TResult Func<out TResult>();

允许:

Func<string> x = ...;
Func<object> y = x;

下面的委托 (定义在 System 命名空间中) 有一个逆变的 T:

delegate void Action<in T> (T arg);

允许:

Action<object> x = ...;
Action<string> y = x;

【C# 7.0 in a Nutshell】目录

【C# 7.0 in a Nutshell】第4章 C#的高级特性——委托相关推荐

  1. 《C#3.0 in a Nutshell ,3rd Edition》之序言篇

    前言 最近在图书馆看到了<C#3.0核心技术>这本书,看了一下觉得挺不错的,C#3.0的技术从基础语法到新特性到涉及到了,是一部学习C#3.0的好书,本想从网上下载电子版却没找不到,却但看 ...

  2. 【C# 7.0 in a Nutshell】第3章 在C#中创建类型——类

    类 (Class) 是最常用的引用类型.最简单的类声明如下: class YourClassName {} 更复杂的类有如下选项: class关键字前面:属性(attributes)和类修饰符(cla ...

  3. 【C# 7.0 in a Nutshell】第2章 C#语言基础——数组

    数组是一个存储相同类型元素的固定大小的顺序集合. 在元素类型后面加上方括号表示数组: char[] vowels = new char[5]; // Declare an array of 5 cha ...

  4. ASP.NET2.0自定义控件组件开发 第六章 深入讲解控件的属性

    深入讲解控件的属性持久化(一) 系列文章链接: ASP.NET自定义控件组件开发 第一章 待续 ASP.NET自定义控件组件开发 第一章 第二篇 接着待续 ASP.NET自定义控件组件开发 第一章 第 ...

  5. 精通Web Analytics 2.0 (7) 第五章:荣耀之钥:度量成功

    精通Web Analytics 2.0 (7) 第五章:荣耀之钥:度量成功 精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第五章:荣耀之钥:度量成功 我们的分析师常常得不到我 ...

  6. 精通Web Analytics 2.0 (8) 第六章:使用定性数据解答”为什么“的谜团

    精通Web Analytics 2.0 (8) 第六章:使用定性数据解答"为什么"的谜团 精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第六章:使用定性数 ...

  7. 精通Web Analytics 2.0 (13) 第十一章:变身分析忍者的指导原则

    精通Web Analytics 2.0 (13) 第十一章:变身分析忍者的指导原则 精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第十一章:变身分析忍者的指导原则 这个激动人 ...

  8. 智能语音应用开发之DPL2.0高级特性

    自从智能屏上市以来,智能语音交互演化成了多模态智能交互,智能语音应用的开发与Web 开发越来越类似,开发者基于DuerOS研发智能语音技能的成本也相应地逐渐降低了.如果把基于模版的技能开发看作是静态的 ...

  9. 服务器电源管理系统SPM 价格,Liebert SPM 2.0服务器电源管理系统:在对比中彰显技术特性和性能优势...

    原标题:Liebert SPM 2.0服务器电源管理系统:在对比中彰显技术特性和性能优势 集机房配电.隔离.接地.监测.管理于一体; 通过界面显示既可及时了解繁杂IT负载的供电系统.主电源.断路器和分 ...

最新文章

  1. linux ip add em,使用iproute2为linux网关设置vlan
  2. 解决EXECL单元格不可以填充颜色
  3. 端午小长假--前端基础学起来02与浏览器交互,表单标签
  4. tcp client.cs
  5. HSF服务注册失败,项目启动后,EDAS列表无法发现注册的服务
  6. DOS 常用命令大全
  7. Rocky4.2下安装金仓v7数据库(KingbaseES)
  8. wps 宏 禁用_WPS宏被禁用如何打开
  9. 把mov格式的视频转换mp4步骤
  10. 计算机专业的英语六级很难过,英语六级多少分算过 通过率高吗
  11. windows下efi编译环境配置(EDK2)
  12. 【报错解决】错误代码18456,SQL Server 登录失败
  13. C语言中【变量】的存储类型共有4种类型
  14. android com.mylhyl,Android 高仿微信朋友圈拍照上传功能
  15. Unity 使用ContentSizeFitter刷新不及时的问题
  16. 论文复审意见及实验规划
  17. Easy Connect无法连接的情况,当前IE代理启用了自动配置脚本,不允许使用CS客户端登录
  18. BDC 弹窗录屏解决办法(ABUMN)
  19. 曹海涛:现在做投资只有一个要求,即使赚不到名声也要把利润赚到!
  20. 机器学习实验二---决策树python

热门文章

  1. 创新工具 | 如何以3×3增长模型矩阵驱动产品规模化增长
  2. 如何让研发管理更透明、可控、高效?
  3. 物理学十大著名经典实验!你知道几个?
  4. 简道云-第6章-仪表盘
  5. 前端使用js-file-download下载文件和下载视频音频
  6. vue 是什么?webpack 是什么?
  7. 《最简单的图形与最复杂的信息》读书浅悟
  8. 51单片机汇编学习例程(13)——DC-Motor篇
  9. eja变送器的选型考量因素
  10. opensuse leap 42.3更换国内源