在引擎盖下使用弱引用实现 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;
}
}
这是一个好方法吗?这种方法有任何不良副作用吗?
关于您的特定实施的一些评论:
在调用 null
之前检查 handler.Target
的值,这样您就不会尝试使用已处置的对象来执行此操作。
C# 对如何使用事件有特殊的访问规则。您不能执行 a.Event1 = a.Event2 + SomeOtherMethod
除非该代码具有对事件的私有访问权限。但是,对于代表来说,这是允许的。您的实现表现得更像委托而不是事件。这可能不是主要问题,但需要考虑一下。
你的运算符方法应该return一个新对象而不是修改第一个参数然后returning它。实施 operator +
允许使用如下语法:a = b + c
,但在您的实施中,您正在修改 b
! 的状态。这不符合人们对这些操作员工作方式的期望;您需要 return 一个新对象而不是修改现有对象。 (此外,由于这一点,您的实现不是线程安全的。在另一个线程引发事件时从一个线程调用运算符 + 会引发异常,因为在 foreach 期间修改了集合。)
因为:
- 您的程序开始变得不确定,因为副作用取决于 GC 的操作。
- GCHandles 以性能成本为代价。
查看链接的答案。这是 95% 的重复,但还不足以结束我认为的问题。我将引用最相关的部分:
还有一个语义差异和非确定性,这将由弱引用引起。如果您将 () => LaunchMissiles()
连接到某个事件,您可能会发现有时会发射导弹。其他时候 GC 已经带走了处理程序。这可以通过 来解决,这又引入了另一层复杂性。
我个人很少发现事件的强引用性是个问题。通常,事件连接在具有相同或非常相似生命周期的对象之间。例如,您可以在 ASP.NET 中的 HTTP 请求上下文中连接所有您想要的事件,因为当请求结束时 所有内容 都将符合收集条件。任何泄漏的大小和寿命都是有限的。
我一直在想是否值得使用类似以下内容(粗略的概念验证代码)来实现弱事件(在适当的地方):
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;
}
}
这是一个好方法吗?这种方法有任何不良副作用吗?
关于您的特定实施的一些评论:
在调用
null
之前检查handler.Target
的值,这样您就不会尝试使用已处置的对象来执行此操作。C# 对如何使用事件有特殊的访问规则。您不能执行
a.Event1 = a.Event2 + SomeOtherMethod
除非该代码具有对事件的私有访问权限。但是,对于代表来说,这是允许的。您的实现表现得更像委托而不是事件。这可能不是主要问题,但需要考虑一下。你的运算符方法应该return一个新对象而不是修改第一个参数然后returning它。实施
operator +
允许使用如下语法:a = b + c
,但在您的实施中,您正在修改b
! 的状态。这不符合人们对这些操作员工作方式的期望;您需要 return 一个新对象而不是修改现有对象。 (此外,由于这一点,您的实现不是线程安全的。在另一个线程引发事件时从一个线程调用运算符 + 会引发异常,因为在 foreach 期间修改了集合。)
- 您的程序开始变得不确定,因为副作用取决于 GC 的操作。
- GCHandles 以性能成本为代价。
查看链接的答案。这是 95% 的重复,但还不足以结束我认为的问题。我将引用最相关的部分:
还有一个语义差异和非确定性,这将由弱引用引起。如果您将 () => LaunchMissiles()
连接到某个事件,您可能会发现有时会发射导弹。其他时候 GC 已经带走了处理程序。这可以通过
我个人很少发现事件的强引用性是个问题。通常,事件连接在具有相同或非常相似生命周期的对象之间。例如,您可以在 ASP.NET 中的 HTTP 请求上下文中连接所有您想要的事件,因为当请求结束时 所有内容 都将符合收集条件。任何泄漏的大小和寿命都是有限的。