こんばんは。
桜の開花宣言がされたりと、春がのそのそと迫ってくる感じですが、
気温の変化に弱いので、いまいち体調がよろしくないのが困りものです。
さて、今回でメインの機能はだいたい終わりかなと思っています。
初期化子とかenumとかyeildとかnullableとか細かいことはまだいろいろありますが、(細かくない?)
今回はLINQ(統合言語クエリ)について見ていきます。
LINQ = Stream API ?
LINQはJavaのStream APIの機能にあたると思って問題ないでしょう。
Stream APIはJava8(2014年3月18日)で導入されましたが、
LINQはC#3.0(2007年12月)で導入されているので、だいぶ先行ですね。
データ集合に対してデータの問い合わせや変換をする機能ですね。
クエリ構文とメソッド構文
LINQにはクエリ構文と呼ばれる書き方とメソッド構文と呼ばれる書き方があります。
クエリ構文は糖衣構文なので、コンパイル時にはメソッド構文に変換されるようです。
例えばクエリ構文はこんな感じ。
SQLと同じような構文ですが、selectが後ろに行きます。
var ary = new[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; var data = from e in ary where !e.StartsWith("J") // 「J」で始まる以外 orderby e // 辞書順ソート select e.ToLower(); // 小文字に変換
同じことをメソッド構文で書くとこうなります。
var data = ary.Where(e => !e.StartsWith("J"))
.OrderBy(e => e)
.Select(e => e.ToLower());
シンプルな変換だったらクエリ式の方が簡単かもしれませんが、
メソッド式の方がいろいろとできる上にStream APIっぽいので、
クエリ式は参考程度ということにしておきます。
LINQとStream APIを比較
比較します。一気に行きます。
中間処理
選択
説明 | Java | C# |
---|---|---|
条件を満たす要素に絞り込み | filter(Predicate<? Super T>) | Where(Func<T, bool>) |
指定件数をスキップ | skip(long) | Skip(int) |
先頭から条件を満たさなくなるまでスキップ | - | SkipWhile(Func<T, bool>) |
指定件数をスキップ | limit(long) | Take(int) |
先頭から条件を満たさなくなるまで要素を返す | - | TakeWhile(Func<T, bool>) |
重複するものを除いた要素を返す | distinct() | Distinct() |
射影
説明 | Java | C# |
---|---|---|
射影(変換) | map(Function<? super T, ? extends R>) | Select(Func<T, R>) |
平坦化して射影 | flatMap(Function<? super T, ? extends Stream<? extends R>>) | SelectMany(Func<T, IEnumerable<R>>) |
グループ化 | - ※1 | GroupBy(Func<T, K>>) |
※1 Javaの場合にはMapに変換されるので中間処理ではなく終端処理
ソート
説明 | Java | C# |
---|---|---|
昇順にソート | sorted(Comparator<? super T>) ※2 | OrderBy(Func<T, K>) |
降順にソート | sorted(Comparator.reverseOrder(Function<? super T, ? extends K>)) | OrderByDescending(Func<T, K>) |
ソートしたシーケンスのキーが同一の場合 さらに昇順にソート |
- | ThenBy(Func<T, K>) ※3 |
ソートしたシーケンスのキーが同一の場合 さらに降順にソート |
- | ThenByDescending(Func<T, K>) |
シーケンスの順序を反転する | - | Reverse() |
※2 引数なしだと自然順序でソートされる
※3 ThenByはOrderByの後でしか使えない
合成
説明 | Java | C# |
---|---|---|
連結 ※4 | Stream.concat((Stream<? extends T> , Stream<? extends T>) ※5 | Concat(IEnumerable<T>) |
和集合 | - | Union(IEnumerable<T>) |
差集合 | - | Except(IEnumerable<T>) |
積集合 | - | Intersect(IEnumerable<T>) |
内部結合 | - | Join(IEnumerable<U>, Func<T, K>, Func<U, K>, Func<T, U, R>) |
外部結合を使ってグループ化 | - | GroupJoin(IEnumerable<U>, Func<T, K>, Func<U, K>, Func<T, IEnumerable<U>, R>) |
2つのシーケンスをマージ | - | Zip(IEnumerable<U>, Func<T, U, R>) |
※4 和集合との違いはdistinctがかからないこと、SQLのUNION ALLとUNIONみたいな感じ
終端処理
要素を取得
説明 | Java | C# |
---|---|---|
先頭の要素を取得 | findFirst() ※5 | First() ※6 FirstOrDefault() |
末尾の要素を取得 | - | Last() ※6 LastOrDefault() |
なんか取得 ※7 | findAny() | - |
ただ一つの要素を取得(複数ある場合例外) | - | Single() SingleOrDefault() |
指定した位置の要素を取得 | - | ElementAt(int) ElementAtOrDefault(int) |
※5 orDefaultがないのは戻り値がOptinal型だから
※6 Func
※7 並列化した場合にfindFirstよりパフォーマンスが良いから用意されているのかしら
集計
説明 | Java | C# |
---|---|---|
畳み込み | reduce(BinaryOperator<T>) | Aggregate(Func<T, T, T>) |
最大値 | max(Comparator<? super T>) ※8 | Max() |
最小値 | min(Comparator<? super T>) ※8 | Min() |
平均値 | average() ※9 | Average() |
合計値 ※9 | sum() | Sum() |
件数 | count() | Count() |
※8 max、minはプリミティブストリームの場合には引数なし
※9 average、sumはプリミティブストリームのみ実装
判定
説明 | Java | C# |
---|---|---|
すべての要素が条件を満たすか | allMatch(Predicate<? super T>) | All(Func<T, bool>) |
条件を満たす要素が存在するか | anyMatch(Predicate<? super T>) | Any(Func<T, bool>) |
条件を満たす要素が存在しないか | noneMatch(Predicate<? super T>) | - |
変換
説明 | Java | C# |
---|---|---|
配列を作成 | toArray() | ToArray() |
リストを作成 | collect(Collectors.toList()) | ToList() |
セットを作成 | collect(Collectors.toSet()) | - |
マップ、ディクショナリを作成 | collect(Collectors.toMap(Function<? super T, ? extends K> Function<? super T, ? extends U>)) | ToDictionary(Func<T, K>>) |
グループ化してマップ、ディクショナリを作成 | collect(Collectors.groupingBy(Function<? super T,? extends K>)) | GroupBy(Func<T, K>>).ToDictionary(Func<IGrouping<K, T>, K>, Func<IGrouping<K, T>, List<T>>) ※10 |
真偽にグループ化してマップ、ディクショナリを作成 | collect(Collectors.partitioningBy(Predicate<? super T>)) | - |
※10 ToLookup(Func<T, K>>).ToDictionary(Func<IGrouping<K, T>, K>, Func<IGrouping<K, T>, List<T>>) でもできる
引数とジェネリクスまで書いたらなんだか読みづらくなってしまいましたね。
大体同じような処理が実装されているようですが、Javaは合成系が弱いのかもしれません。
Zipとかは普通に欲しい。
地道に覚えるべし
さて、ちょっとした考察。
Javaの設計思想としては細かい部品をたくさん作って、
部品の組み合わせでプログラムを組み立てる発想なのだと思います。
なので、Stream APIを使う場合には必ずStreamに変換して処理し、
終端処理でコレクションなり配列なりに再度変換しています。
そしていったん利用したStreamは文字通りに流れて行ってしまいます。
一方でLINQではSystem.LinqがひたすらにIEnumerableへのメソッド拡張をしていることからもわかるように、
コレクションなどとLINQ自体の仕組みをそんなに切り分ける気がないようです。
遅延実行の仕組みなんかを考えると、Stream APIの発想は分かりやすい気もしますが、
やっぱりどうしてもコード自体は長くなってしまいますね。
Eclipse Collectionsなんかはこの部分をスマートにしたかったのでしょう。
なんにせよ、LINQはとっても便利な機能ですよね。
しっかり覚えないと。