C#
.NET
LINQ
.NETStandard

ImportedLinqのススメ ~C#でも他の言語にあるMaxByやFlattenを!~

プログラミングにおいて、配列、リスト、シーケンスなどのコレクションを扱う機会は非常に多いです。「プログラミング言語でコレクションがどれくらい扱いやすいか」は重要な点だと筆者は考えます。

さてC#には、コレクション関連の言語機能として「LINQ」が、10年以上前、C# 3.0の頃からあります。そんなLINQですが、後発のプログラミング言語のコレクション機能と比べると、不足している部分があると筆者は考えました。

そんな考えから筆者は「ImportedLinq」というライブラリを作成しました。

この投稿では、「ImportedLinq」作成の背景と使用例を紹介します。


こんなことしたいけど、LINQじゃできない

このようなクラスを使って説明します。ゲームでのロジックをイメージしてください。

class Monster

{
public int Level { get; private set; }
/* 略 */
}


モンスターのリストがあります。この中から一番高いレベルの値を求めます。どんなコードを書けばいいでしょうか?

IReadOnlyList<Monster> monsterList = LoadMonsterList();

int maxMonsterLevel = ???;

このような場合、LINQのMaxメソッドを使うことで、非常に簡潔に、次のコードのように1行で書くことができます。

IReadOnlyList<Monster> monsterList = LoadMonsterList();

int maxLevel = monsterList.Max(it => it.Level);

このように、LINQを使えば、短く、読みやすいコードで実装できます。


モンスターのリストがあります。この中から「一番レベルの高いやつ」を探します。どんなコードを書けばいいですか?

IReadOnlyList<Monster> monsterList = LoadMonsterList();

Monster maxLevelMonster = ???;

先ほどと似たような問題ですが、違いに注目してください。見つけたいのは、「一番高いレベル」ではなく、「一番高いレベルを持ったモンスターの要素」です。

実は、これを実現できるLINQのメソッドは残念ながらありません。


さて、解決策・コードを紹介する前に、次の例を示します。

モンスターのリストがあります。表示のために、先頭から8個ごとにグループ分けし、リストのリストを作りたいです。 どんなコードを書けばいいでしょうか?

IReadOnlyList<Monster> monsterList = LoadMonsterList();

// 型は
// List<List<Monster>>?
// IEnumerable<IList<Monster>?
// IEnumerable<IReadOnlyList<Monster>?
var monsterListOfList = ???

残念ながら、これも一発でかけるC#・LINQのメソッドはありません。


次の例です。

モンスターのリストのリストがあります。 これを平滑化し、IEnumerable<Monster>なmonstersを作りたいです。どんなコードを書けばいいでしょうか?

List<List<Monster>> monsterListOfList = LoadMonsterListOfList();

IEnumerable<Monster> monsters = ???

これはLINQを使って一行で書けます。

List<List<Monster>> monsterListOfList = LoadMonsterListOfList();

IEnumerable<Monster> monsters = monsterListOfList.SelectMany(it => it);

SelectManyを使えば、リストのリストの平滑化は実現することできます。ですが、上記のSelectManyのコードは 「平滑化する」という意図が伝わりにくいと思います。


最後の例です。

IEnumerable<Monster>なmonstersがあります。 これが空かどうかを調べたいです。 どんなコードを書けばいいですか?

IEnumerable<Monster> monsters = LoadMonsters();

bool isEmptyMonsters = ???

これもLINQで書けます。しかし、゙注意が必要です。

IReadOnlyList<Monster> monsterList = LoadMonsterList(); // 良い例

bool isEmptyMonsters = !monsterList.Any();

// 悪い例
// DBから読み場合最後まで読むという無駄な処理が必要
// 無限なシーケンスだと終わらない
// bool isEmptyMonsters = monsterList.Count() == 0;

コメントにもある通り、IEnumerable<T>のシーケンスがから空かどうかの判定は、Countではなく、Anyを使うべきです。

LINQおよび配列などのメソッド・プロパティとして「空かどうかを判別する」ためのメンバは存在しません。



  • モンスターのリストから一番高いレベルのモンスターを探したい

  • モンスターのリストを先頭からn個ごとにまとめたい

  • モンスターのリストのリストを平滑して、モンスターのシーケンスを作りたい

  • モンスターのリストが空かどうかを判別したい

ゲーム開発、ウェブ開発、モバイル開発、デスクトップアプリ開発などにおいて、このような処理をやりたい状況があると思います。


  • モンスターのリストから一番高いレベルのモンスターを探したい

  • モンスターのリストを先頭からn個ごとにまとめたい

残念ながらこれらができるLINQはありません。このような処理ができるメソッドがあれば嬉しくないですか?


  • モンスターのリストのリストを平滑して、モンスターのシーケンスを作りたい

  • モンスターのリストが空かどうかを判別したい

これらはLINQで書けますが、もっとわかりやすくならないでしょうか?


他の言語にはある


  • モンスターのリストから一番高いレベルのモンスターを探したい

  • モンスターのリストを先頭からn個ごとにまとめたい

  • モンスターのリストのリストを平滑して、モンスターのシーケンスを作りたい

  • モンスターのリストが空かどうかを判別したい

Java、Kotlin、Scala、Groovy、F#、Haskellなど他のプログラミング言語にはこれらができるコレクション機能があります。

しかし、C#・LINQにはそのようなメソッドがありません。

他の言語のコレクションメソッド群と比べて 、「C#・LINQがちょっと弱いのでは?負けているのでは?」と感じてしまいました。また、もっとC#・LINQをもっと改善したい、拡充したいと思いました。


ということで作った!

ということで作りました!

ImportedLinq」というライブラリです。

『「他の言語のコレクションで一般的だけど、C#・LINQにはないメソッド」を、 他のプログラミング言語から輸入(Imported)して、 C#・LINQにフィットするようにしたコレクションメソッドライブラリ』

です。


ImportedLinqの利用例を紹介!

それでは、「ImportedLinq」の利用例を紹介します。


「MaxBy」を使ってモンスターのリストから、一番高いレベルのモンスターを探すコード。

IReadOnlyList<Monster> monsterList = LoadMonsterList();

// MaxByは一番大きい要素(複数)を探すメソッド
IReadOnlyCollection<Monster> maxLevelMonsters = monsterList.MaxBy(it => it.Level);


「Buffer」を使って モンスターのリストを、8個ごとにグループ分けするコード。

IReadOnlyList<Monster> monsterList = LoadMonsterList();

// Bufferは先頭から指定数ごとにまとめるメソッド
IEnumerable<IReadOnlyList<Monster>> bufferedMonsters = monsterList.Buffer(8);


「Flatten」を使ってモンスターのリストのリストを平滑化し、IEnumerableを作るコード。

List<List<Monster>> monsterListOfList = LoadMonsterListOfList();

// Flattenは、リストのリストなどをIEnumerable<T>に平滑化するメソッド
// 「平滑化するよ」ということがメソッド名から伝わる
IEnumerable<Monster> monsters = monsterListOfList.Flatten();


「IsEmpty」を使ってIEnumerableなmonstersが、空どうかを調べるコード

IEnumerable<Monster> monsters = LoadMonsterList();

// メソッド名が超わかりやすい!
// 無限シーケンスでも終わるし、余分な処理しない
bool isEmptyMonster = monsters.IsEmpty();



  • モンスターのリストから一番高いレベルのモンスターを探したい

  • モンスターのリストを先頭からn個ごとにまとめたい

上記の処理はLINQを使って書くことができませんが、ImprotedLinqを使えば一発で書くことができます。


  • モンスターのリストのリストを平滑して、モンスターのシーケンスを作りたい

  • モンスターのリストが空かどうかを判別したい

上記の処理は、ImprotedLinqを使えばより読みやすいコードで実装することができます。


「便利なコレクションメソッドの詰め合わせ」ではない

ImportedLinq」は 「便利なコレクションメソッドの詰め合わせ」 ではありません。

『「ImportedLinq」は「他の言語のコレクションで一般的だけど、C#・LINQにはないメソッド」を、 他のプログラミング言語から輸入(Imported)して、 C#・LINQにフィットするようにしたコレクションメソッドライブラリ』です。

「他の言語の便利なコレクションメソッドを、C#でも使いたい」というのが目的です。

また

「他の言語に慣れている人の「C#になんでこのコレクションメソッド無いの」を解決したい」というのも目的です。

今後、他の言語で活用されていて、C#でも活躍する場面がありそうなメソッドがあれば、追加していきたいと思います。


他のコレクションライブラリと比べて

C#におけるコレクションライブラリ、「ImportedLinq」以外にもあります。たとえば次の二つです。


  • Ix.NET

  • MoreLINQ

ImportedLinqはこれらとは目的やモットーが異なります。


Ix.NETはRx.NETのIEnumerable版です。

Ix.NETはRx.NETにないメソッドは入りません。「こんなメソッドがあればいいのに」と思ってもRx.NETになければ入らないのです。


MoreLINQは便利なメソッドがたくさんあるライブラリです。

覚えきれないくらいたくさんあります。しかし、現段階ではFlattenとかIsEmptyなどは実装しないそうです。詳しくは、こちらをご覧ください。


繰り返しになりますが、ImportedLinq

『「他の言語のコレクションで一般的だけど、C#・LINQにはないメソッド」を、 他のプログラミング言語から輸入(Imported)して、 C#・LINQにフィットするようにしたコレクションメソッドライブラリ』

というライブラリです。


使ってみてね!ImportedLinq!

ソースコードはこちら。他のメソッドの紹介もあります!

NuGetはこちら

Unityでの利用は、こちらからunitypackageをダウンロードできます。

使ってみてください!