JetBrains Rider 2024.3 Help

Code inspection: Access to modified captured variable

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 JetBrains Rider 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.

Last modified: 11 February 2024