Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are my bound parameters all identical (using Linq)?

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
like image 669
Scott Stafford Avatar asked May 13 '10 13:05

Scott Stafford


1 Answers

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)); 
} 
like image 125
Adam Robinson Avatar answered Nov 15 '22 07:11

Adam Robinson