views:

33

answers:

1

When I run this snippet of code:

string[] words = new string[] { "foo", "bar" };
var results = from row in Assets select row;
foreach (string word in words)
{
    results = results.Where(row => row.Name.Contains(word));
}

I get this SQL:

-- Region Parameters
DECLARE @p0 VarChar(5) = '%bar%'
DECLARE @p1 VarChar(5) = '%bar%'
-- EndRegion
SELECT ... FROM [Assets] AS [t0]
WHERE ([t0].[Name] LIKE @p0) AND ([t0].[Name] LIKE @p1)

Note that @p0 and @p1 are both bar, when I wanted them to be foo and bar.

I guess Linq is somehow binding a reference to the variable word rather than a reference to the string currently referenced by word? What is the best way to avoid this problem?

(Also, if you have any suggestions for a better title for this question, please put it in the comments.)

Note that I tried this with regular Linq also, with the same results (you can paste this right into Linqpad):

string[] words = new string[] { "f", "a" };
string[] dictionary = new string[] { "foo", "bar", "jack", "splat" };
var results = from row in dictionary select row;
foreach (string word in words)
{
    results = results.Where(row => row.Contains(word));
}
results.Dump();

Dumps:

bar
jack
splat
+7  A: 

You're using what's called a "closure", which means that you're defining an anonymous function (your lambda) that uses a local variable in its body. Specifically, you're "closing" over the loop variable word. The issue that arises with closures results from delayed execution, which means that the body of your lambda is not run when you define it, but when it's invoked.

Because of this, you almost never want to close over the loop variable. Because of delayed execution, the value of the variable within the lambda will be whatever it is when the lambda is executed. For lambdas declared inside a loop and invoked outside of it, this means that it will always have the last value from the loop.

To counteract this, use a local variable declared inside the loop. This will cause it to capture the value at that point in time and pass a new variable to every lambda that's created. Like so:

string[] words = new string[] { "foo", "bar" }; 
var results = from row in Assets select row; 
foreach (string word in words) 
{ 
    string tempWord = word;

    results = results.Where(row => row.Name.Contains(tempWord)); 
} 
Adam Robinson
Thanks, excellent description and it even works! :)
Scott Stafford