在引擎盖下使用弱引用实现 C# 事件是个好主意吗?

Is it a good idea to implement a C# event with a weak reference under the hood?

我一直在想是否值得使用类似以下内容(粗略的概念验证代码)来实现弱事件(在适当的地方):

class Foo {

    private WeakEvent<EventArgs> _explodedEvent = new WeakEvent<EventArgs>();

    public event WeakEvent<EventArgs>.EventHandler Exploded {
        add { _explodedEvent += value; }
        remove { _explodedEvent -= value; }
    }

    private void OnExploded() {
        _explodedEvent.Invoke(this, EventArgs.Empty);
    }

    public void Explode() {
        OnExploded();
    }

}

允许其他 class 使用更传统的 C# 语法订阅和取消订阅事件,同时实际上是使用弱引用实现的:

static void Main(string[] args) {
    var foo = new Foo();
    foo.Exploded += (sender, e) => Console.WriteLine("Exploded!");

    foo.Explode();
    foo.Explode();
    foo.Explode();

    Console.ReadKey();
}

其中 WeakEvent<TEventArgs> 助手 class 定义如下:

public class WeakEvent<TEventArgs> where TEventArgs : EventArgs {

    public delegate void EventHandler(object sender, TEventArgs e);

    private List<WeakReference> _handlers = new List<WeakReference>();

    public void Invoke(object sender, TEventArgs e) {
        foreach (var handler in _handlers)
            ((EventHandler)handler.Target).Invoke(sender, e);
    }

    public static WeakEvent<TEventArgs> operator + (WeakEvent<TEventArgs> e, EventHandler handler) {
        e._handlers.Add(new WeakReference(handler));
        return e;
    }

    public static WeakEvent<TEventArgs> operator - (WeakEvent<TEventArgs> e, EventHandler handler) {
        e._handlers.RemoveAll(x => (EventHandler)x.Target == handler);
        return e;
    }

}

这是一个好方法吗?这种方法有任何不良副作用吗?

关于您的特定实施的一些评论:

  1. 在调用 null 之前检查 handler.Target 的值,这样您就不会尝试使用已处置的对象来执行此操作。

  2. C# 对如何使用事件有特殊的访问规则。您不能执行 a.Event1 = a.Event2 + SomeOtherMethod 除非该代码具有对事件的私有访问权限。但是,对于代表来说,这是允许的。您的实现表现得更像委托而不是事件。这可能不是主要问题,但需要考虑一下。

  3. 你的运算符方法应该return一个新对象而不是修改第一个参数然后returning它。实施 operator + 允许使用如下语法:a = b + c,但在您的实施中,您正在修改 b! 的状态。这不符合人们对这些操作员工作方式的期望;您需要 return 一个新对象而不是修改现有对象。 (此外,由于这一点,您的实现不是线程安全的。在另一个线程引发事件时从一个线程调用运算符 + 会引发异常,因为在 foreach 期间修改了集合。)

因为:

  1. 您的程序开始变得不确定,因为副作用取决于 GC 的操作。
  2. GCHandles 以性能成本为代价。

查看链接的答案。这是 95% 的重复,但还不足以结束我认为的问题。我将引用最相关的部分:


还有一个语义差异非确定性,这将由弱引用引起。如果您将 () => LaunchMissiles() 连接到某个事件,您可能会发现有时会发射导弹。其他时候 GC 已经带走了处理程序。这可以通过 来解决,这又引入了另一层复杂性。

我个人很少发现事件的强引用性是个问题。通常,事件连接在具有相同或非常相似生命周期的对象之间。例如,您可以在 ASP.NET 中的 HTTP 请求上下文中连接所有您想要的事件,因为当请求结束时 所有内容 都将符合收集条件。任何泄漏的大小和寿命都是有限的。