ラムダ「式」なんて言うから拒否反応を起こしている人、いるんじゃないでしょうか。
そう思って以前こんな記事を書きました。
が、これを読んでいた友人の理解度はいまひとつでした。
たとえが悪かったかな? と思いつつ、もっと簡単にラムダがなんなのか説明してみます。
サンプル その1
class Class1 { public void Start() { methodCall(callA); } void methodCall(Action call) { call(); } void callA() { Debug.WriteLine("call A"); } }
unity の場合、Debug.WriteLine を Debug.Log に置き換えてください。
こんな風に、メソッドの引数に Action を入れる、ということはちょいちょい起こります。
ただし、この callA は他で使われていませんし、いちいちメソッドを記述するのは面倒です。
そういう場合に、callA を省略してやろう、というのがラムダ式さんの得意技です。
class Class2 { public void Start() { methodCall( () => Debug.WriteLine($"call A") ); } void methodCall(Action call) { call(); } }
このように、引数にメソッドを埋め込んでしまうことができます。
callA という関数名を考えなくて済む、というのはちょっとした事ですがそれなりにメリット。
でも、この程度なら……。
これだけなら、元のでもよくない?
新しいこと覚えなくて済むし、callA なら他からも呼び出せるよ?
サンプル その2
そう考える人もいるかもしれません。
では、callA が string の引数を持つ場合はどうでしょうか。
class Class1 { public void Start() { string result = "ok"; methodCall(callA, result); } void methodCall(Action<string> call, string result) { call(result); } void callA(string result) { Debug.WriteLine($"{result}"); } }
急に引数まわりがダルい感じになってきました。
こんな調子で引数が増えていったら、毎回全てに string result のようなものを追加していくんでしょうか。圧倒的に面倒です。
さすがに嫌だからと、こんな風にする人もいるかもしれません。
class Class1A { string result = "ok"; public void Start() { methodCall(callA); } void methodCall(Action call) { call(); } void callA() { Debug.WriteLine($"{result}"); } }
たしかに引数はなくなりましたが、大外に result という残念変数が増えてしまいましたね。
このような影響範囲の多い残念変数が増えていくと、メンテナンス時に問題を起こすケースが増えていくでしょう。
callA には resultA、callB には resultB ……と増えていって、ある時 result でまとめちゃえ! で死亡フラグとか。
そこで、ラムダ式を使うと……?
class Class2 { public void Start() { string result = "ok"; methodCall( () => Debug.WriteLine($"{result}") ); } void methodCall(Action call) { call(); } }
その1の Class2 とほぼ変更なくコードをかけてしまいました。
ダル絡みの引数も一切なし、result は Start の中なので、影響範囲も狭いままです。
これが可能なのは、ラムダ式が Start の中で宣言されているため、Start の持つローカル変数値を取得可能という点です。これは大きなアドバンテージです。
ただし、非同期で使う場合は注意
ラムダは知らないが非同期を使う、という人はあまりいないかもしれませんが個人的には忘れやすいので一応触れておきます。
ローカル変数にアクセルできるアドバンテージ、非同期では問題を起こすことがあります。
以下のようなコード。
class Class3 { public void Start() { string result = "ok"; Task.Run( () => { methodCall( () => Debug.WriteLine($"{result}") ); } ); result = "ng"; } async void methodCall(Action call) { await Task.Delay(1000); call(); } }
1秒後に Debug.WriteLine($"{result}")
が実行されるのですが、その時の result の中身は ng です。
これは、タスク発行元の Start が、直後に値を ng に変えているため起こります。
あくまで実行されるタイミングまで値が保存されているのが前提、という事を忘れてはいけません。
Task が Image のリソースを使う前に解放してしまう……といったダングリングポインターのような重篤な問題(GC の気分で動いたり、動かなかったりするやつ)を引き起こすこともあるので、気を付けましょう。