20 委托

20.1 常规

委托声明定义了一个从类 System.Delegate 派生的类。 委托实例封装了一个调用列表,这是一个包含一个或多个方法的列表,每个方法都被称为可调用的实体。 对于实例方法,可调用的实体由实例和该实例上的方法组成。 对于静态方法,可调用的实体仅由一个方法组成。 使用一组适当的参数调用委托实例会导致使用给定的参数集调用委托的每个可调用的实体。

注意:委托实例的一个有趣且有用的属性是,它不知道或关心它封装的方法的类;重要的是,这些方法与委托的类型兼容 (§20.4)。 这使得委托非常适合“匿名”调用。 尾注

20.2 委托声明

delegate_declaration 是一个 type_declaration (§14.7),它声明了一个新的委托类型。

delegate_declaration
    : attributes? delegate_modifier* 'delegate' return_type delegate_header
    | attributes? delegate_modifier* 'delegate' ref_kind ref_return_type
      delegate_header
    ;

delegate_header
    : identifier '(' parameter_list? ')' ';'
    | identifier variant_type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause* ';'
    ;
    
delegate_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier§23.2 中定义。

同一修饰符在委托声明中多次出现是编译时错误。

提供 variant_type_parameter_list 的委托声明是泛型委托声明。 此外,嵌套在泛型类声明或泛型结构声明中的任何委托本身都是泛型委托声明,因为需要提供包含类型的类型实参才能创建构造类型 (§8.4)。

new 修饰符只允许在另一个类型中声明的委托上使用,在这种情况下,它指定这样的委托隐藏同名的继承成员,如 §15.3.5 中所述。

publicprotectedinternalprivate 修饰符控制委托类型的可访问性。 根据委托声明出现的上下文,可能不允许其中一些修饰符 (§7.5.2)。

委托的类型名称是标识符

与方法 (§15.6.1) 一样,如果存在 ref,则委托按引用返回;否则,如果 return_typevoid,则委托不返回值;否则,委托按值返回。

可选 parameter_list 指定委托的参数。

按值返回或不返回值委托声明的 return_type 指定委托返回的结果类型(如果有)。

按引用返回委托声明的 ref_return_type 指定委托返回的 variable_reference (§9.5) 引用的变量的类型。

可选 variant_type_parameter_list (§18.2.3) 指定委托本身的类型参数。

委托类型的返回类型应为 void 或输出安全的 (§18.2.3.2)。

委托类型的所有参数类型应为输入安全的 (§18.2.3.2)。 此外,任何输出参数或引用参数类型也必须是输出安全的。

注意:由于常见的实现限制,输出参数需要是输入安全的。 尾注

此外,委托的任何类型参数上的每个类类型约束、接口类型约束和类型参数约束都必须是输入安全的。

C# 中的委托类型是名称等效的,而不是结构等效的。

示例

delegate int D1(int i, double d);
delegate int D2(int c, double d);

委托类型 D1D2 是两种不同的类型;因此尽管它们的签名相同,但它们不可互换。

结束示例

与其他泛型类型声明一样,应提供类型参数来创建构造的委托类型。 构造的委托类型的参数类型和返回类型是通过将委托声明中的每个类型参数替换为所构造委托类型的相应类型参数来创建的。

声明委托类型的唯一方法是通过 delegate_declaration。 每个委托类型都是从 System.Delegate 派生的引用类型。 每种代表类型所需的成员详见 §20.3。 委托类型是隐式的 sealed,因此不允许从委托类型派生任何类型。 也不允许声明从 System.Delegate 派生的非委托类类型。 System.Delegate 本身不是委托类型;它是一个类类型,所有委托类型都从中派生。

20.3 委托成员

每个委托类型都继承了 Delegate 类的成员,如 §15.3.4 所述。 此外,每个委托类型都应提供一个非泛型的 Invoke 方法,其参数列表与委托声明中的 parameter_list 匹配,其返回类型与委托声明的 return_typeref_return_type 匹配,对于 returns-by-ref 委托,其返回类型匹配委托声明中的 ref_kindInvoke 方法应至少与包含委托类型一样可访问。 在委托类型上调用 Invoke 方法在语义上等同于使用委托调用语法 (§20.6) 。

实现可以在委托类型中定义其他成员。

除了实例化之外,任何可以应用于类或类实例的操作也可以分别应用于委托类或实例。 特别是,可以通过通常的成员访问语法访问 System.Delegate 类型的成员。

20.4 委托兼容性

如果以下所有条件都为 true,则方法或委托类型 M 与委托类型 D

  • DM 的参数数量相同,D 中的每个参数与 M 中的相应参数具有相同的按引用参数修饰符。
  • 对于每个值参数,存在从 中的参数类型到 中的相应参数类型的标识转换 (D) 或隐式引用转换 (M)。
  • 对于每个按引用参数,D 中的参数类型与 M 中的相同。
  • 下列情况之一存在:
    • DMreturns-no-value
    • DM 是按值返回(§15.6.1§20.2),并且存在从 M 的返回类型到 D 的返回类型的标识或隐式引用转换。
    • DM 都是按引用返回,M 的返回类型和 D 的返回类型之间存在标识转换,并且它们都具有相同的 ref_kind

这种兼容性定义允许返回类型中的协变和参数类型中的逆变。

示例

delegate int D1(int i, double d);
delegate int D2(int c, double d);
delegate object D3(string s);

class A
{
    public static int M1(int a, double b) {...}
}

class B
{
    public static int M1(int f, double g) {...}
    public static void M2(int k, double l) {...}
    public static int M3(int g) {...}
    public static void M4(int g) {...}
    public static object M5(string s) {...}
    public static int[] M6(object o) {...}
}

方法 A.M1B.M1 与委托类型 D1D2 兼容,因为它们具有相同的返回类型和参数列表。 方法 B.M2B.M3B.M4 与委托类型 D1D2 不兼容,因为它们具有不同的返回类型或参数列表。 方法 B.M5B.M6 两者都与委托类型 D3 兼容。

结束示例

示例

delegate bool Predicate<T>(T value);

class X
{
    static bool F(int i) {...}
    static bool G(string s) {...}
}

方法 X.F 与委托类型 Predicate<int> 兼容;方法 X.G 与委托类型 Predicate<string> 兼容。

结束示例

注意:委托兼容性的直观含义是,如果委托的每次调用都可以用方法的调用替换,而不会违反类型安全,将可选参数和参数数组视为显式参数,则方法与委托类型兼容。 例如,在以下代码中:

delegate void Action<T>(T arg);

class Test
{
    static void Print(object value) => Console.WriteLine(value);

    static void Main()
    {
        Action<string> log = Print;
        log("text");
    }
}

方法 PrintAction<string> 委托类型兼容,因为对 Action<string> 委托的任何调用都是对 Print 方法的有效调用。

如果上述方法的 Print 签名更改为 Print(object value, bool prependTimestamp = false) 例如,该方法 Print 将不再与此 Action<string> 子句的规则兼容。

尾注

20.5 委托实例化

委托的实例可以通过 delegate_creation_expression (§12.8.17.5) 创建、转换为委托类型、结合多个委托或删除委托。 然后,新创建的委托实例引用一个或多个委托实例:

  • delegate_creation_expression 中引用的静态方法,或
  • null 中引用的目标对象(不能是 )和实例方法,或
  • 另一位代表(§12.8.17.5)。

示例

delegate void D(int x);

class C
{
    public static void M1(int i) {...}
    public void M2(int i) {...}
}

class Test
{
    static void Main()
    {
        D cd1 = new D(C.M1); // Static method
        C t = new C();
        D cd2 = new D(t.M2); // Instance method
        D cd3 = new D(cd2);  // Another delegate
    }
}

结束示例

委托实例封装的方法集称为调用列表。 当从单个方法创建委托实例时,它封装了该方法,其调用列表仅包含一个条目。 但是,当两个非 null 委托实例组合在一起时,它们的调用列表会按照左操作数然后右操作数的顺序连接起来,形成一个新的调用列表,其中包含两个或多个条目。

从单个委托创建新委托时,生成的调用列表只有一个条目,即源委托(§12.8.17.5)。

委托使用二进制 + (§12.10.5) 和 += 运算符 (§12.21.4) 进行组合。 可以使用二进制 - (§12.10.6) 和 -= 运算符 (§12.21.4) 从委托组合中删除委托。 可以比较委托相等性 (§12.12.9)。

示例:以下示例显示了多个委托的实例化及其相应的调用列表:

delegate void D(int x);

class C
{
    public static void M1(int i) {...}
    public static void M2(int i) {...}
}

class Test
{
    static void Main() 
    {
        D cd1 = new D(C.M1); // M1 - one entry in invocation list
        D cd2 = new D(C.M2); // M2 - one entry
        D cd3 = cd1 + cd2;   // M1 + M2 - two entries
        D cd4 = cd3 + cd1;   // M1 + M2 + M1 - three entries
        D cd5 = cd4 + cd3;   // M1 + M2 + M1 + M1 + M2 - five entries
        D td3 = new D(cd3);  // [M1 + M2] - ONE entry in invocation
                             // list, which is itself a list of two methods.
        D td4 = td3 + cd1;   // [M1 + M2] + M1 - two entries
        D cd6 = cd4 - cd2;   // M1 + M1 - two entries in invocation list
        D td6 = td4 - cd2;   // [M1 + M2] + M1 - two entries in invocation list,
                             // but still three methods called, M2 not removed.
   }
}

当实例化 cd1cd2 时,它们各自封装一个方法。 当实例化 cd3 时,它有一个按顺序包含 M1M2 两个方法的调用列表。 cd4 的调用列表按该顺序包含 M1M2M1。 对于 cd5,调用列表按该顺序包含 M1M2M1M1M2

当从具有 delegate_creation_expression 的另一个委托创建委托时,结果具有与原始结构不同的调用列表,但这会导致以相同的顺序调用相同的方法。 当从 td3 创建 cd3 时,其调用列表只有一个成员;但该成员是方法 M1M2 的列表,这些方法由 td3 以与 cd3 相同的顺序调用。 同样,当实例化 td4 时,它的调用列表只有两个条目,但它按照与 M1 相同的顺序调用了三个方法 M2M1cd4

调用列表的结构会影响委托减法。 委托 cd6 是通过从 cd2(调用 M2cd4M1)中减去 M2(调用 M1)而创建的,它调用 M1M1。 然而,通过从 td6(调用 cd2M2td4)中减去 M1(调用 M2)创建的委托 M1 仍然按顺序调用 M1M2M1,因为 M2 不是列表中的单个条目,而是嵌套列表的成员。 有关组合(以及删除)委托的更多示例,请参阅 §20.6

结束示例

一旦实例化,委托实例总是引用相同的调用列表。

注意:请记住,当两个委托组合在一起,或者一个委托从另一个委托中删除时,会产生一个具有自己调用列表的新委托;合并或删除的委托的调用列表保持不变。 尾注

20.6 委托调用

C# 为调用委托提供了特殊的语法。 当调用其调用列表包含一个条目的非 null 委托实例时,它调用具有相同参数的一个方法,并返回与引用的方法相同的值。 有关委托调用的详细信息,请参阅 §12.8.10.4。如果在调用此类委托期间发生异常,且调用的方法中未捕获该异常,则会在调用该委托的上层方法中继续寻找异常捕获子句,好像上层方法直接调用了委托所引用的方法一样。

调用一个其调用列表中包含多个条目的委托实例的过程,是按顺序同步调用调用列表中的每个方法。 每个这样调用的方法都传递给委托实例相同的参数集。 如果此类委托调用包含引用参数 (§15.6.2.3.3),则每个方法调用将使用对同一变量的引用进行;调用列表中的一个方法对该变量所做的更改将对调用列表下游的方法可见。 如果委托调用包括输出参数或返回值,则其最终值将来自列表中最后一个委托的调用。 如果在处理此类委托的调用过程中发生异常,并且该异常未在所调用的方法中捕获,则在调用委托的方法中继续搜索异常捕获子句,并且不会调用调用列表后面的任何方法。

尝试调用值为 null 的委托实例会导致类型为 System.NullReferenceException 的异常。

示例:以下示例演示如何实例化、合并、删除和调用委托:

delegate void D(int x);

class C
{
    public static void M1(int i) => Console.WriteLine("C.M1: " + i);

    public static void M2(int i) => Console.WriteLine("C.M2: " + i);

    public void M3(int i) => Console.WriteLine("C.M3: " + i);
}

class Test
{
    static void Main()
    {
        D cd1 = new D(C.M1);
        cd1(-1);             // call M1
        D cd2 = new D(C.M2);
        cd2(-2);             // call M2
        D cd3 = cd1 + cd2;
        cd3(10);             // call M1 then M2
        cd3 += cd1;
        cd3(20);             // call M1, M2, then M1
        C c = new C();
        D cd4 = new D(c.M3);
        cd3 += cd4;
        cd3(30);             // call M1, M2, M1, then M3
        cd3 -= cd1;          // remove last M1
        cd3(40);             // call M1, M2, then M3
        cd3 -= cd4;
        cd3(50);             // call M1 then M2
        cd3 -= cd2;
        cd3(60);             // call M1
        cd3 -= cd2;          // impossible removal is benign
        cd3(60);             // call M1
        cd3 -= cd1;          // invocation list is empty so cd3 is null
        // cd3(70);          // System.NullReferenceException thrown
        cd3 -= cd1;          // impossible removal is benign
    }
}

如语句 cd3 += cd1; 所示,委托可以多次出现在调用列表中。 在本例中,每次出现仅调用一次。 在这样的调用列表中,当该委托被删除时,调用列表中的最后一个事件就是实际删除的那个。

在执行最后一条语句 cd3 -= cd1 之前;委托 cd3 引用一个空调用列表。 尝试从空列表中删除委托(或从非空列表中删除不存在的委托)不会出错。

生成的输出为:

C.M1: -1
C.M2: -2
C.M1: 10
C.M2: 10
C.M1: 20
C.M2: 20
C.M1: 20
C.M1: 30
C.M2: 30
C.M1: 30
C.M3: 30
C.M1: 40
C.M2: 40
C.M3: 40
C.M1: 50
C.M2: 50
C.M1: 60
C.M1: 60

结束示例