首页 > 编程笔记 > C#笔记 阅读:2

C#委托用法详解(附带实例)

C# 中,委托(delegate)是一种知道如何调用方法的对象。

委托类型(delegate type)定义了一类可以被委托实例(delegate instance)调用的方法。具体来说,它定义了方法的返回类型(return type)和参数类型(parameter type)。

以下语句定义了一个委托类型 Transformer:
delegate int Transformer(int x);

Transformer 兼容任何具有 int 返回类型和单个 int 类型参数的方法,例如:
int Square(int x){return x * x};
或者可以简洁地写为:
int Square(int x) => x * x;

将一个方法赋值给一个委托变量就能创建一个委托实例:
Transformer t = Square;

可以像调用方法一样调用委托实例:
int answer = t(3);   // 9

以下是一个完整的例子:
Transformer t = Square;
int result = t(3);
Console.WriteLine(result);       // 9

int Square(int x) => x * x;
delegate int Transformer(int x);

委托实例字面上是调用者的代理,调用者调用委托,而委托调用目标方法。这种间接调用方式可以将调用者和目标方法解耦。以下语句:
Transformer t = Square;
是下面语句的简写:
Transformer t = new Transformer(Square);

注意,从技术上讲,当引用没有括号和参数的 Square 方法时,我们指定的是一组方法。如果该方法被重载,C# 会根据赋值委托的签名选择正确的重载方法。

语句 t(3) 是下面语句的简写:
t.Invoke(3);

委托和回调(callback)类似,一般指类似 C 函数指针的结构。

用委托书写插件方法

委托变量可以在运行时指定一个目标方法,这个特性可用于编写插件方法。

在本例中有一个名为 Transform 的公共方法,它对整数数组的每一个元素进行变换。Transform 方法接受一个委托参数并以此为插件方法执行变换操作:
int[] values = { 1, 2, 3 };
Transform(values, Square);   // Hook in the Square method

foreach (int i in values)
    Console.Write(i + " ");  // 1 4 9

void Transform(int[] values, Transformer t)
{
    for (int i = 0; i < values.Length; i++)
        values[i] = t(values[i]);
}

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

delegate int Transformer(int x);
只需将上述程序第二行中的 Square 改为 Cube 即可更改变换的类型。

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

C#实例方法目标与静态方法目标

委托的目标方法可以是局部方法、静态方法或实例方法。以下代码中的目标方法为静态方法:
Transformer t = Test.Square;
Console.WriteLine(t(10));   // 100

class Test
{
    public static int Square(int x) => x * x;
}

delegate int Transformer(int x);

而下列代码中的目标方法是实例方法:
Test test = new Test();
Transformer t = test.Square;
Console.WriteLine(t(10));   // 100

class Test
{
    public int Square(int x) => x * x;
}

delegate int Transformer(int x);

当把实例方法赋值给委托对象时,委托对象不仅会维护方法的引用,还会保护方法所在对象的实例。System.Delegate 类的 Target 属性代表了方法所在对象的实例(如果引用的是静态方法,则该属性的值为 null)。例如:
MyReporter r = new MyReporter();
r.Prefix = "%Complete:";
ProgressReporter p = r.ReportProgress;
p(99);   // 输出:%Complete:99
Console.WriteLine(p.Target == r);   // 输出:True
Console.WriteLine(p.Method);        // 输出:Void ReportProgress(Int32)
r.Prefix = "";
p(99);   // 输出:99

public delegate void ProgressReporter(int percentComplete);

class MyReporter
{
    public string Prefix = "";   // 前缀字符串
    public void ReportProgress(int percentComplete)
        => Console.WriteLine(Prefix + percentComplete); // 输出前缀+进度值
}
由于委托的 Target 属性存储了实例的引用,因此实例的生存期(至少)会延长到与委托生存期一样长。

C#多播委托

所有的委托实例都拥有多播能力,这意味着一个委托实例可以引用一个目标方法,也可以引用一组目标方法。委托可以使用 + 和 += 运算符联结多个委托实例。例如:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
最后一行等价于:
d = d + SomeMethod2;
现在调用 d 不仅会调用 SomeMethod1 而且会调用 SomeMethod2。委托会按照添加的顺序依次触发。

- 和 -= 运算符会从左侧委托操作数中将右侧委托操作数删除。例如:
d -= SomeMethod1;   // 删除 SomeMethod1
现在,调用 d 只会触发 SomeMethod2 调用。

对值为 null 的委托变量进行 + 或者 += 操作等价于为变量指定一个新的值:
SomeDelegate d = null;
d += SomeMethod1;
同样,在只有唯一目标方法的委托上调用 -= 等价于为该变量指定 null 值。

注意,委托是不可变的,因此调用 += 和 -= 的实质是创建一个新的委托实例,并把它赋值给已有变量。

如果一个多播委托拥有非 void 的返回类型,则调用者将从最后一个触发的方法接收返回值。前面的方法仍然调用,但是返回值都会被丢弃。大部分调用多播委托的情况都会返回 void 类型,因此这个细小的差异就不存在了。

此外,所有的委托类型都是从 System.MulticastDelegate 类隐式派生的。而 System.MulticastDelegate 继承自 System.Delegate。C# 将委托中的 +、-、+= 和 -= 运算符都编译成了 System.Delegate 的静态 Combine 和 Remove 方法。

【实例】若方法的执行时间很长,那么可以令该方法定期调用一个委托向调用者报告进程的执行情况。例如,在以下代码中,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);
            System.Threading.Thread.Sleep(100);  // Simulate work
        }
    }
}

为了监视进度,我们在 Main 方法中创建了一个多播委托实例 p。这样就可以通过两个独立的方法监视执行进度了:
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile;
Util.HardWork(p);

void WriteProgressToConsole(int percent) =>
    Console.WriteLine(percent);

void WriteProgressToFile(int percent) =>
    File.WriteAllText("progress.txt", percent.ToString());

C#泛型委托类型

委托类型可以包含泛型类型参数,例如:
public delegate T Transformer<T>(T arg);
根据上面的定义,可以写一个通用的 Transform 方法,让它对任何类型都有效:
int[] values = { 1, 2, 3 };
Util.Transform(values, Square);
foreach (int i in values) Console.Write(i + " "); // 1 4 9

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

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]);
    }
}

C# Func和Action委托

有了泛型委托,我们就可以定义出一些非常通用的小型委托类型,它们可以具有任意的返回类型和(合理的)任意数目的参数。这些小型委托类型就是定义在 System 命名空间下的 Func 和 Action 委托。
delegate TResult Func <out TResult> Func();
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 T16

delegate 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

这些委托都是非常通用的委托。前面例子中的 Transform 委托就可以用一个带有 T 类型参数并返回 T 类型值的 Func 委托代替:
public static void Transform<T>(T[] values, Func<T,T> transformer)
{
    for (int i = 0; i < values.Length; i++)
        values[i] = transformer(values[i]);
}
这些委托中没有涉及的场景只有 ref/out 和指针参数了。

注意,在 C# 诞生之初,并不存在 Func 和 Action 委托(因为那个时候还不存在泛型)。由于这个历史问题,所以 .NET 中很多代码都使用自定义委托类型,而不是 Func 和 Action。

C#委托和接口

能用委托解决的问题,都可以用接口解决。例如,下面的 ITransformer 接口可以代替委托解决前面例子中的问题:
int[] values = { 1, 2, 3 };
Util.TransformAll(values, new Squarer());
foreach (int i in values) Console.WriteLine(i); // 1 4 9

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;
}
如果以下一个或多个条件成立,委托可能是比接口更好的选择:
虽然在 ITransformer 的例子中不需要多播,但接口仅仅定义了一个方法,而且订阅者有可能为了支持不同的变换(例如平方或立方变换)需要多次实现 ITransformer 接口。如果使用接口,由于一个类只能实现一次 ITransformer,因此我们必须对每一种变换编写一个新的类型。这样做很麻烦:
int[] values = { 1, 2, 3 };
Util.TransformAll(values, new Cuber());
foreach (int i in values)
    Console.WriteLine(i);

class Squarer : ITransformer
{
    public int Transform (int x) => x * x;
}

class Cuber : ITransformer
{
    public int Transform (int x) => x * x * x * x;
}

C#委托的兼容性

1) 类型的兼容性

即使签名相似,委托类型也互不兼容:
D1 d1 = Method1;
D2 d2 = d1; // 编译错误
void Method1(){ }
delegate void D1();
delegate void D2();
但是以下写法是有效的:
D2 d2 = new D2(d1);     // 正确

如果委托实例指向相同的目标方法,则认为它们是相等的:
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2);    // 正确

void Method1() { }
delegate void D();
如果多播委托按照相同的顺序引用相同的方法,则它们是相等的。

2) 参数的兼容性

当调用方法时,可以给方法的参数提供更加特定的变量类型,这是正常的多态行为。基于同样的原因,委托也可以有比它的目标方法参数类型更具体的参数类型,这称为逆变。例如:
StringAction sa = new StringAction(ActionObject);
sa("hello");

void ActionObject(object o) => Console.WriteLine(o);  // hello

delegate void StringAction(string s);
和类型参数的可变性一样,委托的可变性仅适用于引用转换。

委托仅仅替其他人调用方法。在本例中,在调用 StringAction 时,参数类型是 string。当这个参数传递给目标方法时,参数隐式向上转换为 object。

注意,标准事件模式的设计宗旨是通过使用公共的 EventArgs 基类来利用逆变特性。例如,可以用两个不同的委托调用同一个方法,一个传递 MouseEvent-Args,而另一个则传递 KeyEventArgs。

3) 返回类型的兼容性

调用一个方法时可能得到比请求类型更特定的返回值类型,这也是正常的多态行为。基于同样的原因,委托的目标方法可能返回比委托声明的返回值类型更加特定的返回值类型,这称为协变。例如:
ObjectRetriever o = new ObjectRetriever(RetrieveString);
object result = o();
Console.WriteLine(result);  // hello

string RetrieveString() => "hello";
delegate object ObjectRetriever();
ObjectRetriever 期望返回一个 object。但若返回 object 子类也是可以的,这是因为委托的返回类型是协变的。

4) 泛型委托类型的参数变化

泛型接口支持协变和逆变参数类型,委托也具有相同的功能。

如果我们要定义一个泛型委托类型,那么建议参考如下的准则:
这样可以依照类型的继承关系自然地进行类型转换。

以下(在 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;

相关文章