連載:C# 3.0入門

第2回 ラムダ式と型推論

株式会社ピーデー 川俣 晶
2008/05/16
Page1 Page2 Page3

使える既存のデリゲート

 ラムダ式を多用するようになると、いちいちデリゲート型を宣言することが面倒になってくる。

 筆者個人としては、用途ごとに個別のデリゲート型を宣言する方がよいとは思うが(別個に宣言されていれば互換性があるとは見なされないので誤代入を防げるし、用途を連想しやすい名前を与えられる)、実際にありとあらゆる用途のデリゲートを個別に宣言すると扱いにくいソース・コードになりがちである。

 そうすると、クラス・ライブラリが提供する基本的なデリゲート型が活用できれば話は簡単になる。ここでは実際に、そのような目的で使用できるデリゲート型をいくつか紹介しよう。

■MethodInvokerデリゲート(System.Windows.Forms名前空間)

 引数なし、戻り型voidに限って使用できるデリゲート。なんと.NET Framework 1.0から存在する最古参のデリゲート型。しかし、System.Windows.Forms.dllを参照しなければ利用できないため、使えないケースが多い。古いソース・コードでは使われている可能性があるので、名前だけは覚えておくとよいだろう。新規に使うことはあまりお勧めではない。

■Actionデリゲート(System名前空間)

 引数なし、戻り型voidに限って使用できるデリゲート。誰もが参照するSystem.Core.dllに含まれているので、MethodInvokerの代わりに汎用的に使用できる。しかし、.NET Framework 3.5からのサポートなので、C# 3.0+.NET Framework 2.0で開発するような場合には使えないジレンマもある。

■Action<T>ジェネリック・デリゲート(System名前空間)

 引数(T)を1つ持ち、戻り型voidのジェネリックなデリゲート型。コレクション関係でもよく使われる。例えば、コレクションの列挙で呼び出すメソッドを指定するデリゲート型はこれで十分。.NET Framework 2.0以降なら使用できるので、基本中の基本。実は、「変数のキャプチャ」を併用することで、これを使えば済むケースは多い。

■Action<T1, T2>ジェネリック・デリゲート(System名前空間)
■Action<T1, T2, T3>ジェネリック・デリゲート(System名前空間)
■Action<T1, T2, T3, T4>ジェネリック・デリゲート(System名前空間)

 引数を2〜4個持ち、戻り型voidのジェネリックなデリゲート型。Action<T>の仲間に見えるが、実はAction<T>と違って、.NET Framework 3.5以降でのみサポートとなっている。

■Predicate<T>ジェネリック・デリゲート(System名前空間)

 引数(T)を1つ持ち、戻り型boolのジェネリックなデリゲート型。基本的にはAction<T>と同様だが、戻り型がbool型であるため、結果を呼び出し元に伝達できる点で異なっている。例えば、コレクションから検索を行うようなケースでは、この型が使われる。引数で渡した値が意図した値であれば、戻り値をtrueにして発見/検索終了を伝達できる。これも意外と出番が多い。

■Func<TResult>ジェネリック・デリゲート(System名前空間)
■Func<T, TResult>ジェネリック・デリゲート(System名前空間)
■Func<T1, T2, TResult>ジェネリック・デリゲート(System名前空間)
■Func<T1, T2, T3, TResult>ジェネリック・デリゲート(System名前空間)
■Func<T1, T2, T3, T4, TResult>ジェネリック・デリゲート(System名前空間)

 デリゲート型の最終兵器。戻り値と引数の型を自由に設定できる究極のカスタマイズ可能デリゲート。引数の数は0〜4個のバリエーションがあり、少なくとも引数が4個以内であればあらゆる用途に対応できる。例えば、

delegate int MyDelegate( string n, double d );

という宣言を行う代わりに、

Func<string, double, int>

というデリゲートで代用することができる(ただしこの2つは異なる型になるので、相互に代入はできない。どちらか片方を使うと決めたら、それを貫徹しなければならない)。

 唯一の死角は、戻り値がvoid型のケースに対応できないことだろう。この場合はActionという名を持つデリゲートの各バリエーションを使用して対処する。

 これ以外にはこれといって問題はないが、強いて弱点を挙げるなら、.NET Framework 3.5からのサポートなので、それよりも古いフレームワークを対象とした場合に使用できないことと、記述に冗長感が多いところだろう。この2つの理由は、依然としてPredicate<T>を使う価値があることを示している。使用できる条件は限られているが、.NET Framework 2.0以降なら使用できるし、戻り型をいちいち明示しないので冗長感も薄くなる。

 以上のような型を活用して迅速にラムダ式を使うコードを記述していくことができる。

ジェネリック・メソッドと型推論

 ジェネリック・メソッドはC# 2.0で追加された機能である。これは、任意の型を呼び出し時に明示して、どのような型も自由に受け入れることができるメソッドを記述するためのものだ。

 例えば、Array.Sortメソッドのジェネリック・メソッド版は、以下のように記述して、指定された型として配列をソートする。

Array.Sort<型名>( 配列 );

 このとき、型名は配列から容易に推定できることが多い。そこで、実際にコンパイル時に推論を行う機能が型推論である。型推論を使えば、以下のようにソース・コード上で型の指定を省くことができる。

Array.Sort( 配列 );

 さて、ジェネリック・メソッドや型推論とラムダ式は密接な関係がある。ラムダ式が導入されたことで、型推論のルールが追加されているためだ。

 まずに、ラムダ式の引数がシンプルに推論されている例を見てみよう。

 以下は、Linqのメソッド構文と呼ばれる機能を用いて、配列nから10よりも小さい値だけを抜き出すコードである(Linqについては、この連載では後で取り上げる)。ここでポイントになるのは、引数によって指定された条件を満たす要素を選び出すWhereメソッドである。

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    int[] n = { 2, 3, 5, 7, 11, 13 };
    IEnumerable<int> numQuery2 = n.Where(num => num < 10);

    foreach (int i in numQuery2)
    {
      Console.WriteLine(i);
      // 出力:
      // 2
      // 3
      // 5
      // 7
    }
  }
}
リスト7 Linqを用いて10よりも小さい値だけ抜き出す

 Whereメソッドは、本来以下のように型パラメータTSourceが指定されているが、推論によってそれがint型であると確定されている。

public static IEnumerable<TSource> Where<TSource>(
  this IEnumerable<TSource> source,
  Func<TSource, bool> predicate
)
Whereメソッド(System名前空間のIEnumerableクラス)の型

 なお、ラムダ式の代わりに匿名メソッドを使用する場合は、以下のように、引数の型をintと明示して記述する必要がある。

n.Where(delegate( int num ) { return num < 10; });

 しかし、推論可能な状況であれば、ラムダ式ではintを指定する必要がない。

 ちなみに、Whereメソッドは本来配列オブジェクトが持つメソッドではなく、拡張メソッドであり、型推論はやや複雑な経路をたどって確定される。しかし、そこまで踏み込んで理解せずとも十分であるため、ここでは詳しくは解説しない(拡張メソッドについては次回以降で解説する)。

 さて、ここでは型推論の特に強力な部分を見ていくことにしよう。以下は、C# 3.0言語仕様書に掲載されているサンプル・コードを若干修正したものである。

using System;

class Program
{
  static Z F<X, Y, Z>(X value, Func<X, Y> f1, Func<Y, Z> f2)
  {
    return f2(f1(value));
  }

  static void Main(string[] args)
  {
    double seconds = F("1:15:30",
                       s => TimeSpan.Parse(s),
                       t => t.TotalSeconds);
    Console.WriteLine(seconds);
  }
}
リスト8 型推論の連鎖によってすべての型パラメータが確定する例

 この例のFメソッドにおいて、型パラメータXは容易に推論できる。指定された「1:15:30」がstring型であるため、Xはstring型である。しかし、型パラメータYとZにはそのような明確なヒントが存在しない。それらを使った引数f1とf2には、引数の型を一切明示しないラムダ式しか書かれていないからだ。

 例えば、引数f1に書かれたラムダ式「s => TimeSpan.Parse(s)」は、このラムダ式の戻り値がDateTime型であることは推論可能だが、引数sの型を推論で導き出すことができない。引数f2も同様である。

 それにもかかわらず、このコードはコンパイルして実行できる。

 その理由は、引数f1で確定できないラムダ式の引数sの型は、第1引数valueによって確定され、引数f2で確定できないラムダ式の引数tの型は、引数f1によって確定されるためである。このような推論の連鎖によって、型パラメータX、Y、Zはすべて確定させることができる。

 しかし、このようにエレガントな解決は常にできるものではない。推論が十分にできないケースでは、型パラメータを明示して書いてしまう方が楽だろう。


 INDEX
  C# 3.0入門
  第2回 ラムダ式と型推論
    1.ラムダ式を使用した事例
  2.使える既存のデリゲート/ジェネリック・メソッドと型推論
    3.オーバーロードの解決/匿名メソッドとラムダ式の違いと式ツリー

インデックス・ページヘ  「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 記事ランキング

本日 月間