Thursday, November 5, 2009

BEWARE Reference variables in LINQ Lambda Expressions

I have come across a 'feature' of LINQ and lambda expressions where the variables used in the lambda are stored by reference, and changing them before you are finished with the result set cause the result set to change on the fly!

Now I am not across the inner workings of LINQ and lambda, however the results from doing the following code are not what I would have expected.

private void button1_Click(object sender, EventArgs e)
{
System.Diagnostics.Debug.WriteLine("Testing:" + DateTime.Now.ToString());
List<Dictionary<int, int>> ld = new List<Dictionary<int, int>>();

ld.Add(new Dictionary<int, int>() { { 1, 10 }, { 2, 20 }, { 3, 30 } });
ld.Add(new Dictionary<int, int>() { { 1, 20 }, { 2, 300000}, { 3, 40000000 }, { 4, 500000000 }});

int indx = 10;

var ds = ld.Where(itm => itm[1] == indx);

if (ds.Count() > 0)
{
System.Diagnostics.Debug.WriteLine("Count=" + ds.First().Count());
indx = ds.First()[2]; //this is a value I want to keep for later
int valueIwant = ds.First()[3]; //this is the value I want from this result

System.Diagnostics.Debug.WriteLine("Value I Want=" + valueIwant);
//oh no ??? that's not the value I wanted ???
System.Diagnostics.Debug.WriteLine("Count=" + ds.First().Count());
//and now the count changed ???
}

}

I would expect the LINQ query to return the first row, (where itm[key==1] == 10).

And valueIwant to equal the third value in that result dictionary (itm[key==3]).

The first count confirms we are working with row 1 with 3 records in the result dictionary.

HOWEVER, changing the indx value prior to getting valueIwant causes the result to change dynamically - thus returning a new result set, which in this case is row 2.

The end result in my variable is 40000000 - big diff. And the final count is 4, as it is the second record now.

This is a real trap - especially when using LINQ/lambda to work down a hierarchy of values.

My suggestion would be to use a pattern where any lambda expressions that use a variable should have the variable assigned to a lambda specific variable prior to creating the expression.

i.e.

int indx = 10;

int tmpidx = intidx;

var ds = ld.Where(itm => itm[1] == tmpidx);

if (ds.Count() > 0)

{

indx = ds.First()[2]; //this is a value I want to keep for later

int valueIwant = ds.First()[3]; //now I get what I wanted !!!!

I can't determine if this is by-design, a feature to support delegates, or a genuine bug, but it is definately something to watch out for!

Update: The legends on the MSDN forum suggest using .ToList() on the end of the Where statement to generate a fixed list result - thus separating the result set from the object query. It is also noted that each call to Count() and First() will cause the expression to re-evaluate!

http://social.msdn.microsoft.com/Forums/en-US/linqprojectgeneral/thread/b1480b00-44ec-4b32-984f-cdd53d87e99d

No comments:

Post a Comment