Anonymous delegates in C# are extremely useful, but in order to use
them correctly, it is important to understand scoping in C#. Just the
other day I came across a situation in which the behavior I expected was
completely different from what actually happened - because it was all
happening in the wrong scope. So, I thought if it confused me, I should
throw it up here as a tutorial, because I'm sure it has confused other
people.
Take a look at the block of code below, where we are creating (and then
returning) a set of delegates that will print out a number to the
console. What do you think will get printed out when each delegate in
the returned array gets called?
public delegate void NoArgDelegate();
static NoArgDelegate[] InitializeOne()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
array[i] = delegate() { Console.WriteLine(i); };
return array;
}
At first glance, you might think that the output would look something
like this:
0
1
2
3
4
But what actually gets output is the following:
5
5
5
5
5
This is because each delegate refers not to a copy of the variable
i
,
but the actual i
from the for loop. And after the for loop completes,
the value of i
is 5, so that is the value each delegate prints out.
If you aren't convinced that all the delegates are pointing to the same
i
, we can write the code this way:public delegate void NoArgDelegate();
static NoArgDelegate[] InitializeOne()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
array[i] = delegate() { Console.WriteLine(i++); };
return array;
}
Now, we are incrementing
i
after the delegate prints out. And the
output of these delegates will look like this:5
6
7
8
9
So how do we get around this problem? Well there are two easy solutions.
First, we can write the code like the following:
public delegate void NoArgDelegate();
static NoArgDelegate[] InitializeTwo()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
{
int extraI = i;
array[i] = delegate() { Console.WriteLine(extraI++); };
}
return array;
}
The output of these delegates will be:
0
1
2
3
4
So why does this work? Well, we are essentially creating a copy of
i
in each interation of the for loop, in the form of extraI
. Because we
are declaring extraI
inside the for loop in each interation, the
extraI
variable is actually different for each delegate. That is the
key - it has to be a different variable for each delegate. For instance,
if we delcared extraI
outside of the loop, we would have even
different behavior:public delegate void NoArgDelegate();
static NoArgDelegate[] InitializeThree()
{
NoArgDelegate[] array = new NoArgDelegate[5];
int extraI;
for (int i = 0; i < array.Length; i++)
{
extraI = i;
array[i] = delegate() { Console.WriteLine(extraI++); };
}
return array;
}
Here, yet again, all the delegates will point to the same variable, and
we will get the following output:
4
5
6
7
8
The output is a little bit different, because
extraI
ends up being the
value 4 after the for loop finishes (unlike before when we were using
i
and it finishes the loop at the value 5).
Finally, theres the secod way to get around this problem, which is a
little bit cleaner. It looks like the following:
public delegate void NoArgDelegate();
static NoArgDelegate[] InitializeFour()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
array[i] = CreateDelegateFour(i);
return array;
}
static NoArgDelegate CreateDelegateFour(int i)
{ return delegate() { Console.WriteLine(i++); }; }
Here, again, the output is the expected:
0
1
2
3
4
What we are doing in this case is pushing the "delegate creation" into a
seperate function. The value of
i
gets copied by default when the
function is called, so each delegate gets a separate copy of i
. Plus,
since its in a separate function, it looks a bit cleaner.
Here is all the code in one block, so you can copy and paste it into
visual studio and play around:
using System;
namespace Scoping
{
class Program
{
public delegate void NoArgDelegate();
static void Main(string[] args)
{
Console.WriteLine("I am Method One:");
NoArgDelegate[] array = InitializeOne();
foreach (NoArgDelegate d in array)
d();
Console.WriteLine();
Console.WriteLine("I am Method Two:");
array = InitializeTwo();
foreach (NoArgDelegate d in array)
d();
Console.WriteLine();
Console.WriteLine("I am Method Three:");
array = InitializeThree();
foreach (NoArgDelegate d in array)
d();
Console.WriteLine();
Console.WriteLine("I am Method Four:");
array = InitializeFour();
foreach (NoArgDelegate d in array)
d();
Console.WriteLine();
Console.Read();
}
static NoArgDelegate[] InitializeOne()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
array[i] = delegate() { Console.WriteLine(i++); };
return array;
}
static NoArgDelegate[] InitializeTwo()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
{
int extraI = i;
array[i] = delegate() { Console.WriteLine(extraI++); };
}
return array;
}
static NoArgDelegate[] InitializeThree()
{
NoArgDelegate[] array = new NoArgDelegate[5];
int extraI;
for (int i = 0; i < array.Length; i++)
{
extraI = i;
array[i] = delegate() { Console.WriteLine(extraI++); };
}
return array;
}
static NoArgDelegate[] InitializeFour()
{
NoArgDelegate[] array = new NoArgDelegate[5];
for (int i = 0; i < array.Length; i++)
array[i] = CreateDelegateFour(i);
return array;
}
static NoArgDelegate CreateDelegateFour(int i)
{ return delegate() { Console.WriteLine(i++); }; }
}
}
Hopefully, this short tutorial on delegates and scoping helps you to
understand who is referring to what in the world of anonymous delegates.
Comments
Post a Comment