連載:[完全版]究極のC#プログラミング

Chapter7 ラムダ式(後編)

川俣 晶
2009/11/02


 本記事は、(株)技術評論社が発行する書籍『[完全版]究極のC#プログラミング ― 新スタイルによる実践的コーディング』から、許可を得て転載しています。
 同書籍は、もともと本フォーラムにて連載していた『C# 2.0入門』、『C# 3.0入門』の記事を整理統合し、加筆、修正されたものです。

  手元でまとめて読みたい方は、ぜひ書店などにてお買い求めください。

 【注意】本記事は、書籍の内容を改変することなく、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。

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.」の略で、もしかしたら必要とされるかもしれない機能は実際には必要とされない可能性が非常に高いことを意味する。つまり、未知の未来に備えるためのコードをあらかじめ書く行為は、たいていの場合無駄になるという教訓である。


 INDEX
  [完全版]究極のC#プログラミング
  Chapter7 ラムダ式(後編)
  1.7.1 ラムダ式は何をもたらすか?
    2.7.2 ラムダ式と匿名メソッドの違い
    3.7.3 ステートメント型のラムダ
    4.7.4 式形式のラムダの可能性
    5.7.5 型指定を省略できる場合、できない場合
    6.7.6 何もしないラムダ式
    7.7.7 ラムダ式の使用例/【C#olumn】「=>」は不等号?
    8.7.8 ラムダ式のさまざまなバリエーション
    9.7.9 ジェネリックメソッドと型推論
    10.7.10 オーバーロードの解決/値型と参照型の相違は何か?/練習問題
 
インデックス・ページヘ  「[完全版]究極のC#プログラミング」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間