連載:C# 3.0入門

第1回 ラムダ式

株式会社ピーデー 川俣 晶
2008/04/04

ラムダ式は何をもたらすか

 今回の内容は、C# 2.0を習得したプログラマーを読者に想定しているので、匿名メソッドをすでに知っているという前提で書いている。もしそうでなければ、まずは「連載:C# 2.0入門」の方を一読いただいた方がよいだろう。

 さて、本題に入ろう。

 ラムダ式(λ式)とは、ひと言でいってしまうと、(厳密には正しくないが)匿名メソッドをより短く記述するための構文である。と書くと、単に書き方が変わるだけでソース・コードの性質が変わるわけではないと思うかもしれないが、これは誤解である。規模が変わればそのものの性質が変化することがある。例えば、実験室のビーカーの中で起きた現象が、そのまま巨大プラントでも同じように起こるとは限らない。同様の現象はソース・コード上でも起こり得る。

 実際に、ラムダ式を使うことで体験した出来事をまずは紹介しよう。

 以下、具体的なコードで説明していくが、実際に書いたコードそのままではなく、C# 2.0の知識で読み取れるように全面的に書き直したコードであることをお断りしておく(以下のコードを“鈍くさい”と思う読者もいると思うが、実際のコードはもっと簡潔でかつC# 3.0の機能を活用している)。

 さて、少し前に実際に筆者が書いていたコードの中に、メニューとして選択可能な項目のリストがあった。メニュー項目は以下のように定義されていた。

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;
  }
}
リスト1 メニュー項目の定義

 これに対して、以下のようなメニュー項目のテーブルがあった。

private static メニュー項目ItemA[] メニューItems1 =
  {
    new メニュー項目ItemA("選択項目1", 実行メソッド),
    new メニュー項目ItemA("選択項目2", 実行メソッド),
    new メニュー項目ItemA("選択項目3", 実行メソッド),
  };
リスト2 メニュー項目のテーブル

 さて、当初はこれで十分と思われていたが、途中で「19時以降にのみ見せるメニューを追加したい」という要求が出てきた。それが1つなら、if文で例外条件を判定して特別処理を挟んでもよいのだが、要求は2つであり、しかも増える可能性もあった。そこで、このテーブル中に条件も含めて記述できるようにしたいと考えた。

 最もシンプルな解決策は、メニュー項目クラスに、「何時以降有効にする」という「時」の整数を保存可能にすることだろう。

 まずメニュー項目ItemAのクラスに、その整数を保持するフィールド「FromHour」を追加する。

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;
  }
}
リスト3 FromHourフィールドを追加したメニュー項目の定義

 テーブルは以下のように書き直す。

private static メニュー項目ItemB[] メニューItems2 =
  {
    new メニュー項目ItemB("選択項目1", 実行メソッド, 0),
    new メニュー項目ItemB("選択項目2", 実行メソッド, 0),
    new メニュー項目ItemB("選択項目3", 実行メソッド, 0),
    new メニュー項目ItemB("選択項目4", 実行メソッド, 19),
  };
リスト4 リスト3用のメニュー項目のテーブル

 これで、必要な情報をテーブルに埋め込むことができた。

 メニューを構築するメソッドは、選択されたメニュー・オブジェクトのFromHourを調べることで、現在の「時」が与えられた数値以上であれば表示することができる。

 さて、このコードはYAGNI*の原則からいえばこれで十分であり、これ以上凝った仕掛けを入れる意味はない。そういう意味で、これは良いコードである。

* YAGNIとは「You Aren't Going to Need It.」の略で、もしかしたら必要とされるかもしれない機能は実際には必要とされない可能性が非常に高いことを意味する。つまり、未知の未来に備えるためのコードをあらかじめ書く行為は、たいていの場合無駄になるという教訓である。

 しかし、このコードは仕様変更の要求に対して、あまりにももろい。例えば、条件が19時から19時30分になったらもう対応できない。あるいは、終了時刻が指定された場合や、時間帯が2つのケース、あるいは曜日によって時間が変動するなど、いくらでも込み入った要求が想定できる。

 そのような要求を想定し、条件をデリゲートで指定するようにコードを修正することができる。

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;
  }
}
リスト5 条件をデリゲートで指定するメニュー項目の定義

 匿名メソッドを使うとすれば、テーブルは以下のように書き直すことになる。

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; } ),
  };
リスト6 リスト5用のメニュー項目のテーブル

 しかし、このコードは将来必要になるか否かも定かではない変更に備えるにしては、あまりにもコードが肥大化しすぎている。本質的にほとんど意味を持たないdelegateキーワードやreturnキーワードが目立ちすぎ、パッと見て意図も読み取りにくい。これは、明らかにYAGNIの原則によって戒められるべき悪いコードの典型例だろうと思う。

 それ故に、もしC# 2.0を使っていれば、このコードは採用しなかっただろう。いくら、筆者が匿名メソッドを湯水のように使うタイプだとしても、このケースはメリットに対するデメリットが大きすぎる。

 だが、これを匿名メソッドではなく、はるかに少ない文字数で記述できるラムダ式で書いたらどうなるだろうか? 以下のコードでは、「()=>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 リスト6にラムダ式を用いたメニュー項目のテーブル

 正直、この程度なら許してよいと思った。YAGNIの原則には反するが、コードの分かりやすさを決定的に損なわない範囲で、未知の修正に対する保険をかけることができている。

 事実としてこの保険は役立った。すぐに、メニューの有効期間が「19時以降」から「19時以降22時未満」へと変更されたのだ。それに伴い、テーブルは以下のように修正された。

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),
  };
リスト8 リスト7の選択項目4の有効期間を変更

 この変更は、たった1つのラムダ式を書き換える局所的な変更で収まったので、一瞬で完了した。しかし、もしも最初のリスト4のコードを採用していたら、メニュー項目クラスに終了時刻の情報を追加したり、メニューを構築するメソッドに終了時刻の判定を追加したり、手間のかかる修正が要求されたことだろう。だが、それにもかかわらず、ラムダ式ではなく匿名メソッドを使うという前提であったとしたら、その手間のかかるコードの方を採用していたかもしれない。つまり、匿名メソッドとラムダ式の長さの差がコードの質に影響を与えたのである。


 INDEX
  C# 3.0入門
  第1回 ラムダ式
    1.C# 3.0とは何か?/C# 3.0の適用範囲
  2.ラムダ式は何をもたらすか
    3.ラムダ式と匿名メソッドの違い/ステートメント型のラムダ
    4.式形式のラムダの可能性/型指定の省略/何もしないラムダ式
    5.ラムダ式の使用例/ラムダ式のさまざまなバリエーション
 
インデックス・ページヘ  「C# 3.0入門」


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 記事ランキング

本日 月間