Code inspection: Access to modified captured variable
Category: Potential Code Quality Issues
ID: AccessToModifiedClosure
EditorConfig: resharper_access_to_modified_closure_highlighting=[error|warning|suggestion|hint|none]
Language: C#, VB.NET
Requires SWA: No
tip
First of all, let's make sure that you understand what a closure is. To put it simply, a closure in C# is a lambda expression or an anonymous method that captures some variables from an outer scope. Here is the simplest example:
// A self-contained lambda. Not a closure.
Action printOne = () => { Console.WriteLine("one"); };
// A closure – a lambda that captures a variable from an outer scope.
string myStr = "one";
Action print = () => { Console.WriteLine(myStr); };
In the example above, print
will capture the variable myStr
(and not its value) and will only get the value of myStr
when you invoke print()
.
In more complex scenarios, when a closure is defined in a changing context, it may not behave as expected.
Here is an example of defining the above closure inside a loop:
var myActions = new List<Action>();
var myStrings = new List<string>() { "one", "two", "three" };
for (var i = 0; i < myStrings.Count; i++)
{
Action print = () => { Console.WriteLine(myStrings[i]); };
myActions.Add(print);
}
myActions[0]();
Surprisingly, this code produces an ArgumentOutOfRangeException
when we invoke myActions[0]();
. The following happens here: instead of executing Console.WriteLine(myStrings[0]);
, which may seem intuitive, this call tries to execute Console.WriteLine(myStrings[i]);
and because i
is scoped to the whole for
cycle, its value not equals 0
, and even not 2
(which was the last time the condition was true). As the result of the last ++
operation the value became 3
just before the condition became false and we exited the loop. As myStrings
only has 3 elements, myStrings[3]
leads to the ArgumentOutOfRangeException
.
Although ReSharper doesn't infer the consequence, which takes the shape of the ArgumentOutOfRangeException
here, it correctly points to the source of the problem — the iteration variable in closure — and suggests to fix it by copying the value of a changing variable to the scope where the closure is defined:
for (var i = 0; i < myStrings.Count; i++)
{
var i1 = i;
Action print = () => { Console.WriteLine(myStrings[i1]); };
myActions.Add(print);
}
This fix makes sure that when you pick an action from myActions
and get the context where this action was created, i1
will hold the value corresponding to the index of the action in the list.