Skip to content

Delegates Gotchas

Delegates are one of the first and most recognizable features of .net
framework. A delegate type can represent any method with a compatible
signature. They are so ubiquitous, that I can hardly imagine any .net
software written without them. Despite the popularity, there are
several overlooked features that can be source of subtle and difficult
to find bugs. I would like to dedicate this post to bring some light
and explain one of those pitfalls.

Let’s start from a puzzle. What do you think will be printed in the
next program?

   1: public class Program
   2: {
   3:     private static void Main(string[] args)
   4:     {
   5:         Action<int> action = x => Console.WriteLine("A {0}", x + 10);
   6:         action += x => Console.WriteLine("B {0}", x + 20);
   7:         Console.WriteLine("----(1)----");
   8:         action(10);
   9:         ModifyAction(action);
  10:         Console.WriteLine("----(3)----");
  11:         action(10);
  12:         Console.ReadLine();
  13:     }
  14:
  15:     private static void ModifyAction(Action<int> action)
  16:     {
  17:         action += x => Console.WriteLine("C {0}",x + 30);
  18:         Console.WriteLine("----(2) ----");
  19:         action(10);
  20:     }
  21: }

And the answer might be surprising:

—- (1) —-

A 20

B 30

—- (2) —-

A 20

B 30

C 40

—- (3) —-

A 20

B 30

I would expect that lines after (2) and after (3) will be the
same. What do you think could go wrong? Using method ModifyAction
looks like reasonably safe thing to do: Action delegate is a reference
type, so passing it to the method shall pass the reference. Operator
+= shall translate into Combine method call … What just happened
here?

The key to understanding the problem is Delegates Immutability. Type
Immutability is a strong term, which means that instance of that type
is an object whose state cannot be modified after it is created.

System.Delegate class is immutable class; once created, the
invocation list of a delegate does not change. Operations, such as
Combine and Remove, do not alter existing delegates. Instead, such an
operation returns a new delegate that contains the results of the
operation, an unchanged delegate.

So although the ModifyAction has a reference to action object on the
stack, the reference itself is replaced as a side effect of +=
operator (call to Combine method).

   1: private static void ModifyAction(Action<int> action)
   2: {
   3:             action += x => Console.WriteLine("C {0}",x + 30);

After we understood the reason for test failure, the trivial
solution for the problem would be an addition of ref classifier to method arguments.

   1: private static void ModifyAction(ref Action<int> action)
   2: {
   3:             action += x => Console.WriteLine("C {0}",x + 30);

This is an uncommon situation when ref appeared to be useful for
passing reference types. This allows called methods to modify the
object to which the reference refers because the reference itself is
being passed by reference.

Concept of immutability is very powerful, and usually
common to multi threaded environment. It requires a deep understanding
of what is going on behind the scenes and may cause subtle and
difficult bugs. Although current latest version of c# compiler does
not warn about this mistake, some tools like Resharper 4.x can spot
dangerous behavior. I hope that by writing this post I saved long bug
hunting session for someone.

Technorati tags: , ,

Post a Comment

Your email is never published nor shared. Required fields are marked *