連載:C# 3.0入門

第1回 ラムダ式

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

式形式のラムダの可能性

 ここで問題になるのは、ソース・コードを簡潔にするためにはぜひとも式形式のラムダを使いたいが、書きたいコードがすべて式形式のラムダで書けるわけではない、という点である。

 では、どこまでなら式形式のラムダで記述できるのだろうか。

 C/C++に慣れたプログラマーであれば、構文の似たC#でも式として相当複雑なコードを書けるかもしれないと思うかもしれないが、残念ながらそれはできない。なぜかといえば、C/C++で複雑な機能を持った式を書くための切り札となるカンマ演算子がC#には存在しないためである。

 例えば、C/C++であれば、以下のようなコードは有効である。

int a,b,c;

c = (a=1, b=2, a+b); // aに1、bに2、cにa + bの値を代入
printf("%d\n",c);

 それ故に、たった1つの式にかなり込み入った処理を記述することができ、それはマクロなどを定義する際に重要な意味を持っていた。

 しかし、このような使い方はC#ではできないのである。

 とはいえ、C#でも三項演算子やnull合体演算子があるので、これらを使うと多少込み入ったコードを式形式のラムダとして記述できる。

 例えば、「引数で指定したファイル名のファイルに文字列を書き込むが、引数がnullの場合は『default.txt』をファイル名とする」というラムダ式は、以下のようにnull合体演算子(??)を用いて式形式のラムダとして記述できる。

using System;

class Program
{
  static void Main(string[] args)
  {
    Action<string> method =
      (filename) => System.IO File.WriteAllText(
                          filename ?? "default.txt", "Hello!");

    method(null); // default.txtを作成
    method("hello.txt"); // hello.txtを作成
  }
}
リスト11 null合体演算子(??演算子)を用いたラムダ式

 あるいは「引数のフラグがfalseならファイル『normal.log』に追加、trueならファイル『system.log』に追加」であれば、三項演算子(?:)を使って以下のように式形式のラムダとして記述できる。

using System;

class Program
{
  static void Main(string[] args)
  {
    Action<bool> method =
      (system) => System.IO.File.AppendAllText(
          system ? "system.log" : "normal.log", "log message\r\n");

    method(false); // normal.logを作成
    method(true); // system.logを作成
  }
}
リスト12 三項演算子(?:演算子)を用いたラムダ式

 しかし、上記のリスト10に記述した例は、if文を三項演算子に置換できない。もし書き換えを試みると以下のような内容になる。

using System;

class Program
{
  static void Main(string[] args)
  {
    Action<string> method = (filename) =>
      filename == null
        ? Console.WriteLine("Hello!")
        : System.IO.File.WriteAllText(filename, "Hello!");

    method(null);
    method("hello.txt");
  }
}
リスト13 リスト10を三項演算子で書き換えた例(コンパイル・エラーとなる)

 このコードは、以下のようなコンパイル・エラーを発生させる。

error CS0201: 割り当て、呼び出し、インクリメント、デクリメント、および新しいオブジェクトの式のみがステートメントとして使用できます。
error CS0173: 'void' と 'void'' の間に暗黙的な変換がないため、条件式の型がわかりません。

これは、ラムダ式にvoidを返すメソッド呼び出しを書けないという意味ではない。以下のコードは問題なく記述できる。

Action<string> method =
  (filename) => System.IO.File.WriteAllText(filename, "Hello!");

 そうではなく、ここでエラーの原因になっているのは、三項演算子の2番目、3番目にvoid型の式を記述できない(記述しようとしても2番目と3番目の型、つまりvoidの間の暗黙的な変換が存在しないため全体の型を確定できない)という理由による。

 このような問題もあるため、void型を返す式に限っては三項演算子を利用しにくいことになる。もっとも、値を返さない式は分かりにくいので、書けない方がよいという考え方もあり得るだろう。

 ちなみに、当然のことながら、void型以外の型の式であれば、三項演算子は問題なく有効に機能する。以下のコードは問題なくコンパイルでき、実行できる。

using System;

class Program
{
  static void Main(string[] args)
  {
    Func<int, bool> method = (year) =>
      year < 1994 ? year % 4 == 0 : year % 4 == 2;

    Console.WriteLine("冬期オリンピックイヤー");
    for (int i = 1988; i < 1999; i++)
    {
      Console.WriteLine("{0}年={1}", i, method(i));
    }
    // 出力:
    // 冬期オリンピックイヤー
    // 1988年=True
    // 1989年=False
    // 1990年=False
    // 1991年=False
    // 1992年=True
    // 1993年=False
    // 1994年=True
    // 1995年=False
    // 1996年=False
    // 1997年=False
    // 1998年=True
  }
}
リスト14 三項演算子を用いたラムダ式

 変数methodに代入したラムダ式は、引数の年が冬期オリンピックの開催年であるかを判定するが、1994年以降は開催年が2年ずれているので判定式が変わってくる。このようなケースでは、当然のことながら三項演算子は有効である。つまり、この程度であれば、式形式のラムダとして容易に記述できる。

型指定を省略できる場合、できない場合

 ほとんどの場合、ラムダ式の引数の型は省略できる。逆にいえば、型の推定ができない状況ではラムダ式が使用できない制限が課せられているともいえる。

 例えば、varキーワードを用いて宣言する「暗黙的に型指定されるローカル変数」にラムダ式を代入することはできない。

var lambda = (int x) => x * 2;

 このコードは「ラムダ式を暗黙的に型指定されたローカル変数に割り当てることはできません」というエラーを発生させてしまう。回避するにはvarではなく、正しく型の名前を書く必要がある。

 しかし、まれにラムダ式が使用できるにもかかわらず、型指定を行わねばならない状況が発生する。

以下はその一例である。

using System;

delegate int delegate1( int x );
delegate int delegate2( string s );

class Program
{
  private static void sample(delegate1 method)
  {
    Console.WriteLine("void Sample(delegate1 method)");
  }

  private static void sample(delegate2 method)
  {
    Console.WriteLine("void Sample(delegate2 method)");
  }

  static void Main(string[] args)
  {
    sample((int x) => 0);
    // sample((x) => 0); // 引数の型指定を外すとエラーになる
  }
}
リスト15 引数の型指定が必須のケース

 このリストで、「sample((x) => 0)」はコンパイル・エラーになる。条件を満たすsampleメソッドが2つあり、どちらを使うべきか判定するだけの情報が与えられていないからである。しかし、引数に型を添えて「sample((int x) => 0)」と記述すればコンパイルでき、実行もできる。引数に型が指定されたことで、2つのsampleメソッドのうち、型が一致する方のデリゲートを明確に選択できるからである。

何もしないラムダ式

 盲点になることもある話題なので、「何もしないラムダ式」の書き方も説明しておく。

 ラムダ式に戻り値がない場合(voidの場合)でかつ、内容を空としたい場合(呼び出しても何も実行しないラムダ式を選択的に使いたい場合)、ステートメント型のラムダとして空の内容を記述することができる。

 例えば、以下のようなラムダ式である。

(x) => { };

 このようなラムダ式は、リファクタリングでいう「ヌル(ナル)オブジェクトの導入」に当たる機能性を持つ。つまり、処理すべき式が存在しないことをnullで示すのではなく、何も処理しないラムダ式を呼ばせるという手法である。

 簡単に、このテクニックを使わない場合と使う場合のコード例をそれぞれ示す。

 まず、このテクニックを使わない場合である。以下のリストは、実行すべき処理がない場合にnull値でそれを示すケースである。Sampleメソッドの引数actionは、値がnullではない場合に限って呼び出される。

using System;

class Program
{
  private static void Sample(Action<string> action)
  {
    if (action != null) action("Hello!");
  }

  static void Main(string[] args)
  {
    Action<string> action = null;
    Sample(action);

    action = (x) => Console.WriteLine(x);
    Sample(action); // 出力:Hello!
  }
}
リスト16 実行すべき処理がない場合をnullで示した例

 これに対して、以下は実行すべき処理がない場合、空のラムダ式で示したケースである。Sampleメソッドは、引数actionがnullか否かを判定する必要がない。もし、処理すべき内容が存在しない場合は、単純に空文のみのラムダ式が実行されて何もせず戻ってくる。

using System;

class Program
{
  private static void Sample(Action<string> action)
  {
    action("Hello!");
  }

  static void Main(string[] args)
  {
    Action<string> action = (x) => { };
    Sample(action);

    action = (x) => Console.WriteLine(x);
    Sample(action); // 出力:Hello!
  }
}
リスト17 実行すべき処理がない場合を空文で示した例

 このような「何もしないラムダ式(あるいは従来は匿名メソッド)」は、筆者が割とよく使うテクニックである。

 例えば、いま書いているプログラムでは、ユーザーの操作をジャーナリングしてプレイバックする機能を持っていて、それによって自動テストを行っている。その際、プレイバック中は出力に関する処理をすべて抑止して高速に実行させるようにしているが、それらは、出力を行うメソッド呼び出しにいちいち条件判断を付けるのではなく、呼び出し先を「何もしないラムダ式(匿名メソッド)」に差し替えることで実現している。これにより、ソース・コードの簡潔さを維持したまま、気軽にいつでも実行できる程度に自動テストを高速化することに成功している。


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

本日 月間