C# やるなら LINQ を使おう

C# では Ver. 3.0 から LINQ という機能が追加されました。
LINQ の処理は C, C++, Java などから移ってきた人には馴染みにくいらしいです。 実際、 LINQ がなくてもアプリは作れないこともないですし、 C# を使っているけど、 LINQ は使っていないという人もいるのではないでしょうか。
しかし、それは非常にもったいないです。
私も C, C++ を使ってきた人間ですが、同時に Ruby や Lisp 好きでもあるので、 LINQ は素晴らしい機能だと思います。
今回はそんな LINQ の魅力の紹介と LINQ を使っていくための入門的な記事を書いてみました。

LINQ の魅力

標準クエリー演算子とクエリー式

LINQ には SQL のようなクエリー式と 通常のメソッド形式の標準クエリー演算子の 2 つの書き方ができます。
その特異さのため、 LINQ というとクエリー式に目が行きがちです。 しかし、 私の思う LINQ の魅力はそんなところにはありません。
SQL に馴染みがある人はクエリー式で書くのもいいとは思いますが、 私はほとんどクエリー式は使ったことがなく、この説明でも標準クエリー演算子の方で説明していきます。

OOP より関数型のコードが短くなるという誤解

まれに "オブジェクト指向プログラミング(OOP)よりも関数型プログラミングの方がコードが短くなる" といわれることがあります。
しかし、これは誤解です。

Scala, Clojure など最近よく使われる関数型言語は C++ や Java に比べたら、 短いコードで書けることは間違いありません。
ただ、その要因の大部分は、最近の関数型言語が 配列などのコレクションを高階関数を使って処理できる機能を持っているところにあります。 この機能は関数型言語にとって大事な機能ですが、 参照透過性などのパラダイムのポイントとは直接は関係ありません。
これは言い換えると、パラダイムに関係なく言語がこの機能を持っていれば、コードを短くすることができるということです。また、短いだけでなく、書きやすくかつわかりやすいコードにもなります。
そのため、 Ruby を始めとする多くの言語で使われるようになってきています。 この高階関数を使った処理機能をC# で実現しているのが LINQ です。

C# が出た当初は、言語仕様としては Java に毛が生えた程度のしょぼいものだったのですが、 この LINQ の登場により、 Java を大きく突き放したと思ったものでした。
ただ Java も Java 8 からは似たような機能が追加され、追いついてきています。 C++ でも C++14 からは同様なことができるようになるらしいです。 それだけこの機能が多くの人に便利だと思われてる証とも言えるでしょう。

高階関数を使ったデータ処理

私が思う LINQ の魅力は Ruby や関数型プログラミングでよく使われる 高階関数を使ったデータ処理です。

前節で書いたように LINQ を使えばいろいろなデータ処理を楽に書けますし、 LINQ ではそのための多くのメソッド(標準クエリー演算子)が用意されています。

しかし、逆にメソッドが多すぎてどれから覚えればいいのか分からないこともあるかもしれません。 今回は LINQ のメソッドの中で最初に覚えておいた方がいいなというメソッドを紹介します。

LINQ の対象

これまで処理の対象をデータと表記していますが、 これは LINQ が配列やリストのようなコンテナーだけでなく、 様々なものに対しても LINQ の操作メソッドを使うことができるためです。
  1. あらかじめ用意されたコンテナーだけでなく、自作のコンテナークラスにも使える
  2. XML 、 データベースなどあらかじめいろいろと用意されている
  3. 用意されているだけでなく、 yield で対象を簡単に自作できる
IEnumerable<T> インターフェースを継承(実装) するだけで自作したコンテナークラスでも多くの操作メソッドが使えるようになります。
これは Ruby の Mix-in の魅力と同じで、キーとなるメソッドを実装するだけで、 自作のコンテナーがあらかじめ用意されたコンテナーばりに高機能になることを意味します。 ただ、 IEnumerable<T> の実装は若干クセがあります。 それについては以前の記事で書いているので、そちらを見てください。 今回は自作コンテナー以外の対象について説明していきたいと思います。

LINQ の主要メソッド

それでは、ここから LINQ のメソッドでまず最初に覚えておいた方がよいと思われる機能について説明していきます。
高階関数を用いたデータ処理における主要な機能は次の 5 つです。
  1. 逐次処理(each)
  2. 写像(map)
  3. フィルター(filter)
  4. 並び替え(sort)
  5. 畳み込み(fold)
このうち、一番基本的な処理は逐次処理ですが、 C# では foreach です。 これは基本的すぎて LINQ 以前に言語機能として用意されているので、 ここでは他の 4 つの処理を紹介します。

ただし、 LINQ では SQL に寄せているため、 他の言語で使われている名前と違う名前になっています。

Select : 写像(map)

写像というのは map, mapping と呼ばれる処理で、 配列などのコレクションのメンバーに一つずつ関数を適用して 戻り値で新しいコレクションを作ります。
cs_linq_map.png

他言語では map という名前になっていることが多いですが、 LINQ では Select() です。
var src = new[] {3, 2, 9, 6};
var mapped = src.Select(elem => elem * 2); // {6, 4, 18, 12}
Select に渡す関数は要素の型を引数にとる関数で、その戻り値が新しいコレクションの要素となります。 Select は渡す関数の戻り値の型を変えれば、 値を変えるだけでなく、型を変えた新しいコレクションも作ることができ、 用途の広いメソッドです。

渡す関数は、通常の関数でもいいですし、匿名メソッドラムダ式のような無名関数でも構いません。
static int foo(int elem)
{
    return elem * 2;
}
    // :
    src.Select(foo);
src.Select(delegate(int elem) {return elem * 2;});
通常の関数などの場合には、渡す関数の型にあったものしか渡せませんが、そういった場合にはカリー化と呼ばれる手法を使うこともできます。 以降のサンプルでは、一番短く書けるラムダ式を使っていくことにします。

Where : フィルター(filter)

フィルターは他言語では filter, find_all, select などメソッド名が使われます。 これはコレクションの中から条件にあう要素を取り出す処理です。
cs_linq_filter.png

LINQ では Where() という名前になっています。
var src = new[] {3, 2, 9, 6};
var filtered = src.Where(elem => elem % 2 == 1); // {3, 9}
データ処理で重要なメソッドをさらに絞るとすると、前節のマップとこのフィルターが、 特に重要度が高いです。

OrderBy, ThenBy : 並び替え(sort)

並び替えは通常 sort という名前のメソッドで、 コレクションの要素の並び替えを行います。

他言語の sort では比較用のメソッドを渡すものなのですが、 LINQ の OrderBy() では比較に使う値を指定する関数を渡します。 OrderBy で指定した値で同じ値になる場合には ThenBy() で順列をつけることができます。
var possrc = new [] {
    new { x = 1, y = 2 },
    new { x = 3, y = 4 },
    new { x = 1, y = 1 }
};
var sorted = possrc.OrderBy(elem => elem.x).ThenBy(elem => elem.y);
// {{ x = 1, y = 1 }, { x = 1, y = 2 }, { x = 3, y = 4 }}
OrderBy, ThenBy は昇順で並び替えを行います。 降順にしたい場合はそれぞれ OrderByDescending, ThenByDescending を使います。

Aggregate : 畳み込み(fold)

畳み込みは他言語では fold, reduce, inject などの名前ですが、 LINQ では Aggregate() (集める)という名前です。
これは他の処理に比べると、利用頻度は低い上に難易度も少し高いです。 ただ、わりと応用も効きますし、使いこなせているとちょっと通っぽいです。

この処理は先に使用例を見てもらった方がわかりやすいでしょう。
var src = new[] {3, 2, 9, 6};
src.Aggregate((sum, elem) => sum + elem);                // 20
src.Aggregate((max, elem) => (max < elem) ? elem : max); // 9
src.Aggregate(0, (count, elem) => count+1);              // 4
渡された関数を各メンバーに適用していき、その結果を重ねたものが Aggregate の戻り値として得られます。
渡す関数は 2 つの引数を取り、 1 つ目がそれまでの計算の結果で、2 つ目が各要素です。 関数の戻り値として返したものが、次の 1 つ目の引数に入るという繰り返しになります。 合計の例を順に記述すると次のようになります。
{3, 2, 9, 6}

(3, 2) => 3 + 2 ↓
                (5, 9) => 5 + 9  ↓
                                (14, 6) => 14 + 6  ↓
                                                   20
  
1 番最初は 1 つ目の引数が最初の要素で、2 番目の要素から始めることになります。 また、 count の例のように初期値を与え、最初の要素から始めることもできます。

このように畳み込みは要素を順に取得し、新しい値を返す処理です。
cs_linq_fold.png

ここで挙げた例は実はどれも Sum, Max, Count とすでに LINQ のメソッドは用意されています。 よく使われるからこそ用意されているのであり、広く応用できるのがわかると思います。

その他のメソット

データ処理の定番のメソッド以外で、 LINQ のメソッドとして抑えておいた方がいいかなと思うものも挙げて置きます。
Count
要素数(サイズ)の取得。
var src = new[] {3, 2, 9, 6};
src.Count();  // 4
Take
指定した要素数の取り出し。
var src = new[] {3, 2, 9, 6};
src.Take(2); // {3, 2}
First, Last
指定した条件に最初(最後)にマッチする要素の検索。
検索もデータ処理ではよく行う処理ですが、 条件にあうすべての要素を取得するのがフィルター(Where)で、 最初の要素を取得するのが First です。
var src = new[] {3, 2, 9, 6};
src.First(elem => elem % 2 == 1); // 3
src.Last( elem => elem % 2 == 1); // 9
Max, Min
要素の最大(最小)値の取得
var src = new[] {3, 2, 9, 6};
src.Min();      // 2
src.Max();      // 9
Contains
要素を含んでいるかの判定。
var src = new[] {3, 2, 9, 6};
src.Contains(9);  // True
src.Contains(1);  // False
All, Any
要素すべて(どれか一つ)が条件を満たすかどうかの判定。
var src = new[] {3, 2, 9, 6};
src.All(elem => elem % 3 == 0);   // False
src.Any(elem => elem % 3 == 0);   // True
Distinct
重複する要素の除去。(2 つ目以降がなくなる)
var src = new[]    {3, 3, 2, 9, 2, 6, 2};
src.Distinct(); // {3,    2, 9,    6   }

サンプルコード

説明で使用したサンプルのコードは以下のリンクからダウンロード(リンク先を保存)できます。 コンパイルする場合は以下のコマンドを実行します。
 > csc StdQueryOperators.cs
csc.exe を使用したコンパイル方法については以前の記事を見て下さい。

遅延評価

LINQ の重要な要素に遅延評価というものがあります。 これについて説明します。

メソッドの連結と遅延評価

遅延評価の説明の前にもう少し基本的なところから始めましょう。

いまさらですが、 LINQ のメソッドの使える対象とは何でしょうか?
いろいろなものに使えると最初に書きましたが、 型で言うと IEnumerable<T> インターフェースを継承したクラスです。 インターフェースですが、これに拡張メソッドという機能を使って、 IEnumerable<T> がメソッドを持てるようになっています。 IEnumerable<T> を継承したものというのも長いので、 ここではシーケンスと呼びます。
先ほどの Select() や Where() は新しいコレクションを返します。 この戻り値がシーケンスです。 正確な型はコンパイラーが生成する複雑なものなので、 変数に入れたい場合などは var を使った型推論が必要となります。
Console.WriteLine("{0}", src.Select(elem => elem * 2));
// => System.Linq.Enumerable+WhereSelectArrayIterator`2[System.Int32,System.Int32]
シーケンスを返すメソッドは続けて書くことができます。
var src = new[] {3, 2, 9, 6};
var seq = src.Where(elem => elem % 2 == 1)
    .Select(elem => elem * 2);
foreach (var elem in seq)
{
    Console.Write("{0} ", elem);
}
Console.WriteLine();  // => 6 18
このシーケンスには必要になるまで実行されないという遅延評価の機能があります。 上記の例では foreach で一つずつ取り出す時が実行のタイミングです。

連結の処理は一見、次のように処理してるように見えます。
              Where          Select
{3, 2, 9, 6}    →   {3, 9}    →   {6, 18}
しかし、実際には遅延評価により、メソッドごとに結果をためるのではなく、 1 要素ずつ流すように次のメソッドに渡していきます。
    Where      Select
3    →    3     →    6
2    →    ☓
9    →    9     →    18
6    →    ☓
実行するのは "必要なとき" です。
そのため、ソート(OrderBy)のような全要素が揃わないと完了しないようなメソッドの場合は、 その時点で要素は溜まることになります。

なお、「遅延評価を本当にやっているの ? 」と気になる方は、 以前の記事で確認用のコードを書いているので、そちらを見て下さい。

遅延評価のメリット

遅延評価は関数型プログラミングでは並列性のために欠かせない機能です。 関数型の話を抜きにしたとしても、遅延評価には大きなメリットがあります。
それは処理結果を溜めないため、使用メモリーを減らせる点です。


例えば、 File クラス には、 指定したファイルの全行を文字列の配列として取得する ReadAllLines() というメソッドがあります。
これはループを回して1行ずつ取り出す必要がなく便利なのですが、 ログファイルのように大きなファイルに使うと大量のメモリーを使用してしまうことになります。

そこで ReadAllLines のシーケンス版である ReadLines() を使えば、 一行ずつ渡してくれるようになります。

CatFile.cs (抜粋) :
foreach (string line in File.ReadLines(fpath))
{
    Console.WriteLine(line);
}
「一行ずつ取りたいなら普通にループ回して一行ずつ読み取ればいいのでは ? 」 と思うかもしれませんが、シーケンスを返しているので、 それに対して LINQ の豊富なメソッドを使えるという利点があります。

LazySample.cs (抜粋) :
public static IEnumerable<int> ReadValues(string fpath)
{
    var ptn = new Regex(@"\d+");
    return File.ReadLines(fpath)
        .Select(line => ptn.Match(line).Value)      // マッチした数字か空文字
        .Where(str => !String.IsNullOrEmpty(str))   // 空文字を抜く
        .Select(str => int.Parse(str));             // 数値に変換
}
    // :

    // 使用       
    foreach (int val in ReadValues(args[0]))
    {
        Console.WriteLine("{0}", val);
    }
~/cs/LINQ $ cat test.txt
# テスト用ファイル
# test.txt

67
28    # 鉄人
999   # 銀河鉄道
009   # サイボーグ
~/cs/LINQ $ ./LazySample.exe test.txt
67
28
999
9
なお、 foreach のところで実行するということは、 ptn の正規表現オブジェクトはローカル変数なので、 もう存在していないのではないかと思うかもしれません。 しかし、そこはクロージャーという技術によって、 無名関数に使うオブジェクトはうまいこと残してくれるようになっています。

コンテナー以外への LINQ の適用

LINQ はコンテナー以外でも、先ほどのファイルの読み取りや データベース(DB)、 XML などいろいろなものに使えます。
というよりも、 LINQ はもともと DB や XML の処理しやすくすることをメインに導入されたものでしょう。

ここからは、コンテナー以外への LINQ の適用例を紹介したいと思います。 ただ、 DB や XML だとサンプルとして大きくなってしまうので、 もっと簡単な例にします。


フォルダーを走査して、 特定の条件にあるファイルのリストを取得したいといった処理はよくあると思います。 これも LINQ を使って処理することができます。

FindFile.cs (抜粋) :
/// <summary>
///   dirPath 以下のファイルの中で str の文字列を含むファイル名のファイル群を返す
/// </summary>
public static IEnumerable<string> SearchDir(string dirPath, string str)
{
    // 指定フォルダーのファイルをサブフォルダーまで列挙
    DirectoryInfo di = new DirectoryInfo(dirPath);
    IEnumerable<System.IO.FileInfo> fiList = di.GetFiles("*.*", SearchOption.AllDirectories);

    return
        fiList
        .Where(fi =>    // str を含むかでフィルター
               (0 <= fi.Name.IndexOf(str, StringComparison.CurrentCultureIgnoreCase)))
        .Select(fi => fi.FullName); // フルパスに変換
}

    // :
    // 使用
    foreach (string path in SearchDir(dirPath, str))
    {
        Console.WriteLine(path);
    }
~/cs/LINQ $ ./FindFile.exe cs .
Search "." for "cs"
d:\home\cs\LINQ\BinFileIo.cs
d:\home\cs\LINQ\CatFile.cs
d:\home\cs\LINQ\FindFile.cs
d:\home\cs\LINQ\LazySample.cs
d:\home\cs\LINQ\StdQueryOperators.cs
d:\home\cs\LINQ\test.cs

シーケンスの自作

前章で LINQ にはいろいろデータのものが用意されていると書きましたが、 そうはいっても、すべてのケースが用意されているはずもありません。
しかし、そんな場合でも、自分でシーケンスを返す関数を作ることができます。


File クラスの ReadLines はテキストファイル用のものでした。 サンプルとして自分で構成を定義したバイナリーファイル版 ReadLines を作ってみましょう。

バイナリーファイルの構成は、ヘッダーとして要素数が記述され、 その後に整数値が連続しているというシンプルなものにします。
|  要素    | バイト数 | 格納値(例) |
|----------|----------|------------|
| ヘッダー | 4 bytes  |    4       |
| 1 st     | 4 bytes  |    3       |
| 2 nd     | 4 bytes  |    2       |
| 3 rd     | 4 bytes  |    9       |
| 4 th     | 4 bytes  |    6       |
シーケンスを返す関数は IEnumerable<T> を戻り値として、 yield return で値を返すようにします。 返す値は IEnumerable<T>の T の型です。(サンプルでは int)

BinFileIo.cs :
// バイナリー版 ReadLines
private static IEnumerable<int> ReadData(string fpath)
{
    using(BinaryReader br = new BinaryReader(File.OpenRead(fpath)))
    {
        // 先頭は要素数
        int siz = br.ReadInt32();
        // 要素数分、読み取った値を返す
        for (int cnt = 0 ; cnt < siz ; cnt++)
        {
            yield return br.ReadInt32();
        }
    }
}
ファイルのオープンに使っている using については以下の記事をご覧ください。 関数の戻り値がシーケンスなので、 ReadLines と同じように使えます。
foreach (int it in ReadData(testfile))
{
    Console.WriteLine("{0}", it);
}
~/cs/LINQ $ ./BinFileIo.exe 
3
2
9
6

まとめ

最後に LINQ を身につけるまでの手順をまとめておきます。

最初は配列やリストなど C# が用意しているコンテナークラスに LINQ のメソッドを使っていきましょう。
まずは Select(マップ)と Where(フィルター) だけでも十分役に立つと思います。 この記事で紹介したメソッドや次のページを見て少しずつ増やしていって下さい。 また、自作のコンテナークラスを作るときには、 以下の記事を参考にして IEnumerable<T> を継承しておくようにしましょう。
コンテナー以外の対象に広げていくにはどれが使えるのか知っておく必要があります。 しかし、これはいろいろあるので、 使うクラスを調べるとき、 IEnumerable<T> クラスを継承していないか、 IEnumerable<T> を返すメソッドを持っていないか なども見ておいて、少しずつ増やしていくしかありません。

ただ、この記事の yield で自作する方法を身につけておけば、 大抵のものには使えるので、増やしていくのはゆっくりで構わないと思います。



関連記事
スポンサーサイト



Prev.    Category    Next 

Facebook コメント


コメント

コメントの投稿

Font & Icon
非公開コメント

このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
31位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
5位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

08月 | 2023年09月 | 10月
- - - - - 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 26 27 28 29 30


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。