連載:C# 3.0入門

第7回 LINQ応用編

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

let句

 クエリ式には、もう1つだけ紹介する価値のある句がある。それがlet句である。

 このlet句は、クエリ式の途中で値を範囲変数に保存する機能を持つのだが、クエリ式を書いてみても効能が分かりにくい。なくてもクエリ式は書けてしまうのである。実は筆者も存在意義が分かっていなかったが、本稿を書くに当たって調べたところ、非常に重要な機能性があることが分かった。

 以下、let句を使わない例と、使った例の差を見ていただきたい。注目点はクエリ式から呼び出している変換メソッドの呼び出し回数である。

using System;
using System.Linq;

class Program
{
  private static int count = 0;

  private static int conversion(int n)
  {
    count++;
    return n;  // 何か有意義な変換を行っていると想定する
  }

  static void Main(string[] args)
  {
    int[] a = { 1, 2, 3 };
    int[] b = { 4, 5, 6 };

    var q = from n in a
            from m in b
            select conversion(n) + m;

    foreach (var n in q)
    {
      Console.WriteLine(n);
    }
    // 出力: 5 6 7 6 7 8 7 8 9

    Console.WriteLine("conversion called: {0}", count);
    // 出力:conversion called: 9
  }
}
リスト11 変換メソッドを呼び出すクエリ式

 let句を使う場合は、以下のようにクエリ式を書き換える。

var q = from n in a
        let cn = conversion(n)
        from m in b
        select cn + m;
リスト12 let句を使ったクエリ式

5 6 7 6 7 8 7 8 9 conversion called: 3
リスト12のクエリ式の場合の実行結果

 さて、見てのとおり変換メソッドが呼び出される回数が9回から3回に激減した。減少量は、扱うデータ量(2番目のfrom句の列挙数)が増えればさらに差が大きくなるだろう。

 このような差が出る理由は、以下のとおりである。

  • リスト11の第1のfrom句は3回、第2のfrom句も3回の列挙を持つため、select句は3*3=9回繰り返される
  • 従って、リスト11ではselect句に書かれたメソッド呼び出しも9回発生する
  • リスト12の第2のfrom句より手前の世界では、第1のfrom句だけが効力を持つ。この世界では、3回だけ列挙が発生する
  • リスト12の第2のfrom句より手前に書かれたlet句は3回だけ実行される。つまり、メソッド呼び出しは3回しか発生しない

 つまりまとめると、1つのクエリ式内で、列挙回数が増えるタイミングよりも手前に時間のかかる処理を行うと、それだけ大きな高速化を達成できるわけである。let句は式を計算して範囲変数に代入する効能しか持たないが、それを使えば計算を行うタイミングを前倒しし、このような高速化を達成することができるわけである。

クエリのインスタンス化

 アルファベット A〜Zの26文字のうち、偶数番目の文字だけからなる文字列を生成したい、といった用途にも、もちろんLINQは使用できる。

 このような目的にかなう文字番号のシーケンスを生成するクエリ式は、以下のように記述できる。とても簡単である。

from n in Enumerable.Range('A', 26)
where (n % 2) == 0
select n

 ところが、この番号のシーケンスから文字列オブジェクトを作成しようとすると、簡単ではない問題に突き当たる。stringクラスのコンストラクタは、列挙を入力として受け入れるインターフェイスを持っていないのである。

 このギャップを解消するには、「クエリのインスタンス化」を行うとよい。つまり、クエリ式をchar型の配列に変換してしまうのである。char形の配列なら、stringクラスのコンストラクタが受け入れてくれる。そのためには、クエリ式に対してToArray()メソッドを実行する。これでクエリ式とstringクラスのコンストラクタが直結した。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    string s = new string((from n in Enumerable.Range('A', 26)
                           where (n % 2) == 0
                           select (char)n).ToArray());

    Console.WriteLine(s); // 出力:BDFHJLNPRTVXZ
  }
}
リスト13 インスタンス化されるクエリ式

 ここで注意する点は2つある。1つは、結果をchar型で得るため、「select (char)n」とselect句にキャストを入れている点である。Enumerable.Rangeメソッドはint型の値範囲を提供するだけなので、このキャストがないとクエリ結果をchar型配列に変換できない。

 もう1つは、必要リソースの増加である。列挙は単に列挙するだけならメモリをそれほど食わないが、配列に変換すれば配列の分だけメモリを消費する。配列に変換しないで実行できるときは、変換せずに行う方がよいだろう。

 ちなみに、ToArrayメソッドと同様に、List<T>オブジェクトを生成するToListメソッドもある。

クエリ結果の個数を得る

 クエリの結果の個数だけを欲しい場合には、クエリに対してCountメソッドを呼び出す。これは列挙を行って、結果の個数だけを返してくれるメソッドである。

 例えば、上記の例を「アルファベットA〜Z の26文字のうち、偶数番目の文字は合計何文字か」という意図に修正してみよう。この場合、文字列を生成する必要はまったくない。さらにいえば、個数だけが分かればよいので、配列を生成する必要もない。

 そこで、ToArrayメソッドをCountメソッドに差し替えることができる。これで、より軽量に意図を実行できるプログラムに仕立てることができ、かつ、ソース・コードに列挙の構文を書き込む必要はない。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    int count = (from n in Enumerable.Range('A', 26)
                 where (n % 2) == 0
                 select n).Count();

    Console.WriteLine(count); // 出力:13
  }
}
リスト14 クエリの即時実行で個数を得る

 ちなみに、このケースでは個数だけを見るので、select句で返す内容は意味を持たない。極端なことをいえば、「select 0」のように書き直して常に0を結果としても、このプログラムの出力そのものは正しいままである。

 このような即時実行形のメソッドはCountのほかにMax、Average、Firstなどがある。これらを活用することも、もちろん価値がある。

Anyメソッドと存在チェック

 クエリ式を日常的に書いていると、しばしばあるクエリ式にヒットする要素が1つでもあるかどうかを調べたい場合がある。該当する要素が1つでもあれば機能を発動させる、といった処理を記述することが比較的多いからである。

 この場合、安易に考えると上記のCountメソッドを使って、このメソッドの返却値が0よりも大きければ要素があるというコードを書きたくなる。しかし、これはお勧めではない。単に存在するか否かを調べるだけならAnyメソッドの方がお勧めである。実行速度が劇的に違うからである。その速度差を以下の例で実感していただきたい。

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    const int count = 1000000;
    int[] a = new int[1000];
    bool dummy;

    for (int i = 0; i < a.Length; i++) a[i] = i;

    DateTime start1 = DateTime.Now;
    for (int i = 0; i < count; i++)
    {
      dummy = (from n in a select n).Count() > 0;
    }
    Console.WriteLine(DateTime.Now - start1);

    DateTime start2 = DateTime.Now;
    for (int i = 0; i < count; i++)
    {
      dummy = (from n in a select n).Any();
    }
    Console.WriteLine(DateTime.Now - start2);
  }
}
リスト15 CountメソッドとAnyメソッドの速度差比較

00:00:43.6623658
00:00:00.2390239
リスト15の実行結果例
Pentium D搭載の筆者のPCによる。Visual Studio 2008 SP1のデバッグビルド。

 まさに圧倒的。100倍以上の差がついている。データ量が増えれば、この差はもっと広がることになる。

 さて、なぜこれだけの速度差が出るのだろうか。その理由は、Anyメソッドは列挙を打ち切ることができる点にある。要素の個数を数える処理は最後まで列挙をやめることはできない。しかし、「任意の要素が存在する」ことを調べるだけのAnyメソッドは、たった1つでも要素があればそこで列挙を打ち切ることができる。このケースであれば、列挙の最初の1個を取り出した時点で勝負が付いている。残り999個の要素は調べる必要がない。

まとめ・前回の最初のサンプルを通して

 最後に、前回の最初に紹介したまま解説を後回しにしたプログラム(前回のリスト1)を解説しよう。ディレクトリのツリーの中で、別ディレクトリに同じファイル名のファイルが存在することがある。それをすべてリストアップするプログラムである。前回のリスト1を以下にに再掲する(リスト16)。

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

class QueryDuplicateFileNames改
{
  static void Main(string[] args)
  {
    // 次の1行は必要に応じて書き換える
    string startFolder =
        @"d:\program files\Microsoft Visual Studio 9.0\";

    int charsToSkip = startFolder.Length;

    IEnumerable<FileInfo> fileList =
        Directory.GetFiles(
            startFolder, "*.*", SearchOption.AllDirectories)
            .Select(x => new FileInfo(x));

    var queryDupNames =
                from file in fileList
                group file.FullName.Substring(charsToSkip)
                               by file.Name into fileGroup
                where fileGroup.Count() > 1
                select fileGroup;

    foreach (var filegroup in queryDupNames)
    {
      foreach (var fileName in filegroup)
      {
        Console.WriteLine(fileName);
      }
      Console.WriteLine();
    }
  }
}
リスト16 重複するファイルを見つける

readme.htm
Microsoft Visual Studio 2008 Professional Edition - JPN\readme.htm

Xml\SnippetsIndex.xml
VC#\Snippets\1041\SnippetsIndex.xml

Xml\1041\Snippets\xsd\SimpleTypes\enum.snippet
Xml\1041\Snippets\xsd\Attributes\enum.snippet
VC#\Snippets\1041\Visual C#\enum.snippet

Xml\1041\Snippets\xsd\SimpleTypes\integer.snippet
Xml\1041\Snippets\xsd\Attributes\integer.snippet
(以下略)
リスト16の実行結果例

 このプログラムにはLINQのポイントとなるエッセンスが詰まっている。今回のまとめとして、そのエッセンスを列挙してみよう。

■列挙できるものはクエリできるもの
  Directory.GetFilesメソッドにせよ、FileInfoクラスにせよ、いずれもLINQなど夢想すらしなかった時代から使われる伝統的な機能である。しかし、LINQによってクエリの対象とすることができる。なぜなら、クエリの本質は列挙であり、列挙できるものはクエリできるからである。Directory.GetFilesメソッドはクエリできるオブジェクトを返し、FileInfoクラスは列挙される対象として使われている。LINQを使うには、それだけで十分である。問題なくfrom句でソースとして指定できる。

■ソートやグループ分けができる
 LINQの本質は列挙であるが、通常は列挙される順番が保存される。しかし、個々の要素はソース上で列挙される順番に常に従うわけではない。ソートやグループ分けの機能により、要素ごとに分かれ、新しい秩序によって並び変わる。この例では、列挙順でははるか遠くに離れた別ディレクトリの同名ファイルがグループとしてひとまとめに集約されている。それ故に、LINQとは単なる検索手段ではない。実際には、データの集まりの構造を変換するデータ加工手段としても使用できる。

■複数のソース
 LINQは複数のソースからの入力を複合して処理することができる。複数のソースは別個の外部にあってもよいし、1つのソースから新しいソースが作り出されてもよい。この例では、全ファイル一覧というソース(fileList)から、同じファイル名のファイル一覧(fileGroup)という新しいソースが作り出されている。そして、その新しいソースに対してさらに処理が続く。

■フィルタリングできる
 where句と条件式を使うことで、対象となる要素を絞り込むことができる。単純な一致検索ではなく、きめ細かく自由な式を記述して絞り込める。ここでは、「fileGroup.Count() > 1」という条件式により、同じファイル名のファイルが1つよりも多く(2つ以上)存在する要素だけを選び取っている。より特殊な条件も容易に記述できる。

■結果の創造
 クエリの結果は、最終的にクエリを実行するプログラム本体に引き渡されねばならない。その際、引き渡す形式、内容も自由に加工して作り出すことができる。この例では、「select fileGroup」としてselect句を使い、fileGroupを返しているが、このfileGroupというデータはクエリ式の内部で創造されたものである。外部から与えられたデータではない。さらにselect句には自由な式を書くこともできるので、クエリの中で創造されたデータでもない、もっと別のデータとして返すこともできる。それはまったくの自由である。

 これらのエッセンスにより、このプログラムは、以下の処理をわずか数行のクエリ式でやってのけている。

  • すべての対象データを列挙させる(from句)
  • データの並び順としては離れた「同じファイル名を持つ別ディレクトリのファイル」をグループにまとめる(group句)
  • 同じファイル名が2つ以上あるグループのみに絞り込み(where句)
  • 結果を外部から列挙可能にする(select句)

次回予告

 ここまででLINQ to Objectの説明は一段落なのだが、実は肝心なことがまだ説明できていない。それはクエリ式ではなく、メソッドとしてクエリを記述するメソッド形式による問い合わせである。

 ここで、クエリ式さえ書ければ十分と思うのは早計である。なぜなら、実際にはメソッド形式でしか使用できない機能がLINQには存在するからである。そのほか、可能なら次回はLINQ to SQLやLINQ to XMLの解説にも踏み込んでいきたい。

 

 INDEX
  C# 3.0入門
  第7回 LINQ応用編
    1.LINQの難しさ/クエリ式のデバッグ・テクニック
    2.join句のグループ化結合/左外部結合/DefaultIfEmptyメソッド/from句の2重使用
  3.let句/クエリのインスタンス化/クエリ結果の個数/Anyメソッドと存在チェック
 
インデックス・ページヘ  「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 記事ランキング

本日 月間