雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

C# 5.0 で変わった事 - foreach の破壊的変更

C# Advent Calendar 2012 12 日目、まもなく折り返しですね。
さて、 C#er としては参加しないわけにもいかないので、去年に続いて参加表明したものの、やっぱり去年同様、書くネタに困る訳です。。。皆、よくネタ見つかるなぁ、と。
……で、ほとほと困ったので忘れ去られたネタを引っ張り出して取り繕おう、と云うのがこの記事の趣旨です。(
知ってる人には今更であり、知らない人には特に知る必要もない誰得ネタではありますが、よろしくお付き合いくださいますませ。
C# 5.0 で大きく変わった事と云えば、何をおいても非同期処理のためのシンタックスシュガー (async, await) の追加が挙げられると思いますが、非同期処理については、既に多くの方が記事を書かれているのでそちらを見ていただくとして、今回は「 foreach の破壊的変更」について取り上げたいと思います。

破壊的変更?

そもそも言語仕様に於ける「破壊的変更」とは、既存の動作と互換性のない(仕様変更の前後で既存コードの動作が変わらない事が保証されない)様な変更の事を言います。
これは、 C# 4.0 以前のバージョンで記述されたコードを C# 5.0 でコンパイルした際に、 C# 4.0 でコンパイルしたものと動作に差異が発生する可能性がある事を意味する為、通常ではこの様な変更は極力行われない様にされています。(少なくとも C# では)
# でも今回は行われました。。。
# まぁ、後で紹介する通りこの変更によって困るケースはほとんどないと思われるので、その辺が加味されたんだと思います。

C# 4.0 まで

まず、以下のコードを見てください。
# 普通、こんなコードは書かないと思いますが、そこは目を瞑ってください。。。
# いい実例が思い浮かばなかったんです><

public MainWindow()
{
    InitializeComponent();

    // 設定ファイルから取ったつもり
    this.rssList = new[]
    {
        "http://d.hatena.ne.jp/masa-k/rss",
        "http://d.hatena.ne.jp/mknet/rss",
        "http://miku.sega.jp/info/public/archive/miku/index.xml"
    };

    // ボタンクリックに紐づけ
    foreach (var rss in this.rssList)
    {
        this.button1.Click += (s, e) => this.GetRss(rss);
    }
}

これは、「画面上のボタン (button1) を押した際に、予め設定ファイルに登録されている複数箇所の RSS を取得する」事を意図したコードです。
しかし、このコードを C# 4.0 でコンパイルして実行すると、設定ファイルに定義された RSS の個数分、最後に定義された RSS (この例だと週刊ディーヴァ・ステーションの更新情報)の情報を取得してしまいます。
これは、 foreach 内のラムダ式に於ける、変数のスコープが関係しています。
以前書いた様に、ラムダ式内で局所変数を使用すると、対象の変数がスコープ外になる時点でコピーが保存されます。
ここで、 foreach 内で使用している rss 変数のスコープが問題になるわけですが、 C# 4.0 までは foreach は以下の様なコードに内部展開されていたそうです。(以下のコードは ufcpp さんの記事を参考にさせていただきました)
※あくまでもイメージです

using (var e = rssList.GetEnumerator())
{
    string rss;
    while (e.MoveNext())
    {
        rss = (string)e.Current;
        button1.Click += (s, e) => this.GetRss(rss);
    }
}

ここで変数 rss の宣言位置が while 句の外である事に注目してください。 rss 変数のスコープがループ処理の完了時点まで続く事がわかると思います。
これにより、 button1 のクリックイベントに紐づけられたラムダ式には、全てループ処理完了時の rss の値( rssList の最後の値)がコピーされる事になります。
……と、これが C# 4.0 までの foreach の動作でした。

C# 5.0 での破壊的変更

ここからが本題です。
C# 4.0 まででの動作はあまり嬉しくないので(かどうかは定かではありませんが ^^; )、 C# 5.0 ではこの仕様が変更されました。
具体的には、 C# 5.0 では先のコードの内部展開が以下の様に変更されました。(こちらもあくまでイメージです)

using (var e = rssList.GetEnumerator())
{
    // (1)
    while (e.MoveNext())
    {
        string rss; // (2)
        rss = (string)e.Current;
        button1.Click += (s, e) => this.GetRss(rss);
    }
}

変数 rss の宣言位置が (1) から (2) に移動したのがわかると思います。
これにより rss のスコープはループ (while) の 1 ターン内に限定される様になったので、処理対象データ毎に( e.Current の値が)コピーされる様になりました。

……破壊的?

この修正は、一応既存コードの動作に影響を与えるため、「破壊的変更」に位置付けられます。
但し、そもそも C# 4.0 以前の動作を意図しているケースは考えにくいため、基本的には影響はないと思われます。
むしろ、この変更によって直感的に正しい書き方が出来る様になったと喜んでよいのではないでしょうか。