目次

概要

Ver. 3.0

LINQ は、元々はシーケンス(IEnumerable 実装クラス)やデータベーステーブルに対するメソッド群としてのみ提供される予定だったそうです。 (要するに、from x in list のようなクエリ式を導入する予定はなくて、 .Select などのメソッドのみを提供するつもりだった。)

でも、メソッド提供だけでは、 join や let などがどうしてもきれいに表現できなかったので、 やむなく SQL 風のクエリ式を導入したそうです。 (プログラミング言語の中に別の言語を埋め込むというのはデメリットも大きくて、 言語制作者にとっては結構ためらわれる行為。) (join や let をきれいに書くためには、 どうしても「透過識別子」のような考え方が必要だった。)

というような背景から、 標準クエリ演算子と呼ばれるメソッド群は、 クエリ式の形で書けるもの以外にも多数 (というか、むしろクエリ式で書けないものの方が多数)あります。

その他の標準クエリ演算子

クエリ式で書けるもの以外にも、 メソッド呼び出しの形でだけ利用できる標準クエリ演算子として、以下のようなものもあります。

パーティション分割演算子」: Take、Skip、TakeWhile、SkipWhile

連結演算子」: Concat

順序付け演算子」: Reverse

セット演算子」: Distinct、Union、Intersect、Except

変換演算子」: AsEnumerable、ToArray、ToList、ToDictionary、ToLookup、OfType、Cast

等価演算子」: SequenceEqual

要素演算子」: First、FirstOrDefault、Last、LastOrDefault、Single、SingleOrDefault、ElementAt、ElementAtOrDefault、DefaultIfEmpty

生成演算子」: Range、Repeat、Empty

限定子」: Any、All、Contains

集計演算子」: Count、LongCount、Sum、Min、Max、Average、Aggregate

これらの説明は次節以降で行っていきます。 その際、例として以下のようなデータを使います。

var a = new[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 };
var b = new[] { 0, 2, 4, 6, 8, 10, 12 };

また、結果の出力用に、以下のような補助関数を使います。

static void Show<T>(IEnumerable<T> a)
{
  foreach (var x in a)
    Console.Write("{0} ", x);
  Console.Write("\n");
}

パーティション分割演算子

シーケンスを部分的に区切るため、 Take、Skip、TakeWhile、SkipWhile の4つのメソッドがあります。

Take 先頭 n 個のみ取り出す
Skip 先頭 n 個を読み飛ばす
TakeWhile 先頭から、条件を満たす間だけ取り出す
SkipWhile 先頭から、条件を満たす間だけ読み飛ばす

使用例を以下に示します。

Show(a.Take(5));
Show(a.Skip(5));
Show(a.TakeWhile(x => x != 2));
Show(a.SkipWhile(x => x != 2));
0 0 1 1 2
2 3 3 4 4
0 0 1 1
2 2 3 3 4 4

連結演算子

Concat で、2つのシーケンスを連結できます。

Show(a.Concat(b));
0 0 1 1 2 2 3 3 4 4 0 2 4 6 8 10 12

ちなみに、Concat や、後述する Union などは拡張メソッドなので、 Concat(a, b) という書き方も可能です。 a.Concat(b) と書いて a と b の間の2項演算とみなすか、 後者の書き方をして英語的に concatenate a and b と読むか、 ちょっと悩みますが、お好きな方をご利用ください。

順序付け演算子

Reverse で、シーケンスの中身の順序を真逆にできます。

Show(a.Reverse());
4 4 3 3 2 2 1 1 0 0

セット演算子

Distinct、Union、Intersect、Except の4つの セット(set: 数学の集合論でいうところの集合。Collection と区別するために横文字にしておきます)演算子があります。

Distinct コレクションから重複を取り除きます。
Union 合併(和集合)を求めます。
Intersect 共通部分(積集合)を求めます。
Except a から b に含まれる要素を取り除きます(差集合)。
Show(a.Distinct());
Show(a.Union(b));
Show(a.Intersect(b));
Show(a.Except(b));
0 1 2 3 4
0 1 2 3 4 6 8 10 12
0 2 4
1 3

注: 数学的な意味での集合は要素の重複を認めません。 セット演算子の結果は重複が除かれたものになります。

変換演算子

型の変換のための演算子がいくつかあります。

シーケンス → シーケンス

まず、AsEnumerable、ToArray、ToList は、 シーケンスをそれぞれ、 IEnumeragle<T>、配列、List<T> に変換します。

var a = new[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 };
IEnumerable<int> a1 = a.Distinct().AsEnumerable();
int[] a2            = a.Distinct().ToArray();
List<int> a3        = a.Distinct().ToList();

AsEnumerable は、この例のような場合だとあまり役に立ちませんが、 IQueryable(LINQ to SQL などで使う)を IEnumerable に変換したりする場合に使います。

AsEnumerable が as なのに、ToArray や ToList が to を使っているのには理由があって、 as の方は遅延評価、to の方はその場での評価になります。 例えば、以下のようなコードを実行したとします。

Func<int, int> hook = x =>
{
  Console.Write("{0}", x);
  return x;
};

Console.Write("AsEnumerable\n");
Console.Write("before ");
var a1 = a.Select(hook).AsEnumerable();
Console.Write(" middle ");
foreach (var x in a1) ;
Console.Write(" after\n\n");

Console.Write("ToList\n");
Console.Write("before ");
var a2 = a.Select(hook).ToList();
Console.Write(" middle ");
foreach (var x in a2) ;
Console.Write(" after\n\n");

上半分と下半分は、AsEnumerable と ToList の部分以外はほぼ同じコードですが、 実行結果は以下のように変わります。 前者は foreach の行で初めて hook が実行され、 後者は ToList の時点で実行されます。

AsEnumerable
before  middle 0011223344 after

ToList
before 0011223344 middle  after
シーケンス → 辞書

ToDictionary と ToLookup は、シーケンスを辞書(キーと値のペア)化します。 ToDictionary は Dictionary(1つのキーに対して1つの値を持つ)を、 ToLookup は Lookup型(1つのキーに対して複数の値(1つの IEnumerable)を持つ辞書)の値を返します。

var list = new[] {
  new { Name = "糸色望", CV = "神谷浩史" },
  new { Name = "風浦可符香", CV = "野中藍" },
  new { Name = "大草麻菜実", CV = "井上喜久子" },
  new { Name = "音無芽留", CV = "????" },
  new { Name = "加賀愛", CV = "後藤沙緒里" },
  new { Name = "木津千里", CV = "井上麻里奈" },
  new { Name = "木村カエレ", CV = "小林ゆう" },
  new { Name = "小節あびる", CV = "後藤邑子" },
  new { Name = "小森霧", CV = "谷井あすか" },
  new { Name = "関内・マリア・太郎", CV = "沢城みゆき" },
  new { Name = "常月まとい", CV = "真田アサミ" },
  new { Name = "日塔奈美", CV = "新谷良子" },
  new { Name = "藤吉晴美", CV = "松来未祐" },
  new { Name = "三珠真夜", CV = "谷井あすか" },
  new { Name = "久藤准", CV = "水島大宙" },
  new { Name = "新井智恵", CV = "矢島晶子" },
  new { Name = "臼井影郎", CV = "上田陽司" },
  new { Name = "隣の女子大生", CV = "野中藍" },
  new { Name = "万世橋わたる", CV = "上田陽司" },
  new { Name = "甚六先生", CV = "上田陽司" },
  new { Name = "糸色景", CV = "子安武人" },
  new { Name = "糸色命", CV = "神谷浩史" },
  new { Name = "糸色倫", CV = "矢島晶子" },
  new { Name = "糸色交", CV = "矢島晶子" },
};

var dicByName = list.ToDictionary(x => x.Name);
Console.Write("{0}\n", dicByName["日塔奈美"].CV);
Console.Write("{0}\n", dicByName["小節あびる"].CV);

var lookupByCV = list.ToLookup(x => x.CV);
Show(lookupByCV["矢島晶子"].Select(x => x.Name));
Show(lookupByCV["神谷浩史"].Select(x => x.Name));
新谷良子
後藤邑子
新井智恵 糸色倫 糸色交
糸色望 糸色命

ToLookup を使えば、例えば、名前の1文字目を使ったインデックスを作ったりといったことも出来ます。

var lookupByFirstChar = list.Select(x => x.Name).ToLookup(x => x[0]);
Show(lookupByFirstChar['糸']);
Show(lookupByFirstChar['小']);
糸色望 糸色景 糸色命 糸色倫 糸色交
小節あびる 小森霧
要素の型変換

OfType、Cast で要素の型を変換できます。 Cast はすべての要素のキャストを試みます。 キャストに失敗した場合は例外が発生します。 一方、OfType は、変換可能な要素だけを抽出します。

var numList = new object[] {
  1, 1.1, 2, 2.2, 3, 3.3
};

var miscList = new object[] {
  0, "test 1", 1, 3.14, "test 2", 2.72,
  new List<int>(),
  new Stack<int>(),
  new Queue<int>()
};

Show(numList.Cast<int>());
// Show(miscList.Cast<int>()); // 例外発生

Show(numList.OfType<int>());
Show(miscList.OfType<IEnumerable<int>>().Select(x => x.GetType().Name));
1 1 2 2 3 3
1 2 3
List`1 Stack`1 Queue`1

.OfType<T>().Where(x => x is T).Cast<T>() と同じ結果になります。

等価演算子

SequenceEqual で、2つのシーケンスの中身が(順序も含めて)一致するかどうかを調べられます。

var x = new[] { 0, 3, 1, 2 };
var y = new[] { 0, 3, 1, 2 };
var z = new[] { 1, 2, 3 };

Console.Write("{0}\n", x.SequenceEqual(y));
Console.Write("{0}\n", y.SequenceEqual(z));
Console.Write("{0}\n", z.SequenceEqual(x));
True
False
False

要素演算子

シーケンスの中から特定の要素を1つ取り出すため、 First、FirstOrDefault、Last、LastOrDefault、Single、SingleOrDefault、ElementAt、ElementAtOrDefault、DefaultIfEmpty という演算子が用意されています。

First、FirstOrDefault 条件を満たす最初の要素を返します。
Last、LastOrDefault 条件を満たす最後の要素を返します。
Single、SingleOrDefault 条件を満たす唯一の要素を返します。もし、条件を満たす要素が複数あった場合、例外を発生させます。
ElementAt、ElementAtOrDefault n 番目の要素を返します。
DefaultIfEmpty もしシーケンスが空の場合、デフォルトの値が1つだけ入ったシーケンスを返します。

OrDefault が付かないもの、 もし条件を満たす要素が1つもなければ例外を発生させます。 一方、OrDefault が付くものは、 もし条件を満たす要素が1つもなければ規定値 (例えば、数値型なら 0、参照型なら null)を返します。

var list = new[] {
  new { X = 0, Y = 0 },
  new { X = 0, Y = 1 },
  new { X = 0, Y = 2 },
  new { X = 1, Y = 0 },
  new { X = 1, Y = 1 },
  new { X = 1, Y = 2 },
  new { X = 2, Y = 0 },
};

Console.Write("{0}\n", list.First(p => p.X == 0));
// Console.Write("{0}\n", list.First(p => p.X == 3)); // 例外発生
Console.Write("{0}\n", list.Last(p => p.X == 1));
Console.Write("{0}\n", list.Single(p => p.X == 2));
// Console.Write("{0}\n", list.Single(p => p.X == 0)); // 例外発生
{ X = 0, Y = 0 }
{ X = 1, Y = 2 }
{ X = 2, Y = 0 }

First、Last、Single には引数を持たないバージョンもあって、 その場合、First、Last はシーケンス全体の中の最初・最後の要素を返します。 引数なしの Single は、シーケンスがただ1つの要素からなるときにはその要素の値を返し、 そうでなければ例外を発生させます。

var x = new[] { 0 }.Single();    // x == 0
var y = new[] { 0, 1 }.Single(); // 例外発生

生成演算子

シーケンスに対するフィルタリングではなく、 シーケンスそのものを生成するような演算子が3つあります。

Range ある範囲の整数列を生成します。
Repeat 同じ値を指定回数繰り返すシーケンスを生成します。
Empty 空のシーケンスを生成します。
Show(Enumerable.Range(5, 3));
Show(Enumerable.Repeat("abc", 3));
Show(Enumerable.Empty<int>());
5 6 7
abc abc abc

例えば、Range を使って任意個数の乱数列を生成したりできます。

Random rnd = new Random();
var randomSeq = Enumerable.Range(0, 100).Select(x => rnd.NextDouble());

限定子

Any、All、Contains は、 シーケンスがある条件を満たすかどうかを調べるための演算子(限定子(quantifier))です。

Any 条件を満たす要素がシーケンス中に1つでもあれば true を返す。
All シーケンス中の全ての要素が条件を満たせば true を返す。
Contains シーケンス中に要素が含まれるかどうかを調べる。
Func<int, bool> isEven = x => (x & 1) == 0;

Console.Write("{0}\n", a.Any(isEven)); // a は偶数も含むので true
Console.Write("{0}\n", b.Any(isEven)); // b は偶数を含むので true

Console.Write("{0}\n", a.All(isEven)); // a は奇数を含むので false
Console.Write("{0}\n", b.All(isEven)); // b は全て偶数なので true

Console.Write("{0}\n", a.Contains(0)); // a は 0 を含むので true

集計演算子

シーケンス中の要素の個数、和、平均値などを集計するための演算子が7つあります。

Count 要素の個数を返します。
LongCount 要素の個数を long 型で返します。
Sum 要素の和を求めます。
Min 要素の最小値を求めます。
Max 要素の最大値を求めます。
Average 要素の平均値を求めます。
Aggregate より一般的な集計処理を行います。

list.Aggregate(func); は、以下のコードと同じ結果を得ます。

static T Aggregate<T>(IEnumerable<T> list, Func<T, T, T> func)
{
  var acc = list.First();
  foreach (var x in list.Skip(1))
  {
    acc = func(acc, x);
  }
  return acc;
}

したがって、 list.Aggregate((s, x) => s + x);list.Sum(); と同じ意味になります。

他の集計演算子もほぼ同様の動作をしています。 なので、 例えば、以下のようなコードを書くと、foreach ループを5回まわすことになります

var num = a.Count();
var min = a.Min();
var max = a.Max();
var ave = a.Average();
var sum = a.Sum();

そのため、 以下のようなコードと比べると、圧倒的に動作速度が遅くなります。 (筆者の環境では約10倍の差。)

var num = 0;
var min = int.MaxValue;
var max = int.MinValue;
var sum = 0;

foreach (var x in a)
{
  ++num;
  if (min > x) min = x;
  if (max < x) max = x;
  sum += x;
}
double ave = sum / (double)num;

更新履歴

ブログ