1

I have a warning with access to modified closure for string variable.

foreach (string s in splits)
{
    regexes.Where(x => x.Pattern.Contains(s));
}

Is it safe to leave the code the way it is? I presume delegate created with lambda will receive string by value since strings are immutable and each new "s" will be poiting to different memory.

Thanks

Marek
  • 2,419
  • 6
  • 34
  • 38
  • possible duplicate of [Access to Modified Closure](http://stackoverflow.com/questions/235455/access-to-modified-closure) – Jon Mar 17 '11 at 09:56

4 Answers4

3

In this case it doesn't hurt you because you aren't using the delegate at a place outside the loop. Of course, you aren't really doing anything useful in the example, so it is unclear if this "stands" for your actual code.

Threads and deferred callbacks would suffer though, so yes it is (in a way) valid. If unsure, fix it:

foreach (string s in splits)
{ 
    var clone = s;
    regexes.Where(x => x.Pattern.Contains(clone));
}

To clarify, this problem only hurts when the captured variable is used outside of the normal flow of the foreach loop. When used directly inside the loop, and on the same thread (no Parallel), the captured variable will always be at the expected value. Anything that breaks that will see random data; typically (but not always) the last value.

Note in particular that this "deferred delegate" includes things like building up Where filters on a query, as you'll find they all use the last value. The following would be bad:

var query = ...
foreach (string s in splits)
{ // BAD CODE!
    query = query.Where(x => x.Foo.Contains(s));
}

Re any "better way" (comments)... well, the cloned variable trick has the advantage of working, but here's a cute trick - fixes the above "BAD CODE!":

query = splits.Aggregate(query, (tmp, s) => tmp.Where(x => x.Foo.Contains(s)));

Personally I'd find the following easier to grok though:

foreach (string s in splits)
{
    var tmp = s;
    query = query.Where(x => x.Foo.Contains(tmp));
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • thanks, this is actually what I wanted to do. Is there better way to build up where clause? – Marek Mar 17 '11 at 10:10
2

I presume delegate created with lambda will receive string by value since strings are immutable and each new "s" will be poiting to different memory.

Creating a delegate does not evaluate the variables. That's the whole point about closures: They close over the variables. So, all your lambdas will capture the same variable s, which might not be what you want. If it affects your code or not depends on when the lambdas are evaluated: If the call of Where already "executes" the lambda (and throws it away afterwards), you should be fine. If your regexes object stores the lambdas and uses them later, you're in trouble.

Thus, to be on the safe side, copy the value to a variable local to the inside of the loop first.

Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • thanks. If I get it right this means delegate receives pointer to variable and not variable value it self. – Marek Mar 17 '11 at 10:06
  • 2
    @marek: In a nutshell, yes. What the compiler actually does it to create a helper class, move your variable and the delegate into the helper class and share an instance of that class with your code. That way, your code and the delegate both have a "pointer" to the same variable. This is explained in detail here: http://blogs.msdn.com/b/oldnewthing/archive/2006/08/02/686456.aspx – Heinzi Mar 17 '11 at 10:09
1

Doesn't look safe, and I generally try to avoid warnings when possible since they often indicate potential problems.

Googling "access to modified closure" brings up a number of seemingly relevant hits, including as #2 Linq: Beware of the 'Access to modified closure' demon which you definitely want to look closely at.

user
  • 6,897
  • 8
  • 43
  • 79
1

Here is a quick example that proves that it's this is not safe for strings, immutable or not.

List<Func<int>> lambdas = new List<Func<int>>();
List<string> strings = new List<string> 
{ "first", "second", "supercalifragilisticexpialidocious" };

foreach (string s in strings)
{
  lambdas.Add(new Func<int>(() => s.Length));
}

Console.WriteLine(lambdas[0]()); //34
Console.WriteLine(lambdas[1]()); //34
Console.WriteLine(lambdas[2]()); //34

However, aside from getting bitten by LINQ's defered execution, I don't really think it's a problem to use lambdas in this way, since mostly the entire lifespan of the lambda is within the scope of the foreach.

SWeko
  • 30,434
  • 10
  • 71
  • 106
  • What C# version have you tried this with? VS2010 / C# 4 prints 5, 6, 34. After some playing around with this it seems like they have changed the behavior with foreach-loops and it now works "as expected". Note that the problem still exists with for-loops though. – stmax Mar 22 '12 at 19:50
  • I've just tried it with VS2010/C#4 and I still get 34, 34, 34. Are you sure you do not have a C#5 beta installed, as this will be fixed in C# 5. – SWeko Mar 23 '12 at 16:02