(1 item) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(6 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(4 items) |
|
(2 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(5 items) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(8 items) |
|
(2 items) |
|
(7 items) |
|
(2 items) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(3 items) |
|
(2 items) |
|
(2 items) |
|
(8 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(6 items) |
|
(1 item) |
|
(2 items) |
|
(5 items) |
|
(5 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(16 items) |
|
(10 items) |
|
(27 items) |
|
(15 items) |
|
(15 items) |
|
(13 items) |
|
(16 items) |
|
(15 items) |
A few weeks ago, when Don Box and Chris Anderson were in London, they gave a talk at which they showed various forthcoming Whidbey, Indigo and Longhorn features. Although I had been to the PDC, I decided to go along to this talk in London anyway. At the PDC I had concentrated on the client-focused sessions, so I thought I might learn some stuff about the other areas.
And I did see something that surprised me. In fact it was doubly surprising - it was on a topic I thought I already knew about, so I was surprised to be surprised by this particular subject, if you see what I mean.
One of the new C# language features that Whidbey introduces is anonymous delegates. I already knew about these, because they have been pretty widely talked about. I also knew that, as with similar 'closure' mechanisms offered by many functional programming languages, an anonymous delegate gets access to its surrounding scope. There are lots of situations in which this will offer a more elegant solution than is available with v1.0 and v1.1 of C#. For example, in Windows Forms applications, you often have to deal with callbacks that come in on some random thread, and you must marshal the call back onto the UI thread before dealing with it. (Windows Forms objects all have thread affinity, and it's illegal to touch them from anything other than the UI thread.) The techniques for dealing with this are reasonably well documented, but they are a little cumbersome. One technique involves writing two methods: the one that receives the initial callback on the wrong thread, and then the second one that processes the callback on the correct thread. Another approach involves sticking some boilerplate code at the top of the handler function causing the call to be marshalled back into the same method, but onto the UI thread if it isn't already on the UI thread. Both of these are a bit messy.
With anonymous delegates, a slightly cleaner solution can be used. You can do this:
private void CalledOnNonUIThread(string data) { BeginInvoke((MethodInvoker) delegate() { // This runs on the UI thread! this.Text = data; }); }
Note: these code samples are all written to run against the pre-release build of Whidbey that was given out at the PDC. Bear in mind that things might change by the time the final version ships.
Actually this example is not the most elegant I could have chosen. Generally speaking you don't need a cast before the
delegate
keyword. However, the compiler needs to be able to deduce what kind of delegate
is required. The problem here is that Control.BeginInvoke
is declared as taking a System.Delegate
,
meaning that it's quite happy to accept any kind of delegate. Unfortunately, that's too much flexibility - the C# compiler needs to
know specifically what kind to create here. That cast before the delegate()
tells it what to do here.
(If you're wondering why the compiler doesn't just create a System.Delegate
, try changing the cast
to that type. You will get a rather surprising error: "error CS1660: Cannot convert anonymous method block to type
'System.Delegate' because it is not a delegate type"
. Yes, that's right - System.Delegate
isn't a delegate
type! This is one of the slightly confusing features of the .NET type system; it's similar to the fact that System.ValueType
is not a value type. These are both marker classes, and you have to be derived from them to qualify as a delegate
or value type respectively. Fortunately, very few methods use the base System.Delegate
type, so you would rarely
need a cast like this.)
Using an anonymous delegate provides an attractive feature not available in the pre-Whidbey solutions. Anything in scope
in the CalledOnNonUIThread
method at the point at which the anonymous delegate appears is also in scope in
the body of the anonymous delegate. This example exploits that by accessing the data
parameter. Prior to Whidbey,
we would have needed to use some other mechanism get that information to the method being invoked by BeginInvoke
.
(This would typically be done either by defining an appropriate new delegate type, or by wrapping the information in some object.)
But this wasn't what surprised me at Don and Chris's talk - I already knew about all this. What surprised me was that they showed that you can also do more or less the opposite. I forget the exact code they used, but it was something like this:
int i = 40; EventHandler myDelegate = delegate { i += 2; }; myDelegate(null, EventArgs.Empty); Console.WriteLine(i);
(There's no significance to the use of the EventHandler
here, by the way. I just switched to it to make sure
nobody thought there was anything magic about the less well known MethodInvoke
delegate type.) In thi s example, the
inline delegate not only reads the value of the i
variable from the surrounding scope, it also writes a modified
value back. That was the part that amazed me.
Why would that amaze me? On the face of it, it may seem like precisely the kind of thing you should obviously be able to do - if you can read the variable, there's a logical symmetry to being able to write to it too. Why on earth wouldn't this work?
It surprised me for two reasons. One was that I was sure I had read something that said it wouldn't work - I have a
distinct memory of reading an article that said that the variables picked up from the containing scope would be read-only.
But try as I might, I can no longer find this article, so it's possible that I imagined it. However, more importantly, it would be
unsurprising if it didn't work. Think about what's going on here. We're creating an inline delegate here whose lifetime could easily outlive the scope of the method, so the containing
scope might well not even be around when the delegate is invoked. In fact in the first example that's almost certain to
be the case - the CalledOnNonUIThread
method is highly likely to have exited by the time
Control.BeginInvoke
gets around to running the anonymous delegate. So for it to be able to use that data
variable,
it seems pretty clear that it must be using a copy, not the original variable, because the original variable is
long gone. (If the delegate really did have a reference to the original parameter we'd be in trouble - that variable is in
the stack frame of the original method. This would be as bad an idea as writing some C++ code that dereferences
a pointer into a stack frame of a function that has exited.)
We can investigate what happens when the anonymous method runs asynchronously by modifying the second example:
int i = 40; EventHandler myDelegate = delegate { Thread.Sleep(200); i += 2; }; IAsyncResult iar = myDelegate.BeginInvoke(null, EventArgs.Empty, null, null); Console.WriteLine(i); myDelegate.EndInvoke(iar);
Here, we are invoking the delegate asynchronously - it will run on a system thread pool thread because we used the
delegate's BeginInvoke
method. (Not to be confused with the Control.BeginInvoke
method.
I really wish those had different names.) We also make the anonymous delegate wait for a short while before it does
anything. This means that the Console.WriteLine
will typically run before the delegate updates i
.
So it's unsurprising that this example prints out 40
.
So far, this is entirely consistent with the 'obvious' mental model - that the delegate is updating the original
i
variable. The reason this example prints out the initial value is that we are reading i
before
the delegate updates it. (However, this leaves an unanswered question hanging: if I left out that final EndInvoke
,
what would happen when the update occurs? What exactly is it updating if i
is no longer in scope? As it
happens, nothing untoward happens, for reasons we'll see shortly.) So what about this:
int i = 40; EventHandler myDelegate = delegate { Thread.Sleep(200); i += 2; }; IAsyncResult iar = myDelegate.BeginInvoke(null, EventArgs.Empty, null, null); Thread.Sleep(500); Console.WriteLine(i); myDelegate.EndInvoke(iar);
Here we are kicking off the delegate asynchronously as before, but making sure we give it time to
finish before printing out the results. This behaves correctly - it prints out 42
. Again, this
surprised me slightly. I had made an incorrect guess about how the compiler was making this all work. I thought
it was probably building an object to hold the variables that the inline delegate uses, and after the delegate was
used, the containing method would read those values back into the original variables. (As it turns out, I wasn't
all that far wrong with this guess. As we'll see, my only mistake was to assume that it would actually need to copy
the data back out of the object into the local variables at all.)
I also tried some more exotic examples, launching off lots of asynchronous calls simultaneously. It still
behaves as predicted by the obvious logical model, where they all share access to the same i
variable. This is slightly surprising if you are familiar with how variables and parameters work down at
the IL level. Their lifetime is inextricably bound up with the method - a method's variables and parameters
simply disappear when that method exits, because the stack frame that defines them goes away.
The way the compiler gets around this is by not relying on the stack frame to hold variables used from within
inline delegates. Steve Maine wrote an excellent
article on anonymous delegates, and explains the details of what the compiler does in the "Underneath the Hood" section.
What might not be immediately clear from his explanation though is that it's not just the anonymous delegates that
rely on the generated object to provide access to the variables inherited from the containing scope. The code in
the containing scope also uses this generated object - that's how all the methods come to be sharing the same 'variable'. So
if you look at the IL for the Console.WriteLine
in these examples, you'll see this kind of thing:
ldloc.s V_4 ldfld int32 MyForm/__LocalsDisplayClass$00000004::i call void [mscorlib]System.Console::WriteLine(int32)
That first instruction retrieves a reference to the object that contains all the variables accessible both to the original
method and also to the inline delegate. The second retrieves the current value of the i
variable. This is rather
more complex from how variables are usually accessed. Here's how it looks in an example with no anonymous
delegates:
ldloc.0 call void [mscorlib]System.Console::WriteLine(int32)
So the C# compiler has gone to great lengths to maintain conceptual integrity here. It makes sure that there are no surprises insofaras this behaves in the 'obvious' way - the inline delegate really is using the same variable as the method. The only potential surprise is that this moves C# away from the CLR's model. In C# V1.x, parameters and local variables mapped directly onto CLR parameters and local variables. In C# V2.0 this is no longer always the case - parameters and local variables in a C# program might still map onto their CLR equivalents, but if inline delegates are involved, they may instead map onto the fields of a compiler-generated object. So for those of us used to thinking about C# variables and parameters as being essentially the same as their CLR equivalents, this behaviour is initially surprising.
But I think I like it, now I know how it works.