65
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

はじめに

 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とそれ以降は変更後の挙動となります。 2 3 4

65
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?