How to capture a variable in C# and not to shoot yourself in the foot

Introduction

void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach(var a in actions)
{
a();
}
}
10
10
10
10
10
10
10
10
10
10

Why does it happen so?

.method assembly hidebysig instance void '<Foo>b__0'() cil managed
{
.maxstack 8
// Puts the current class item (equivalent to 'this')
// to the top of the stack.
// It is necessary for the access to
// the fields of the current class.
IL_0000: ldarg.0

// Puts the value of the 'i' field to the top of the stack
// of the current class instance
IL_0001: ldfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// Calls a method to output the string to the console.
// Passes values from the stack as arguments.
IL_0006: call void [mscorlib]System.Console::WriteLine(int32)

// Exits the method.
IL_000b: ret
}
.method private hidebysig instance void  Foo() cil managed
{
.maxstack 3

// -========== DECLARATION OF LOCAL VARIABLES ==========-
.locals init(
// A list of 'actions'.
[0] class [mscorlib]System.Collections.Generic.List'1
<class [mscorlib]System.Action> actions,

// A container class for the lambda.
[1] class TestSolution.Program/
'<>c__DisplayClass1_0' 'CS$<>8__locals0',

// A technical variable V_2 is necessary for temporary
// storing the results of the addition operation.
[2] int32 V_2,

// Technical variable V_3 is necessary for storing
// the enumerator of the 'actions' list during
// the iteration of the 'foreach' loop.
[3] valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action> V_3)

// -================= INITIALIZATION =================-
// An instance of the Actions list is created and assigned to the
// 'actions' variable.
IL_0000: newobj instance void class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::.ctor()
IL_0005: stloc.0

// An instance of the container class is created
// and assigned to a corresponding local variable
IL_0006: newobj instance void
TestSolution.Program/'<>c__DisplayClass1_0'::.ctor()
IL_000b: stloc.1

// A reference of the container class is loaded to the stack.
IL_000c: ldloc.1

// Number 0 is loaded to the stack.
IL_000d: ldc.i4.0

// 0 is assigned to the 'i' field of the previous
// object on the stack (an instance of a container class).
IL_000e: stfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i



// -================= THE FOR LOOP =================-
// Jumps to the command IL_0037.
IL_0013: br.s IL_0037

// The references of the 'actions'
// list and an instance of the container class
// are loaded to the stack.
IL_0015: ldloc.0
IL_0016: ldloc.1

// The reference to the 'Foo' method of the container class
// is loaded to the stack.
IL_0017: ldftn instance void
TestSolution.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()

// An instance of the 'Action' class is created and the reference
// to the 'Foo' method of the container class is passed into it.
IL_001d: newobj instance void
[mscorlib]System.Action::.ctor(object, native int)

// The method 'Add' is called for the 'actions' list
// by adding an instance of the 'Action' class.
IL_0022: callvirt instance void class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::Add(!0)

// The value of the 'i' field of the instance of a container class
// is loaded to the stack.
IL_0027: ldloc.1
IL_0028: ldfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// The value of the 'i' field is assigned
// to the technical variable 'V_2'.
IL_002d: stloc.2

// The reference to the instance of a container class and the value
// of a technical variable 'V_2' is loaded to the stack.
IL_002e: ldloc.1
IL_002f: ldloc.2

// 1 is loaded to the stack.
IL_0030: ldc.i4.1

// It adds two first values on the stack
// and assigns them to the third.
IL_0031: add

// The result of the addition is assigned to the 'i' field
// (in fact, it is an increment)
IL_0032: stfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// The value of the 'i' field of the container class instance
// is loaded to the stack.
IL_0037: ldloc.1
IL_0038: ldfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// 10 is loaded to the stack.
IL_003d: ldc.i4.s 10

// If the value of the 'i' field is less than 10,
// it jumps to the command IL_0015.
IL_003f: blt.s IL_0015


// -================= THE FOREACH LOOP =================-
//// The reference to the 'actions' list is loaded to the stack.
IL_0041: ldloc.0

// The technical variable V_3 is assigned with the result
// of the 'GetEnumerator' method of the 'actions' list.
IL_0042: callvirt instance valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<!0> class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::GetEnumerator()
IL_0047: stloc.3

// The initialization of the try block
// (the foreach loop is converted to
// the try-finally construct)
.try
{
// Jumps to the command IL_0056.
IL_0048: br.s IL_0056

// Calls get_Current method of the V_3 variable.
// The result is written to the stack.
// (A reference to the Action object in the current iteration).
IL_004a: ldloca.s V_3
IL_004c: call instance !0 valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>::get_Current()

// Calls the Invoke method of the Action
// object in the current iteration
IL_0051: callvirt instance void
[mscorlib]System.Action::Invoke()

// Calls MoveNext method of the V_3 variable.
// The result is written to the stack.
IL_0056: ldloca.s V_3
IL_0058: call instance bool valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>::MoveNext()

// If the result of the MoveNext method is not null,
// then it jumps to the IL_004a command.
IL_005d: brtrue.s IL_004a

// Finishes the try block execution and jumps to finally.
IL_005f: leave.s IL_006f
} // end .try
finally
{
// Calls the Dispose method of the V_3 variable.
IL_0061: ldloca.s V_3
IL_0063: constrained. Valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>
IL_0069: callvirt instance void
[mscorlib]System.IDisposable::Dispose()

// Finishes the execution of the finally block.
IL_006e: endfinally
}

// Finishes the execution of the current method.
IL_006f: ret
}

Conclusion

void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
var index = i; // <=
actions.Add(() => Console.WriteLine(index));
}
foreach(var a in actions)
{
a();
}
}
0
1
2
3
4
5
6
7
8
9
// -================= THE FOR LOOP =================-
// Jumps to the command IL_002d.
IL_0008: br.s IL_002d
// Creates an instance of a container class
// and loads the reference to the stack.
IL_000a: newobj instance void
TestSolution.Program/'<>c__DisplayClass1_0'::.ctor()
IL_000f: stloc.2
IL_0010: ldloc.2
// Assigns the 'index' field in the container class
// with a value 'i'.
IL_0011: ldloc.1
IL_0012: stfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::index
// Creates an instance of the 'Action' class with a reference to
// the method of a container class and add it to the 'actions' list.
IL_0017: ldloc.0
IL_0018: ldloc.2
IL_0019: ldftn instance void
TestSolution.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()
IL_001f: newobj instance void
[mscorlib]System.Action::.ctor(object, native int)
IL_0024: callvirt instance void class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::Add(!0)

// Performs the increment to the 'i' variable
IL_0029: ldloc.1
IL_002a: ldc.i4.1
IL_002b: add
IL_002c: stloc.1
// Loads the value of the 'i' variable to the stack
// This time it is not in the container class
IL_002d: ldloc.1
// Compares the value of the variable 'i' with 10.
// If 'i < 10', then jumps to the command IL_000a.
IL_002e: ldc.i4.s 10
IL_0030: blt.s IL_000a

--

--

--

The developer, the debugger, the unicorn. I know all about static analysis and how to find bugs and errors in C, C++, C#, and Java source code.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Go make your own malicious binary

Train/Test Complexity and Space Complexity of Linear Regression

Great tests reflect system behaviour

Aloia CMS: The road to version 1.0

Ruby Inheritance and Object Hierarchy

Introduction to Logging

Kubernetes K3s Cluster Setup

How to Structure CSS in Vue Like a Pro

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Unicorn Developer

Unicorn Developer

The developer, the debugger, the unicorn. I know all about static analysis and how to find bugs and errors in C, C++, C#, and Java source code.

More from Medium

C# Quick Sessions: Pattern matching — Chapter 1 The Declaration Pattern

Tips on migrating an existing C# project to using async

Real World Use Cases: Factory Design Pattern (in C#)

C# Problem: Alternative Builder Patterns