C#開発における落とし穴~ラムダ編~
技術担当の大林です。
前回に引き続き、C#の開発における落とし穴、所謂うっかりやらかしたり勘違いしやすい部分について少し書こうと思います。
※当記事はVisualStudioでの開発を想定して執筆しております。
開発環境次第では当てはまらない事もある事を、あらかじめご了承ください。
さて、今回はラムダ式について触れようと思います。
()=>{};
みたいな感じで書くアレです。
こちらのラムダ式、いちいち管理するほどでもないけどちょっと冗長な記述避けたいとか、メンバ変数等では持ちたくないけど、データ毎に別々の値を使って共通の処理をさせたいみたいな場合等、色々便利ではあるんですが、知らずに使うとちょっと想定外の動作をしてしまう危険があります。
まずは、下のコードを少し見てみてください。
1 2 3 4 5 6 | var list = new List<Func<int>>(); for(int i = 0; i < 100; i++) { list.Add(()=> { return i + 1; }); } |
こちらのラムダ式は、一見、ループで0から99まで処理をしてくれるように見えます。
では、実際に結果がどうなるか見てみましょう。
1 2 3 4 | foreach (var func in list) { Console.WriteLine(func().ToString()); } |
はい、全てのラムダ式の実行結果で、101(iの最後の数値100+1)の結果が出てきて、見事に想定した結果と違っていますね。
というわけで、今回の落とし穴になるポイントは、ラムダ内でローカル変数を使用した時の振る舞いについてです。
こちら、どうしてこんな事になってしまうかというと、全てのラムダからループカウンタのiを参照している形で処理が出来上がっているからなんですね。
ラムダ式が実行されるタイミング自体はラムダ式を呼び出したConsole.WriteLineのタイミングですし、関数を抜けてもローカル変数を保持し続けて使用できるが故に、きちんと参照が残っていてスコープにあわせた振る舞いをしてくれています。
ちなみにこちら、下のように毎回ループ内のみのスコープでローカル変数が確保されるような形にしてあげると、この問題は解決します。
var list = new List<Func<int>>();
1 2 3 4 5 | for(int i = 0; i < 100; i++) { int val = i; list.Add(() => { return val + 1; }); } |
この辺り、参照とは何ぞやとか、C言語等で言うポインタの仕組みを念頭に置いたり、ラムダ式がどんな風にビルドされてどう動くか?といった所を意識していたら何となく気づけるんですが、知らないとうっかりやらかしてしまいかねないのでご注意を。
では!
見ていただき、ありがとうございました!