There's the following pattern which is used to avoid a race condition when raising events in case another thread unsubscribes from MyEvent, making it null.
class MyClass
{
public event EventHandler MyEvent;
public void F()
{
EventHandler handler = MyEvent;
if(handler != null)
handler(this, EventArgs.Empty);
}
}
as opposed to the wrong way of doing it which is prone to this race condition:
class MyClass
{
public event EventHandler MyEvent;
public void F()
{
if(MyEvent != null)
MyEvent(this, EventArgs.Empty);
}
}
My question is, given that System.Delegate
is a reference type: in case MyEvent is not null, how come
EventHandler handler = MyEvent;
seems to copy its invocation list instead of obtaining the reference.
I would expect that having the MyEvent delegate assigned to the 'handler' variable, then once somebody changed MyEvent that the object that 'handler' references would be changed as well.
Obviously, that is not the case, otherwise this nifty little pattern wouldn't work.
I've looked into the .NET source code and still could not find my answer there (it's probably there, but I've looked for about an hour and couldn't find it, so here I am.) I've also read what the C# Language Specification has to say about events and delegates, but it doesn't address this matter.
Thanks for your time.
Although you are correct that delegate-types are references-types, they are immutable reference-types. From
System.Delegate
:On another note, the only issue this pattern addresses is preventing the attempted invocation of a null delegate-reference. Events are prone to races despite this "fix".
Update
Here are some diagrams that should hopefully clear up confusion about copying references and assignment.
First: copying a reference.
In the above diagram, the reference contained in
y
is copied intox
. No one's saying the object is copied; mind you—they point to the same object.Second: assigning a new reference to a variable.
Forget about the
+=
operator for a moment; what I want to highlight above is thaty
is being assigned a different reference, to a new object. This does not affectx
becausex
is its own variable. Remember, only the reference (the "address" in the diagram) had been copied toy
.Third: same thing, only to
x
.The above diagrams depict
string
objects, only because those are easy to represent graphically. But it's the same thing with delegates (and remember, standard events are just wrappers around delegate fields). You can see how by copying the reference iny
intox
above, we have created a variable which will not be affected by subsequent assignments toy
.That is the whole idea behind the standard
EventHandler
race condition "fix" we're all familiar with.Original Answer
You are probably confused by this tricky little syntax:
What's important to realize is that, as Ani points out in his answer, delegates are immutable reference types (think: just like
string
). Many developers mistakenly think they are mutable because the above code looks like I am "adding" a handler to some mutable list. This isn't so; the+=
operator is an assignment operator: it takes the return value of the+
operator and assigns it to the variable on the left side.(Think:
int
is immutable, and yet I can doint x = 0; x += 1;
right? It's the same thing.)EDIT: OK, technically this is not quite right. Here is what really happens. An
event
is actually a wrapper around a delegate field that is accessible only (to external code) by the+=
and-=
operators, which are compiled to calls toadd
andremove
, respectively. In this way it is very much like a property, which is (typically) a wrapper around a field, where accessing the property and calling=
are compiled to calls toget
andset
.But the point still remains: when you write
+=
, theadd
method that gets called is internally assigning a reference to a new object to the internal delegate field. I apologize for oversimplifying this explanation in my initial answer; but the key principle to understand is the same.By the way, I am not covering custom events where you can put your own logic inside the
add
andremove
methods. This answer only applies to the "normal" case.In other words, when you do this...
...you are indeed copying a reference into a variable. Now that reference is in the local variable and won't itself be modified. If it was pointing to an actual object at the time of assignment, then it will continue to point at that same (immutable) object on the next line. If it was not pointing to an object (
null
), then it will still not point to an object on the next line.So if code elsewhere subscribed or unsubscribed to the event using
+=
, what it really did was change the original reference to point to a completely new object. The old delegate object is still around, and you've got a reference to it: in your local variable.I would like to point out that comparing this incident to the 'int' case is probably inherently wrong since even though 'int' is atomic, it is a value type.
But I think we've solved the case:
Delegate.CombineImpl Method shows the implementation.
I looked over the implementation of Delegate and MulticastDelegate in the .NET 4 source code. Neither of them declare the += or -= operator. Coming to think of it, in Visual Basic.NET you don't even have them, you use AddHandler, etc...
This means that the C# compiler implements this functionality and that the type doesn't really have anything to do with defining specialized operators.
So this leads me to a logical conclusion which is, when you do:
the C# compiler translates it to
I'm surprised how quickly this question was solved thanks to your help.
Thank you very much indeed!