有以下模式用于在引发事件时避免竞争条件,以防另一个线程从MyEvent取消订阅,使其为空。
class MyClass
{
public event EventHandler MyEvent;
public void F()
{
EventHandler handler = MyEvent;
if(handler != null)
handler(this, EventArgs.Empty);
}
}
与错误的做法相反,而这种做法很容易发生这种竞争:
class MyClass
{
public event EventHandler MyEvent;
public void F()
{
if(MyEvent != null)
MyEvent(this, EventArgs.Empty);
}
}
我的问题是,鉴于System.Delegate
是引用类型:如果MyEvent不为null,为什么会这样?
EventHandler handler = MyEvent;
似乎复制其调用列表,而不是获取引用。
我希望将MyEvent委托分配给'handler'变量,然后一旦某人更改了 MyEvent,那么'handler'引用的对象也会被更改。
显然,事实并非如此,否则这个漂亮的小模式将无效。
我查看了.NET源代码但仍然无法在那里找到我的答案(它可能在那里,但我已经找了大约一个小时但找不到它,所以我在这里。) 我还阅读了C#语言规范对事件和代表的看法,但它没有解决这个问题。
感谢您的时间。
答案 0 :(得分:9)
一旦我得到了,我会期待的 '处理程序'中的MyEvent委托 参考,一旦有人改变 MyEvent那个'处理程序'的对象 引用也将被更改。 [..] 请注意,System.Delegate是一个类而不是结构。
虽然委托类型是引用类型是正确的,但它们是不可变引用类型。来自System.Delegate
:
“代表是不可变的; 一次 创建,一个的调用列表 代表不会改变。 [...] 组合操作,例如Combine 和删除,不要改变现有的 与会代表。相反,这样的 operation返回一个新的委托 包含操作的结果, 一个没有变化的代表,或者没什么。
另一方面,此模式解决的唯一问题是阻止尝试调用null委托引用。尽管有“修复”,但事件仍为prone to races。
答案 1 :(得分:6)
以下是一些图表,希望能够清除复制引用和赋值的混淆。
在上图中,y
中包含的参考被复制到x
。没有人说复制对象;请注意 - 他们指向同一个对象。
暂时忘掉+=
运营商;我要强调的是,y
正在为新对象分配不同的引用。这不会影响x
,因为x
是它自己的变量。请记住,只有引用(图中的“地址”)已复制到y
。
x
。
以上图表描述了string
个对象,只是因为它们很容易以图形方式表示。但是对于委托来说却是一样的(记住,标准事件只是委托字段的包装)。您可以通过将<{1}}中的引用复制到上面的y
进行查看,我们创建了一个不会受x
后续作业影响的变量。< / p>
这是我们都熟悉的标准y
种族条件“修复”背后的整个想法。
你可能会对这个棘手的小语法感到困惑:
EventHandler
重要的是要知道,作为Ani points out in his answer,委托是不可变的引用类型(想想:就像someObject.SomeEvent += SomeEventHandler;
)。许多开发人员错误地认为它们是可变的,因为上面的代码看起来像是在为一些可变列表“添加”处理程序。事实并非如此; string
运算符是赋值运算符:它接受+=
运算符的返回值,并将其赋值给左侧的变量。
(想想:+
是不可变的,但我可以int
做对吗?这是同样的事情。)
编辑:好的,从技术上讲这不太对。这是真正发生的事情。 int x = 0; x += 1;
实际上是一个包装围绕一个委托字段,event
和+=
只能 <(外部代码)访问运算符,分别编译为调用-=
和add
。通过这种方式,它非常像一个属性,它(通常)是字段的包装器,其中访问属性并调用remove
被编译为对=
的调用和get
。
但重点仍然是:当您编写set
时,被调用的+=
方法会在内部将对新对象的引用分配给内部委托字段。我为在最初的答案中过度简化这个解释而道歉;但理解的关键原则是一样的。
顺便说一句,我不涵盖自定义事件,您可以将自己的逻辑放在add
和add
方法中。这个答案仅适用于“正常”案例。
换句话说,当你这样做时......
remove
...您确实正在将引用复制到变量中。现在该引用位于局部变量中,并且本身不会被修改。如果它在赋值时指向实际对象,那么它将继续指向下一行上的相同(不可变)对象。如果它不指向一个对象(EventHandler handler = SomeEvent;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
),那么它仍然不会指向下一行的对象。
因此,如果其他地方的代码使用null
订阅或取消订阅该事件,那么它真正做的是将原始引用更改为指向一个全新的对象。旧的委托对象仍然存在,你有一个对它的引用:在你的本地变量中。
答案 2 :(得分:1)
我想指出,将此事件与'int'案例进行比较可能本质上是错误的,因为即使'int'是原子的,它也是一种值类型。
但我认为我们已经解决了这个问题:
组合操作,例如Combine 和删除,不要改变现有的 与会代表。相反,这样的操作 返回包含的新委托 操作的结果, 不变的委托,或null。一个 组合操作返回null时 操作的结果是 没有参考的代表 至少一种方法。结合 操作返回不变 在请求的操作时委托 没有效果。
Delegate.CombineImpl Method 显示了实施。
我查看了.NET 4源代码中Delegate和MulticastDelegate的实现。 它们都没有声明+ =或 - =运算符。想到它,在Visual Basic.NET中你甚至没有它们,你使用AddHandler等...
这意味着C#编译器实现了这个功能,并且该类型与定义专用运算符实际上没有任何关系。
因此,当我这样做时,这会导致我得出一个合乎逻辑的结论:
EventHandler handler = MyEvent;
C#编译器将其转换为
EventHandler handler = EventHandler.Combine(MyEvent)
由于你的帮助,我很惊讶这个问题很快就解决了。
非常感谢你!