#はじめに
Visual Basic 2010以降で使えるようになった「複数行の無名関数(匿名関数/ラムダ式)」について、LINQ以外の具体的かつシンプルな活用事例を用いて紹介した記事です。記事中のサンプルコードは仕事で書いたコードを脳内で別の形に再構成して書いているだけで実際に動かしていない為、誤字脱字などありましたらごめんなさい。実際に動いたものが元になっているので、考え方は間違っていないと思います。
さて、Visual Basic 2008より、VB.NETでも**無名関数(ラムダ式)**を使えるようになっています。
ただ、2008ではまだ単一行の無名関数しか使えず、業務開発で使えるシーンは少ない…というか、クエリ式を書いたり、特定のライブラリを呼び出す際の検索条件などを記述する為の記法というイメージがあり、個人的にスルーしておりました。
'単一行の無名関数
filtered = mydata.Filter(Function(d) d.ID = "00001")
しかし、その後、Visual Basic 2010からは、複数行の無名関数を使えるようになっていたようで、最近改めてこれに気が付き、仕事の業務アプリ開発でも積極的に使っていきたいと思い始めました。
ところが、いざ使ってみようとしてネットで情報を検索すると、意外とVB.NETでの無名関数を使った実例が多くは見つからず(LINQの事例はたくさんあるのですが)、とりあえず自分用のメモとしてまとめておくことは、他の方の参考になるのではと思った次第です。
#子フォーム(モードレス)の戻り値をテキストボックスに格納する
以下の例は、VB6の遺産をVB.NETに引き継いだようなシステムのソースコードによく見られる、子フォームの呼び出し結果をテキストボックスに格納する処理です。
Button1をクリックした場合はTextBox1に、Button2をクリックした場合はTextBox2に、それぞれ店舗検索フォームからの戻り値を設定していますが、何らかの理由で(受け継がれてきた秘伝のソースなので、理由は分かりません(笑))ShowModalではなく普通にShowしている為、FormClosedイベントで子フォームが閉じた事を検知し、そこで戻り値を設定しています。
「Showしちゃったら、何度もButton1が押されちゃっておかしい事にならない!?」と思われるかもしれませんが、親フォームをEnabled = Falseしたりして、疑似的にモーダル表示にしている感じだと想像してください(そういうソースコード、見た事ありますよね、あなたも…!)。
Private WithEvents search As StoreSearchForm()
Private ctrl As Forms.Control
Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
'店舗検索フォームの戻り値を設定する為のコントロールを設定する
ctrl = CType(TextBox1, Forms.Control)
'店舗検索フォームを呼び出し、選択された店舗IDを指定したコントロールに設定する。
search = New StoreSearchForm()
search.Show()
End Sub
Private Sub Button2_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button2.Click
'店舗検索フォームの戻り値を設定する為のコントロールを設定する
ctrl = CType(TextBox2, Forms.Control)
'店舗検索フォームを呼び出し、選択された店舗IDを指定したコントロールに設定する。
search = New StoreSearchForm()
search.Show()
End Sub
Private Sub search_FormClosed(sender as Object, e as FormClosedEventArgs) Handles search.FormClosed
'選択された店舗IDを呼び出した歳に指定したコントロールに設定し、フォーカスを移す。
If ctrl IsNot Nothing Then
ctrl.Text = search.GetSelectedValue()
ctrl.Focus()
ctrl = Nothing
End If
search = Nothing
End Sub
#旧来のやり方の問題点
ソースコードを見るとお分かりのように、Button1_ClickとButton2_Clickでは、検索フォームを呼び出す前に「ctrl」というプライベートメンバ変数に、子フォームの戻り値を格納する対象となるテキストボックスのインスタンスを設定しています。
'店舗検索フォームの戻り値を設定する為のコントロールを設定する
ctrl = CType(TextBox1, Forms.Control)
そして、検索フォームが閉じた時に、FormClosedイベント側でこのctrlに戻り値を設定しています。
If ctrl IsNot Nothing Then
ctrl.Text = search.GetSelectedValue()
ctrl.Focus()
ctrl = Nothing
End If
こうすることで、「戻り値をどこへ設定すればよいか」という情報を、メソッドを超えて持ちまわっているわけです。
しかしこのやり方は、「検索フォームを呼び出す前にctrlに設定しないとダメ」という、フォームの利用手順に「暗黙の了解」が存在する事になり、今後のメンテナンス次第ではバグの温床になりかねないでしょう。そもそも、ある特定のボタンを押した際の為だけの持ち回り情報をフォーム全体で共有するプライベートメンバ変数として持たねばならないというのも、「なんでもかんでもグローバル変数」の古い考え方であり、同じくバグの原因となりやすいものです。
業務アプリ開発の現場ではよくこの手のパターンを見かけるのですが、元のコードを書いた人間以外が機能追加しようとしてソースをいじくり、「あれ?なんで検索フォームの戻り値が設定されないんだ? ていうか戻り値どこで設定してるんだ?」「え、フォームを呼び出す前にctrlに設定しなきゃならないの? そんな暗黙の了解知らねえよ…」的な事が多発します。それだけならまだしも、長い時を経てctrl変数が全然違う目的で別の場所で再利用されていたりして、とんでもないバグを生み出す原因にもなりかねません。
単純な解決策としては、例えばStoreSearchFormに .TargetControl As Control のようなプロパティを付け、それが設定されていたらそこに戻り値を設定してフォーカスを移す、と言う処理を StoreSearchForm自身に書いておく、というの一つの手かもしれません。そうすれば、FormClosedイベントハンドラは不要となります。ただ、業務アプリ開発というのはそう単純なものではなく、フォームの戻り値をどう処理するかは使う業務によって様々です。必ずしも同じ処理で済むとは限らないのです。そう考えると、StoreSearchFormに場当たり的に共通処理を増やしていくよりも、検索フォームは検索するという責務だけに集中し、戻り値をどうするかは、やはり使用側の責務としたいところです。
#無名関数(ラムダ式)を用いて書きなおす
というわけで、無名関数(ラムダ式)の出番です。
Visual Basic 2010からは、複数行の無名関数を記述することができるようになりました。
search.FormClosed に対してイベントハンドラを割り当てる AddHandler ステートメントを用い、イベントハンドラを無名関数として記述してみましょう。
Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim search As New StoreSearchForm()
'店舗検索フォームが閉じた時の処理を設定
AddHandler search.FormClosed, _
Sub(sender as Object, e as FormClosedEventArgs)
'選択された店舗IDを指定したコントロールに設定する。
TextBox1.Text = search.GetSelectedValue()
TextBox1.Focus()
End Sub
'店舗検索フォームを呼び出す。
search.Show()
End Sub
Private Sub Button2_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim search As New StoreSearchForm()
'店舗検索フォームが閉じた時の処理を設定
AddHandler search.FormClosed, _
Sub(sender as Object, e as FormClosedEventArgs)
'選択された店舗IDを指定したコントロールに設定する。
TextBox2.Text = search.GetSelectedValue()
TextBox2.Focus()
End Sub
'店舗検索フォームを呼び出す。
search.Show()
End Sub
このように、Sub ~ End Sub という構文を使って、無名関数(というかこれは無名プロシージャですが)を定義し、AddHandlerに渡しています。また後で説明しますが、Functionも同じように書けます。Functionの場合、戻り値の型は通常通り As句を用いて指定できます。**"_"**を使って改行しているのは、こうしないと変な位置に Sub ~ End Subの自動インデントが発生してしまうからです(やってみると分かります。笑)。
#無名関数(ラムダ式)を使った場合の利点とは
さて、大きく変わった点が2つあります。まず1つめは、検索フォームのインスタンス search がプライベートメンバ変数ではなく、メソッドのローカル変数になったことです。そもそも search がプライベートメンバ変数だったのは、別にフォームインスタンスを使い回す為ではなく(毎回Nothingを入れて初期化しています)、単に WithEventsステートメントを使ってFormClosedをメインフォームで受け取りたいというだけの理由です。ですが、AddHandlerを使えば WithEventsも不要なので、プライベートメンバに持つ必要はなくなります。無名関数を使わずともAddHandlerは使えたでしょうが、無名関数を使った場合ほどの恩恵は得られません。これでひとつバグの温床を無くせました。
もう一つは、ctrlメンバ変数をなくせたことです。この変数はそもそも、Clickイベント時に決まる「どのコントロールに戻り値を設定すれば良いのか」という情報を、FormClosedで必要とする為でした。しかし、無名関数を使う事で、Clickイベント内で「戻り値を設定する処理」まで完結させることができる為、ctrlメンバ変数も不要になったのです。またひとつ堅牢な世界へと進む事が出来たようです。
総合すると、無名関数を用いる事により、「子フォームを表示した後、戻り値を特定のコントロールに設定する」という一連の仕様を、ひとまとまりのコードとして記述することができるようになりました。余計な広いスコープの変数での情報持ち回りも不要で、後でソースコードを見た人とっても意図が明確に伝わるシンプルなコードです。仕様変更の際にも、一つの仕様が一か所にまとまっているので、理解しやすいでしょう。
#AddHandlerを使わない方法
ただ、検索を実行する前にいちいちAddHandlerと書かなくてはいけないのは、なんとなく気持ち悪い気がします(C#ではもっとシンプルに書けるので、単にVB.NetのAddHandlerの文法が悪いだけだとは思いますが…)。ここでは子フォームをその場で生成して使い捨ててますので問題になりませんが、もし最初のようにメンバ変数に持って使い回すような子フォームの場合、イベントハンドラの解除を管理しなければならなくなり、いろいろと面倒になりますし…。そもそも一回しか呼び出されてはいけないような処理をイベントハンドラに設定しちゃうというのは、ちょっと怖いです。
そこで、StoreSearchForm側のShowメソッド自体に直接無名関数を渡せるように変更してみましょう。
'Showメソッドの引数で渡されたイベントハンドラを保持するメンバ変数
Private mFormClosedHandler As Action
Public Sub Show(formClosedHandler As Action(Of Object, FormClosedEventArgs))
'メンバ変数にイベントハンドラを保持する
Me.mFormClosedHandler = formClosedHandler
'フォームを開く
Me.Show()
End Sub
Private Sub form_FormClosed(sender as Object, e as FormClosedEventArgs) Handles Me.FormClosed
'ハンドラが登録されていたら呼び出す
If Me.mFormClosedHandler IsNot Nothing Then
Me.mFormClosedHandler(sender, e)
Me.mFormClosedHandler = Nothing
End If
End Sub
ここで、Showメソッドの引数 formClosedHandlerの型に注目です。
formClosedHandler As Action(Of Object, FormClosedEventArgs)
Actionというのは、戻りの無いSubプロシージャを表す型です。Action(Of T1, T2, T3...)のように、プロシージャの引数をリストアップしていきます。今回は、FormClosedと同じ引数リストを引き継ぎたかったので上記のようになっていますが、引数なんて要らないと思ったら、以下のようにしても良いでしょう。これだと、引数無しのプロシージャの型を表します。
formClosedHandler As Action
さて、上記のStoreSearchFormを使う側はこんな風に書けるようになります。
Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim search As New StoreSearchForm()
'店舗検索フォームを呼び出す
search.Show( _
Sub(sender as Object, e as FormClosedEventArgs)
'選択された店舗IDを指定したコントロールに設定する。
TextBox1.Text = search.GetSelectedValue()
TextBox1.Focus()
End Sub
)
End Sub
Private Sub Button2_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim search As New StoreSearchForm()
'店舗検索フォームを呼び出す
search.Show( formClosedHandler:=_
Sub(sender as Object, e as FormClosedEventArgs)
'選択された店舗IDを指定したコントロールに設定する。
TextBox2.Text = search.GetSelectedValue()
TextBox2.Focus()
End Sub
)
End Sub
Showメソッドを呼び出す際のAddHandlerという「暗黙のお約束」が無くなり、単にShowを呼び出すだけで良くなりました。シンプルで良いですね! 使う側にとっても、IDE上で「Show(」まで入力すれば、あとはIDEがメソッドの使い方を教えてくれるでしょう。
Button2_Clickでは、名前付き引数を使って呼び出しています。こうすることで、その無名関数がいつ呼び出されるかがコード上で分かりやすくなると思います。
#もっと無名関数を活用(やりすぎかも)
StoreSearchForm側も無名関数を使ってもっとシンプルに書きたい場合は、こんな感じでも良いでしょう。
Public Sub Show(formClosedHandler As Action)
'イベントハンドラの定義
Dim handler As Action(Of Object, FormClosedEventArgs) = _
Sub(sender as Object, e as FormClosedEventArgs)
'引数で指定されたイベントハンドラの実行
If formClosedHandler IsNot Nothing Then
formClosedHandler(sender, e)
End If
'イベントハンドラが後で再実行されないようにハンドラを解除する
RemoveHandler Me.FormClosed, handler
End Sub
'フォームを閉じた時に、引数で指定されたイベントハンドラを実行する
AddHandler Me.FormClosed, handler
'フォームを開く
Me.Show()
End Sub
メンバ変数に持っていた mFormClosedHandler変数が不要になりました! こんな感じでサクサクと書けるようになると素敵ですね。
ただ、このケースでAddHandlerを使うと、一度実行されたらハンドラを解除しないといけないので、ちょっとソースコードが分かりにくくなっているような気もします。こういうのはVB.Netに似つかわしくないかもしれません。個人的にはここまで複雑なコードは避けたいと思っていて、上記のコードも実際の業務でこういうものを書いたわけではなく、想像で書いています(なので、動かなかったらごめんなさい)。
#無名Functionのサンプル
尚、今回は「よくあるパターン」の例としてFormClosedイベントハンドラを無名関数で置き換えた為、無名Functionではなく無名Subになってしまいましたが、ご参考までに無名Functionを使った場合のサンプルも簡単に示しておきます。無名Subの型はAction(Of T1, T2, T3...)で表現できましたが、無名Functionは、Func(Of T1, T2, T3.., TResult)で表現できます。
例えば、次のように無名Functionを定義して使う事が出来ます。
keySearch As String, list As String() の2つの引数を取り、Integer型の戻り値がある関数の型は、Func(Of String, String(), Integer)になります。Funcの引数リストの最後のところに戻り値の型を指定する感じです。
'無名関数findKeyの定義
Dim findKey As Func(Of String, String(), Integer) = _
Function(keySearch As String, list As String()) As Integer
Dim result As Integer = -1
If list IsNot Nothing AndAlso list.Length > 0 Then
For index As Integer = 0 To list.Length - 1 Then
If list(index) = keySearch Then
result = index
Exit For
End If
End If
End If
Return result
End Function
'検索用データ
Dim data() As String = { "abc", "def", "ghi" }
'無名関数の使用
Debug.Print(findKey("def", data).ToString()) '"1"が出力される
こんな感じで、「とりあえず今はこの場所でしか使わない処理だけど、汎用性ありそうな処理だから関数の形にしておきたい」みたいな事は多くて、変数スコープも分離できるし、私は結構こういう使い方をします。
しかし、こういう単純な例ではなく、無名関数を引数として渡す高階関数を作ったりするような使い方をしだすと、ともすると一見して何をやっているのか分かりにくいケースになる事も多く(慣れてる人からすれば分かりやすいのですが)、正直に言ってしまうと、VB.Netを使用するような現場では、嫌われるかもしれません。冒頭のFilterメソッドみたいなシンプルな例ならいいと思うんですけども。
本当は、入力内容のチェック処理なんかを無名関数で定義して渡すような処理、書きたいんですけどね…。
そもそも、今回の無名プロシージャですら、いきなり使うと「何これ!?」と言われかねないので、しっかり理論武装して、「これを使うと余計な変数が減ってバグが減ります」と提案できるようにしておいた方が良いでしょう。実際、効能は明らかな上、使い方も大して難しいわけではないですから、積極的に使っていった方がみんな幸せになれると思います。
またVB.Netでの業務アプリ開発の現場で無名関数、というか、関数型プログラミング的な手法が役立つシーンがあれば、ご紹介していきたいと思います。