連載:C# 3.0入門

第8回 LINQメソッド形式編

株式会社ピーデー 川俣 晶
2008/11/07
Page1 Page2 Page3 Page4

メソッド形式のクエリの接続

 ここでは別の形で、クエリ式とはガラッと雰囲気の変わるメソッド形式を紹介しよう。

 前々回、into句を用いて2つのクエリを接続した例を紹介した。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    var query = from n in Enumerable.Range(1, 10)
                select n * n into m
                where m > 50 select m;

    foreach (int n in query) Console.WriteLine(n);
  }
}
リスト12 1から10を2乗した値のうち50を超えるものだけを抜き出す

 これは、メソッド形式ではどのように記述すればよいのだろうか。

 where句にはWhereメソッド、select句にはSelectメソッドがあったように、into句にはIntoメソッドがあるのだろうか? 結論からいうと、Intoメソッドは存在しない。なぜなら、「列挙できるものはクエリできるもの」という原則からいえば、そもそもinto句抜きでクエリを記述できるからだ。例えば、リスト12のクエリは、以下のように書き直せばinto句抜きで記述できる。

var query = from m in (
                from n in Enumerable.Range(1, 10) select n * n)
            where m > 50 select m;

 ただし、これは「2つのクエリ式」を含む式となる。into句は、これを「1つのクエリ式」で表現するために使われるものである。

 さて、これをメソッド形式に置き換えるとき、式は全体で1つの式となるため、「1つの」「2つの」という区別は意味がなくなる。つまり、into句の存在意義そのものが消滅してしまう。

 そして、「列挙できるものはクエリできるもの」という原則からいえば、Selectの出力は「列挙」であるから、これを入力として直接Whereメソッドの前段階に接続してよい。つまり、上記のクエリは以下のようなシンプルな1本の式に書き換えられる。

var query = Enumerable.Range(1, 10)
            .Select((n) => n * n)
            .Where((m) => m > 50)
            .Select((m) => m);

 さらにいえば、実はWhereメソッドの返却値そのものが「列挙」であるため、同じ値を列挙させる効能しか発揮していない最後の「.Select((m) => m)」はなくても結果は変わらない。

 つまり、以下の記述だけで、同等の結果が得られる。

var query = Enumerable.Range(1, 10)
            .Select((n) => n * n)
            .Where((m) => m > 50);

 このことは、今回の最初に紹介したメソッド形式のarray.Select((x) => x);という式でも、Select((x) => x)はなくても同じであることを示す。

 このように、クエリ式とは別の形の最適化、シンプル化を行うことができるのが、メソッド形式の特徴といえる。

クエリ結果のグループ化

 group句を使ったクエリ結果のグループ化は、シンプルにメソッド形式に置き換えられる。難しくはないので、前々回の「リスト グループ化」のクエリ式を置き換えた例のみを紹介しておこう。

var query = from n in 商品情報データ group n by n.Cpu;
クエリ式版(前々回掲載)

var query = 商品情報データ.GroupBy((n) => n.Cpu);
メソッド形式版

 基本的にgroup句をGroupByメソッドの呼び出しに置き換えればOKである。

■メソッド形式で複数のソースを関連付ける

 複数のソースを関連付けるjoin句は、「join …… in …… on …… equals ……」といった長い構文であり、equalsのような特殊なキーワードを使う(==演算子は使わない)などの特徴があった。

using System;
using System.Linq;

class Program
{

  class 商品情報
  {
    public int Id;
    public string 名前;
  }


  class 商品販売価格
  {
    public int Id;
    public int 価格;
  }

  static void Main(string[] args)
  {
    商品情報[] 商品情報データ =
    {
      new 商品情報() { Id = 1, 名前 = "PC-8001" },
      new 商品情報() { Id = 2, 名前 = "MZ-80K" },
      new 商品情報() { Id = 3, 名前 = "Basic Master Level-3" },
    };

    商品販売価格[] 商品販売価格データ =
    {
      new 商品販売価格() { Id = 1, 価格 = 148000 },
      new 商品販売価格() { Id = 2, 価格 = 178000 },
      new 商品販売価格() { Id = 3, 価格 = 229000 },
    };

    var query = from x in 商品情報データ
          join y in 商品販売価格データ on x.Id equals y.Id
          select new { Name = x.名前, Price = y.価格 };

    foreach (var 商品 in query)
    {
      Console.WriteLine("{0} {1:C}", 商品.Name, 商品.Price);
    }
    // 出力:
    // PC-8001 \148,000
    // MZ-80K \178,000
    // Basic Master Level-3 \229,000
  }
}
リスト13 複数のソースを関連付ける

 これをメソッド形式に置き換えると、equalsのような特殊キーワードが消滅するのは当然だが、それとは別に興味深い変化が生じる。

 以下は実際に置き換えた例である。

var query = 商品情報データ.Join(
                商品販売価格データ,
                (x) => x.Id,
                (y) => y.Id,(x,y) => new {
                                Name = x.名前, Price = y.価格 });

 このように、join句はストレートにJoinメソッドに置き換えられ、in、on、equalsのようなキーワードはなくなり、すべてJoinメソッドの引数で渡す形になる。

 しかしここで注目すべき点は、クエリ式のSelect句に相当する機能も、Joinメソッドの引数に指定されている点である。つまり、クエリ式のjoin句をメソッド形式に置き換える際には、実際にはその後のselect句もまとめてJoinメソッド呼び出しに置き換える必要がある。

■メソッド形式のグループ化結合

 join句を使用して「join …… in …… on …… equals …… into ……」としてグループ化結合を行った場合は、JoinメソッドではなくGroupJoinメソッドを使用する。それ以外は、上記の例と大差ない。

 前回で紹介した「リスト6 グループ化結合の例」のクエリ式は以下のようにメソッド形式に書き換えられる。

var query = from x in 商品情報データ
            join y in 商品販売価格データ on x.Id equals y.Id into z
            select new { Name = x.名前, 商品データ = z };
 
var query = 商品情報データ.GroupJoin(
                  商品販売価格データ,
                  (x) => x.Id,
                  (y) => y.Id,
                  (x, z) => new { Name = x.名前, 商品データ = z });

 このとおり、GroupJoinメソッドはselect句の機能も含むので、Selectメソッドを記述する必要はなくなっている。

■メソッド形式の左外部結合

 join句を用いた3種類の結合の最後は左外部結合である。しかし、もともと左外部結合はクエリ式とはいっても、DefaultIfEmptyというメソッドの力を借りて実現していたものなので、メソッド形式とは相性が良いはずである。……と思って軽視していたのだが、いざ書き直してみると一筋縄ではいかなかった。

 例えば、以下は前回紹介した左外部結合のクエリ式である。

var query = from x in 商品情報データ
            join y in 商品販売価格データ on x.Id equals y.Id into z
            from a in z.DefaultIfEmpty(
                        new 商品販売価格() { 店名 = "取扱店なし" })
            select new { Name = x.名前, 販売店 = a };

 これをメソッド形式に書き直したところ、以下のような内容になった。

var query
    = 商品情報データ
        .GroupJoin(
            商品販売価格データ,
            (x) => x.Id,
            (y) => y.Id,
            (x, z) => new {
                Name = x.名前,
                販売店 = z.DefaultIfEmpty(
                         new 商品販売価格() { 店名 = "取扱店なし" })
            })
          .SelectMany(
            (x) => x.販売店,
            (x, a) => new { Name = x.Name, 販売店 = a } );

 このとおり、クエリ式では1つしかなかった“new”が2つに増えてしまっている。しかし、これは効率が落ちたことを意味するわけではない。クエリ式を使ったコードをリリース・ビルドして.NET ReflectorのC#モードで逆コンパイルすると、上記のメソッド形式とほぼ同等のコードが生成されていることが分かる。つまり、ソース・コードの見た目上はnewの数が増えているが、実行ファイルに含まれるnew呼び出しの数は変わらないわけである。

 余談だが、実は上記のクエリ式のメソッド形式への書き換えは、なかなかうまく書けずに非常に焦っていた。検索しても書き換え事例はヒットして来ないし、頑張って書いたクエリも上記のように冗長感が漂う。最終的にハタと気付いたのは、クエリ式をコンパイルして.NET Reflectorで逆コンパイルするという方法であった。それにより、自分で書ける最善のコードと、リリース・ビルドの逆コンパイル結果がほぼ一致したことで、やっと「間違っていなかった」と納得することができた。

 なお、逆コンパイルは有効であることを特に付記しておこう。クエリ式をメソッド形式に書き換えたいケースは、クエリ式では記述できない機能を使うなど、いくつかの事例が考えられる。しかし、込み入ったクエリ式は、とっさにどのようなメソッド形式に置き換えれば等価であるか分かりにくいこともある。そういう場合は、コンパイルして.NET Reflectorで逆コンパイルするという方法が使用できる。クエリ式は糖衣構文であるため、逆コンパイルでは(いまのところ)復元されない。その結果、逆コンパイル出力は、実体として生成されたメソッド形式になるわけである。

 さて、話を戻そう。このコードの内容を説明しておく。このコードは、GroupJoinメソッド自体がSelect機能を含むため、それとSelectManyメソッドにより、Select相当の機能が2回動作している。1回目は以下の部分がそれに当たる。

new {
  Name = x.名前,
  販売店 = z.DefaultIfEmpty(
           new 商品販売価格() { 店名 = "取扱店なし" })

 ここでは「商品の名前」と「販売店の列挙」のペアとなる結果を生成しているが、その際にDefaultIfEmptyメソッドを用いて、対応する販売店がない商品名について「取扱店なし」という「デフォルト値を持つ列挙」を挿入している。

 2回目は以下の部分がそれに当たる。

SelectMany(
    (x) => x.販売店,
    (x, a) => new { Name = x.Name, 販売店 = a }

 これにより、1回目では列挙であった「販売店」を展開して、商品の名前と販売店のペアを生成している。

 まとめると、第1段階では「値」と「列挙」のペア(すべて列挙するには2重の繰り返しを要する)を生成したが、第2段階では「値」と「値」のペア(単層の繰り返しですべて列挙できる)を生成していることになる。


 INDEX
  C# 3.0入門
  第8回 LINQメソッド形式編
    1.予約語のエスケープ/メソッド形式のLINQ
    2.メソッド形式でのみ可能なクエリ/メソッド形式のソート/複数のソースのクエリ
  3.メソッド形式のクエリの接続/クエリ結果のグループ化
    4.メソッド形式のlet/句効率的に列挙可能にするという問題
 
インデックス・ページヘ  「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 記事ランキング

本日 月間