3

今週、オランダで開催された TechDays 2013 に参加していたところ、興味深いクイズが出題されました。問題は次のプログラムの出力です。コードは次のようになります。

class Program
{
    delegate void Writer();

    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        for (int i = 0; i < 10; i++)
        {
            writers.Add(delegate { Console.WriteLine(i); });
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

明らかに、私が出した答えは間違っていました。int は値型であるため、渡される実際の値Console.WriteLine()がコピーされるため、出力は 0...9 になります。ただしi、この場合は参照型として扱われます。正解は、10 かける 10 と表示されることです。理由と方法を説明できる人はいますか?

4

4 に答える 4

6

int は値型であるため、Console.WriteLine() に渡される実際の値がコピーされます。

その通りです。 呼び出すとWriteLine、値がコピーされます。

それで、いつ電話しWriteLineますか?forループにはありません。その時点では何も書いていません。デリゲートを作成しているだけです。

foreachデリゲートを呼び出したときのループまでではなく、変数の値iが への呼び出しのためにスタックにコピーされるのはその時ですWriteLine

では、ループi中の値は何ですか? ループforeachの反復ごとに 10です。foreach

だから今、あなたは尋ねていiますforeach loop, isn't it out of scope。任意の期間である可能性のある匿名メソッド. 特別なことをまったく行わない場合、変数の読み取りは、たまたまメモリ内のその場所にスタックしたものを含むランダムなガベージになります. C# は、そのような状況が発生しないことを積極的に確認します.

それで、それは何をしますか?クロージャ クラスを作成します。これは、閉じられたすべてのものを表す多数のフィールドを含むクラスです。つまり、コードは次のようにリファクタリングされます。

public class ClosureClass
{
    public int i;

    public void DoStuff()
    {
        Console.WriteLine(i);
    }
}

class Program
{
    delegate void Writer();

    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        ClosureClass closure = new ClosureClass();
        for (closure.i = 0; closure.i < 10; closure.i++)
        {
            writers.Add(closure.DoStuff);
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

これで、匿名メソッドの名前が両方ともわかり (すべての匿名メソッドにはコンパイラによって名前が付けられます)、匿名関数を参照するデリゲートが存続する限り、変数が存続することを保証できます。

10このリファクタリングを見ると、結果が10 回印刷される理由が明確になると思います。

于 2013-03-08T20:36:29.810 に答える
4

これは、キャプチャされた変数であるためです。これにも発生してforeachいましたが、C# 5で変更されたことに注意してください。

class Program
{
    delegate void Writer();

    class CaptureContext { // generated by the compiler and named something
        public int i;      // truly horrible that is illegal in C#
        public void DoStuff() {
            Console.WriteLine(i);
        }
    }
    static void Main(string[] args)
    {
        var writers = new List<Writer>();
        var ctx = new CaptureContext();
        for (ctx.i = 0; ctx.i < 10; ctx.i++)
        {
            writers.Add(ctx.DoStuff);
        }

        foreach (Writer writer in writers)
        {
            writer();
        }
    }
}

ご覧のとおり、 は 1 つctxしかないため、 は 1つしかなく、 を超えるctx.iまでに 10 になっています。foreachwriters

ところで、古いコードを機能させたい場合:

for (int tmp = 0; tmp < 10; tmp++)
{
    int i = tmp;
    writers.Add(delegate { Console.WriteLine(i); });
}

基本的に、capture-context は変数と同じレベルでスコープされます。ここで、変数はループ内でスコープされるため、次のように生成されます。

for (int tmp = 0; tmp < 10; tmp++)
{
    var ctx = new CaptureContext();
    ctx.i = tmp;
    writers.Add(ctx.DoStuff);
}

ここでは、それぞれDoStuffが異なるキャプチャ コンテキスト インスタンス上にあるため、異なる別個のi.

于 2013-03-08T20:37:37.970 に答える
1

あなたの場合、委任されたメソッドは、ローカル変数 ( forループ index ) にアクセスする匿名メソッドです。つまり、これらはクローシュアです。i

匿名メソッドはforループの後に 10 回呼び出されるため、 iの最新の値を取得します。

同じ参照にアクセスするさまざまなクロージャの簡単なサンプル

これはクロージャの動作の簡略化されたバージョンです:

int a = 1;

Action a1 = () => Console.WriteLine(a);
Action a2 = () => Console.WriteLine(a);
Action a3 = () => Console.WriteLine(a);

a = 2;

// This will print 3 times the latest assigned value of `a` (2) variable instead
// of just 1. 
a1();
a2();
a3();

C#/.NET クロージャとは何かについて詳しくは、StackOverflow のこの他の Q&A ( What are clousures in .NET? ) を確認してください。

于 2013-03-08T20:37:12.077 に答える
0

私にとってActionは、カスタムの代わりにネイティブ クラスを使用して、古い動作と新しい動作を比較すると理解しやすいと思いWriterます。

C# 5 クロージャーより前は、for、foreach 変数、およびローカル変数のキャプチャの場合、同じ変数 (変数の値ではない) をキャプチャしていました。したがって、コードが与えられます:

    var anonymousFunctions = new List<Action>();
    var listOfNumbers = Enumerable.Range(0, 10);

    for (int forLoopVariable = 0; forLoopVariable < 10; forLoopVariable++)
    {
        anonymousFunctions.Add(delegate { Console.WriteLine(forLoopVariable); });//outputs 10 every time.
    }

    foreach (Action writer in anonymousFunctions)
    {
        writer();
    }

変数に設定した最後のだけが表示されます。ただし、C# 5 では foreach ループが変更されています。ここで、個別の変数をキャプチャします。 forLoopVariable

例えば

    anonymousFunctions.Clear();//C# 5 foreach loop captures

    foreach (var i in listOfNumbers)
    {
        anonymousFunctions.Add(delegate { Console.WriteLine(i); });//outputs entire range of numbers
    }

    foreach (Action writer in anonymousFunctions)
    {
        writer();
    }

したがって、出力はより直感的です: 0,1,2...

これは重大な変更であることに注意してください (マイナーなものであると想定されていますが)。そのため、C# 5 で for ループの動作が変更されないままになっている可能性があります。

于 2013-03-08T20:57:38.943 に答える