2014年12月25日木曜日

LINQの重箱の隅

C#の便利な言語機能のLINQですが、ちょっと気になったことがあったのでいくつか試したり、まとめてみました。LINQの入門が終わった人が次に読むくらいの中身のつもりです。

AsEnumerable

LINQにはAsEnumerableというメソッドがあります。MSDNには、LINQの拡張メソッドと同じ名前のpublicなメソッドを持つクラスでその拡張メソッドを呼び出したいときにこれを使うと書いています。
例えばListクラスにはReverse()というメソッドがあります。これはLINQのReverse()メソッドと(表記上の)パラメーターが同じなので区別できず、そういう時は優先的にListクラス側のReverse()が呼ばれるようになっています。そういう時に、AsEnumerableを使えばIEnumerableになるので、LINQのReverse()が呼べるようになるよというためのメソッドのようです。
もちろん、AsEnumerableを使わなくても、例えばEnumerable.Reverse(hoge);と書いたり、((IEnumerable<hogeT>)hoge).Reverse();と書いたりすることでLINQ側のReverseを呼び出せるようになりますが、こういう書き方よりAsEnumerableのほうがLINQらしい書き方ですね。



ところで、ここで気になったことが1つありました。
例えば、なんかのクラスでIEnumerable<T>型の読み取り専用プロパティみたいなのを定義することがあったとしましょう。そのようなプロパティの値は、クラス内でprivateなListなどで管理していることがよくあります。そんな時、

List<int> value = new List<int>();

public IEnumerable<int> Value
{
    get { return value; }
}

みたいな書き方をしてしまうと、このクラスの利用者側は、

(List<int>)hoge.Value).Add(13);

というキャストさえすれば本来privateなはずのvalueをクラス外から自由に操作できてしまいます。これはもしもこうやって操作されることを意図していなかったのなら、このようなプロパティの実装のしかたは大問題ですよね。

ここで、AsEnumerableがあるんですよ~。

というのは嘘です。
ここでAsEnumerableを使うことができたらすごくそれらしい書き方で便利なんですが、AsEnumerableの内部実装としては、私が試してみたところ、単にIEnumerable<T>にキャストされているだけっぽいです。
本当に、上記のMSDNに書いてあるような使い方にしか使えないっぽいですね。

List<string> hoge = new List<string>();
IEnumerable<string> foo;

try {
    foo = hoge.AsEnumerable();
    ((List<string>)foo).Add("hoge");
    Console.WriteLine("OK");
}
catch(InvalidCastException e) {
    Console.WriteLine(e.Message);
}

try {
    foo = hoge.Select(p => p);
    ((List<string>)foo).Add("foo");
    Console.WriteLine("OK");
}
catch(InvalidCastException e) {
    Console.WriteLine(e.Message);
}

このコードの実行結果は
OK
型 'WhereSelectListIterator`2[System.String,System.String]' のオブジェクトを型 'System.Collections.Generic.List`1[System.String]' にキャストできません。
となります。AsEnumerableはListへのキャストに成功してしまうんですね。

なので、外から無理やりいじられないプロパティ等を実装したかったら、上記のようにSelect(p => p);を使うか、もしくはyield returnで書いてしまうかになりそうですね。

Cast<T>()とOfType<T>()

比較的マイナーだと思われるメソッドなので取り上げました。Cast<T>()OfType<T>()はLINQの中では珍しく型推論がきかないメソッドです。明示的に型を示してあげなければなりません。
どちらもIEnumerable型(IEnumerable<T>ではない)の要素を型パラメーターの型に変換してIEnumerable<T>にして返すメソッドです。Cast<T>()は変換できない要素があると例外を発生させ、OfType<T>()は変換できない要素は省いたコレクションを返します。

System.Collections.ArrayList nongeneric = new System.Collections.ArrayList();

nongeneric.Add(123);
nongeneric.Add("hoge");
nongeneric.Add("foo");
nongeneric.Add(456);

foreach(var o in nongeneric)
    Console.WriteLine(o);
Console.WriteLine("-----");
foreach(var o in nongeneric.OfType<int>())
    Console.WriteLine(o);
Console.WriteLine("-----");
try {
    foreach(var o in nongeneric.Cast<int>())
        Console.WriteLine(o);
}
catch(InvalidCastException e) {
    Console.WriteLine(e.Message);
}

こんなコードを書いて実行すると、実行結果は下のようになります。
123
hoge
foo
456
-----
123
456
-----
123
指定されたキャストは有効ではありません。
非ジェネリックのコレクションなんて使う機会はほとんど無い気がしますが、例えば正規表現周りのクラスにあるMatchCollectionなんかは非ジェネリックなので(これはジェネリックスが導入される前の.NET1.1の時代からあるクラスだから非ジェネリックのまま今まで来てしまったものだと私は勝手に思っています)、これをLINQで扱いたいときはCast<T>()を使うと便利です。

要素のインデックス

要素のインデックスが欲しいことっていうのも多々あります。ElementAt()を使うと任意のインデックスの要素を取得することはできますが、逆にある要素のインデックスを取得したいときなんかはそれに見合ったメソッドはありません。
で、どうするかと言うと、Selectのオーバーロードを使います。例えば、シーケンスの中から条件に一致する要素のインデックスを取得するには、このような書き方で解決できます。

IEnumerable<string> hoge = new[] {"apple", "orange", "application", "operating system"};

var indeces = hoge
    .Select((p, i) => new { Content = p, Index = i })
    .Where(p => p.Content.StartsWith("app"))
    .Select(p => p.Index);

foreach(int i in indeces)
    Console.WriteLine(i);

Selectメソッドはインデックスも与えてくれるオーバーロードがあるので、それで要素とインデックスを抱き合わせた匿名クラスを作ってあげればあとはWhereなどで任意のフィルターを掛けたりすることができます。

隣り合う要素

これは個人的に自分が情弱だっただけなんでしょうが、長い間、コレクションの隣り合った要素同士の関係に関して何かしら計算するのが、スマートにできずにいました。上に要素のインデックスを得る方法を書いたのはこの記事の布石だったのですが、私は長い間、要素とJoinを使う方法を取っていましたが、とても冗長でパッと見で何をやっているのかがわからないような式になってしまいました。

例えば、パスカルの三角形を計算するプログラムを書くとします。その場合、私は今までずっとこんな式を書いていました。

IEnumerable<int> Row = new[] { 1 };

for(int j = 0; j < 20; j++) {    //20段目まで
    Console.WriteLine(string.Join(" ", Row.Select(p => p.ToString())));    //表示

    var RowWithZero = new[] { 0 }.Concat(Row).Concat(new[] { 0 });    //両端にゼロを付ける

    var RowWithIndex = RowWithZero.Select((p, i) => new { Number = p, Index = i });    //インデックスを抱き合わせる
    var ShiftedRowWithIndex = RowWithZero.Skip(1).Select((p, i) => new { Number = p, Index = i });    //最初の1つ飛ばしたものにもインデックスを抱き合わせる

    Row = RowWithIndex.Join(ShiftedRowWithIndex, p => p.Index, p => p.Index, (o, i) => o.Number + i.Number).ToArray();    //ToArrayしないととても重くなる
}

計算アルゴリズムとしては、段の数列に対して両端に0を付加し、その数列と、その数列の1つ目をSkipした数列の2つに関してそれぞれ上記のようにインデックスを抱き合わせた匿名クラスを作り、その2つの数列に対してインデックスが等しい組み合わせをJoinで抽出し、その組み合わせの要素を足したものを数列化して次の段を求めています。
数列の先頭を1つSkipしてインデックスを付けているので、2つのコレクションのインデックスが同じ要素は隣り合ったものというわけですね。

はい。
自分で説明文書いてて 意味わからなくなってしまうくらい簡潔さに欠けます。そもそも「隣り合う要素の組み合わせをSelectしたい」ってだけなのに、こんなにまどろっこしいことをやっているわけです。

はい。
Zip()を使いましょう。
Zip()というメソッドは、2つのコレクションを同時にSelectする拡張メソッドみたいな感じです。

IEnumerable<int> Row = new[] { 1 };

for(int j = 0; j < 20; j++) {    //20段目まで
    Console.WriteLine(string.Join(" ", Row.Select(p => p.ToString())));    //表示

    var RowWithZero = new[] { 0 }.Concat(Row).Concat(new[] { 0 });    //両端にゼロを付ける

    Row = RowWithZero.Zip(RowWithZero.Skip(1), (p, n) => p + n).ToArray();
}

さっきに比べてだいぶスッキリしましたね。Zip()はSelect()やWhere()ほど頻出ではないですが、かなり有用なメソッドだと思います。

Aggregate()

LINQには一般的な集計処理をするためのAggregate()というメソッドがあります。しかし、このメソッドはオーバーロードがいくつかあり、全体像をつかむのに苦労しました。


まず1つ目のAggregate<TSource>(IEnumerable<TSource>, Func<TSource, TSource, TSource>)についてです。
このメソッドで指定するデリゲートでは、1つ目の引数で今までの集計値、2つ目の引数で現在の要素が渡されます。ちなみに、現在の要素は2つ目の要素から始まり、その2つ目の要素に関しては今までの集計値として1つ目の要素の値が渡されます。このメソッドが現時点での集計値を返すことによって、次の要素に対する呼び出しの第1引数がその値になります。

IEnumerable<int> array = new[] { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };
Console.WriteLine(array.Aggregate((working, now) => Math.Min(working, now)));

このコードはコレクションの最小値を求めるものです。ラムダ式の1回目の呼び出しにはworkingに1が入り、nowに3が入ってきます。Math.Minで1を返すので、次の呼び出しではworkingが1(前回の呼び出しの返却値)、nowに5が入ってきます。
ちなみに、要素数が1個のコレクションだとデリゲートは1回も呼ばれずに1つ目の要素が返され、要素数が0個のコレクションだと例外(InvalidOperationException)が吐かれます。


次に、2つ目のAggregate<TSource, TAccumulate>(IEnumerable<TSource>, TAccumulate, Func<TAccumulate, TSource, TAccumulate>)です。
このメソッドではシード、すなわち初期値を指定することができます。上述のAggregateのほうでは初期値がコレクションの1つ目の要素になるので自動的に集計値が要素の型と同じになりますが、こちらのAggregateでは初期値を指定できるので任意の型にすることが可能です。

IEnumerable<Func<int, int>> calc = new Func<int, int>[]{
    (i) => i * 9,
    (i) => i / 10 + i % 10,
    (i) => i + 1,
};

Console.WriteLine(calc.Aggregate(3, (working, now) => now(working)));

例えば、1~10の任意の整数を与えても答えが10に落ち着く計算みたいなのがあります。子供だまし?のちょっとした電卓遊びですね。
それぞれの計算アクションをFunc<int, int>の配列として用意しておきます。そして、初期値から順にこのメソッドを順に呼び出していきます。
計算結果は1~10の任意の整数を与えても必ず10になります。


最後に、3つ目のAggregate<TSource, TAccumulate, TResult>IEnumerable<TSource>, TAccumulate, Func<TAccumulate, TSource, TAccumulate>, Func<TAccumulate, TResult>)です。
このメソッドは2つ目のAggregateの計算が終わった後、その値を変換するデリゲートが付いています。

int[] array = new[] { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };

Console.WriteLine(array.Aggregate(0, (working, now) => working + now, p => (double)p / array.Aggregate(0, (w, n) => w + 1)));

これはAggregateだけで平均を求めるメソッドです。平均は、全要素を足してから最後に要素数で割って求めますよね。(要素をそれぞれ要素数で割ってから足してもいいですが…)
すなわち、全て足した後のアクションとして、要素数で割るというアクションを追加する必要がでてきます。というわけで、処理後のselectorを使うわけです。
ちなみに、要素数を数えるのもAggregateでできますね。


はい、ここまでAggregateを見てきました。ここまで来てお気づきの人も多いかと思いますが、Count()Min()Max()Sum()Average()はこのAggregateの特別な形です。


余談:
intとかdoubleとかの一般的な数値型の元締めとなるような親構造体(or インターフェース)が無いせいで、Min()、Max()などはあらゆる数値型に対してオーバーロードが定義されています。なんていうか、実装した人お疲れ様ですって感じですね…。

ThenBy()

これもまただいぶマイナーなメソッドだと思っています。
ThenBy()は、ソートされたコレクションに対して、そのうちのソート基準が同じだったものを別の条件でソートするメソッドです。
これだけ言われると何を言っているんだ?と思ってしまうかもしれませんが、例えば、ファイルの一覧をまず種類(拡張子)でソートして、そのうち同じ拡張子のファイルは日付順に並べたいなんてことがあると思います。

IEnumerable<FileInfo> files = Directory.GetFiles(@"C:\Windows").Select(p => new FileInfo(p));

files = files
    .OrderBy(p => p.Extension.ToLower())
    .ThenByDescending(p => p.LastWriteTime)
    .ThenBy(p => p.Name);

foreach(FileInfo f in files)
    Console.WriteLine("{0:yyyy/MM/dd hh:mm:ss} {1}", f.LastWriteTime, f.Name);

このコードでは、まず拡張子で昇順にソートし、同じ拡張子のものに関しては最終更新日時で降順にソートし、さらに同じ最終更新日時のものに関してはファイル名で昇順にソートしています。

OrderBy()安定なソートなので、そのままでは同じ拡張子のファイルに関してGetFilesの返却値の順序がそのまま使われます。そこで、ThenByの出番ってわけですね。



ここまで、私の独断と偏見に基づいてLINQの重箱の隅をつついてみました。いかがでしたでしょうか。
一度、一通りEnumerableクラスのメソッドを眺めておくとLINQで書くときに最もスマートな方法で書くことができるようになると思います。

これであなたもLINQ星人♪

2 件のコメント:

  1. >なので、外から無理やりいじられないプロパティ等を実装したかったら、上記のようにSelect(p => p);を使うか、もしくはyield returnで書いてしまうかになりそうですね。

    AsReadOnlyメソッドを使ってどうぞ
    https://ideone.com/nT2ADC
    http://msdn.microsoft.com/ja-jp/library/e78dcd75%28v=vs.110%29.aspx

    返信削除
    返信
    1. おっと、これはまた情弱を発揮してしまいましたね。
      AsReadOnlyが全てのIEnumerableで実装されているわけではないので個別に対応する必要がありそうですが、ListならList.AsReadOnly()、配列ならArray.AsReadOnly()、DictionaryならReadOnlyDictionaryなど割と通常の範囲内では事足りそうな感じですね。
      ありがとうございます。

      削除