0

This code is the reduced down version of the issue I'm having

public void MyMethod()
{
    System.Action[] actions = new System.Action[5];
    for (var i = 0; i < actions.Length; i++)
    {
        actions[i] = () => TestMethod(i);
        actions[i]();
    }

    for (var i = 0; i < actions.Length; i++)
    {
        Console.WriteLine(i);
        actions[i]();
    }
}

public void TestMethod(int index)
{
    // Just writing to the console for now
    Console.WriteLine(index);
}

I'm expecting the output to be like this:

First for loop: 0,1,2,3,4 Second for loop: 0,1,2,3,4

but turns out it's actually like this: First for loop: 0,1,2,3,4 Second for loop: 5,5,5,5,5

No clue what's happening in the background machine code, but is is not saving the value itself? I really need this additional passed in parameter action = () => foo(bar);to work. is there some way I can turn the i variable into just a value if that makes sense? I have a feeling it gets passed in as a reference and after the loop i is of course 5. I thought in c# all value type variables get passed in as values instead of references. Thanks in advance for the help!!

Ken White
  • 123,280
  • 14
  • 225
  • 444
  • Because `i` and `() => TestMethod(i);` are promoted to a field / method on a generated class with the scope of `MyMethod`. (see https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpGYZQAs6AKgKYDOALgLLX0AWA9sABQCWAdvXQ8AlHgC+hYmkwVGXYfknEpcGAG0AuugCGAY3o92fWugC86PtQDumOADooqtQFYNAbiXL0AM3YAndC4AN20AnjN0AAY3IXQAHh19Q2M7ABlqPgBzNhieMDBhT2UCJC8yxIMjWjUeLXN5MwA+KjomFg5uEQ9S8okeoj6xIA) You need to create a new variable inside the scope of the for loop. – Jeremy Lakeman Mar 22 '22 at 02:40
  • This is a scope issue. Whatever is being left as the argument for each of your `actions` is a value on the stack that is equal to 5. The CRL is using the same slot for `index` on each of your actions---each of which is unique. That is why the first loop produces the output you expect. In the second loop, you loop through each of 5 unique actions each of which points with `index` to the same address which is an `int` equal to 5 (since it was iterated up to 5 before leaving the first loop). – dmedine Mar 22 '22 at 02:49

1 Answers1

0

Your code is equivalent to;

public class MyMethodLocals(){
    public int i;
    public MyClass _this;
    public void Lambda() => _this.TestMethod(i);
}

public void MyMethod()
{
    var locals = new MyMethodLocals(){
        i = 0,
        _this = this
    };
    System.Action[] actions = new System.Action[5];
    for (locals.i = 0; locals.i < actions.Length; locals.i++)
    {
        actions[i] = locals.Lambda;
        ...
    }
    ...
}

Since the lambda has captured the local variable i, both the lambda and variable are promoted to members of a generated class, created with the same scope as the method.

All you need to do is introduce a new variable with the scope you require;


    for (var i = 0; i < actions.Length; i++)
    {
        var j = i;
        actions[i] = () => TestMethod(j);
    ...
Jeremy Lakeman
  • 9,515
  • 25
  • 29