C # : 事件还是观察者接口? 优缺点?

我得到了以下(简化) :

interface IFindFilesObserver
{
void OnFoundFile(FileInfo fileInfo);
void OnFoundDirectory(DirectoryInfo directoryInfo);
}


class FindFiles
{
IFindFilesObserver _observer;


// ...
}

我很矛盾。这基本上是我在 C + + 中写的,但是 C # 有事件。我应该更改代码以使用事件,还是应该不去管它?

与传统的观察者接口相比,事件的优点和缺点是什么?

25849 次浏览

事件可以用来实现观察者模式。事实上,使用事件可以被看作是观察者模式 imho 的另一个实现。

优点是事件更“点-网”。如果您正在设计可以放到窗体上的非可视化组件,则可以使用设计器将它们连接起来。

缺点是一个事件只意味着一个事件——你需要为你想通知观察者的每个“事件”单独设置一个事件。除了每个被观察的对象都需要为每个事件的每个观察者提供一个参考之外,这并没有什么实际的影响。在有很多被观察对象的情况下,内存膨胀(这也是他们在 WPF 中用不同的方式管理观察者/可观察对象关系的原因之一)。

对你来说,我觉得没什么区别。如果观察者通常对所有这些事件感兴趣,那么使用观察者接口而不是单独的事件。

将一个事件视为一个回调接口,其中该接口只有一个方法。

只钩住你需要的事件
对于事件,您只需要为您感兴趣的事件实现处理程序。在观察者接口模式中,您必须实现整个接口中的所有方法,包括实现您实际上并不关心处理的通知类型的方法体。在您的示例中,必须始终实现 OnFoundDirectory 和 OnFoundFile,即使您只关心其中的一个事件。

减少维护
事件的另一个好处是,您可以向特定类添加一个新的事件,这样它就会引发事件,而且您不必更改每个现有的观察者。然而,如果想要向接口添加新方法,则必须遍历已经实现该接口的每个类,并在所有类中实现新方法。但是,对于事件,您只需要修改现有的类,这些类实际上希望响应您要添加的新事件。

这种模式是内置在语言中的,所以每个人都知道如何使用它
Events are idiomatic, in that when you see an event, you know how to use it. With an observer interface, people often implement different ways of registering to receive notifications and hook up the observer.. with events though, once you've learnt how to register and use one (with the += operator), the rest are all the same.

接口的优点
对于接口,我没有太多的优点。我猜他们强迫某人实现接口中的所有方法。但是,你不能强迫别人正确地实现所有这些方法,所以我不认为这有很大的价值。

语法
有些人不喜欢为每个事件声明委托类型的方式。中的标准事件处理程序。NET 框架具有以下参数: (对象发送器,EventArgs 参数)。由于发送方没有指定特定的类型,所以如果要使用它,必须向下强制转换。这在实践中通常是很好的,但是感觉不太对,因为您正在失去静态类型系统的保护。但是,如果您实现自己的事件并且不遵循。NET 框架约定,您可以使用正确的类型,因此不需要潜在的向下转换。

界面优点-解决方案:

  • 如果添加方法,现有的观察者需要实现这些方法。这意味着您不太可能忘记将现有的观察者连接到新的功能上。当然,您可以将它们作为空方法来实现,这意味着您仍然可以对某些“事件”什么也不做。但你不会轻易忘记。
  • 如果使用显式实现,反过来也会得到编译器错误,如果删除或更改现有接口,那么实现它们的观察者将停止编译。

缺点:

  • 需要更多地考虑规划,因为观察者接口中的变更可能会强制对整个解决方案进行更改,这可能需要不同的规划。由于一个简单的事件是可选的,所以除非其他代码应该对该事件作出反应,否则很少或根本不需要更改其他代码。

出于以下原因,我更喜欢事件库解决方案

  • 它降低了进入的成本。说“ + = new EventHandler”比实现一个完整的接口要容易得多。
  • 它降低了维护成本。如果您向类中添加一个新事件,那就是所有需要做的事情。如果向接口添加新事件,则必须更新代码基中的每个使用者。或者定义一个全新的接口,随着时间的推移,这个接口会让消费者感到厌烦: “我应该实现 IRRandom Event2还是 IRRandom Event5?”
  • 事件允许处理程序不基于类(即某处的静态方法)。不存在强制所有事件处理程序都为实例成员的功能原因
  • 将一大堆事件分组到一个接口就是对事件的使用方式做出一个假设(这只是一个假设)
  • 与原始事件相比,接口并没有提供真正的优势。

Java 有对匿名接口的语言支持,所以回调接口是在 Java 中使用的东西。

C # 支持匿名委托-lambdas-因此在 C # 中使用事件。

最好的决定方法是这样的: 哪一个更适合这种情况。这听起来可能是一个愚蠢或无益的答案,但我不认为你应该认为其中之一是“适当的”解决方案。

我们可以给你一百个小费。当观察者需要侦听任意事件时,事件是最好的。当观察者被期望列出到所有给定的事件集时,接口是最好的。事件在处理 GUI 应用程序时是最好的。接口消耗更少的内存(用于多个事件的单个指针)。等等,等等。列出利弊是需要考虑的,但不是一个明确的答案。您真正需要做的是在实际应用程序中同时尝试这两种方法,并对它们有一个很好的了解。然后你可以选择一个更适合这种情况的。从实践中学习。

如果你必须使用一个定义问题,那么问问自己哪个更好地描述你的情况: 一组松散相关的事件,其中任何一个都可能被使用或忽略,或者一组密切相关的事件,通常都需要由一个观察者处理。但是,我只是在描述事件模型和接口模型,所以我又回到了起点: 哪一个更适合这种情况?

事件的进一步好处。

  • 您可以免费获得适当的多播行为。
  • 如果更改事件的订阅方以响应该事件,则行为是明确定义的
  • 它们可以很容易地被反省(反映)
  • 工具链对事件的支持(仅仅因为它们是.net 中的习惯用法)
  • 您可以选择使用它提供的异步 apis

你可以自己完成所有这些任务(除了工具链) ,但是难度惊人。例如: 如果使用类似 List < > 的成员变量来存储观察员列表。 如果您使用 foreach 对其进行迭代,那么任何在 OnFoo ()方法回调中添加或删除订阅者的尝试都将触发异常,除非您编写进一步的代码来干净地处理它。

接口的一个好处是,它们更容易应用装饰符:

subject.RegisterObserver(new LoggingObserver(myRealObserver));

相比之下:

subject.AnEvent += (sender, args) => { LogTheEvent(); realEventHandler(sender, args); };

(我是修饰模式的忠实粉丝)。

如果您的对象需要以某种方式序列化,以保留诸如 NetDataContractSerializer或者 普罗布福事件之类的引用,那么这些事件将无法跨越序列化边界。由于观察者模式仅仅依赖于对象引用,所以如果需要的话,它可以使用这种序列化方式,没有任何问题。

前女友。您有许多业务对象,它们彼此双向链接,您需要将它们传递给 Web 服务。

  • 事件很难通过对象链传播,例如,如果使用 FACADE 模式或将工作委托给其他类。
  • 要允许对象被垃圾收集,您需要非常小心地从事件中取消订阅。
  • 事件比简单的函数调用慢2倍,如果对每次引发执行 null 检查则慢3倍,并且在 null 检查和调用之前复制事件委托以使其线程安全。

  • 也是 阅读关于 new (在4.0中) IObserver<T>接口的 MSDN。

考虑一下这个例子:

using System;


namespace Example
{
//Observer
public class SomeFacade
{
public void DoSomeWork(IObserver notificationObject)
{
Worker worker = new Worker(notificationObject);
worker.DoWork();
}
}
public class Worker
{
private readonly IObserver _notificationObject;
public Worker(IObserver notificationObject)
{
_notificationObject = notificationObject;
}
public void DoWork()
{
//...
_notificationObject.Progress(100);
_notificationObject.Done();
}
}
public interface IObserver
{
void Done();
void Progress(int amount);
}


//Events
public class SomeFacadeWithEvents
{
public event Action Done;
public event Action<int> Progress;


private void RaiseDone()
{
if (Done != null) Done();
}
private void RaiseProgress(int amount)
{
if (Progress != null) Progress(amount);
}


public void DoSomeWork()
{
WorkerWithEvents worker = new WorkerWithEvents();
worker.Done += RaiseDone;
worker.Progress += RaiseProgress;
worker.DoWork();
//Also we neede to unsubscribe...
worker.Done -= RaiseDone;
worker.Progress -= RaiseProgress;
}
}
public class WorkerWithEvents
{
public event Action Done;
public event Action<int> Progress;


public void DoWork()
{
//...
Progress(100);
Done();
}
}
}