うにてぃブログ

UnityやUnreal Engineの記事を書いていきます

【C#】for ループ内 で Action や コールバックなどで index を利用した際に、最終的な値になる件について

例えば 下記のコードを実行すると 結果は 10 になる

using System;
using System.Collections.Generic;

public class C {
    public static void Main() {
        var actions = new List<A>();
       
        for (var i= 0; i < 10; i++)
        {
            actions.Add(new A(() => {Console.Write(i);}));
        }
        
        actions[0].Invoke();
    }
}

public class A {
    
    Action _action;
    
    public A(Action action)
    {
        _action = action;
    }
    
    public void Invoke()
    {
        _action.Invoke();
    }
}

そのため、index の値を正しく取得するには一度値をキャッシュしてから使う必要がある

        for (var i= 0; i < 10; i++)
        {
            var index = i;
            actions.Add(new A(() => {Console.Write(index);}));
        }

プログラム的には 0番目は 0 だから 0 が出そうですが何故出ないのかが気になったので調査してみた

生成コードの調査

SharpLab より上記のコードから生成される最終的な C# コードを見てみる

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class C
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public int i;

        internal void <Main>b__0()
        {
            Console.Write(i);
        }
    }

    public static void Main()
    {
        List<A> list = new List<A>();
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.i = 0;
        while (<>c__DisplayClass0_.i < 10)
        {
            list.Add(new A(new Action(<>c__DisplayClass0_.<Main>b__0)));
            <>c__DisplayClass0_.i++;
        }
        list[0].Invoke();
    }
}
public class A
{
    private Action _action;

    public A(Action action)
    {
        _action = action;
    }

    public void Invoke()
    {
        _action();
    }
}

注目すべきは コンパイラが出力している c__DisplayClass0_0 クラスです

Action で実行される メソッドがになっており

        internal void <Main>b__0()
        {
            Console.Write(i);
        }

このメソッドが参照する 値はループ前に生成されたインスタンスi になる

    private sealed class <>c__DisplayClass0_0
    {
        public int i;

そのため、ループ後に Action を呼び出すと 加算後 つまり i = 10 となっている <>c__DisplayClass0_.i
が呼び出されるため、結果が10になっていたことが分かる

生成コード調査その2

            var index = i;

を追加した場合のコードを確認する

public class C
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public int index;

        internal void <Main>b__0()
        {
            Console.Write(index);
        }
    }

    public static void Main()
    {
        List<A> list = new List<A>();
        int num = 0;
        while (num < 10)
        {
            <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
            <>c__DisplayClass0_.index = num;
            list.Add(new A(new Action(<>c__DisplayClass0_.<Main>b__0)));
            num++;
        }
        list[0].Invoke();
    }
}

c__DisplayClass0_0 が生成されているのは変わらないが

ループ内で c__DisplayClass0_0インスタンスが作成されている

そのため、Invoke 時に index に対応したインスタンスが呼び出されるので、想定した挙動をすることが分かる