Wednesday, February 11, 2009
For Loops, Variable Scope and Anonymous Delegates with Outer Variables
There is no reason why any programming language must support built in looping. After all, even if
The
Consider the following code that loops ten times (from x = 0 to x = 9).
Given that most basic example above...
As it turns out, the
But, in that code above, the variable
In C#, variables declared in an outer scope are automatically available within the inner scopes. For example, if you declare a field on a class then it is accessible from within the class methods. If you declare a variable in a method then it is available inside other scopes within the method, such as within conditions and loops.
So, why is it important to know that? For starters, it means that you can declare the loop variable outside of the loop and it will still be available in "loop scope". And, if you declare it outside of the loop, then you can still access it after the loop has completed. Plus, it seems to be a bit more efficient to declare a variable outside of the loop body if it will be assigned a new value on every pass through the loop.
In the example above, using
In addition to the slight performance difference, there can actually be a big difference between declaring something outside of the loop vs. inside the body of the loop. For example, in the following code, are variables
Well, ok, that is kind of a trick question. In that example, they are equal. But what about in the following example using
Now it becomes a much tricker question. The answer is no, they are not necessarily equal inside of DoSomething()! Don't believe me? Create the following method and give it a try.
If you ran the loop on the main UI thread, you may suprised to see the following written to the console/output window:
What the heck? Why is
When
If the loop was run on the main UI thread, then none of the calls to
But, if the loop is run on the background thread instead of the main UI thread, then the values of
For example, if I run the following code, I will get something slightly different each time. Note that I use the "delegate BeginInvoke trick" to invoke
With that code above, I had the following output to the console.
If you run the code multiple times, you may get slightly different results each time.
for, foreach, while or do loops do not exist within the language itself, you could always make any of the above with a few variables, conditions and a goto statement. But, looping is such any essentially part of computer programming that most languages provide numerous loop constructs. C#.NET is no exception.The
for loop is undoubtably the most used and beloved loop construct. But, have you ever stopped to think about the details?Consider the following code that loops ten times (from x = 0 to x = 9).
for (int x = 0; x < 10; x++)
{
DoSomeStuff();
}
Given that most basic example above...
- Do you know the real scope of variable
x? - Do you know whether the condition is checked at the beginning or end of the loop?
- Do you know when variable
xis incremented?
As it turns out, the
for loop is almost a short hand representation for slightly more verbose code using the dreaded goto statement. But, using the goto leaves little doubt about the scope of the variable, when the condition is checked, and when the variable is incremented.
int x = 0;
TopOfLoop:
if (x < 10)
{
DoSomeStuff();
x++;
goto TopOfLoop;
}
But, in that code above, the variable
x continues to exist after the loop has completed (which is the way some programming languages do work). In C#, however, a for is really more like this, where I have added an extra set of curly braces to ensure that x goes out of scope when the loop is over.
// Variable x does not exist
{
int x = 0; // Now variable x does exist
TopOfLoop:
if (x < 10)
{
DoSomeStuff();
x++;
goto TopOfLoop;
}
// Variable x no longer exists
In C#, variables declared in an outer scope are automatically available within the inner scopes. For example, if you declare a field on a class then it is accessible from within the class methods. If you declare a variable in a method then it is available inside other scopes within the method, such as within conditions and loops.
So, why is it important to know that? For starters, it means that you can declare the loop variable outside of the loop and it will still be available in "loop scope". And, if you declare it outside of the loop, then you can still access it after the loop has completed. Plus, it seems to be a bit more efficient to declare a variable outside of the loop body if it will be assigned a new value on every pass through the loop.
int value1;
for(int q = 0; q < 10; q++)
{
value1 = GetValue(q);
int value2 = GetValue(q);
}
In the example above, using
value1 is more efficient than value2 because value2 goes in and out of scope on each pass, whereas value1 remains in scope the entire time.
// Variable x does not exist
for (int x = 0; x < 10; x++) // Now variable x does exist
{
// Variable x still exists
DoSomeStuff();
}
// Variable x no longer exists
int y; // Now variable y does exist
for (y = 0; y < 10; y++) // Variable y still exists
{
// Variable y still exists
DoSomeStuff();
}
// Variable y still exists
// Pretty much the same for variable z as for y,
// but we initialized the loop variable z outside of
// the loop instead of just declaring it outside
// of the loop.
int z = 0; // Now variable z exists
for ( ; z < 10; z++ ) // Variable z still exists
{
// Variable z still exists
DoSomeStuff();
}
// Variable z still exists
In addition to the slight performance difference, there can actually be a big difference between declaring something outside of the loop vs. inside the body of the loop. For example, in the following code, are variables
a, b and c always equal when DoSomething() is called?
int a = 0;
for(int b = 0; b < 10; b++)
{
int c = a = b;
// Are a, b and c equal inside of DoSomething()?
DoSomething(a, b, c);
}
Well, ok, that is kind of a trick question. In that example, they are equal. But what about in the following example using
BeginInvoke, MethodInvoker and an anonymous delegate?
int a = 0;
for(int b = 0; b < 10; b++)
{
int c = a = b;
// Are a, b and c equal inside of DoSomething()?
// NOTE: this.BeginInvoke assumes we are in a WinForm
this.BeginInvoke(new MethodInvoker(
delegate() { DoSomething(a, b, c); }));
}
Now it becomes a much tricker question. The answer is no, they are not necessarily equal inside of DoSomething()! Don't believe me? Create the following method and give it a try.
private void DoSomething(int aa, int bb, int cc)
{
if ( (aa != bb) || (aa != cc) || (bb != cc) )
{
Console.WriteLine(string.Format("{0}, {1}, {2}", aa, bb, cc));
}
}
If you ran the loop on the main UI thread, you may suprised to see the following written to the console/output window:
9, 10, 0
9, 10, 1
9, 10, 2
9, 10, 3
9, 10, 4
9, 10, 5
9, 10, 6
9, 10, 7
9, 10, 8
9, 10, 9
What the heck? Why is
a always 9 and b always 10? The call to this.BeginInvoke() "schedules" a call to DoSomething(a,b,c) to be run on the main UI thread via the MethodInvoker and anonymous delegate. Variables a and b are declared outside of the loop's body, whereas variable c is declared within the loop's body. Therefore, a and b never go out of scope during the entire loop. But, variable c goes in and out of scope on each pass through the loop.When
DoSomething() finally runs inside an anonymouse delegate on the main UI thread, it gets the current value of the outer variables a, b and c. If the variable has gone out of scope, then the current value is whatever it's value was when the variable went out of scope. Since c goes in and out of scope on each pass through the loop, the value you would expect to be passed into DoSomething() for c is passed in (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9). But, since a and b do not go out of scope on each pass, their current values at the time DoSomething() is actually called are the not the values on that pass through the loop.If the loop was run on the main UI thread, then none of the calls to
DoSomething() will occur until the loop has completed. And, in that case, a will always be 9 (because it was set to 9 on the last pass through the loop) and b will always be 10 because it was incremented to 10 before breaking out of the loop.But, if the loop is run on the background thread instead of the main UI thread, then the values of
a and b becomes a bit more unpredictable. This is because you never know when a context switch to the main UI thread to call DoSomething() will actually occur. It could occur while still running the loop, or after the loop has completed. And, if the context switch occurs around the time of the assigment c = a = b then it is possible that b as not yet been assigned to a or c; perhaps b has even been assigned to a but not c. This may not be so evident with a loop of only 10, but should become more evident if the loop is increased to, say, 100.For example, if I run the following code, I will get something slightly different each time. Note that I use the "delegate BeginInvoke trick" to invoke
DoLoop onto a background thread, and have added a Sleep(0) about half-way through the loop to make sure a context switch happens at least once so that DoSomething() has a likely chance to run at least one time on the main UI thread before the loop finishes on the background thread.
private void button1_Click(object sender, EventArgs e)
{
MethodInvoker del = new MethodInvoker(DoLoop);
del.BeginInvoke(DoLoop_Callback, del);
}
private void DoLoop()
{
int a = 0;
for(int b = 0; b < 100; b++)
{
int c = a = b;
this.BeginInvoke(new MethodInvoker(
delegate(){
DoSomething(a, b, c);
}));
if( (c % 50) == 0)
{
Thread.Sleep(0);
}
}
}
private void DoLoop_Callback(IAsyncResult ar)
{
MethodInvoker del = ar.AsyncState as MethodInvoker;
del.EndInvoke(ar);
}
private void DoSomething(int aa, int bb, int cc)
{
if ((aa != bb) || (aa != cc) || (bb != cc))
{
Console.WriteLine(string.Format("{0}, {1}, {2}", aa, bb, cc));
}
}
With that code above, I had the following output to the console.
46, 46, 0
99, 100, 1
99, 100, 2
...
99, 100, 98
99, 100, 99
If you run the code multiple times, you may get slightly different results each time.
Subscribe to Posts [Atom]