シンプルなソート
foreach文を使った単なる列挙に対するLINQの長所の1つは、簡単にソートができることだろう。orderby句をクエリ式に追加するだけである。
以下は、与えられた数値のマイナス符号を無視し、値の大きい順に並べ替える例である。
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 select x;
foreach (int n in query) Console.WriteLine(n);
// 出力:
// -2
// 2
// -1
// 1
// 0
}
}
|
|
リスト11 絶対値の逆順ソート |
ここで、「orderby Math.Abs(x) descending」という部分がソートの指定に当たる。「Math.Abs(x)」はソートの根拠になる値を計算する式となる。「descending」は逆順(降順)を指定するキーワードで、昇順を指定するキーワードはascendingだが、これは省略できる。
さて、この実行結果を見ると、2よりも先に-2が来ている。Math.Abs(2)とMath.Abs(-2)はどちらも2になるので、どちらが先に来るかはそれだけでは思いどおりに制御できない(ただし、順序が保存された安定した並べ替えを行うので、結果は一定している)。
しかし、orderby句はソート条件を複数指定できるので、カンマ区切りでもう1つの条件を付加すれば、思いどおりにコントロールできる。-2よりも2を先にするには、絶対値ではない値の降順(x descending)を追加すればよい。
var query = from x in array
orderby Math.Abs(x) descending, x descending select x;
|
|
リスト12 複数のソート条件を指定したクエリ |
この場合の実行結果は以下のようになる。
クエリの接続
以下のようなクエリ式があったとしよう。このクエリ式は、1から10までの整数を2乗した値が50を超えるもののみを選び出す。
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
var query = from n in Enumerable.Range(1, 10)
where n * n > 50 select n * n;
foreach (int n in query) Console.WriteLine(n);
// 出力:
// 64
// 81
// 100
}
}
|
|
リスト13 2乗すると50を超える数値のみを抽出 |
しかし、この式には「n * n」という計算が2回出てきて冗長である。この計算を1つにするには、例えば次のように「1から10までの整数を2乗した値」のシーケンスを得るクエリ式と、値が50を超えるもののみを選び出すクエリ式を分離すればよい。
var query1 = from n in Enumerable.Range(1,10) select n * n;
var query2 = from m in query1 where m > 50 select m;
foreach (int n in query2) Console.WriteLine(n);
|
|
リスト14 2つのクエリ式に分離 |
これで「n * n」は1つになった。
だが、これはこれでクエリ式が2つあり、別の意味で冗長である。ここで、into句を使うと、1つのクエリの結果を別のクエリのソースとすることができる。つまり、2つのクエリ式をinto句で接続することで、これを1つのクエリ式にまとめることができる。
var query = from n in Enumerable.Range(1,10)
select n * n into m where m > 50 select m;
|
|
リスト15 2つのクエリをinto句により接続 |
この例で、「from n in ……」がソースからの入力を範囲変数mで受けるのと同様に、「…… into m」は手前のクエリ式の結果を範囲変数mで受けている。それにより、m > 50という条件判断が可能となっている。
クエリ結果のグループ化
通常、クエリ式はselect句で終わり、これによって結果の内容を確定させる。しかし、このほかにgroup句を使い、結果を分類することができる。
以下の例は、名前とCPUのペアとなる情報を、CPUによってグループ化して分類する。つまり、CPUが「6800」のものだけ集め、「Z-80」のものだけ集め……という処理をすべて行うわけである。
using System;
using System.Linq;
class Program
{
class 商品情報
{
public string 名前;
public string Cpu;
}
static void Main(string[] args)
{
商品情報[] 商品情報データ =
{
new 商品情報() { 名前="Altair 680b ", Cpu="6800" },
new 商品情報() { 名前="FP-1100", Cpu="Z-80" },
new 商品情報() { 名前="H68/TR", Cpu="6800" },
new 商品情報() { 名前="LKIT-16", Cpu="MN1610" },
new 商品情報() { 名前="MZ-80K", Cpu="Z-80" },
new 商品情報() { 名前="TRS-80 Color Computer", Cpu="6809" },
};
var query = from n in 商品情報データ group n by n.Cpu;
foreach (IGrouping<string, 商品情報> r in query)
{
Console.WriteLine("CPU={0}", r.Key);
foreach (商品情報 p in r) Console.WriteLine("\t{0}", p.名前);
}
// 出力:
// CPU=6800
// Altair 680b
// H68/TR
// CPU=Z-80
// FP-1100
// MZ-80K
// CPU=MN1610
// LKIT-16
// CPU=6809
// TRS-80 Color Computer
}
}
|
|
リスト16 group句によるグループ化 |
group句は、「group 要素 by キー」という書式で使用される。キーの値ごとにグループ化された要素のコレクションが得られる。
また、group句はselect句と異なり、クエリ結果の型を自由に指定できず、常に「IGrouping<TKey, TElement>」という型に固定される。これは結果が指定型のリストという形で提供されるためである。TKeyはキーの型、TElementは要素の型である。
そして、IGrouping<TKey, TElement>型の値を列挙すれば、1つのグループに含まれる要素を得ることができる。また、Keyプロパティを参照することで、キーの値を得ることができる。「CPU=Z-80」のような出力は、このKeyプロパティの値を参照することで実現している。
複数ソースを関連付けるjoin句
複数のソースの値を関連付けることは、複数のfrom句を使うことでも実現できる。例えば、以下のリスト17のようなクエリ式を書くことで、同じId番号を持つ商品情報オブジェクトと、商品販売価格オブジェクトを関連付けることができ、名前と価格を組み合わせたリストを出力することができる。
using System;
using System.Linq;
class Program
{
class 商品情報
{
public int Id;
public string 名前;
}
class 商品販売価格
{
public int Id;
public int 価格;
}
static void Main(string[] args)
{
商品情報[] 商品情報データ =
{
new 商品情報() { Id = 1, 名前="PC-8001" },
new 商品情報() { Id = 2, 名前="MZ-80K" },
new 商品情報() { Id = 3, 名前="Basic Master Level-3" },
};
商品販売価格[] 商品販売価格データ =
{
new 商品販売価格() { Id = 1, 価格 = 148000 },
new 商品販売価格() { Id = 2, 価格 = 178000 },
new 商品販売価格() { Id = 3, 価格 = 229000 },
};
var query = from x in 商品情報データ
from y in 商品販売価格データ
where x.Id == y.Id
select new { Name = x.名前, Price = y.価格 };
foreach (var 商品 in query) {
Console.WriteLine("{0} {1:C}", 商品.Name, 商品.Price);
}
// 出力:
// PC-8001 \148,000
// MZ-80K \178,000
// Basic Master Level-3 \229,000
}
}
|
|
リスト17 複数のソースを関連付ける |
しかし、このようなコードは効率が良くない。上の掛け算の九九表サンプルを見て分かるとおり、2つのソースについてすべて総当たりで調べることになるためだ。つまり、商品情報が3つ、商品販売価格が3つなら、3×3=9回の判定が実行される。
これを改善するには、関連付けという役割に特化したjoin句を使用するとよい。具体的には、クエリ式を以下のように書き直す。
var query = from x in 商品情報データ
join y in 商品販売価格データ on x.Id equals y.Id
select new { Name = x.名前, Price = y.価格 };
|
|
リスト18 join句への書き換え |
これは、join句の使い方の1つである「内部結合」を使用したものである。この場合、join句は、以下のような書式で記述する。
join [型名、省略可] 識別子 in 式1 on 式2 equals 式3
|
|
join句の構文 |
ここで「join …… in ……」の部分は、「from …… in ……」の構文と似ている。しかし、決定的に違うのは「on」以降の構文が存在する点である。これは、2つのソースを関連付ける条件を指定するために存在する。
ここで注意すべき点は、from句の場合、関連付けるソースを選ぶ条件として、「where x.Id == y.Id」と記述したが、join句では「on x.Id equals y.Id」と記述していることである。より具体的にいえば、「==」という汎用の演算子を使っているか、「equals」というjoin句専用キーワードを使っているかの相違である。つまり、where句で条件を指定する場合は、あらゆる式を自由に記述できたが、join句では「等価」という判定しか行うことができない。
この制約は、逆にいえば、join句を使用するには「等価」と判定できる値を2つのソースが含んでいなければならないことも示す。
from句とjoin句のパフォーマンス
さて、join句は制約が大きいことから、できればfrom句を使いたいと思う読者もいるだろう。果たして、from句とjoin句の差はどれぐらいあるのだろうか。
ここでは、上のリスト17を少し変更し、クエリ式からIdの値が何回参照されているかを調べてみよう。
using System;
using System.Linq;
class Program
{
class 商品情報
{
private int id;
public static int ReadCount = 0;
public int Id
{
get { ReadCount++; return id; }
set { id = value; }
}
public string 名前;
public int 定価;
}
class 商品販売価格
{
private int id;
public static int ReadCount = 0;
public int Id
{
get { ReadCount++; return id; }
set { id = value; }
}
public int 価格;
}
static void Main(string[] args)
{
(商品情報データと商品販売価格データの定義は
リスト17と同じなので省略)
#if false // from句を試す場合はtrueに書き換える
var query = from x in 商品情報データ
from y in 商品販売価格データ
where x.Id == y.Id
select new { Name = x.名前, Price = y.価格 };
#else
var query = from x in 商品情報データ
join y in 商品販売価格データ on x.Id equals y.Id
select new { Name = x.名前, Price = y.価格 };
#endif
foreach (var 商品 in query) {
Console.WriteLine("{0} {1:C}", 商品.Name, 商品.Price);
}
Console.WriteLine(
"商品情報.ReadCount={0},商品販売価格.ReadCount={1}",
商品情報.ReadCount, 商品販売価格.ReadCount);
}
}
|
|
リスト19 Idの参照回数を調べる |
……中略……
商品情報.ReadCount=9,商品販売価格.ReadCount=9
|
|
リスト19の実行結果(from句の場合) |
……中略……
商品情報.ReadCount=3,商品販売価格.ReadCount=3
|
|
リスト19の実行結果(join句の場合) |
この結果を見て分かるとおり、from句を2つ使った場合、クエリ対象の情報は、
ソース1の個数(3)×ソース2の個数(3)×2=18回
の参照だが、join句を使用した場合は、
ソース1の個数(3)+ソース2の個数(3)=6回
しか参照されていない。これは、クエリ時のデータ参照の回数がfrom句では掛け算で増えるのに対して、join句では足し算で増えることを意味する。つまり、データの個数が増えれば増えるほど回数の差は劇的に増えていく。
もちろん、データを参照する回数だけでパフォーマンスが決まるわけではない。では、具体的な時間差はどの程度あるのだろうか。以下はそれを調べるために作成したソース・コードである。
using System;
using System.Linq;
class Program
{
(商品情報と商品販売価格の定義はリスト17と同じなので省略)
static void Main(string[] args)
{
(商品情報データと商品販売価格データの定義は
リスト17と同じなので省略)
var query1 = from x in 商品情報データ
from y in 商品販売価格データ
where x.Id == y.Id
select new { Name = x.名前, Price = y.価格 };
var query2 = from x in 商品情報データ
join y in 商品販売価格データ on x.Id equals y.Id
select new { Name = x.名前, Price = y.価格 };
const int COUNT = 1000000;
DateTime start1 = DateTime.Now;
for (int i = 0; i < COUNT; i++) foreach (var 商品 in query1) { }
Console.WriteLine("from句: {0}", DateTime.Now - start1);
DateTime start2 = DateTime.Now;
for (int i = 0; i < COUNT; i++) foreach (var 商品 in query2) { }
Console.WriteLine("join句: {0}", DateTime.Now - start2);
}
}
|
|
リスト20 from句とjoin句の時間差を調べる |
from句: 00:00:03.5363536
join句: 00:00:01.9231923
|
|
リスト20の実行結果(デバッグ・ビルド時。筆者のPCでの結果) |
このように、処理時間としても、歴然とした差が見て取れる。また、データの個数が増えれば、差はさらに拡大するだろう。両者には歴然とした効率の差があると考えてよいだろう。join句が使用できるときは、join句の利用がお勧めである。
次回予告
join句の主な使い方にはここで紹介した「内部結合」のほかに、「グループ化結合」と「左外部結合」もある。また、クエリ式にはメソッド構文という別の書き方もある。クエリのインスタンス化やlet句なども解説が残っている。
LINQの解説はまだまだ入り口にすぎない。筆者も、LINQの深層に触れてワクワクしている。次回に続く。