|
![](/fdotnet/extremecs/index/extremecs_l.jpg) |
連載:[完全版]究極のC#プログラミング
Chapter7 ラムダ式(後編)
川俣 晶
2009/11/02 |
|
7.1 ラムダ式は何をもたらすか?
実際にラムダ式を使うことで体験した出来事を、まずは紹介しよう。
以下、具体的なコードで説明していくが、実際に書いたコードそのままではなく、C# 1.xの知識で読み取れるように全面的に書き直したコードであることをお断りしておく(以下のコードを“鈍くさい”と思う読者もいると思うが、実際のコードはもっと簡潔で、かつC# 3.0の機能を活用している)。
さて、少し前に実際に筆者が書いていたコードの中に、メニューとして選択可能な項目のリストがある。メニュー項目は次のリスト7.1のように定義されていた。
public delegate bool SimpleMenuAction();
public class メニュー項目ItemA
{
public readonly string Name; // 名前
public readonly SimpleMenuAction Action; // 実行内容
public メニュー項目ItemA(string name, SimpleMenuAction action)
{
Name = name;
Action = action;
}
}
|
|
リスト7.1 メニュー項目の定義 |
これに対して、次ページのリスト7.2のようなメニュー項目のテーブルがあった。
private static メニュー項目ItemA[] メニューItems1 =
{
new メニュー項目ItemA("選択項目1", 実行メソッド),
new メニュー項目ItemA("選択項目2", 実行メソッド),
new メニュー項目ItemA("選択項目3", 実行メソッド),
};
|
|
リスト7.2 メニュー項目のテーブル |
さて、当初はこれで十分と思われていたが、途中で「19時以降にのみ見せるメニューを追加したい」という要求が出てきた。それが1つなら、if文で例外条件を判定して特別処理を挟んでもよいのだが、要求は2つであり、しかも、増える可能性もあった。そこで、このテーブル中に条件も含めて記述できるようにしたいと考えた。
最もシンプルな解決策は、メニュー項目クラスに、「何時以降有効にする」という「時」の整数を保存可能にすることだろう。
まず、メニュー項目ItemAのクラスに、その整数を保持するフィールドFromHourを追加する(リスト7.3参照)。
public class メニュー項目ItemB
{
public readonly string Name;
public readonly SimpleMenuAction Action;
public readonly int FromHour;
public メニュー項目ItemB(string name, SimpleMenuAction action, int fromHour)
{
Name = name;
Action = action;
FromHour = fromHour;
}
}
|
|
リスト7.3 FromHourフィールドを追加したメニュー項目の定義 |
テーブルは、次のリスト7.4のように書き直す。
private static メニュー項目ItemB[] メニューItems2 =
{
new メニュー項目ItemB("選択項目1", 実行メソッド, 0),
new メニュー項目ItemB("選択項目2", 実行メソッド, 0),
new メニュー項目ItemB("選択項目3", 実行メソッド, 0),
new メニュー項目ItemB("選択項目4", 実行メソッド, 19),
}; |
|
リスト7.4 リスト7.3用のメニュー項目のテーブル |
これで、必要な情報をテーブルに埋め込むことができた。
メニューを構築するメソッドは、選択されたメニューオブジェクトのFromHourを調べることで、現在の「時」が与えられた数値以上であれば表示することができる。
このコードは「YAGNI*2」の原則からいえばこれで十分であり、これ以上凝った仕掛けを入れる意味はない。そういう意味で、これは良いコードである。
しかし、このコードは仕様変更の要求に対して、あまりにももろい。たとえば、条件が19時から19時30分になったらもう対応できない。そのほかにも、終了時刻が指定された場合や、時間帯が2つのケース、あるいは曜日によって時間が変動するなど、いくらでも込み入った要求が想定できる。
そのような要求を想定し、条件をデリゲートで指定するようにコードを修正することができる(リスト7.5参照)。
public delegate bool SimpleMenuAvailability();
public class メニュー項目ItemC
{
public readonly string Name;
public readonly SimpleMenuAction Action;
// 現在有効なメニューか?
public readonly SimpleMenuAvailability IsAvailable;
public メニュー項目ItemC(string name, SimpleMenuAction action,
SimpleMenuAvailability isAvailable)
{
Name = name;
Action = action;
IsAvailable = isAvailable;
}
}
|
|
リスト7.5 条件をデリゲートで指定するメニュー項目の定義 |
しかし、この構造はC# 2.0の時代であれば採用しなかっただろう。もし、ラムダ式が使用できないC# 2.0で、匿名メソッドを使って実現するとすれば、テーブルは次ページのリスト7.6のように書き直すことになる。
private static メニュー項目ItemC[] メニューItems3 =
{
new メニュー項目ItemC(
"選択項目1", 実行メソッド, delegate() { return true; }),
new メニュー項目ItemC(
"選択項目2", 実行メソッド, delegate() { return true; }),
new メニュー項目ItemC(
"選択項目3", 実行メソッド, delegate() { return true; }),
new メニュー項目ItemC(
"選択項目4", 実行メソッド,
delegate() { return DateTime.Now.Hour >= 19; } ),
};
|
|
リスト7.6 リスト7.5用のメニュー項目のテーブル |
このコードは、将来必要になるか否かも定かではない変更に備えるにしては、あまりにもコードが肥大化している。本質的にほとんど意味を持たないdelegateキーワードやreturnキーワードが目立ちすぎ、パッと見て意図も読み取りにくい。これは、明らかにYAGNIの原則によって戒められるべき、悪いコードの典型例だろうと思う。
それゆえに、もしC# 2.0を使っていれば、このコードは採用しなかっただろう。いくら、筆者が匿名メソッドを湯水のように使うタイプだとしても、このケースではメリットに対してデメリットが大きすぎる。
だが、これを匿名メソッドではなく、はるかに少ない文字数で記述できるラムダ式で書いたらどうなるだろうか? 次のリスト7.7では、「()=>true」や「()=>DateTime.Now.Hour >= 19」がラムダ式に当たる。
private static メニュー項目ItemC[] メニューItems4 =
{
new メニュー項目ItemC("選択項目1", 実行メソッド, ()=>true),
new メニュー項目ItemC("選択項目2", 実行メソッド, ()=>true),
new メニュー項目ItemC("選択項目3", 実行メソッド, ()=>true),
new メニュー項目ItemC("選択項目4", 実行メソッド,
()=>DateTime.Now.Hour >= 19),
};
|
|
リスト7.7 リスト7.6にラムダ式を用いたメニュー項目のテーブル |
正直、この程度なら許してよいと思った。YAGNIの原則には反するが、コードのわかりやすさを決定的に損なわない範囲で、未知の修正に対する保険をかけることができている。
事実、この保険は役立った。すぐに、メニューの有効期間が「19時以降」から「19時以降22時未満」へと変更されたのだ。それに伴い、テーブルは次のリスト7.8のように修正された。
private static メニュー項目ItemC[] メニューItems5 =
{
new メニュー項目ItemC("選択項目1", 実行メソッド, ()=>true),
new メニュー項目ItemC("選択項目2", 実行メソッド, ()=>true),
new メニュー項目ItemC("選択項目3", 実行メソッド, ()=>true),
new メニュー項目ItemC("選択項目4", 実行メソッド,
()=>DateTime.Now.Hour >= 19 && DateTime.Now.Hour < 22),
}; |
|
リスト7.8 リスト7.7の選択項目4の有効期間を変更 |
この変更は、たった1つのラムダ式を書き換える局所的な変更で収まったので、一瞬で完了した。しかし、もしも最初のリスト7.4のコードを採用していたら、メニュー項目クラスに終了時刻の情報を追加したり、メニューを構築するメソッドに終了時刻の判定を追加したりと、手間のかかる修正が要求されたことだろう。だが、それにもかかわらず、ラムダ式ではなく匿名メソッドを使うという前提であったとしたら、その手間のかかるコードのほうを採用していたかもしれない。つまり、匿名メソッドとラムダ式の長さの差がコードの質に影響を与えたのである。
*2 YAGNIとは「You Aren't Going to Need It.」の略で、もしかしたら必要とされるかもしれない機能は実際には必要とされない可能性が非常に高いことを意味する。つまり、未知の未来に備えるためのコードをあらかじめ書く行為は、たいていの場合無駄になるという教訓である。
|