連載:C# 3.0入門

第6回 LINQ基礎編

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

最も基本的なLINQ

 最も基本的なLINQの構文を見てみよう。以下は、整数(int)の配列の内容を「一切条件を設けず、加工もせずに」にクエリする例である。つまり、元の配列の内容がそのまま得られる(この式に条件や加工処理を肉付けしていくことで、実用クエリに仕上げていくことができる)。

using System;
using System.Linq;

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

    var query = from x in array select x;

    foreach (int n in query) Console.WriteLine(n);
    // 出力:
    // 1
    // 2
    // 3
  }
}
リスト2 最も基本的なLINQ

  の行を見ていただきたい。LINQはfrom句で始まり、select句またはgroup句で終わる。「from A in B」は、Bという対象(データソース)を列挙して、個々の値をAという名前の変数(範囲変数)で受けることを意味する。これは、「foreach ( var A in B )」と似ている。Bという対象が持つ個々の値を、Aという変数で受けているわけである。

 ちなみに、範囲変数は本来、型を付けて「from int x」のように書くが、型名は省略できるので、「int」は書かれていない(もちろん、型は自動的に確定するのであって、あいまいな型で扱われるわけではない)。

 そして、select句はクエリ結果に含める値を指定する。ここでは、「x」を記述しているので、from句で取り出された値がそのまま格納される。

 以上により、クエリ・オブジェクト(リスト2では「query」)をforeach文で列挙すると、元の配列と同じ値が同じ順番に出てくる。

LINQの本質は列挙

 さて、この例を見て何の意味もないプログラムだと思った読者もいるだろう。だがそうではない。このプログラムは以下のプログラムと比較することで、LINQの重要な性質を明らかにできるのである。ではその話題に進もう。

 先ほど「LINQを使って記述できる処理は、LINQを使用しなくても書くことができる。foreach文で繰り返しを行えばよい」と書いた。そこで、上の例をLINQではなくforeach文で書き直してみよう。

using System;
using System.Collections.Generic;

class Program
{
  static void Main(string[] args)
  {
    int[] array = { 1, 2, 3 };
    var list = new List<int>();

    foreach (int n in array) list.Add(n); // クエリ式を置き換えた行
    foreach (int n in list) Console.WriteLine(n);
    // 出力:
    // 1
    // 2
    // 3
  }
}
リスト3 リスト2のクエリ式をforeach文に置き換えた

 この2つのプログラムの動作は、使用されるコレクションの種類などのさまつな問題を除けば、同じといってよいのだろうか? つまり、

var query = from x in array select x;

を、

var list = new List<int>();
foreach (int n in array) list.Add(n);

に置き換えて、常に同じ結果が得られると期待してよいのだろうか?

 実はそうではない。たった1行追加するだけで、その差を見ることができる。

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

  var query = from x in array select x;

  array[1] = 4;  // 追加行

  foreach (int n in query) Console.WriteLine(n);
  // 出力:
  // 1
  // 4
  // 3
}
リスト4 クエリ式版(リスト2)に1行追加(抜粋)

static void Main(string[] args)
{
  int[] array = { 1, 2, 3 };
  var list = new List<int>();
  foreach (int n in array) list.Add(n);

  array[1] = 4;  // 追加行

  foreach (int n in list) Console.WriteLine(n);
  // 出力:
  // 1
  // 2
  // 3
}
リスト5 foreach版(リスト3)に1行追加(抜粋)

 見てのとおり、1行書き加えただけで、結果は違ってしまった。値を書き換えた結果は、クエリ式版にのみ反映されている。

 なぜかといえば、foreach版がコレクションの複製を作成しているのに対して、クエリ式版は列挙を行うオブジェクトを作成しているという相違があるためだ。つまり、クエリ式版は列挙が行われるまで結果が何になるか確定しない。クエリ対象が変化すれば、列挙結果も変化する。

 だから、foreach版の動作をクエリ式版に近づけるには、コレクションを複写するのではなく、列挙インターフェイスを取得して、それを変数に保存するとよい(リスト6)。実行されている機能性はまったく異なっているが、少なくとも結果は同じになる。

using System;
using System.Collections.Generic;

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

    // 列挙インターフェイスの取得
    IEnumerable<int> enumerator = array;

    array[1] = 4;  // 追加行

    foreach (int n in enumerator) Console.WriteLine(n);
    // 出力:
    // 1
    // 4
    // 3
  }
}
リスト6 リスト2は本当はこれに近い

LINQの基本的な挙動

 LINQの基本は列挙である、ということは、以下の点に注意を払う必要があることを意味する。

  • クエリ対象のコレクションの中身が変化しても、クエリ・オブジェクトを作り直す必要はない。
  • 逆に、ある瞬間のクエリ結果を保存しておきたければ、クエリ・オブジェクトを即座に列挙しておく必要がある(次回解説予定のインスタンス化も参照)。
  • どれほど膨大なデータがヒットするクエリであろうと、クエリ・オブジェクトを作成するだけならほとんど時間はかからないし、メモリも消費しない。
  • どれほど膨大なデータがヒットするクエリであろうと、単にそれらを列挙するだけならほとんどメモリは消費しない(列挙されたアイテムは、保存しなければそのまま破棄されていく)。

クエリ結果を加工する

 select句には自由に式を書き、結果を加工して構わない。また、加工結果は同じ型である必要もない。以下は、リスト2の例に説明文を補って、クエリ結果をint型ではなくstring型とした例である。

using System;
using System.Linq;

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

    var query = from x in array select
                  string.Format("配列の内容は{0}です。", x);

    foreach (string s in query) Console.WriteLine(s);
    // 出力:
    // 配列の内容は1です。
    // 配列の内容は2です。
    // 配列の内容は3です。
  }
}
リスト7 select句における結果の加工

複数のソースからクエリする

 from句は1つのクエリ式に複数あってよい。複数のfrom句は、それぞれがクエリのソースとして扱われる。例えば、整数の1から9までを提供するソース2つをfrom句で取り込めば、掛け算の九九の一覧を生成するクエリ式を記述できる。9個のソースが2つなので、クエリ結果は9×9=81個となる。

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
  }
}
リスト8 掛け算の九九一覧

 ちなみに、ここで使用したEnumerable.RangeメソッドはSystem.Linq名前空間にあり、.NET Framework 3.5で追加されたメソッドである。連続した整数を列挙するオブジェクトを生成して返してくれる(実際に返却される型はIEnumerable<int>型)。

 Rangeメソッドでは、第1引数で開始値を、第2引数で個数を指定する。forループでカウントすれば簡単に得られるのになぜ……と思う読者も多いだろうが、値のシーケンスを生成するオブジェクトは、リスト8のソースを見て分かるとおり、クエリ式と相性がとても良い。forループで同じことは書けないのである。シーケンスを生成する範囲オブジェクトを使うメリットはほかにもあるが、本題ではないので今回は割愛する。

条件で絞り込む

 クエリに条件を付けるには、from句とselect句の間に、where句を挟む。

 例えば、奇数(x % 2が1になるようなx)のもののみを選び出すなら、以下のようにクエリ式に「where x % 2 == 1」を挿入する。

using System;
using System.Linq;

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

    var query = from x in array where x % 2 == 1 select x;

    foreach (int n in query) Console.WriteLine(n);
    // 出力:
    // 1
    // 3
  }
}
リスト9 where句による条件を追加

一部項目のみselectする

 実際にLINQを使用する場合、クエリの対象が単なる整数というケースは少ない。それよりも、さまざまな情報を格納したオブジェクトなどを対象にクエリすることになるだろう。

 そこで問題になるのは、多数の情報を含んだオブジェクトを列挙すると、それが重い処理になる可能性があることである。例えば、参照型オブジェクトを単に右から左に渡すだけならよいが、値型であったり、ファイルやデータベース内の値を取り出したりするとなると、情報量の増加は即、パフォーマンスの悪化に直結する。

 このような問題に対処するために、select句で匿名型オブジェクトを生成するということが行われる。

 具体的な例を紹介しよう。以下の例では、「名前」「性別」「所属」という3つの情報を持つオブジェクト群から、性別が男であるアイテムのみを抽出する。結果を表示する際、「性別」の情報を扱う必要はないので(男だけを選んだのでどれも男であるはず)、クエリ結果は「性別」抜きで「名前」「所属」だけを受け取りたい。このような意図を記述した例である。

using System;
using System.Linq;

class Program
{
  class Person
  {
    public string 名前;
    public string 性別;
    public string 所属;
  }

  static void Main(string[] args)
  {
    Person[] persons =
    {
      new Person() { 名前="古代進", 性別="男", 所属="柔道部" },
      new Person() { 名前="島大介", 性別="男", 所属="ボート部" },
      new Person() { 名前="森雪",   性別="女", 所属="パソコン部" },
    };

    var query = from x in persons where x.性別 == "男"
                              select new { x.名前, x.所属 };

    foreach (var s in query) {
      Console.WriteLine("{0} {1}", s.名前, s.所属);
    }
    // 出力:
    // 古代進 柔道部
    // 島大介 ボート部
  }
}
リスト10 匿名型オブジェクトをselectする

 ここでポイントになるのは、select句に記述された「new { x.名前, x.所属 };」という匿名型オブジェクトの生成部分である。これは以下の省略形表記である。

new { 名前 = x.名前, 所属 = x.所属 };

 フィールドの名前を指定せず、使用した初期値の名前をそのままフィールドの名前に転用する仕様は、それ単体では意味が見いだしにくいかもしれない。しかし、select句でクエリ結果の対象のサブセットを作ると思えば、同じ名前が転用されるのは使いやすい仕様である。つまり、含める項目を減らしたいだけなので、含める項目の名前は同じでよいのである。

 さて、ここではクエリ結果を列挙するために「foreach (var s in query)」と「型を明示しない変数宣言」を行う「var」を使用しているが、これは必須の措置である。匿名型にはソース・コード上で明示的に参照できる型の名前が存在しないため、それを明示できないのである。

 さらにこのケースでは、クエリ式本体をvarで宣言した変数で受けねばならない。例えばリスト2のクエリ式は、varで宣言した変数ではなく、以下のような型を明示した変数で受けてもよい。

IEnumerable<int> query = from x in array select x;

 しかし、このリスト10の例では同じやり方では受けられない。selectで返される型が匿名なので、「IEnumerable<……>」という書式に書き込むべき型の名前が存在しないからである(もちろん、内部的には型の名前が自動生成されており、リフレクションを使えば取得できるが)。

 つまり、LINQの存在を前提にすると、匿名型やvarキーワードは必須の機能といえるのである。決して、ちょっと便利だから追加した“なくてもよい”機能ではない。C#は何でも機能を増やす無節操な言語ではない。


 INDEX
  C# 3.0入門
  第6回 LINQ基礎編
    1.LINQの面白さ/LINQとは何か?/「値の集まり」に対する演算/なぜLINQなのか
  2.最も基本的なLINQ/本質は列挙/結果の加工/複数ソース/絞り込み
    3.ソート/クエリの接続/グループ化/join句/from句とjoin句のパフォーマンス
 
インデックス・ページヘ  「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 記事ランキング

本日 月間