はじめに
第一回は楽しんで頂けたでしょうか。今回は、Forループに注目して、関数型プログラミングの世界を少し学んでみましょう。未だにVB6的なForループを書いている人が多くいると思います。しかし、今やもっと効率的に書ける時代なのです。
VB6のコントロール配列を再現したコード
VB6では「コントロール配列」というものがあり、フォームデザイナー上で複数のコントロールに同じ名前を付けると、自動的にそれらは配列として扱われるようになりました。これは便利な機能でしたが、VB.NETではなくなっています。
しかし、コントロール配列が使えなくなったわけではなく、VB.NETでも、以下のようにForm_Load等で配列を作っておけば、複数のコントロールを自由に配列として扱う事が可能になります。
Dim ctrl() As Control = { TextBox1, TextBox2, TextBox3 }
For index As Integer = 0 To ctrl.Length - 1
ctrl(index).Text = index.ToStrin()
Next
VB6から移行したシステムでは、このやり方を用いて、例えばフォーカス制御等を行っているケースも多いと思いますが、どうしてもFor文の作り方が「VB6時代」から抜け出せておらず、一見して何をやっているか分かりづらいソースコードになっている事が多いように思います。こういうソースコードは、修正時に誤ってバグを作りこみやすいですし、また、修正そのものにも気を遣います。
VB6的なForループ
例えば、複数のテキストボックスが格納されたコントロール配列 ctrls() があるとします。ここには10個前後のテキストボックスが格納されており、格納されている順番は特にルールがありません。
この中から、「Textプロパティが空っぽものにフォーカスを移す。但し、複数ある場合はその中でTabIndexが最も小さいものにフォーカスを移す」という処理を考えてみます。
VB6的なForループでは、次のようなコードを書いているでしょう。
Dim ctr() As Control = {TextBox1, TextBox2, TextBox3 .... }
Dim index As Integer = 0
Dim foundIndex As Integer = -1
For index = 0 To ctrls.Length - 1
If ctrls(index).Text = "" Then
If foundIndex = -1 OrElse ctrls(foundIndex).TabIndex > ctrls(index).TabIndex Then
foundIndex = index
End If
End If
Next
If foundIndex <> -1 Then
ctrls(foundIndex).Focus()
End If
一見して何をしているか分かるまでは、少し時間がかかるかと思います。最後の行を見て「あ、フォーカスするコントロールを探しているんだな」ぐらいは分かるかもしれませんが、「一番小さいTabIndexを探しているんだな」まで分かるには、多少の時間が必要です。
これを、「TabIndexが一番大きいものを選択するように変更して」と言われて、自信をもって変更箇所を特定するには、コード全体を理解しなければなりません。これぐらいのコード量ならばまだなんとかなっても、もう少し込み入ってきたり、こんなコードが大量にあるソースだと、やっかいな仕事になるでしょう。
このソースコードの問題点の一つは、「複数の目的を一つのForループ内でごちゃ混ぜにしてコーディングしている」という点です。「Textプロパティが空っぽのものを探す」「TabIndexが一番小さいものを探す」というものを混ぜているので、一見して分かりにくく、また、一つの修正が他の仕様に影響しやすいコードになっているのです。もう一つは、単純に、「配列から何かを探す」とか「一番小さいものを探す」というような、業務的にありがちな「同じような処理」を毎回書いてしまっている事です。
.NET的な配列・コレクション処理
しかし今やVB.NETの時代です。もうこんな単純なコードに手間をかけるのはやめましょう。
Imports System.Linq
この魔法を唱えれば、上記の面倒なコードはこんな風にスッキリ書けます。
Dim target = ctrls.Where(Function(ctrl) ctrl.Text = "").
OrderBy(Function(ctrl) ctrl.TabIndex).
FirstOrDefault()
If target IsNot Nothing Then target.Focus()
えっ、なんだかよく分からない?
逆に難しそう?
元のソースコードの方がまだ理解できる?
それは、WhereメソッドやOrderByメソッドの使い方、また、匿名関数の書き方・読み方が良く分かっていないからです。その辺りが理解できれば、これらは特に難しいものではなく、「なんてシンプルで、意図がはっきりと表現されていて、且つ、ミスがしにくい書き方なんだ!」と思えるようになるでしょう。
「もうそんな新しい事を覚えるのは嫌だ。VB6と同じやり方で動くなら、それでいいじゃないか」
と思う方もいるかもしれませんが、大丈夫、本当にそんな難しいものではないんです。そんな事よりも、Forループとindex変数を使って配列を操作するやり方で妙なバグが入り込んでしまったりして残業になってしまう事の方が嫌じゃないですか?
今後、こういう書き方をするコードに遭遇する事も増えてくるかもしれません。今のうちにやり方を覚えておいて、損はないのです。
Whereメソッド
さあ、それでは一つ一つ説明していきましょう。
.Where(Function(ctrl) ctrl.Text = "")
Whereメソッドは、引数に与えた匿名関数を使って配列を絞り込む命令です。SQLに似せてメソッド名が作られているので、直感的に分かりやすいですね。ここでは、以下の匿名関数を引数に指定しています。
Function(ctrl) ctrl.Text = ""
単一行形式の匿名関数なので、いろいろと省略されてなんだか頼りなく見えますが、複数行形式にするとこんな感じの意味になります。
Function(ctrl) As Boolean
Return (ctrl.Text = "")
End Function
「なんだ、ctrl.Text = "" って、Textプロパティに空文字を設定しているのかと思ったら、Boolean型を返す比較式だったのか」
そうそう、そういうことです。単一行形式にすると Returnを省略する事が出来るので、逆にちょっと分かりにくいですよね。でも、それだけの事なんです。単一行の場合、そこに書かれた内容がそのまま戻り値の型も表すので、Functionの戻り値の型(As Boolean)も書かなくて良くなります。なので、最初のような書き方になるわけですね。
もし、「Textプロパティに何か文字が入っているものを選択する」ということならば、この部分はこうなります。
.Where(Function(ctrl) ctrl.Text <> "")
もう、何が書かれているかお分かりですよね?
このWhereメソッドの戻り値は、IEnumerable(Of TextBox)です。アイエニュメラブルと読みます。この型は、簡単に言うと、「For Eachループで扱える配列・コレクションのインタフェース」です。まぁとにかく、(Of T)のTの型の配列みたいなもんだと思っておけば大丈夫です。今回はこのTがTextBoxになっていますが、これは、元となる配列のctrl()が、TextBoxの配列だから自動的にそうなっています。自動的にそうなるってすごいですよね。これは「ジェネリクス」と言われるVB.NETの言語使用の一つです。なんか聞いたことありますよね、ジェネリクス。そう、薬局でお薬を貰う時に・・・って、それは「ジェネリック」や!(*'▽') まぁ、言葉の意味は同じく「一般化したもの」なんですけどね。
まぁ、難しい事は考えず、とにかくWhereメソッドは、与えられた匿名関数に、自分が持っている配列の各要素を引数として与えて呼び出していきます。その結果がTrueのものだけを集めて、新しい配列を戻してくれるわけです。
OrderByメソッド
この時点で、Textプロパティが空っぽのTextBoxだけが集められました。今度はこの中で「TabIndexが一番小さいもの」を絞り込みましょう。考え方としては、「TabIndex順にソートして、先頭のものを取り出す」と考えます。
まず、TabIndex順にソートする部分を見てみます。
OrderBy(Function(ctrl) ctrl.TabIndex)
OrderByメソッドは、Whereメソッドと同様に、引数に匿名関数を取ります。その関数は、配列の1要素を受け取って、ソートに使用する値を返すようにします。その値を使って、OrderByメソッドは要素をソートしてくれるのです。ここでは、以下のような匿名関数を渡しています。
Function(ctrl) ctrl.TabIndex
これも単一行の匿名関数になっているので、複数行に置き換えみましょう。
Function(ctrl) As Integer
Return ctrl.TabIndex
End Function
TabIndex順にソートして欲しいので、TabIndexの値を返しているわけですね。
「うーん、なんかわかったようでわからないぞ? じゃあ、ここで ctrl.Name を返せば、コントロール名でソートされるってことか?」
そうそう、その通りです。こんな風に書けますよ!
Function(ctrl) ctrl.Name
「なんだかすごいな。簡単でいいじゃないか!」
気に入ってきたようですね。次も行ってみましょう!
FirstOrDefaultメソッド
.FirstOrDefault()
おや、今度は引数も匿名関数もありませんね。それもそのはず、これはこのメソッドチェーンの執着駅だからです。
FirstOrDefaultは、現在までに絞り込んだりソートしたりされた配列(コレクション)のうち、先頭要素を1つだけ取り出して返します。OrDefaultと付いているのは、配列が空っぽだった場合に配列要素の型のデフォルト値を返す、という意味になります。この場合はTextBox型なので、デフォルト値は「Nothing」になります。先頭のTextBoxか、Nothingが返ってくる、ということですね。
.First()
というメソッドもあるのですが、この場合、もし配列が空っぽだと例外が発生しますのでご注意下さい。
というわけで、最初の2行の意味、分かって頂けたでしょうか?
Dim target = ctrls.Where(Function(ctrl) ctrl.Text = "").OrderBy(Function(ctrl) ctrl.TabIndex).FirstOrDefault()
If target IsNot Nothing Then target.Focus()
・Textプロパティが空っぽのもので絞り込み、
・TabIndexでソートして、
・先頭要素を取り出す。
既に用意されているいくつかの配列・コレクション操作メソッドを組み合わせるだけで、こんなにシンプルに記述できてしまうのです。
しかも、組み込みのメソッドですから、バグフリー! 自分が記述する部分は条件式とかの部分だけですから、ここさえちゃんと書けばあとはキッチリ動いてくれます。
「うーん、コードの意味は分かったけど、中でどんな事をやっているのか分からなくて不気味だなぁ。それに、配列の中から一番小さいTabIndexを持つ要素を取り出すだけなのに、ソートだなんて大げさじゃない? パフォーマンスが悪くなるのは困るよ…」
あなたはとても優秀なプログラマーのようですが、どうやらPC-9801か何かの時代を未だに引きずっているようです。もし配列要素数が1万件とかになる場合、確かにパフォーマンスを気にする必要があり、その為に専用のコードを書くメリットはあると思います。しかし、今やパソコンの処理能力は飛躍的に上がっており、たかだか数十個~数百個の配列要素を処理するのに、「いちいちソートしてると遅いのでは?」などと考えるのは無意味なのです。全ては一瞬でカタがつきます!
それよりも、これまでごちゃ混ぜになっていた複数の処理(ここでは「Textプロパティが空のものを探す」ことと「TabIndexが一番小さいものを探す」という処理)をシンプルに分離できていること、また、絞り込みや検索といった「よくあるパターン」の処理をライブラリに任せてバグの入り込みを少なくできているという事の方が重要です。
ミリ秒単位のパフォーマンスに気を遣うよりも、シンプルで堅牢なコードを書くことの方が、何よりお客様の為になることなのです。
これからは、もし、複数の何か、つまり配列やコレクションに対して何か処理をする時、ForループではなくWhereやOrderBy等が使えないか考えてみてください。
例えば、Forループの中で要素の内容をひとつひとつIf文を使って検証し、条件に合致するものだけに対して何かの処理をする、というようなケースは多いと思います。しかしこれも、「実は、配列を絞り込んだ結果に対して処理をしているという考え方ができるのでは?」と思えたら、Whereなどを使ってみると、コードがスッキリとして、意図が明確になることがあります。
また、要素の中で条件に合致するものを検索するようなケースも、これからはWhereメソッドに任せることができるようになります。実はFirstOrDefaultメソッドにも引数に匿名関数を渡せて、Whereメソッドの代わりに使う事ができてしまうので、さらにシンプルに書けるでしょう。
コードがシンプルになれば、可読性・メンテナンス性も上がり、結果的にバグが減り、残業も減り、コストも下がり…と、良い事づくめです。「あまり難しい書き方をすると、他のプログラマーがメンテナンスできなくなってしまう」と言う人が居たら、その人には、もう少しプログラマーを信じて欲しいなと思います。
古いやり方にしがみついて、もう7年前には可能になった(VB2010から可能です!)効率的でなやり方を使わないでおくなんて、本当にもったいないことです。リスクを恐れるあまり、もっと大きなリスクを背負っていることに気づいて欲しいですね。
より洗練された書き方
尚、先ほどのコードは、次のように書く事もできます。
For Each item In ctrls.Where(Function(ctrl) ctrl.Text = "").OrderBy(Function(ctrl) ctrl.TabIndex).Take(1)
item.Focus()
Next
何をやっているか分かりますか?
OrderByメソッドの後に、FirstOrDefault()ではなく、Take(1)というメソッドを呼び出しています。
Take(n)は、配列の先頭n件を取り出して新しい配列を返すというメソッドです。
それを入力として、For Eachループを回しているのです。For Eachループは配列indexを意識せずとも配列の処理ができる、大変便利でバグの入り込みにくい構文ですので、こうやってコレクション処理ライブラリと組み合わせて使うと、より強力です。
また、FirstOrDefaultと違い、要素が1件もない場合は単に空の配列が返りますので、いわゆるNothingチェックとも無縁です。空っぽならば、単にFor Eachループが実行されないだけです。問題の起こりにくい、非常に堅牢な書き方と言えるでしょう。ただ、この書き方だと、パッと見て、複数のTextBoxにFocusさせているようにも見えてしまうかもしれないので、場合によりけりかもしれません。慣れている人ならば、「要素が無しのケースも想定している、これはOption型のような事をしているのだな」とすぐ分かってくれるとは思うのですが。
さぁ、くだらない配列処理はライブラリに任せ、我々プログラマーは、もっと全体をスッキリ記述することに目をむけようではありませんか。