委托
委托(delegate)是一种知道如何调用方法的对象。
委托类型(delegate type)定义了一种委托实例(delegate instance)可以定义的方法。具体来说,它定义了方法的返回类型(return type)和参数类型(parameter type)。
-
以下语句定义了一个委托类型
Transformer
:定义委托delegate int Transformer(int x);
Transformer
兼容任何返回类型为int
并有一个int
类型的参数的方法,如:static int Square(int x) { return x * x; } // static int Square(int x) => x * x;
-
将一个方法赋值给一个委托变量就是创建一个委托实例:实例委托
Transformer t = Square;
-
就可以像调用方法一样调用:调用委托
int answer = t(3); // answer is 9
-
以下是一个完整的例子:
delegate int Transformer(int x); static int Square(int x) => x * x; class Test { static void Main() { Transformer t = Square; // Create delegate instance int result = t(3); // Invoke delegate Console.WriteLine(result); // 9 } }
委托实例字面上是调用者的代理:调用者调用委托,而委托调用目标方法。这种间接调用方式可以将调用者和目标方法解耦。
以下语句:
Transformer t = Square;
t(3);
是下面语句的简写:
Transformer t = new Transformer(Square);
t.Invoke(3);
🌱 技术上,当引用没有括号和参数的 Square
方法第,我们指定的是一组方法。如果该方法被重载,csharp会根据赋值委托的签名选择正确的重载方法。
🌾 委托和回调(callback)类似,一般指捕获类似 C 函数指针的结构。
🌴 使用委托
- 相当于用方法作为另一方法的参数进行传递(类似于 C 的函数指针)。
- 在两个不能直接调用的方法中作为桥梁,如在多线程中的跨线程的方法调用就要用委托。
- 当不知道方法具体实现什么时使用委托,如事件中使用委托。
💦 delegate 是委托,本身不能解决跨线程访问控件的问题,直接调用委托还是会报错。
Invoke 指定用主线程中的控件去调用这个委托,相当于主线程来执行这个函数,因此在委托的,目标方法中,需要使用:
theDelegate += OnDelegateMethod; // 其他线程的 theDelegate 委托绑定主线程 OnDelegateMethod
private void OnDelegateMethod(string args)
{
if (this.InvokeRequired)
{
this.BeginInvoke(theDelegate, new object[] { args });
return;
}
... // 处理
}
1 用委托书写插件方法 Plug-in Methods
方法是在运行时才赋值给委托变量的,这个特性可用于编写插件方法。
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 method
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
values = new int[]{1, 2, 3};
Util.Transform (values, Cube); // Hook in the Cube method
foreach (int i in values)
Console.Write (i + " "); // 1 8 27
}
static int Square(int x) => x * x;
static int Cube(int x) => x * x * x;
}
本例中有一个名为 Transform
的公共方法,它对整数数组的每一个元素进行变换。Transform
方法接受一个委托参数并以此为插件方法执行变换操作。
Transform
方法是一个高阶函数(higher-order function),因为它是一个以函数作为参数的函数。(返回委托的方法也称为高阶函数)。
2 多播委托 Multicast Delegates
-
所有的委托实例都拥有多播实例。这意味着一个委托实例可以引用一个目标方法,也可以引用一组目标方法。
-
委托可以使用
+
和+=
运算符联结多个委托实例。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
值。 -
如果一个多播委托拥有非
void
的返回类型,则调用者将从最后一个触发的方法接收返回值。前面的方法仍然调用,但是返回值都会被丢弃。大部分多播委托的情况都会返回void
类型,因此这个细小的差别就没有了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lh9oGDpa-1598252782375)(_v_images/20200818152546239_10266.png =121x)]
-
🌰 1
public delegate int Transformer(int x); static int Square(int x) { int result = x * x; Console.WriteLine(result); return result; } static int Cube(int x) { int result = x * x * x; Console.WriteLine(result); return result; } class Test { static void Main() { Transformer t = null; t += Square; t += Cube; // t 先调用 Square,在调用 Cube,输出:9 27 Console.WriteLine(t(3)); // 27,接收最后一个返回值 } }
-
🌰 2
若方法的执行时间很长,且该方法定期调用一个委托向调用者报告进程的执行情况。例如,在以下代码中,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 delegate 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()); }
🌴 委托是不可变的,因此调用 +=
和 -=
的实质是创建一个新的委托实例,并把它赋值给已有变量。
🌴 所有的委托类型都是从 System.MulticastDelegate
类隐式派生的。而 System.MulticastDelegate
继承自 System.Delegate
。csharp 将委托中的 +
、-
、+=
、-=
运算符都编译成了 System.Delegate
的静态方法 Combine
和 Remove
。
3 实例目标方法和静态目标方法
委托实例引用一组目标方法,该方法可以是某个实例的方法或者静态方法。
- 将一个实例方法赋值给委托对象时,这个委托对象不仅要保留对方法的引用,还要保留方法所属实例的引用。
System.Delegate
类的Target
属性就代表这个实例。 - 如果委托引用的是一个静态方法,则该属性的值为
null
。
public delegate void ProgressReporter(int percentComplete);
class Test
{
static void Main()
{
X x = new X();
ProgressReporter p = x.InstanceProgress;
p(99); // 99
Console.WriteLine(p.Target == x); // True
Console.WriteLine(p.Method); // Void InstanceProgress(Int32)
}
}
class X
{
public void InstanceProgress(int percentComplete)
=> Console.WriteLine(percentComplete);
}
4 泛型委托类型
委托类型可以包含泛型类型参数。例如:
public delegate T Transformer<T>(T arg);
根据上面的定义,可以写一个通用的 Transform
方法,让它对任何类型都有效:
// 定义委托类型
public delegate T Transformer<T>(T arg);
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 };
string[] strings = {"4", "5", "6"};
Util.Transform (values, IntSquare); // Hook in IntSquare
foreach (int i in values)
Console.Write(i + " "); // 1 4 9
Util.Transform(strings, StringSquare); // Hook in StringSquare
foreach (string s in strings)
Console.Write(s + " "); // 16 25 36
}
// 定义委托引用的目标方法
static int IntSquare(int x) => x * x;
static string StringSquare(string x)
{
try
{
return (Convert.ToDouble(x) * Convert.ToDouble(x)).ToString();}
}
catch (Exception ex)
{
Console.WriteLine(ex);
return "Error!";
}
}
}
5 Func 和 Action 委托
使用泛型委托,就可以写出这样一组委托类型,它们调用的方法可以具有任意的返回类型和(合理的)任意数量的参数。
定义在 System
命名空间下的 Func
和 Action
委托:
Func
委托有TResult
类型参数的返回值。Action
委托没有返回值。
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 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]);
}
🌳 在 .Net Framework 2.0 之前并不存在 Func
和 Action
委托(因为那个时候还不存在泛型)。由于这个历史问题,所以 Framework 里面很多代码都是用自定义委托,而不是 Func
和 Action
委托。
以上纯是废话。🐴
6 委托和接口
能用委托解决的问题,都能用接口解决。例如,下面的 ITransformer
接口可以替委托解决前面例子的问题:
// 定义 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]);
}
}
// 定义类实现 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 Squarer());
foreach (int i in values)
Console.WriteLine(i);
values = new int[] { 1, 2, 3 };
Util.TransformAll(values, new Cuber());
foreach (int i in values)
Console.WriteLine(i);
}
在以下情况委托是更好的选择;
- 接口内仅定义了一个方法
- 需要多播能力
- 订阅者需要多次实现接口
虽然在 ITransformer
的例子中不需要多播,但接口仅仅定义了一个方法,而订阅者为了支持不同的变换(平方、立方等)需要多次实现 ITransformer
接口。由于使用接口,就必须对每一种变换编写一个新的类型,这样就很麻烦。
7 委托的兼容性 Compatibility
7.1 类型的兼容性
-
即使签名相似,委托类型也互不兼容:
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
-
如果多播委托按照相同的顺序引用相同的方法,则认为它们是等价的。
7.2 参数的兼容性
当调用方法时,可以将方法的参数提供更加具体(more specific)的变量类型,这是正常的多态行为。同样,委托可以有比它的目标方法参数类型更具体的参数类型,称为逆变(Contravariance)。太 🗼 🐴 绕口了。
delegate void StringAction(string s);
class Test
{
static void Main()
{
// ActOnObject 对应的应该是 StringAction(object o),
// 发生逆变转换为 StringAction(string s),
// 嗯,我觉得是这样,md 好像不是这样
StringAction sa = new StringAction(ActOnObject);
sa("hello");
}
static void ActOnObject(object o) => Console.WriteLine(o); // hello
}
委托仅替他人调用方法,本例中,在调用 StringAction
时,参数类型是 string
,当这个参数传递给目标方法时,参数隐式向上转换为 object
。
🌴 目标方法的参数类型是 A 类,传入目标方法的参数类型可以是 A 的父类,因为委托的参数类型是逆变的。
🌴 和类型参数的可变性一样,委托的可变性仅适用于引用转换。
🐳 标准事件模式的设计宗旨是通过使用公共的 EventArgs
基类来利用逆变特性。例如,可以用两个不同的委托调用同一个方法,一个传递 MouseEventArgs
而另一个传递 KeyEventArgs
。
7.3 返回类型的兼容性
调用一个方法时可能得到比请求类型更具体的返回值类型,这是正常的多态行为。同样,委托的目标方法可以返回比委托声明的返回值更加具体的返回值类型,称之为协变(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
的子类也是可以的,因为委托的返回类型是协变的。
7.4 泛型委托类型的参数可变性
-
泛型委托类型支持协变和逆变类型参数。
-
定义泛型委托类型参考以下准则:
- 将只用于返回值类型的类型参数标记为协变
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;