28 Kasım 2009 Cumartesi

.NET 4.0 and Lazy

.NET 4.0 is so Lazy

With .NET 4.0 there is a new class added to the System namespace called Lazy. This class is what the name says, lazy. Here is an example where Lazy is used:

var lazy = new Lazy>(
() =>
{
var rows = //get order rows;
return rows;
});

var rows = lazy.Value;

The Lazy’s constructor can take a Func as an argument, the function passed as an argument to the contractor will first be invoked when the Value property of the Lazy class is used, but not invoked the next time the Value property is used. The code above will first execute the function passed as an argument when the we request the value of the Lazy, the returned value of the function will be cached. The next time Value is used, the function will not be invoked, instead the cached value will be returned. This class can for example be used when we want some kind of Lazy Loading.


Beware of the Lazy .NET 4.0 type. The closure trap.


The System.Lazy type in the new .NET 4.0 framework came to my attention recently. This type is not at all revolutionary. In fact, everyone could have written it themselves in under 10 lines of code going as far back as the .NET 2.0 framework.

Some less experienced programmers won’t realize however, that deferring the call to a delegate could have nasty side effects.

Consider the following code snippet:

01
static void Main(string[] args)
02
{
03
List> lazyInit = new List>();
04
for (char letter = 'A'; letter <= 'Z'; letter++)
05
{
06
var lazy = new Lazy(() => letter.ToString());
07
lazyInit.Add(lazy);
08
}
09
foreach (var lazy in lazyInit)
10
{
11
Console.Write(lazy.Value);
12
}
13
Console.ReadLine();
14
}
The code is pretty straight forward, but what gets printed here? Would you think it’s the alphabet? You would be very wrong.

The output is exactly ‘ZZZZZZZZZZZZZZZZZZZZZZZZZ’. Go ahead, run it yourself if you don’t trust me.

To understand why this happens, you need to understand how closures work.

In the constructor to the Lazy<> class, you’re passing in a delegate, in the form of a lambda. This delegate captures the letter variable (attention, not its value at the time of the call, but the variable as a whole) and creates a closure class around it behind the scenes. This may be counter-intuitive to the average programmer. For further information on what happens behind the scenes with captured variables and closures, read this great post by Marc Gravell.

This is not specific behavior of the new Lazy class, it’s what happens every time a delegate is stored for deferred execution.

If you rewrite the code without deferred execution, you’ll see that the problem doesn’t manifest itself:

01
static void Main(string[] args)
02
{
03
for (char letter = 'A'; letter <= 'Z'; letter++)
04
{
05
var lazy = new Lazy(() => letter.ToString());
06
Console.Write(lazy.Value);
07
}
08
Console.ReadLine();
09
}
10
// Output: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Same delegate is being used, but it is executed immediately. The output is now the complete English alphabet, as you would expect.

This isn’t very useful however, We’ve completely thrown away the advantages of the Lazy class and we might as well not be using it.

So, how can we fix it? We need to use an intermediary variable inside the body of the for loop that is simply a copy of the outer variable. The inner variable’s scope is unique to each loop iteration, so it matters a whole lot where you define your variables.

Fixed code:

01
static void Main(string[] args)
02
{
03
List> lazyInit = new List>();
04

05
// char letter2; // <- for the sake of exercise, uncomment this line and remove the ‘char’ keyword from the initialization of the letter2 variable inside the for loop below. the ‘bug’ will manifest itself again
06
for (char letter = 'A'; letter <= 'Z'; letter++)
07
{
08
char letter2 = letter; // value re-captured in inner block (remove ‘char’ keyword and uncomment line above to see the ‘bug’ manifest itself again)
09
var lazy = new Lazy(() => letter2.ToString());
10
lazyInit.Add(lazy);
11
}
12
foreach (var lazy in lazyInit)
13
{
14
Console.Write(lazy.Value);
15
}
16
Console.ReadLine();
17
}
One tool that can automatically check for this condition so you can avoid headaches later is JetBrain’s Resharper. With Resharper installed, you’d see a warning underneath () => letter.ToString() which spells ‘Access to modified closure’ and suggests the same fix I described in this blog post.

If anyone’s interested in seeing how the same Lazy class could be implemented in .NET 2.0 and up, leave a comment and I’ll post it here!

Hiç yorum yok: