委托 vs. 事件

8

当一个类不能(或不应该)做某些事情时,事件或委托可能是解决方案。

请说出您需要翻译的内容。

class President
  Event AskedQuestion(QuestionEventArgs)
  Delegate GetAnswerToQuestion

class Scientist
  AnswerToQuestion()

// delegate approach
myPresident.GetAnswerToQuestion = AddressOf myScientist.AnswerToQuestion
// called once myPresident need it

// event approach
myScientist.AnswerToQuestion(questionEventArgs) Handles President.AskedQuestion
{ 
   // executed once president asked a question
}

在委托方法中,总统类直接使用科学家方法,以便在一位总统提出问题时,科学家能够用答案做出反应。

在.NET Framework代码中,我没有观察到直接使用委托的情况。如果直接使用委托,是否有问题?如果有,为什么?

4个回答

9

直接使用它有问题吗?如果有,为什么?

不,没有问题。

我这样理解。委托字段对于事件就像字符串字段对于属性一样。也就是说,你可能会有:

class Car
{
    private string modelName;
    public string ModelName { get { return this.modelName; } }
    ...

车型名称是车辆的属性。当有人问你开什么车,你说“福特福克斯”,你正在描述一种车的属性。你不认为“福特福克斯”是一个“字段”或者“字符串”,你认为它是一种车的名称。在计算机程序中,字符串字段只是名称存储的实现细节。该属性可以是字符串,也可以是枚举类型等;重点是从逻辑上讲,车有车型名称,而不是字符串字段。
事件和委托也是同样的道理。汽车可以有“爆炸”事件(也许你正在编写一个视频游戏!),并通过委托类型的字段来实现爆炸事件。爆炸是汽车逻辑上所做的事情;委托字段是实现该事件的机制。
那么直接使用委托是“错误”的吗?当然不是。就像直接使用字符串一样,并没有什么“错误”。有时候需要处理不是属性的字符串,有时候需要处理不是事件的委托。
关键是编写清晰地将“机械”过程与“业务”过程分开的代码。如果你发现自己在混合大量的字符串逻辑和属性逻辑,或者在混合大量的委托操作和事件处理,那么你可能考虑更加分离机制代码和业务代码,以便更容易看清哪个是哪个。

1
apropos,微软不建议直接使用公共字段,而是将其封装到属性中... - serhio
@serhio:这是更好的表达我的观点的方式。谢谢! - Eric Lippert
@Eric:我不这么认为。如果你将委托看作字段,事件看作属性,那么公共委托的使用应该受到限制,就像公共字段的使用一样。 - serhio
@serhio:委托是类型,就像字符串一样。操作委托实例是完全可以接受的,就像实现字符串实例一样。公开字段不好的想法与字段的类型无关;这就是我的观点。 - Eric Lippert
@Eric:除了类型,委托还是指向函数的指针。将它们公开可能会暴露一些安全风险,就像Moussa所描述的那样。 - serhio
@serhio:哦,当然,委托存在各种安全问题;但是事件也有安全问题。如果您让不受信任的代码向按钮添加事件处理程序,然后有人单击该按钮并发生了一些糟糕的事情。像传递数据一样传递行为的能力通常是一件危险的事情,因此在部分信任方案中要小心。 - Eric Lippert

2

框架中广泛使用委托。LINQ是其中一个明显的例子:

var result = someCollection.Where(input => input.MatchesSomeCriteria);

Where接受一个特定签名的委托,用于确定是否将项目包含在结果中。最常见的用法是使用上面所示的lambda方法,但您也可以传递一个方法:

string[] nums = new[]{ "1", "2", "3"};
int sum = nums.Select(int.Parse).Sum();

int.Parse是符合此案例所需的Select期望的委托签名(Func<string,int>),因此将为nums中的每个字符串调用它。

通常情况下,当委托被直接使用时,它们被作为输入传递给将使用它们的方法调用。然而,有些地方它们是消费者状态的一部分(例如HttpListener具有几个委托类型的属性),但这样的情况并不多见。


说到 lambda 表达式和 LINQ,它们在某种程度上是“隐式”使用委托。显式使用它我认为并不是很流行,特别是在没有 LINQ 的时代,当它们被用于一些像比较器之类的东西时... - serhio
@sergio - 或许这句话可以用于匿名委托,但委托类型的使用一直是框架的基本组成部分。 - David Neale
@David:你是否在你的自定义类中使用委托? - serhio
委托在事件中经常使用。 例如:public event EventHandler<GridViewRowClickedEventArgs> RowClicked; - Magnus
@Magnus:我认为@serhio的意思是,这个问题是关于直接使用委托的,而不是与事件一起使用的。 - Fredrik Mörk
显示剩余2条评论

1

事件

事件是我非常喜欢 .net 的一件事情,因为它允许您声明一个更清晰的接口。您可以有一个 president 类来宣布它需要一个答案,而不将其绑定到回答代理的实现方式,例如:

interface IPresident
{
     event Action<QuestionArgs, IPresident> HasQuestion;
     void RecieveAnswer(QuestionArgs,Answer);
}

然后在你的科学家类中

partial class Scientist
{
     public Scientist(IPresident president)
     {
          president.HasQuestion += TryToAnswerQuestion;

     }

     private void TryToAnswerQuestion(QuestionArgs question, IPresident asker)
     {
         if(CanAnswerQuestion(question))
         {
             asker.RecieveAnswer(question,GetAnswer(question));
         }
     }
}

如果一个新的类想要回答总统的问题,他们只需要监听事件信号以表明有需要回答的问题,然后如果他们能够回答,就回答它。 如果科学家想要回答别人的问题,我们只需要实现一个方法来连接他们的事件。
"直接委托调用"
你上面概述的委托方法的问题是它会破坏封装性。 它紧密地耦合了科学家和总统的实现,并使代码变得脆弱。 当你有其他回答问题的人时会发生什么? 在你的例子中,你需要修改你的科学家实现以添加新功能,这被称为“脆弱”的代码,是一件坏事。 这种技术在组合中确实有一些作用,但它很少是最好的选择。
LINQ 的情况不同,因为你没有将委托公开为类/接口的成员。 相反,你将其作为由调用者声明的函数对象使用,让你知道调用者感兴趣的信息。 由于你正在进行“往返”操作,所以封装保持完整。
这让您定义非常清晰而强大的 API。
我们可以采用这种技术来扩展科学家示例,以允许某个人了解我们可以如何回答问题。
 partial class Scientist
 {
     public IEnumerable<QuestionArgs> FindQuestions(Predicate<QuestionArgs> interest, IPresident asker)
     {
         return this.Questions.Where( x => interest(x) == true && x.IsAuthorizedToAsk(asker))
     }
 }

 // ...

partial class President
{
    FirePhysicists()
    {
        foreach(var scientist in scientists)
        {
             if(scientist.FindQuestions(x => x.Catagory == QuestionCatagory.Physics, this).Count != 0)
             {
                 scientist.Fire();
             }
         }
      }
 }

请注意,FindQuestions方法使我们不必实现大量其他代码来询问科学家,而这是我们在没有能力传递委托的情况下所需的。虽然这不是您将直接调用委托的唯一情况,但它是最常见的情况之一。

1

在原始代码中使用委托可能需要一些样板代码(定义委托,声明必要的成员变量,并创建自定义的注册/注销方法以保留封装等)。

除了输入时间之外,在原始代码中使用委托作为应用程序的回调机制的另一个问题是,如果您没有将类的委托成员变量定义为私有的,则调用者将直接访问委托对象。如果是这种情况,调用者将能够将变量重新分配给新的委托对象(有效地删除当前要调用的函数列表),更糟糕的是,调用者将能够直接调用委托的调用列表。


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