委托应该在哪些常见情况下使用?涉及到IT技术方面的问题。

13
我了解委托和事件是如何工作的。我也可以想象一些常见情景,在这些情况下我们应该实现事件,但我很难理解在什么情况下应该使用委托。
谢谢。
回复用户KVB的帖子:
a)
你基本上可以在任何你本来会使用一个单方法接口的地方使用委托。
我认为我有点理解以下内容:
- 类C可以定义方法C.M,该方法将接受一个接口IM作为参数。此接口将定义一个方法IM.A,因此想要调用C.M的任何人都需要实现此接口。 - 或者,方法C.M可以接受(而不是接口IM)具有与方法IM.A相同签名的委托D作为参数。
但我不理解的是,即使我们的接口IM除了方法A之外还定义了几个其他方法,为什么C.M不能使用委托D作为其参数呢?因此,类C的其他方法可以要求一个接口IM作为它们的参数,但C.M可以要求一个委托D(假设C.M只需要调用方法A而不需要调用IM中定义的任何其他方法)?
b)
var list = new List<int>(new[] { 1, 2, 3 });
var item = list.Find(i => i % 2 == 0);
  • 上述代码是否是用户jpbochi在本主题帖中所称的依赖注入的示例?

  • 我假设以上代码不能使用事件而不是“纯”委托来实现,对吗?


1
依赖注入是允许调用者控制被调用者所执行的操作。在典型的方法调用方式中,调用者传递一组参数,然后被调用者执行它认为适当的操作。交叉点仅在方法的开始和结束时(在纯粹的意义上)。然而,通过提供一个委托,被调用者必须在中间向调用者询问如何处理某种情况,在这种情况下,被调用者处理枚举,但调用者必须有效地处理过滤。 - Guvante
2
实际上,您总是可以用事件替换委托。理论上,您可以有一个使用筛选表达式种子的事件,并在查找查找期间调用它。但是,现在您为调用者引入了额外的工作,特别是他们必须设置和拆除事件以避免奇怪的行为。更不用说可能出现的线程问题了。通过使用委托,可以避免这些问题并使代码更易读。另一方面,从实现的角度来看,事件只是保存的委托,没有其他东西。 - Guvante
a) “交叉点只在方法的开头和结尾(在纯粹的意义上)”,这里的“交叉点”是什么意思?b) 另一方面,事件只是保存委托的东西,从实现的角度来看,并没有其他超出此范围的东西。这让我感到困惑——我如何决定是否需要事件的安全性,或者是否不需要该安全性并应该实现“纯”的委托而不是事件呢? - AspOnMyNet
9个回答

6
当您想提供一个在某个事件上执行的函数时,您需要给事件处理程序的委托一个将要被执行的函数。当进行事件驱动编程时,它们非常有用。
此外,当您有一些以函数为参数的函数(如LINQ表达式、谓词、映射函数、聚合函数等)时,这些函数通常称为高级函数。
另外,当调用者不需要访问实现该方法的对象的其他属性、方法或接口时,可以包装一些功能。在这种情况下,它取代了继承,某种程度上。

5

在我看来,委托是最简单的依赖注入方式。当一个组件接收到一个委托(无论是在事件中还是在普通方法中),它允许另一个组件将行为注入到它中。


我知道"依赖注入"这个术语在委托之前就被发明了,但这并不否定这个想法。 - jpbochi
1
实际上,委托只是一流函数(Scheme 和 Lisp 在35年前就已经有了),比 Fowler 2004年的《依赖注入》论文早了几十年。像 C# 这样的“现代”语言中的委托比功能性编程语言晚了几十年;当微软最终添加了委托时,它似乎只是“新”的,因为它对主流面向对象编程的群体来说是新的。 - Jared Updike

4

基本上,您可以在任何需要一种方法接口的地方使用委托。虽然这并不总是适合,但通常使用委托而不是接口可以大大提高可读性,因为逻辑更接近于它被使用的地方。例如,下面这些示例中哪个更容易理解和检查正确性?

var list = new List<int>(new[] { 1, 2, 3 });
var item = list.Find(i => i % 2 == 0);

与之相反:

var list = new List<int>(new[] { 1, 2, 3 });
list.Find(new DivisibleBy2Finder());

// Somewhere far away
private class DivisibleBy2Finder : IFinder<int> {
    public bool Matches(int i) {
        return i % 2 == 0;
    }
}

更新

让我稍微解释一下我的答案。从概念上讲,委托与只有一个方法的接口非常相似,但是使用特殊的语法调用该方法而不使用其名称(也就是说,给定委托 D,您可以通过语法 D() 调用其方法)。有两件事使委托比单方法接口更有趣:

  1. 您可以从方法组构造委托。例如,您可以像这样创建一个 Action<string> 委托:Action<string> action = new Action<string>(Console.WriteLine);。这将创建一个委托,当传递字符串时,它将在控制台上打印其参数。虽然这允许您有效地传递方法,但那些方法必须已经在某个类上定义。
  2. 您可以创建匿名委托。对我来说,这是委托在 C# 中特别有用的关键原因。一些其他语言使用不同的结构在其使用点封装一些逻辑(例如,Java 有匿名类)。C# 没有匿名类,因此,如果您想创建一个可自由传递到另一个方法中的逻辑片段,则使用匿名委托(或多个匿名委托)通常是 C# 中最好的方法。这就是我在原始帖子中尝试说明的内容。

事件和委托之间的关系有点棘手。虽然事件是基于委托实现的,但我不确定这是最好的思考方式。像其他类型的实例一样,委托可以在许多不同的上下文中使用;它们可以是类的成员,它们可以被传递到方法中或从方法中返回,它们可以在方法内部的本地变量中存储等等。另一方面,事件是类的特殊成员,支持以下操作:

  1. 可以将委托添加到事件中或从事件中删除委托。当触发/调用事件时,将调用这些委托。
  2. 只有在声明事件的类的上下文中,事件才能被视为标准委托,这意味着它可以被调用,并且可以检查/操作其调用列表。

因此,事件经常在类上公开,以允许其他组件注册回调,在需要时从事件的类中调用它们。但是,委托可以在更广泛的各种情况下使用。例如,在基础类库中,它们经常用作方法参数,以对集合执行通用操作。

希望这有助于澄清一些问题。


2
@AspOnMyNet - 我稍微扩展了我的回答;看看是否有帮助。虽然它并没有完全回答你修改后的问题,但希望能够澄清一些事情。 - kvb
1
+1 对于代码比较,但我不太喜欢将委托作为一种方法接口的术语。这是一个非常面向对象的描述,正如你所指出的那样,它并不能涵盖所有内容。我认为将委托实例视为一级函数更有用,这可以启用函数式编程技术。 - user24359
1
@Isaac - 你说得对,这个问题有许多不同的观点。一方面,委托基本上只是一个带有无名方法的接口;另一方面,接口只是一组命名函数。任何一种观点都可以用另一种观点来描述,哪种观点在任何特定情况下更有帮助可能取决于问题和程序员。鉴于原始问题的性质,我认为从面向对象的角度描述事物可能会给发帖者更多的见解,尽管我经常发现面向函数的视角更有用。 - kvb
2
@AspOnMyNet - 大致而言,在支持“一等函数”的语言中,函数可以像普通值一样对待(即它们可以被传递到其他函数中、存储在变量中、从函数中返回等)。不同的编程语言对此的支持各不相同(例如,在C#中,代理可以被视为一等函数的实现,而Java目前不支持任何这样的东西)。有关更多详细信息,请参阅维基百科。 - kvb
1
@AspOnMyNet - 维基百科上也有一篇不错的关于函数式编程的文章(尽管这是一个模糊的术语,不同的人使用方式也不同)。简而言之,函数式编程利用一级函数来实现通用的“高阶”操作。举个例子,使用函数式编程很容易通过逐个“映射”项目来表示将一个列表生成另一个列表的概念;“map”函数接受列表和映射函数作为参数,并返回新列表。 - kvb
显示剩余4条评论

3

实现回调和事件监听器。

例如,如果您有一个执行远程请求的函数(例如检索您的Facebook好友列表),则可以将委托作为最后一个参数传递给该函数,并在服务器响应接收后执行它。


例如,如果您有一个执行远程请求的函数……难道事件不更合适吗? - AspOnMyNet
1
@AspOnMyNet - 这主要取决于您是否有一个对象可以附加事件,但如果将其抽象为静态方法调用,该怎么办?除非您假定一次只有一个调用者,否则必须使用委托来确保在完成时调用正确的方法。 - Guvante
1
@AspOnMyNet:即使您使用事件,也必须将委托附加到该事件。 - Powerlord
这很大程度上取决于您是否有一个对象来附加事件,但如果它被抽象为静态方法调用,那该怎么办?除非您假定一次只有一个调用者,否则必须使用委托来确保在完成时调用正确的方法。我不理解您的论点,即为什么静态方法调用会对事件造成问题,以及为什么需要委托(而不是事件)来确保调用正确的方法(顺便说一句 - 我确实知道事件在幕后使用委托)。 - AspOnMyNet

2
异步回调是另一个很好的例子。

1

我喜欢委托和事件(它们是相辅相成的)用于关注点分离(SOC)。

什么是委托?简单来说,委托是一种类型安全的方法签名。事件基本上存储对一组方法的引用... 事件和委托提供了一种向多个消费者提供上下文外更改通知的方式...

发布者调用事件,订阅者接收通知。

这是如何工作的?以下是一个快速示例。

假设您的代码需要在处理订单之前验证输入。在过程化方法中,您的代码(控制器)可能会触发“order”方法。然后,Order进行验证,然后提交或拒绝...

在发布者/订阅者方法中,您可能会有以下事件:OrderSubmitted、OrderValidated和OrderRejected。那些将是您的发布者。然后您有几个订阅者,ValidateOrder、CommitOrder和RejectOrder... ValidateOrder订阅OrderSubmitted,CommitOrder订阅OrderValidated,最后RejectOrder订阅OrderRejected。

作为事件的参数,您传递订单。然后,事件序列将是...

您的控制器接收订单。代码假定正在进行事件空值检查...

void Init()
{
    ValidateOrder += SomeValidateMethod;
    CommitOrder += SomeCommitMethod;
    RejectOrder += SomeRejectMethod;
}

void OrderReceived(Order o)
{
  OrderEventArgs OEA = new OrderEventArgs(o);

  ValidateOrder(this, OEA);

  if (OEA.OrderIsValid)
      CommitOrder(this, OEA);
  else
      RejectOrder(this, OEA);
}

就像这样,我们有了一些事件。那么,为什么要使用事件/委托呢?假设拒绝订单的代码更新了数据库,没问题。但是有人说,当订单被拒绝时,让我们给客户发送电子邮件。你需要重构SomeRejectMethod吗?不需要,你只需创建一个新方法EmailOrderRejected,并将其作为订阅者添加到RejectOrder事件中。

这只是一个非常小的例子,但在整个系统中使用事件代理非常有帮助。它有助于解耦方法之间的依赖关系...

我稍后会提供一些链接,祝好运。


1

我还没有看到提到delegate的一个事情是,它使得将方法存储在数据结构中变得容易。例如,在功能需求中,我经常会发现像这样的东西:

通过将状态日期设置为事件日期,更新此费用的所有相关句子记录。如果使用引导转介而被处置,相关的句子就是具有“DV”、“ DCV”或“DVS”的句子类型。如果使用缓刑入境被处置,相关的句子就是具有“DEJ”的句子类型。忽略所有其他费用和句子。

解决此问题的一种方法是为句子构建一个类,为费用构建一个类,并从数据集中填充它们,然后将上面的所有逻辑都放入方法中。另一种方法是构建一个不错的大型嵌套条件语句集。

第三种方式更符合Steve McConnell的观察结果,即调试数据比代码更容易,它是定义包含可用于测试语句行的谓词的查找表:

private static readonly HashSet<string> DiversionTypes = 
    new HashSet() { "DV", "DCV", "DVS" };
private bool SentenceIsDiversion(DataRow r) { return (DiversionTypes.Contains(r.Field<string>("Type"))); }

private bool SentenceIsDEJ(DataRow r) { return r.Field<string>("Type") == "DEJ"; }

// Map charge disposition codes for diversion and DEJ to predicates that
// test sentence rows for relevance.  Only sentences for charges whose disposition
// code is in this map and who are described by the related predicate should be
// updated.
private static readonly Dictionary<string, Func<DataRow, bool>> DispoToPredicateMap =
    new Dictionary<string, Func<DataRow, bool>>
{
   { "411211", SentenceIsDiversion },
   { "411212", SentenceIsDiversion },
   { "411213", SentenceIsDEJ },
   { "411214", SentenceIsDEJ },
}

这使得更新逻辑看起来像这样:

string disposition = chargeRow.Field<string>("Disposition");
if (DispoToPredicateMap.ContainsKey(disposition))
{
    foreach (DataRow sentenceRow in chargeRow.GetChildRows("FK_Sentence_Charge"))
    {
       if (DispoToPredicateMap[disposition](sentenceRow))
       {
          sentenceRow.SetField("StatusDate", eventDate);
       }
    }
}

在这三种方法中,这种方法首先很难想出来(或者如果您不熟悉该技术,也很难理解)。 但是编写覆盖100%代码的单元测试要容易得多,并且当触发条件发生更改时更新也很容易。


1

委托用于将比较函数传递给通用排序例程;委托可用于实现策略模式;委托还用于调用异步方法,等等其他用途。

编辑添加:

在某种程度上,这种委托和事件之间的比较并没有什么意义。这就像问我们为什么需要整数,而不是将字段标记为“public”。

C#中的事件实际上只是一个对委托类型字段的访问限制。它基本上表示另一个类或对象可以访问该字段进行添加和删除,但不能检查字段的内容或对字段的值进行大规模更改。但事件的类型始终是委托,而委托有很多用途,不需要事件机制提供的访问限制。


“…将比较函数传递给通用排序例程”在这种情况下,接口是否更合适?“…委托用于调用异步方法”我们通常不是使用事件来调用异步方法吗? - AspOnMyNet
1
如果对象的排序永远不会改变,例如数字,那么接口是适合进行排序的。如果需要支持多种排序顺序(例如需要使用各种组合中的多个字段的排序),则比较函数更为适合。 - Jeffrey L Whitledge
1
“BeginInvoke”系列方法接受委托,而不是事件。 - Jeffrey L Whitledge
“BeginInvoke 方法族接收委托,而不是事件。哦,我还没讲到线程和异步方法调用,我只是假设事件也可以在那里使用。对此抱歉。” - AspOnMyNet
如果对象的排序顺序永远不会改变,例如数字,则接口适用于排序。如果需要支持各种排序顺序(例如需要多个字段以各种组合方式进行排序),则比较函数更为适合。为什么不能通过使用定义几种方法的接口来实现这一点,其中每种方法将用于不同的排序算法?因为此时将接口作为参数传递给排序例程的用户无法指定排序例程应调用接口的哪些方法。 - AspOnMyNet
1
指定排序顺序的方法有很多种,包括使用接口并为每个排序定义单独的具体类等(实际上,SortedDictionary<TKey,TValue> 类就是这样做的)。但在 C# 编程语言的上下文中,我想不到任何一种方法比传递委托更简单、清晰和灵活(这也是 Array.Sort() 的某些重载所接受的方法)。现在有了 lambda 表达式语法,这一点甚至更加明显。 - Jeffrey L Whitledge

1

我使用委托来保持我的对象和类库之间松散 耦合

例如:

  • 在 MainForm 上有两个控件 TabControlA 和 TabControlB。它们的代码驻留在 MainForm 的依赖项中的不同库中。

  • TabControlA 有一个公共的 SetShowMessage 方法,它设置一个称为 ShowMessage 的私有成员,该成员可以设置为任何类型为 Action<string> 的委托。

  • 当 MainForm 加载时,它可以通过调用 TabControlA.SetShowMessage(TabControlB.PrettyShowingFunction) 来设置事务并将 TabControlB 的(部分)与 TabControlA 的(部分)连接起来。

  • 现在,在内部,TabControlA 可以检查 ShowMessage 是否非空,并调用 ShowMessage("Hurray, a message that will be displayed on TabControlB!"),此时 TabControlB.PrettyShowingFunction 将被调用,允许 TabControlA 与 TabControlB 通信,后者可以显示此消息。

  • 这可以扩展到允许 TabControlC 做同样的事情并在 TabControlB 上显示消息等。

我不知道这个叫什么名字,但我认为它是中介者模式。使用中介者对象,您可以将更多的委托捆绑在一起,例如在主窗体上的进度条和状态标签,任何控件都可以更新。


你能解释一下为什么在你提供的例子中,实现一个事件(而不仅仅是委托)更有意义吗? - AspOnMyNet
1
很好的问题。我之前没有考虑过这个。Guvante在原始问题中的评论(“实际上,您总是可以使用事件来替换委托...”)似乎概括了这个问题。对于大多数简单事情,我发现使用委托比创建完整事件来打包相同的信息更加清洁。 - Jared Updike

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接