連載:[完全版]究極のC#プログラミング

Chapter15 LINQとクエリ式

川俣 晶
2010/03/17
Page 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

15.25 まとめ―本章の最初のサンプルを通して

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

 このプログラムには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句)

【C#olumn】LINQの難しさ

 個人的には、C#プログラミングでのLINQ使用量は増える一方である。たとえば、いま書いているプログラムの中で、クエリ式を含む最長の行を探してみたところ、次に挙げる行が見つかった(オブジェクトの初期化の一部)。

Is可視 = (m) => General.IsMission達成( Missions.初めての企画成功 ) && (from n in Items.GetItemListIncludeItemNull() where State.GetItemCount(n) > 0 && t.IsTargetItem(n) select n).Any(), ……

 余談だが、この行は冗長である。実際は、次のように書けば十分である。

Is可視 = (m) => General.IsMission達成( Missions.初めての企画成功 ) && Items.GetItemListIncludeItemNull().Any( State.GetItemCount(n) > 0 && t.IsTargetItem(n) );

 それにもかかわらずLINQのクエリ式を使ったのは、引数のないAnyメソッドの使い勝手がなかなか良いことに気づいたので、使ってみたかったからである。

 ところで、クエリ式は最低でも「from」、「in」、「select」などの3つ程度のキーワードを含む2つの句を必要とし、実用上はこれにwhere句を足した“4キーワード3句”程度が最低の長さとなる。つまり、式としては長くなりがちである。さらに、カッコでくくって、「.Any()」や「.Count()」などを付けたり、クエリ式を丸ごとforeach文に入れたりしてしまうことも珍しくなくなる。こうなると、行の長さは増す一方だ。

 だが、そこで発生する最大の問題は、実は行の長さにはない。いくら行が長くなっても、適当なところで改行を入れることができるからだ。問題は、そこにはない。長いクエリ式ではデバッグの際に不便を生じることが多い、と感じるのである。いや、短いクエリ式でも問題が出るときには出てしまう。一例を以下に紹介しよう。

 以下のプログラムは、1から4までの整数値に対して、偶数か奇数かを判定するが、「odd」と書くべきところに誤って「null」と書いているため、例外で落ちてしまう。これを、従来型のコーディング(リスト15.33)とクエリ式を使ったコーディング(リスト15.34)で比較してみる。

using System;

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

    foreach (var n in t)
    {
      if (n >= 2)
      {
        string s = n % 2 == 0 ? "even" : null;
        Console.WriteLine(s.ToUpper());
      }
    }
  }
}
リスト15.33 従来型のコーディング例

using System;
using System.Linq;

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

    foreach (var m in
                  from n in t
                  where n >= 2
                  select n % 2 == 0 ? "even" : null)
    {
      Console.WriteLine(m.ToUpper());
    }
  }
}
リスト15.34 クエリ式を使ったコーディング例

 これをVisual Studioでデバッグ実行しても、当然、例外でプログラムの実行は止まる。このとき知りたいのは、配列tの値のうち、どの値を処理しているときに例外が起きたかだ。

 従来型のコーディング例(リスト15.33)では、変数nにそれが入っている。変数nの値を調べれば、それが「3」だとわかる。その結果、「3 % 2」は0ではないので、このときnullが選択されていることが判明し、バグの原因に到達できる。

 ところが、クエリ式を使ったコーディング例(リスト15.34)では同じことができない。例外で止まったタイミングで知ることができるのは、配列tと変数mの値だけで、配列tの要素の値を格納した変数nの値は知ることができない。これはあくまでクエリ式内でのみ有効な変数であり、クエリ式外で停止した状態では、スコープ外なのである。この例では、変数mの値がnullなので、そこから容易にバグの原因を推定できてしまうが、実際のコードはもっと複雑でわかりにくい。

 なぜこのような問題が起きるのだろうか?

 それは、クエリ式がそれ単体で1つの世界を構成しているからだ。クエリ式の外部から見れば、クエリ式は1つのブラックボックスとなる。これに対して、foreach文を用いて自前で処理する場合は、foreach文は外部世界の一部となり、ホワイトボックスとして機能するので、デバッグ時に内外の状況を調べやすい。

 では、クエリ式とはデバッグしにくい難物であり、使わないほうがよいのだろうか? そうではない。

 なぜなら、実際に書かれたコードの機能が違うからだ。クエリ式を使ったコーディング例とほぼ等価のコードをクエリ式抜きで記述すると、次のリスト15.35のようになる。

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

class Program
{
  class SampleIterator
  {
    public int[] T;

    public IEnumerator<string> GetEnumerator()
    {
      foreach (var n in T)
      {
        if (n >= 2) yield return n % 2 == 0 ? "even" : null;
      }
    }
  }

  static void Main(string[] args)
  {
    int[] t = { 1, 2, 3, 4 };

    foreach (var m in new SampleIterator() { T = t })
    {
      Console.WriteLine(m.ToUpper());
    }
  }
}
リスト15.35 リスト15.34をクエリ式抜きで記述

 このコードを見れば、Console.WriteLineで停止したとき、変数nの値が確認できないことは当然だとわかるだろう。そして、Console.WriteLineの行から変数nがアクセスできないのは、きわめて合理的な仕様である。Mainメソッドを書いているプログラマーは、実際に列挙を行うコードの詳細など知りたくはないのだ。それはすべてうまく稼働するという前提で、Main関数の記述に集中したいのだ。見えないことで困るのは、うまく稼働しなかった場合に、値のチェックが不便になることだけである。

 つまり、クエリ式を使うとデバッグが難しくなるというのは誤解であり、実際には従来となんら変わりはないのである。ただ、クエリ式を使うと局所的な1つの式に多くの機能を詰め込めるために、「直感的に見えそうだと思ってしまう変数」が出現してしまうだけの話である。

【Exercise】練習問題

 foreach文でコレクションを列挙しながら特定の値を探す場合、値を発見した時点でbreak文を実行してそれ以後のチェックを取りやめ、実行効率を上げることができた。では、LINQの場合、同じように“意図した値を発見した時点で探索を打ち切る”コードはどのようにして記述するだろうか? 正しい説明を選べ。

  1. クエリ式のwhere節に打ち切り条件を追加する
  2. クエリ式の中に書き込むラムダ式にbreak文を含める
  3. ラムダ式の中ではreturn文がbreak文相当になるので、return文を書き込む
  4. クエリを列挙させるが、最初の1つの値だけ取り出し、残りは列挙しない
  5. クエリ式はつねにすべての対象を処理してしまうので、途中で打ち切ることはできない

 ◎解答:「4」(この行をマウスで選択してください)End of Article


 INDEX
  [完全版]究極のC#プログラミング
  Chapter15 LINQとクエリ式
    1.15.1 LINQの面白さ
    2.15.2 LINQとは何か?
    3.15.3 「値の集まり」に対する演算
    4.15.4 なぜLINQなのか?
    5.15.5 最も基本的なLINQ
    6.15.6 LINQの本質は列挙
    7.15.7 LINQを使ううえでの注意点
    8.15.8 クエリ結果を加工する
    9.15.9 複数のソースからクエリする
    10.15.10 条件で絞り込む
    11.15.11 一部項目のみselectする
    12.15.12 シンプルなソート
    13.15.13 クエリの接続
    14.15.14 クエリ結果のグループ化
    15.15.15 複数ソースを関連付けるjoin句
    16.15.16 from句とjoin句のパフォーマンス
    17.15.17 join句のグループ化結合
    18.15.18 join句の左外部結合
    19.15.19 単独で使うDefaultIfEmptyメソッド
    20.15.20 内部列挙を伴うfrom句の二重使用
    21.15.21 let句
    22.15.22 クエリのインスタンス化
    23.15.23 クエリ結果の個数を得る
    24.15.24 Anyメソッドと存在チェック/【C#olumn】クエリ式のデバッグテクニック
  25.15.25 まとめ/【C#olumn】LINQの難しさ/【Exercise】練習問題
 
インデックス・ページヘ  「[完全版]究極のC#プログラミング」


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

本日 月間