【Unity勢に】C#のforeachとラムダ式の落とし穴、そしてその破壊的言語仕様変更【今読んでほしい】

  • 56
    いいね
  • 0
    コメント

はじめに

 C#アドベントカレンダー2016、19日目は『【Unity勢に】C#のforeachとラムダ式の落とし穴、そしてその破壊的言語仕様変更【今読んでほしい】』と題して室星が担当させていただきます。

 タイトルはラムダ式としていますが、正確にはデリゲートです。

foreachとデリゲートの落とし穴

 次のコードを実行するとどのようなログが出力されるでしょうか?

var names = new List<string> { "Taro", "Jiro", "Saburo" };
var actions = new List<Action>();

foreach (string name in names)
{
    actions.Add(() =>
    {
         Console.WriteLine(name);
    });
}

foreach(var action in actions)
{
    action();
}

 ラムダ式を用いてnamesの要素を表示するAction型のデリゲートを生成し、それをactionsというリストに追加しています。そして、actionsの要素を列挙しデリゲートを実行しています。

 さぁ、実行結果はどうなるでしょうか?

 なんと結果はC#のバージョンなど、実行環境によって異なります!

 C# 5.0もしくはそれ以降で実行すると1、次のようにTaro、Jiro、Saburoと順に表示されます。「こう表示されるのではないか?」と予想した方も多いのではないでしょうか?

Taro
Jiro
Saburo

 ところがC# 5.0以前1で実行すると、先ほどのコードは次のような結果になります。

Saburo
Saburo
Saburo

 どういうことでしょうか?

C# 5.0以前とforeachとデリゲート

Saburo
Saburo
Saburo

 という結果が予測と違った方もいたのではないでしょうか?

 C# 5.0以前1では先ほどのコードのように、foreachで列挙した変数をデリゲートの中で利用した場合、最後に列挙した要素が常に使われてしまいます。

 そのため、TaroやJiroではなくSaburoが表示されてしまうのです。

 もし、C# 5.0以前1で次のような結果にしたいのであれば、

Taro
Jiro
Saburo

 コードを次のように変更する必要があります。

var names = new List<string> { "Taro", "Jiro", "Saburo" };
var actions = new List<Action>();

foreach (string name in names)
{
    string targetName = name; // ここがポイント
    actions.Add(() =>
    {
         Console.WriteLine(targetName);
    });
}

foreach(var action in actions)
{
    action();
}

 foreachで列挙した要素nameを、foreachの中で宣言した変数targetNameに代入しています。ここがポイントです。

C# 5.0での破壊的な仕様変更

 foreachとデリゲートのコードを再掲します。

var names = new List<string> { "Taro", "Jiro", "Saburo" };
var actions = new List<Action>();

foreach (string name in names)
{
    actions.Add(() =>
    {
         Console.WriteLine(name);
    });
}

foreach(var action in actions)
{
    action();
}

 繰り返しになりますが、このコードはC# 5.0で挙動が変わりました。C# 5.0とそれ以降では、

Taro
Jiro
Saburo

 と表示されます。

 この変更は破壊的な変更であることに注意してください。(元の挙動に依存したコードってかなりあれですが。。。)

 このことに関しては、@ufcppさんの「C# 5.0 の新機能」により詳しい説明があるので、さらに詳しく理解したい方はそちらも読まれるのをお勧めします。

Unityクラスタにこそ、今知ってほしい!

 原稿執筆時(2016年12月中旬)、C# 7.0がそろそろ出そうな時期になぜ私がこんなC# 5.0関連の記事を書いたのか。

 それはゲームエンジンの方のUnityが関連します。(タイトルの通りこの記事は、UnityでC#を書いている人向けに書かせていただきました。)

 実は2016年の11月末にリリースされたUnity 5.5で、コンパイラのアップデートにより、このforeachとデリゲートの挙動の変更が適用されました。

 Unity 5.5以前は、

Saburo
Saburo
Saburo

 と表示されていたものが、Unity 5.5からは

Taro
Jiro
Saburo

 と表示されます。

 背景と流れをすこし説明します。

 Unityでは、今まで古いC# (原則 C# 3.0で一部 C# 4.0の機能も使える)が使えていました。

 2016年4月にUnityが.NET Foundationに入るなど、もろもろ様々な動きがありUnityで使えるC#のアップデートが決まりました。

 原稿執筆時はまだ製品版でC# 6.0はまだ使うことはできません。(Experimental Preview版でC# 6.0を試すことは可能です)

 しかし、コンパイラのアップデートは製品版でもすでに始まっていて、C# 5.0で行われたforeachとデリゲートの破壊的な言語仕様変更は、Unity 5.5ではすでに行われているようです。

 繰り返しになりますが、次のコードはUnity 5.5の前と後で挙動が異なります。特にUnity 5.5以前の挙動は期待した挙動とことなり、思わぬバグの原因になりうるという点に注意してください!

var names = new List<string> { "Taro", "Jiro", "Saburo" };
var actions = new List<Action>();

foreach (string name in names)
{
    actions.Add(() =>
    {
         Console.WriteLine(name);
    });
}

foreach(var action in actions)
{
    action();
}

さいごに

 UnityのAsset製作者さんは、古いUnityも対象に含めていることが多いと思いますので、Unity 5.5とそれ以前で挙動が異なる点に注意してください!

 コンパイラは始まったアップデート、Unityでも早くC# 6.0が使えるといいですね!


  1. Unityでは、Unity 5.5以前は変更前挙動に、Unity 5.5とそれ以降は変更後の挙動となります。 

この投稿は C# Advent Calendar 201619日目の記事です。