更新:为了阅读本文的任何人的利益,自 .NET 4 以来,由于自动生成事件的同步发生变化,因此不需要锁定,所以我现在只使用它:
public static void Raise<T>(this EventHandler<T> handler, object sender, T e) where T : EventArgs
{
if (handler != null)
{
handler(sender, e);
}
}
并提高它:
SomeEvent.Raise(this, new FooEventArgs());
一直在阅读 Jon Skeet 的一本书 articles on multithreading ,我试图封装他提倡的在扩展方法中引发事件的方法(具有类似的通用版本):
public static void Raise(this EventHandler handler, object @lock, object sender, EventArgs e)
{
EventHandler handlerCopy;
lock (@lock)
{
handlerCopy = handler;
}
if (handlerCopy != null)
{
handlerCopy(sender, e);
}
}
然后可以这样调用:
protected virtual void OnSomeEvent(EventArgs e)
{
this.someEvent.Raise(this.eventLock, this, e);
}
这样做有什么问题吗?
此外,我对锁的必要性一开始有点困惑。据我了解,在文章的示例中复制了委托(delegate),以避免它在 null 检查和委托(delegate)调用之间发生变化(并变为 null)的可能性。但是,我的印象是这种访问/分配是原子的,那么为什么需要锁呢?
更新:关于下面马克·辛普森的评论,我做了一个测试:
static class Program
{
private static Action foo;
private static Action bar;
private static Action test;
static void Main(string[] args)
{
foo = () => Console.WriteLine("Foo");
bar = () => Console.WriteLine("Bar");
test += foo;
test += bar;
test.Test();
Console.ReadKey(true);
}
public static void Test(this Action action)
{
action();
test -= foo;
Console.WriteLine();
action();
}
}
这个输出:
Foo
Bar
Foo
Bar
这说明方法的委托(delegate)参数 (action
) 没有反射(reflect)传递给它的参数 (test
),这是意料之中的,我猜测。我的问题是这会影响我的 Raise
扩展方法上下文中锁的有效性吗?
更新:这是我现在使用的代码。它不像我希望的那样优雅,但它似乎有效:
public static void Raise<T>(this object sender, ref EventHandler<T> handler, object eventLock, T e) where T : EventArgs
{
EventHandler<T> copy;
lock (eventLock)
{
copy = handler;
}
if (copy != null)
{
copy(sender, e);
}
}
最佳答案
锁的目的是在覆盖默认事件连接时保持线程安全。如果其中一些解释了您已经能够从 Jon 的文章中推断出的事情,我们深表歉意;我只是想确保我对所有事情都完全清楚。
如果你这样声明你的事件:
public event EventHandler Click;
然后事件订阅会自动与 lock(this)
同步。您不需要编写任何特殊的锁定代码来调用事件处理程序。完全可以这样写:
var clickHandler = Click;
if (clickHandler != null)
{
clickHandler(this, e);
}
但是,如果您决定覆盖默认事件,即:
public event EventHandler Click
{
add { click += value; }
remove { click -= value; }
}
现在您遇到了问题,因为不再有隐式锁。您的事件处理程序刚刚失去了线程安全性。这就是你需要使用锁的原因:
public event EventHandler Click
{
add
{
lock (someLock) // Normally generated as lock (this)
{
_click += value;
}
}
remove
{
lock (someLock)
{
_click -= value;
}
}
}
就我个人而言,我对此并不在意,但 Jon 的理由很充分。但是,我们确实有一个小问题。如果您使用私有(private) EventHandler
字段来存储您的事件,您可能在类内部有执行此操作的代码:
protected virtual void OnClick(EventArgs e)
{
EventHandler handler = _click;
if (handler != null)
{
handler(this, e);
}
}
这很糟糕,因为我们正在访问相同的私有(private)存储字段,而没有使用属性使用的相同锁。
如果类外部的一些代码:
MyControl.Click += MyClickHandler;
通过公共(public)属性的外部代码正在兑现锁。但你不是,因为你接触的是私有(private)领域。
clickHandler = _click
的变量赋值部分是原子的,是的,但是在该赋值期间,_click
字段可能处于 transient 状态,由外部类编写了一半。当您同步对字段的访问时,仅同步写访问是不够的,您还必须同步读访问:
protected virtual void OnClick(EventArgs e)
{
EventHandler handler;
lock (someLock)
{
handler = _click;
}
if (handler != null)
{
handler(this, e);
}
}
更新
事实证明,评论周围的一些对话实际上是正确的,OP 的更新证明了这一点。这不是扩展方法本身的问题,而是委托(delegate)具有值类型语义并在赋值时被复制的事实。即使您从扩展方法中取出 this
并将其作为静态方法调用,您也会得到相同的行为。
您可以使用静态实用方法来绕过此限制(或功能,取决于您的观点),尽管我很确定您不能使用扩展方法。这是一个有效的静态方法:
public static void RaiseEvent(ref EventHandler handler, object sync,
object sender, EventArgs e)
{
EventHandler handlerCopy;
lock (sync)
{
handlerCopy = handler;
}
if (handlerCopy != null)
{
handlerCopy(sender, e);
}
}
此版本有效,因为我们实际上并未传递 EventHandler
,只是对其的引用(请注意方法签名中的 ref
).不幸的是,您不能在扩展方法中将 ref
与 this
一起使用,因此它必须保留为纯静态方法。
(如前所述,您必须确保传递与在公共(public)事件中使用的 sync
参数相同的锁定对象;如果传递任何其他对象,则整个讨论没有实际意义。)
关于c# - 这是在 C# 中引发事件的有效模式吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2123608/