应该使用代表的常见情况是什么?

时间:2010-02-15 18:04:45

标签: c# .net delegates

我理解委托和事件的工作方式。我还可以想象一些我们应该实现事件的常见场景,但是我很难理解在什么情况下应该使用委托。

感谢名单

回复用户KVB的帖子:

A)

  

除非您使用单方法接口,否则基本上可以使用委托。

我想我有点理解以下内容:

  • C 可以定义方法 C.M ,它将接口 IM 作为参数。此界面将定义方法 IM.A ,因此任何想要调用 C.M 的人都需要实现此接口。

  • 或者,方法 CM 可以(而不是界面 IM )作为参数 D 具有相同的签名作为方法 IM.A

但我不明白的是,即使我们的界面 IM CM 也不能将委托 D 用作参数除了方法 A 之外还定义了其他几种方法?因此, C 类的其他方法可能需要接口 IM 作为参数,但 CM 可能需要委托 D < / strong>(假设 CM 只需要调用方法 A 而不是 IM 中定义的任何其他方法)?

b)

var list = new List<int>(new[] { 1, 2, 3 });
var item = list.Find(i => i % 2 == 0);
  • 以上代码是用户 jpbochi 调用(请参阅此帖子中的帖子/他的帖子)依赖注入的示例吗?

  • 我认为上述代码无法使用事件而不是“纯”代理来实现?

9 个答案:

答案 0 :(得分:6)

当您想要提供一个函数时,将在某个事件上执行。您为事件的事件处理程序提供了即将执行的函数的委托。当你进行事件驱动编程时,它们很棒。

此外,当您具有函数作为参数(LINQ表达式,谓词,映射函数,聚合函数等)时。此功能通常称为更高级别的功能。

此外,当调用者不需要访问实现该方法的对象上的其他属性,方法或接口时,您可以包装一些功能。在这种情况下,它会以某种方式替换继承。

答案 1 :(得分:5)

在我看来,代表是最简单的依赖注入方式。当组件接收委托时(在事件中或在常规方法中),它允许另一个组件向其注入行为。

答案 2 :(得分: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#中最好的方法。这就是我在原帖中用我的例子来说明的内容。
  3. 事件和代表之间的关系有点棘手。尽管事件是按照代表的方式实现的,但我不确定这是考虑它们的最佳方式。与其他类型的实例一样,代表可以在许多不同的环境中使用;它们可以是类的成员,它们可以传递给方法或从方法返回,它们可以存储在方法中的局部变量等中。另一方面,事件是支持以下操作的类的特殊成员:

    1. 可以在活动中添加或删除代表。在触发/调用事件时将调用这些代理。
    2. 只有在声明事件的类的上下文中,才能将事件视为标准委托,这意味着可以调用它并可以检查/操作其调用列表。
    3. 因此,事件经常暴露在类上以允许其他组件注册回调,这些回调将在需要时从事件的类中调用。但是,代表可以在更广泛的情况下使用。例如,在基类库中,它们经常被用作方法的参数,以对集合执行泛型操作。

      希望有助于澄清一些事情。

答案 3 :(得分:3)

实现回调和事件监听器。

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

答案 4 :(得分:2)

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

答案 5 :(得分:1)

委托用于将比较函数传递给泛型排序例程;代表可能被用来实施战略模式;和委托用于调用异步方法,以及其他用途。

编辑添加:

委托和事件之间的这种比较在某种程度上并没有任何意义。这就像在我们将字段标记为“公共”时询问为什么我们需要整数。

C#中的event只不过是对委托类型字段的访问限制。它基本上说,另一个类或对象可以访问该字段以进行添加和删除,但可能不会检查该字段的内容或使批发更改为该字段的值。但事件的类型始终是委托,并且委托有许多用途,不需要事件机制提供的访问限制。

答案 6 :(得分:1)

我使用委托来保留我的对象和类库loosely coupled

例如:

  • 我在MainForm上有两个控件TabControlA和TabControlB。它们的代码驻留在独立的库中,这些库是MainForm的依赖项。

  • TabControlA有一个公共SetShowMessage方法,它将名为ShowMessage的私有成员设置为任何委托(类型为Action&lt; string&gt;,比如说)。

  • 当MainForm加载时,它可以通过调用TabControlA.SetShowMessage(TabControlB.PrettyShowingFunction)来设置事物来连接TabControlB(只是那部分)TabControlA。

  • 现在在内部,TabControlA可以检查ShowMessage是否为非null并调用ShowMessage(“Hurray,将在TabControlB上显示的消息!”),现在调用TabControlB.PrettyShowingFunction,允许TabControlA与TabControlB通信,可以显示此消息。

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

我不知道这叫什么,但我认为这是the Mediator Pattern。使用Mediator对象,您可以将更多代理捆绑在一起,例如MainForm上的进度表和状态标签,任何控件都可以更新。

答案 7 :(得分:1)

我喜欢代表和事件(他们齐头并进)以分离关注(SOC)。

什么是代表?简而言之,委托是一种类型安全的方法签名。事件基本上存储对方法集合的引用...事件和委托提供了一种向多个消费者提供上下文更改通知的方法......

发布商呼叫事件和订阅者接收通知。

这是如何工作的?这是一个简单的例子。

假设您的代码在处理订单之前需要有效输入。在过程方法中,您的代码(控制器)可能会触发'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事件中。

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

我稍后会尝试跟进一些链接,祝你好运。

答案 8 :(得分:1)

我还没有看到的一件事是,委托使得在数据结构中存储方法变得容易。例如,我在功能要求中找到类似的东西并不罕见:

  

通过将其状态日期设置为活动日期,更新此费用的所有相关句子记录。如果收费与转移转介一起处理,则相关句子是句子类型为“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%代码的单元测试要容易得多,并且在触发条件发生变化时也很容易更新。