ラベル LINQ の投稿を表示しています。 すべての投稿を表示
ラベル LINQ の投稿を表示しています。 すべての投稿を表示

2020年6月28日日曜日

System.Interactiveで快適LINQ生活

久しぶりにプログラム書いてたらライブラリの名前をすっかり忘れて探すのにだいぶ苦労したのでメモしておきます。

C#3.0で導入されたLINQも、少し痒いところに手が届かないことがあります。
例えばDistinctメソッドは基本的に要素本体でしか重複判定をすることができません。LINQならば、例えば以下のようなことしたいですよね。
var uniqueName = data.Distinct(p => p.Name);
何かのデータで同じ名前の重複を削除したいときはこう書きたいです。
実際には他のプロパティが違うデータでも同じ名前だったら1つを残して削除されちゃいますし、どれが削除されるのかは直感的にはわかりません。そういった不都合があってこのようなメソッドが作られなかったのかもしれませんが、でも実際問題使いたいことが結構あります。

他にも、例えばSelectManyメソッドは「配列の配列」をそのまま縦につなげて1つの配列にするのに便利ですが、その逆、すなわち配列をいくつかごとに分割して「配列の配列」にするメソッドはありません。

そんなもろもろの痒いところに手を届かせたライブラリは実はMicrosoft公式で存在します。


Nugetから入れれば、一発でDistinctメソッドにキー選択機能があるオーバーロードが追加されます。めでたしめでたし。ちなみに後者の「SelectManyの逆」はBufferメソッドです。

このライブラリはReactive Extensionsの一部です。まあ、LINQを配列のように「位置的に並んでいるもの」だけじゃなく「時系列的に並んでいるもの」にまで拡張させたのがReactive Extensionsですから、その流れでできた新しい便利な拡張メソッドをIEnumerable<T>用の拡張メソッドに逆輸入した形なのでしょう(たぶん)。

備忘録でブログに残しておいただけなので、今日はこれくらいで。

2018年6月2日土曜日

OrderByとThenBy - IOrderedEnumerableの仕組み

LINQには要素を並び替えるOrderByと、OrderByで同じ大きさだった要素をその中でさらに並び替えるThenByという拡張メソッドが用意されています。
var files = Directory.GetFiles(@"(中略)\OrderByTest")
    .Select(p => new FileInfo(p))
    .OrderBy(p => p.Extension.ToLower())
    .ThenByDescending(p => p.Name);

foreach(var file in files)
    Console.WriteLine(file.Name);    
例えばソースコードのフォルダに対してこんなコードを実行してみました。
まずは拡張子に対して昇順でソートをかけ、拡張子が同じものについては降順でファイル名にソートをかけています。
 確かに拡張子は昇順(config→cs→csproj)ですが、ファイル名は降順(Program→Enumerable)になっています。

ちょっと考えれば、ThenByが無くてもファイル名で降順にソートしてから拡張子で昇順にソートすれば(安定なソートである前提で)同じ結果が得られることがわかります。まあ、2回ソートをすることになりますが。

ですが、OrderBy().ThenBy()という書き方は「まずソートして、同じだったらさらに条件を追加して」となり流れが非常にわかりやすいです。また、詳しくは後述しますが、このやり方ではソートは1回しか行われておらず、上記の2回ソートする方法に比べれば効率が良いです。

さて、どうなっているか仕組みを見てみましょう。

IOrderedEnumerable<T>

当然ですが、ThenBy()はソートされたシーケンスにしか適用できません。すなわち、IEnumerable<T>に対する拡張メソッドではないのです。OrderBy()やThenBy()はIOrderedEnumerable<T>を返し、これに対する拡張メソッドとしてThenBy()が定義されています。

IOrderedEnumerable<T>はIEnumerable<T>とIEnumerableを継承しており、
IOrderedEnumerable<T>としてはCreateOrderedEnumerable()メソッドが追加されています。
IOrderedEnumerable<TElement> CreateOrderedEnumerable<TKey>(
    Func<TElement, TKey> keySelector,
    IComparer<TKey> comparer,
    bool descending
)
最初このメソッドを見たときは「なんやねんこれ」と思いました。何をやるメソッドかまるでわからない、悪いネーミングのメソッドです。

実はこれ、単にThenBy()が自分が渡されたパラメーターを渡しているだけなのです。
ソースコードを見れば一目瞭然です。
public static IOrderedEnumerable<TSource> ThenBy<TSource, TKey>(this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
    if (source == null) throw Error.ArgumentNull("source");
    return source.CreateOrderedEnumerable<TKey>(keySelector, null, false);
}
すなわちこれがThenByの実体なわけですね。第1引数はキーを返すデリゲート、第2引数はキーの比較クラス、3つ目の引数は昇順降順を指定するbooleanです。
ThenBy()メソッドは第1引数にOrderByなどが返したIOrderedEnumerableを受け取りますから、この実体に対してCreateOrderedEnumerableを呼び出してThenByの動作を規定するわけです。

IOrderedEnumerableの実体は、LINQの場合はOrderedEnumerableクラスが担っています。このクラスはinternalなのでライブラリ外からは見えません。
上記のソースコードを追っていくとわかりますが、OrderedEnumerable.CreateOrderedEnumerableは自身を親条件に持つOrderedEnumerableを返します。そして、実際に列挙されはじめたとき(GetEnumeratorが呼び出されたとき)にクイックソートを用いてソートを行いますが、そのソート時の比較で親条件から順に適用していき、比較結果が"等価"になったときに再帰的に子条件を用いることでThenByを実現しています。
internal override int CompareKeys(int index1, int index2) {
    int c = comparer.Compare(keys[index1], keys[index2]);
    if (c == 0) {
        if (next == null) return index1 - index2;
        return next.CompareKeys(index1, index2);
    }
    return descending ? -c : c;
}
(ソースコードはこちら


余談ですが、クイックソートは比較ソートの中では最も速い(ソート時間の期待値が最も小さい)と言われていますが、安定なソートではありません。そこで、この比較メソッドで、値が等価ならインデックスの大小関係を返すことで安定なソートを実現しているようです。


まとめ

  • OrderBy()やThenBy()はIEnumerable<T>を継承したIOrderedEnumerable<T>を返す
  • IOrderedEnumerable<T>.CreateOrderedEnumerable()はThenByの条件を追加したIOrderedEnumerable<T>を返す
  • IOrderedEnumerable<T>のLINQ実装であるOrderedEnumerableクラスはクイックソートを行い、その比較時に最も親の条件から順に適用してThenByを実現している (すなわちソートは1回のみ)
ということで、もしも俺の考えた最強のソートアルゴリズムをLINQで使いたかったら、IOrderedEnumerable<T>を実装した(CreateOrderedEnumerable()を実装した)クラスを返すことで、自動的にThenByも定義できてしまうということになります。

まあ、比較ソートの中では最速のクイックソートをがOrderByに使われている以上、OrderByを改善したくなることはまず無いとは思いますけどね。意図的に最悪なピボットを選ばせるようなシーケンスを与えない限り、マージソートのほうが良いなんていうことは無いでしょう(ちなみにシーケンスの中央の値をピボットとするアルゴリズムを採用しているようなので、ソート済みシーケンスに対してOrderByを呼び出しても$O(n^2)$にはならないようです。

2015年8月7日金曜日

LINQ to Twitter ver.3.1.2のUserStream

C#からTwitterするならLINQ to Twitterですが、今まで、これには不満点が1つありました。

それは、UserStreamの実装がだいぶ雑だったことです。

UserStreamはそもそもツイートだけでなく、例えばふぁぼ通知、フォロー通知、ツイ消し通知など、様々な情報が流れてきます。そのため、UserStreamは各種Rest APIのJSONとは違っていて、それこそ多種多様なデータが流れてきていました。そして、その多種多様なデータを適当に振り分けてくれるような機能が以前のLINQ to Twitterにはありませんでした。

なので、UserStreamの処理を書くときに限って、自前でJSONの処理をしなければいけませんでした。せっかくの.NET向けライブラリなのにとても残念なことになっていたわけですね。

無論、流れてくるツイートのみをパースしたければ、StatusクラスのコンストラクタにJsonData型を引き渡すことができるオーバーロードがありますから、それにぶち込んで、もしも例外等が吐かれたらツイート以外のデータだったんだなということで無視する、なんて方法はありました。私も以前からその方法をよく使っていましたし、それで事足りることもしばしばありました。

もしくは、UserStreamsParser for LinqToTwitterのような別のライブラリと組み合わせてパースさせることもできましたが、しばしば仕様変更で使えなくなってしまったり、いろいろと不都合なこともありました。

さて、ここまで勿体ぶったからには何が言いたいか見当が付いてきたかと思いますが、ついにLINQ to TwitterのほうでこのUserStreamのパース機能が実装されました。 (´◔౪◔)۶ヨッシャ!

現在のLINQ to Twitterの最新バージョンはver.3.1.2ですが、どうも3.1.0あたりから実装されたっぽいです(未検証)。

というわけで、さっそく使ってみました。

SingleUserAuthorizer auth = new SingleUserAuthorizer()
{
    CredentialStore = new SingleUserInMemoryCredentialStore()
    {
        ConsumerKey = "****",
        ConsumerSecret = "****",
        AccessToken = "****",
        AccessTokenSecret = "****"
    }
};

TwitterContext context = new TwitterContext(auth);

(from p in context.Streaming where p.Type == StreamingType.User select p).StartAsync(async s => {
    await Task.Run(() => {
        switch(s.EntityType) {
            case StreamEntityType.Unknown:    //Unknownはスルー
                break;
            case StreamEntityType.Status:    //ツイートが流れてきた
                ShowStatus((Status)s.Entity);
                Console.WriteLine();
                break;
        }                    
    });
});


こんな感じになります。
UserStreamで流れてくるStreamContentクラスにEntityTypeとEntityという2つのプロパティが追加されました。EntityTypeはStreamEntityType列挙体、Entityはobject型になっています。

例えば、ツイートが流れてくるときは、EntityTypeがStreamEntityType.Statusになっていて、EntityにStatus型のオブジェクトが入っています。なので、適宜キャストしてあげれば流れてきたツイートを適当に処理することができます。

ほかにも、例えばふぁぼられたときはStreamEntityType.Eventが流れてきます。この時のEntityはEventクラスのオブジェクトで、EventNameに"favorite"、SourceとTargetにそれぞれふぁぼった人とふぁぼられた人のUserデータが入っています。
ふぁぼられたツイートはTargetObjectだと思われますが、実際には"JsonData object"というテキストが入っているだけで、ふぁぼられたツイートはわかりません。これはおそらくLINQ to Twitterのバグでしょう。しっかり検証していませんが、おそらくフォロー通知などもEntityがEventとして流れてくると考えられるので、そのあたりで実装が不十分になっているのでしょう。これは是非とも今後のアップデートに期待したいですね。

ほかにもDeleteやDirectMessageなど、様々なUserStreamの情報に対応しているようですから、いろいろいじってみると面白いかもしれません。

 ちなみに、RetweetedみたいなEntityが無いなと思っていたら、どうもこれ、普通にStatusと一緒にやってくるみたいですね。要するに「リツイートされたツイートの投稿主が自分だったらリツイートされたってことでしょ 」という体みたいです。まあそれくらいの実装なら全然難しくはなさそうですよね。ハマらないように、念のため。

2015年1月27日火曜日

LINQメソッドのストリーミングと非ストリーミング

この前私の無知ゆえにTwitterで議論を起こしてしまったのでここにまとめておきます。

LINQの基本的な思想として遅延評価というものがあります。

Console.WriteLine("Start");
var e = Enumerable.Range(0, 10).Select(p => { Console.WriteLine(p); return p; });
Console.WriteLine("foreach Starting");
foreach(int i in e)
    Console.WriteLine("foreach " + i);

例えばこんなプログラムを実行したらどのような結果が出力されるでしょう。
Start
foreach Starting
0
foreach 0
1
foreach 1
2
foreach 2
3
foreach 3
4
foreach 4
5
foreach 5
6
foreach 6
7
foreach 7
8
foreach 8
9
foreach 9

こんな出力がされます。Select()メソッドが呼ばれたときではなく、値が必要になったときに値が列挙されています。これが遅延評価です。
では、次のようなコードの場合どうなるでしょう。Select()の後ろにReverse()が付きました。

Console.WriteLine("Start");
var e = Enumerable.Range(0, 10).Select(p => { Console.WriteLine(p); return p; }).Reverse();
Console.WriteLine("foreach Starting");
foreach(int i in e)
    Console.WriteLine("foreach " + i);

実行結果はこんな感じになります。
Start
foreach Starting
0
1
2
3
4
5
6
7
8
9
foreach 9
foreach 8
foreach 7
foreach 6
foreach 5
foreach 4
foreach 3
foreach 2
foreach 1
foreach 0

先ほどと違って、foreachで各値が列挙されるごとに1回ずつSelect()に与えたデリゲートが呼ばれるのではなく、foreachが始まるときに全ての値が評価されたうえで逆順に列挙されています。
というのも割と当然で、IEnumerator<T>は現在の値を示すCurrentプロパティと次の値へ移動するMoveNext()メソッドしか持っていません(まあReset()とかDispose()とかありますがここでは関係ないので置いておきます)。すなわち、コレクションを逆順にたどることができないので、Reverse()が返すIEnumerable<T>のオブジェクトは、いったん全ての値を評価して配列とかにストックしておき、それを逆順に返すことでその機能を実現しています。

私は、これを遅延評価とは言わないと思ってしまっていました。
最初の例のように、その値を使用しようとしたときに初めて評価するような機構が遅延評価であり、そうすることで分散してデータベースにアクセスすることができるメリットが享受できるところまで含めて遅延評価の定義だと認識していました。そのため、いったん全ての値を評価しないと最初の値を提供できないReverse()メソッドは遅延評価に当たらないと思っていました。

しかし、上記のWikipediaを見ていただくとわかるように、遅延評価そのものの定義には分散といったような言葉は出てきません。遅延評価では分散して実行できることは保証されておらず、都合が良いデータならもしかしたら分散して処理できるかもしれないくらいのもののようです。いわば、分散して処理ができるのは遅延評価の効果でしかなく、あくまでも「必要になった時に必要最低限の評価をする」が遅延評価です。

後者の場合もforeachが始まるまで値は評価されていません。なので遅延評価です。ただし、始まった瞬間に全ての値を評価しないと逆転させた時の最初の値(=元のコレクションの最後の値)がわからないので、必要最小限の評価として全ての値を評価しているというだけのことです。

私のこのもともとの発想は実は遅延評価とは若干違ったところで存在していたようです。

実行方法による標準クエリ演算子の分類

LINQには「即時評価」と「遅延評価」の2種類のメソッドがあり、その遅延評価のメソッドはさらに「ストリーミング」と「非ストリーミング」に分類できるようです。私がもともと遅延評価の定義だと勘違いしていた「実行時に分散的に評価を行う」メソッドがここでいうストリーミングに分類されます。逆に遅延評価ではあるけど、評価開始時にすべての値を評価してしまうのが「非ストリーミング」に分類されています。
例えばSelect()はストリーミングですが、Reverse()は非ストリーミングになっています。なるほど納得です。
ちなみに、ストリーミングと非ストリーミングの両方にチェックが付いているものがあります。例えばExcept()なんかがそれです。2つ以上のコレクションを扱うメソッドのときは両方の特性が付く場合があるようで、Except()の場合は例えばsecondで与えたコレクションを全て評価しないとその値を持つかどうかがわからないので、そういった仕様になっているということです。firstの値そのものはストリーミングで実行されます。


これでまた一つ賢くなりました。
今後LINQを使うときは単に「遅延評価」だけでなく、この辺も意識しながら使いたいですね。

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星人♪