【问题标题】:What are some common scenarios where delegates should be used? [duplicate]应该使用委托的一些常见场景是什么? [复制]
【发布时间】:2011-01-17 02:21:21
【问题描述】:

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

感谢

回复用户 KVB 的帖子:

一)

您基本上可以在任何需要使用单一方法接口的地方使用委托。

我想我有点明白以下几点:

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

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

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

b)

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

  • 我假设无法使用事件而不是“纯”委托来实现上述代码?

【问题讨论】:

  • 依赖注入允许调用者控制被调用者所做的事情。在典型的方法调用方式中,调用者传入一组参数,然后被调用者执行它认为合适的操作。唯一的交叉点是在方法的开始和结束处(纯粹意义上的)。但是,通过提供委托,被调用者必须有效地询问调用者中间流如何处理情况,在这种情况下,被调用者处理枚举,但调用者必须有效地处理过滤。
  • 实际上你总是可以用事件替换委托。理论上,您可以有一个使用过滤器表达式播种的事件,然后在查找查找期间调用。但是,您现在已经为调用者引入了额外的工作,特别是他们必须设置和拆除事件以避免奇怪的行为。更不用说可能的线程问题了。这些可以通过使用委托来避免并且代码更具可读性。而另一方面,事件只是保存的委托,从实现的角度来看,没有什么比这更重要的了。
  • a) "唯一的交叉是在方法的开始和结束处(纯粹意义上的"你所说的交叉是什么意思?b) "在硬币的另一边事件只是保存的委托,从实现的角度来看,没有什么比这更“这是另一件事让我感到困惑 - 我如何决定我是否需要事件的安全性或我是否不需要该安全性,因此应该实现“纯”委托而不是事件?

标签: c# .net delegates


【解决方案1】:

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

当您有函数作为参数时(LINQ 表达式、谓词、映射函数、聚合函数等)。这些函数通常称为更高级别的函数。

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

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

      编辑添加:

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

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

      【讨论】:

      • "...将比较函数传递给通用排序例程" 在这种情况下接口不是更合适吗? “……委托是用来调用异步方法的”我们平时不是用事件来调用异步方法吗?
      • 如果对象的顺序从不改变(例如数字),则该接口适用于排序。如果需要支持多种排序顺序,例如需要多种组合的多个字段,则比较函数更合适。
      • BeginInvoke 系列方法接受委托,而不是事件。
      • “BeginInvoke 系列方法接受委托,而不是事件”啊,我还没有讨论线程和异步方法调用,我只是假设事件也可以在那里使用。对此感到抱歉
      • 指定排序顺序的方法数量没有限制,包括使用为每个排序定义单独的具体类的接口等。(事实上,SortedDictionary 类正是这样做的。)但我想不出在 C# 编程语言的上下文中,没有任何方法能像传递委托那样简单、清晰和灵活。 (这是 Array.Sort() 的一些重载所接受的。)现在 lambda 表达式语法可用,这一点更是如此。
      【解决方案4】:

      实现回调和事件监听器。

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

      【讨论】:

      • “例如,如果您有一个执行远程请求的函数......”事件不是更合适吗?
      • @AspOnMyNet - 这在很大程度上取决于,如果你有一个对象来附加事件,但如果它被抽象为一个静态方法调用呢?除非您假设一次只有一个调用者,否则您必须使用委托来确保在完成时调用正确的方法。
      • @AspOnMyNet:即使你使用了一个事件,你也必须为那个事件附加一个代理。
      • "这在很大程度上取决于,如果你有一个对象来附加事件,但是如果它被抽象为一个静态方法调用呢?除非你假设一次只有一个调用者,你有使用委托来确保在完成时调用正确的方法。”我不明白你的论点。也就是说,为什么静态方法调用会代表事件的问题,为什么需要委托(而不是事件)来确保调用正确的方法(顺便说一句——我确实知道事件在后台使用委托)?
      【解决方案5】:

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

      【讨论】:

      • 我知道“依赖注入”这个词是在代表之前发明的,但这并不会使这个想法失效。
      • 实际上,委托只是一流的函数(Scheme 和 Lisp 在 35 多年前就有了),比 Fowler 2004 年的依赖注入论文早了几十年。使用 C# 等“现代”语言的代表落后于函数式编程语言数十年;当 MS 最终添加代表时,它看起来只是“新”,因为它对主流 OOP 人群来说是新的。
      【解决方案6】:

      我使用委托来保存我的对象和类库looselycoupled

      例如:

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

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

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

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

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

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

      【讨论】:

      • 您能解释一下为什么在您提供的示例中实现一个事件(而不仅仅是一个委托)更有意义吗?
      • 好问题。我没有想到这一点。 Guvante 对原始问题的评论(“实际上,您总是可以用事件替换代表......”)似乎总结了这一点。我发现与创建一个完整的事件来打包相同的信息相比,代表在大多数简单的事情上更干净。
      【解决方案7】:

      我喜欢委托和事件(它们齐头并进)以实现关注点分离 (SOC)。

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

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

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

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

      在发布者/订阅者方法中,您可能有以下事件 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】:

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

        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&lt;string&gt; 委托:Action&lt;string&gt; action = new Action&lt;string&gt;(Console.WriteLine);。这将创建一个委托,该委托将在将字符串传递给它时将其参数打印到控制台。虽然这允许您有效地传递方法,但这些方法必须已经在某个类上定义。
        2. 您可以创建一个匿名委托。对我来说,这是委托在 C# 中特别有用的关键原因。其他一些语言使用不同的结构在其使用点封装一些逻辑(例如 Java 具有匿名类)。 C# 没有匿名类,因此如果您想创建一些可以传递给另一个方法的独立逻辑,使用匿名委托(或多个匿名委托)通常是 C# 中的最佳方法。这就是我试图用我在原始帖子中的示例来说明的内容。

        事件和委托之间的关系有点棘手。尽管事件确实是根据委托来实现的,但我不确定这是考虑它们的最佳方式。与其他类型的实例一样,委托可以在许多不同的上下文中使用;它们可以是类的成员,可以传递给方法或从方法返回,它们可以存储在方法内的局部变量中,等等。另一方面,事件是支持以下操作的类的特殊成员:

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

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

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

        【讨论】:

        • @AspOnMyNet - 我已经扩展了我的答案;看看是否有帮助。它并不能完全解决您修改后的问题,但希望它仍然可以解决一些问题。
        • +1 用于代码比较,但我不太喜欢将代表作为一个方法接口的术语。这是一个非常以 OOP 为中心的描述,正如您所指出的,它并没有涵盖所有内容。我认为将委托实例视为一流的函数更有用,它支持函数式编程技术。
        • @Isaac - 你是对的,有很多方法可以查看这个问题。一方面,委托基本上只是一个带有无名方法的接口;另一方面,接口只是一组命名函数。任何一种观点都可以用另一个观点来描述,在任何特定情况下哪种观点更有帮助可能取决于问题和程序员。考虑到原始问题的性质,我认为从 OO 角度描述事物可能会让发帖者更有洞察力,尽管我自己经常发现 FP 角度更有用。
        • @AspOnMyNet - 粗略地说,在支持“一等函数”的语言中,函数可以像普通值一样处理(也就是说,它们可以传递给其他函数,存储在变量中,从函数等返回)。语言对此有不同的支持(例如,在 C# 中,委托可以看作是一流函数的实现,而 Java 目前不支持这些方面的任何东西)。有关详细信息,请参阅维基百科。
        • @AspOnMyNet - 维基百科也有一篇关于函数式编程的不错的文章(尽管它是一个模糊的术语,不同的人使用不同)。简而言之,函数式编程利用一等函数来实现一般的“高阶”操作。作为一个例子,使用函数式编程很容易通过单独“映射”每个项目来表示从另一个列表生成一个列表的概念。 “map”函数将一个列表和一个映射函数作为参数并返回新列表。
        【解决方案9】:

        我还没有看到提到的一件事是委托使在数据结构中存储方法变得容易。例如,我经常在功能需求中找到这样的内容:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-11-14
          • 2019-09-26
          • 2016-04-19
          • 2014-04-06
          • 2010-11-20
          • 1970-01-01
          • 2011-05-30
          相关资源
          最近更新 更多