はじめに
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が使えるといいですね!