原则24:使用委托来表达回调
我:“儿子,到院子去除草。要去看会书。”
Scott:“爸爸,我清理好院子了。”
Scott:“爸爸,我已经把草放在除草机上了。”
Scott:“爸爸,除草机不能启动。”
我:“让我来启动它。”
Scott:“爸爸,我已经做完了。”
这个简单的交互展示了回调。我给儿子一个任务,它(重复)报告他的状态以打断我。而当我在等待他完成任务的每一个部份时,我不用阻塞我自己的进程。他可以在有重要(或者事件)状态报告时或者向我询求帮助时,可以定时的打断我,。回调就是用于异步的提供服务器与客户之间的信息反馈。它们可能在多线程中,或者可能是简单的提供一个同步更新点。在 C# 语言里是用委托来表达回调的。
委托提供类型安全的回调定义。虽然大多数是作为事件使用,那不是 C# 语言唯一使用这个特性的地方。任何时候,如果你想在两个类之间进行通信,而你又期望比使用接口有更少的偶合性,那么委托是你正确的选择。委托可以让你在运行时确定目标并且通知用户。委托就是包含了某些方法的引用的对象。这些方法可以是静态方法,也可以是实例方法。使用委托,你可以在运行时确定与一个或者多个客户对象进行交互。
回调和委托是 C# 语言常见的习惯,组合 lambda 表达式语法的以表达委托。此外,.NET 框架定义了许多常见的委托形式,如 Predicate<T> , Action<> 和 Func<> 。Predicate<T> 是测试条件的布尔函数。 Func<> 传入多个参数并产生一个单一的结果。是的,这意味着 Func<T,bool> 和 Predicate<T> 是相同的形式。尽管编译器不会将 Predicate<T> 和 Func<T,bool> 看做相同的。最后, Action<> 有多个参数并且有 void 的返回值类型。
LINQ 就是由这些概念构建出来的。 List<T> 类也包含很多使用回调的方法。看下面的代码:
List<int> numbers = Enumerable.Range(1, 200).ToList();
var oddNumbers = numbers.Find(n => n % 2 == 1);
var test = numbers.TrueForAll(n => n < 50);
numbers.RemoveAll(n => n % 2 == 0);
numbers.ForEach(item => Console.WriteLine(item));
Find() 方法传入一个委托,形式为 Predicate<int> 以检查队列中的每个元素。它是很简单的回调。 Find() 方法使用回调对每个元素进行检查,并且返回通过谓词测试的元素。编译器会将 lambda 表达式,转换为委托,并使用委托表达回调。
TrueForAll() 同样它检查每个元素,而且返回谓词为 true 的项。 RemoveAll() 移除那些谓词为 true 的项以修改队列。
最后, List.ForEach() 方法对队列的元素执行指定的动作。和前面一样,编译器转换 lambda 表示为方法并创建委托引用这个方法。
你会在 .NET 框架中发现很多这样的例子。所有 LINQ 都是构建与委托之上的。回调函数是用来处理在 WPF 和 Window Form 上的多线程编程。.NET 框架需要很简单方法的地方,它会使用委托,即调用者可以用 lambda 表达式来表达。当你需要在 API 中需要回调约定可以遵循这个例子的做法。
由于历史原因,所有委托都是多播委托。多播委托会一次调用所有添加的目标函数。有两点需要注意的:如果有异常是不安全的,而且最后执行的目标函数的返回值会作为回调的返回值。
在多播委托调用的内部,每个目标对象会被连续调用。委托不捕捉任何异常。因此,任何异常的抛出将终止委托链的调用。
返回值存在一个相似的问题。你可以定义委托有返回类型不是 void 。你可以写一个回调来检查用户中止:
public void LengthyOperation(Func<bool> pred)
{
foreach (ComplicatedClass cl in container)
{
cl.DoLengthyOperation();
// Check for user abort:
if (false == pred())
return;
}
}
单一委托是可以正常工作的,但是对于多播是有问题的:
Func<bool> cp = () => CheckWithUser();
cp += () => CheckWithSystem();
c.LengthyOperation(cp);
委托调用的返回值是多播链最后一个函数调用的返回值。所有其他返回值都会被忽略。CHeckWithUser() 位置的返回值是会被忽略的。
你可以通过自己调用目标委托解决这个问题。每个创建的委托包含一个委托队列。自己检查这个队列并遍历调用:
public void LengthyOperation2(Func<bool> pred)
{
bool bContinue = true;
foreach (ComplicatedClass cl in container)
{
cl.DoLengthyOperation();
foreach (Func<bool> pr in pred.GetInvocationList())
bContinue &= pr();
if (!bContinue)
return;
}
}
在这个例子,我已经定义了每个委托返回值必须是 true 才会继续执行的语义。
委托提供在运行时利用回调的最好方式,满足简单的客户类上的需求。你可以在运行时确定委托目标。你可以支持多个回调目标。客户端回调需要使用 .NET 的委托实现。
小结:
使用委托来表达回调。
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!