連載:C# 3.0入門

第8回 LINQメソッド形式編

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

メソッド形式でのみ可能なクエリ

 さて、すでに説明したとおり、クエリ式は糖衣構文であり、実体としての本体はメソッド形式となる。このことは、メソッド形式が用意されていても、糖衣構文が用意されていない機能は、クエリ式では記述できないことを意味する。

 具体的な例を1つ紹介しよう(後でもう1つ紹介する)。

 ここで取り上げるのはソースの順序を逆転するReverseメソッドである。最初の項目が最後になり、最後の項目が最初になる。まず、これを単体で使った例を紹介しておこう(リスト7)。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    int[] array = { 1, 2, 3 };

    foreach (var n in array.Reverse())
    {
      Console.WriteLine(n);
    }
    // 出力:
    // 3
    // 2
    // 1
  }
}
リスト7 ソースの順序を逆転する

 さて、これが本当にクエリの機能か? という疑問を持った読者もいると思うが、これはメソッド形式のクエリの中で使用する、れっきとしたクエリ関連の機能である。

 例えば、「守」という文字を含む名前のリストを抽出し、リスト最後の2人だけを取り出す処理を記述してみよう。ここで難しいのは、列挙において「最後の2つ」を抜き出す処理である。最後の1つを抜き出すだけならLastメソッドという便利なメソッドがあるのだが、いつ列挙が終わるか回数を事前に予測できないので、最後から2つ目の項目を、その項目が列挙された時点で確定するのは難しい。強いて書くなら、常に列挙された最新の2つの項目を保持し続ける仕掛けを組み込む必要がある。

 しかし、列挙の順番を逆転させると、比較にならないぐらい簡単になる、要するに最初の2つを取り出せば、それが最後の2つなのである。このような処理はReverseメソッドが使用できるメソッド形式では容易に記述できる。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    string[] array = { "押井守", "細田守", "神戸守", "日高政光" };

    var query = array.Where((n) => n.Contains('守')).Reverse();

    int count = 0;
    foreach (var name in query)
    {
      Console.WriteLine(name);
      count++;
      if (count >= 2) break;
    }
    // 出力:
    // 神戸守
    // 細田守
  }
}
リスト8 「守」を含む名前のリストを抽出し、リストの最後の2人だけを取り出す

メソッド形式のソート

 さて、ここまではおおむねクエリ式の「句」と、メソッド形式のメソッドが1対1に対応していた。しかし、1対1の対応が存在しないケースもある。例えば、複数項目のソートはその一例である。以下は、前々回に紹介した絶対値の逆順ソート(複数のソート条件)のサンプル・コードである。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    int[] array = { -2, -1, 0, 1, 2 };

    var query = from x in array
                orderby Math.Abs(x) descending, x descending
                select x;

    foreach (int n in query) Console.WriteLine(n);
    // 出力:
    // 2
    // -2
    // 1
    // -1
    // 0
  }
}
リスト9 絶対値の逆順ソート(複数のソート条件)

 この事例では、orderby句によって、2つのソート条件が示されている。1つはMath.Abs(x)の降順(descending)、もう1つはxの降順(descending)である。

 これをメソッド形式に置き換える場合、以下のように記述する。

var query = array
            .OrderByDescending((x) => Math.Abs(x))
            .ThenByDescending(x => x)
            .Select((x) => x);

 まず、OrderByDescendingメソッドに注目しよう。orderby句は、メソッド形式では昇順ならOrderByメソッド、降順ならOrderByDescendingメソッドに置き換えられる。ここでは降順なので、OrderByDescendingメソッドが指定されている。ここまではよいだろう。問題はこの先である。このクエリ式は条件が2つ記述されているが、OrderByDescendingメソッドに指定できる条件は1つだけである。では2つ目の条件はどのように記述するのか。その答えがThenByメソッド(昇順)またはThenByDescendingメソッド(降順)である。

 上記のように、OrderBy(Descending)メソッドに続けてThenBy(Descending)メソッドを追加すれば、2つ目以降のソート条件を追加することができる。ThenBy(Descending)メソッドは2つ以上連続して記述してもよく、3つ以上のソート条件を付けることもできる。

 このように、ソートを行う場合はクエリ式では見られないThenBy(Descending)という名前のメソッドを使用しなければならない。

■orderbyのカスタム比較演算子

 さて、上記の例はメソッド形式を使うと、もう1つ別のスタイルに書き換えることができる。これは、クエリ式では記述できないがメソッド形式で記述できる事例の1つである。

 上記のサンプル・コードは、以下のように書き直すと、ThenByDescendingメソッドも、複数のソート条件の記述も抜きに、同等条件の並べ替えを行うことができる。

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

class Program
{
  class Comparer : IComparer<int>
  {
    public int Compare(int x, int y)
    {
      int result = Math.Abs(x) - Math.Abs(y);
      if (result == 0) return x - y;
      return result;
    }
  }

  static void Main(string[] args)
  {
    int[] array = { -2, -1, 0, 1, 2 };

    var query = array.OrderByDescending((x) => x, new Comparer());

    foreach (int n in query) Console.WriteLine(n);
  }
}
リスト10 カスタム比較オブジェクトで比較する

 このコードのポイントは、OrderByDescendingメソッドの第2引数に指定された比較オブジェクトである。このような比較オブジェクトは、比較の条件が込み入ったとき、同じ条件を何回も比較のために使い回すときにあると便利な存在である。いたずらにクエリ式を長く書くよりも、このような比較オブジェクトを用意するとコードがスマートになることもあるだろう。そのようなスマートさを得るには、メソッド形式を使わねばならない。

メソッド形式の複数のソースからクエリする

 クエリ式のfrom句に相当するメソッドは、メソッド形式には存在しない。列挙できるオブジェクトがクエリの起点になるのである。しかし、そうすると以下のようなクエリ式をメソッド形式で記述できなくなる。クエリの起点が2つあるからだ。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    var query = from x in Enumerable.Range(1, 9)
                from y in Enumerable.Range(1, 9)
                select x + "×" + y + "=" + (x * y).ToString();

    foreach (string s in query) Console.WriteLine(s);
    // 出力:
    // 1×1=1
    // 1×2=2
    // 1×3=3
    // ……中略……
    // 9×7=63
    // 9×8=72
    // 9×9=81
  }
}
リスト11 掛け算の九九一覧表示

 この場合、同等のクエリをメソッド形式で記述するには以下のように記述する。

var query = Enumerable.Range(1, 9)
              .SelectMany(
                  (x) => Enumerable.Range(1, 9),
               (x, y) => x + "×" + y + "=" + (x * y).ToString());

 この場合、2つのfrom句というクエリ式を、select句に対応するSelectメソッドの代わりにSelectManyメソッドを用いて実現する。このような回りくどい形を取らねばならない。しかも、この例のSelectManyメソッドの引数には2つのラムダ式を渡しており、直感的に動作が分かりやすいとはいいにくい。

 この場合のSelectManyメソッドは以下のように機能する。


SelectManyメソッドの動作

 この図を見ると分かるとおり、2つのソースから結果を生成する機能を持っていることは分かると思うが、2つのソースのうち1つは前段階から、もう1つは引数から受け取る形式であり、直感的には分かりにくい。

 ちなみに、この例ではSelectManyメソッドの第1引数のラムダ式の仮引数xは使用されていない。しかし、これを使用することもできる。これを使えば、以下のような第2段階の列挙が第1段階で列挙された値によって変わるクエリも記述できる。

var query = Enumerable.Range(1, 3)
            .SelectMany(
                (x) => Enumerable.Range(1, x),
             (x, y) => x + "×" + y + "=" + (x * y).ToString());
// 出力:
// 1×1=1
// 2×1=2
// 2×2=4
// 3×1=3
// 3×2=6
// 3×3=9
第2段階の列挙が第1段階で列挙された値によって変わるクエリ
リスト11からクエリのみ差し替え。

 このように、1×……は1回しか列挙されないが3×……は3回列挙されており、回数が変化していることが分かる。これは、仮引数の値を活用したためである。


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

本日 月間