はじめに
次の2つのようなコードを書いたことはありませんか?
listのインデックスが奇数番目の要素だけを抽出するコードです。
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var result = new List<string> ();
for(int i = 0; i < names.Length; i++)
{
if(i % 2 == 1) {
result.Add(names[i]);
}
}
listのインデックスを文字列の頭に連結して、新たな文字列のリストに射影するコードです。
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var result = new List<string> ();
for(int i = 0; i < names.Length; i++)
{
result.Add (string.Format("index{0} {1}", i, names[i]));
}
for文やListのインデクサーを用いていますね。これをLINQで書き換えてスッキリさせてみせましょう。抽出はWhereというメソッド、射影はSelctというメソッドを使えばいいですね。
ですが上のコードでは、抽出にも射影にもインデックスを用いてます。for文で定義し、ループの度にインクリメントされるインデックスiは使えるのでしょうか。
ちょっと前に、LINQ、そのWhere本当に必要ですか?というタイトルで、Count、First、AnyというLINQのメソッドの便利なオーバーロードについて書きました。実は、WhereとSelectにもオーバーロードが用意されていて、それを使えばインデックスも用いて抽出、射影を行うことが可能です。
この投稿では、インデックス付きで抽出、射影を行う方法。その使用例や別アプローチ、別の言語(groovyとscala)での似たメソッドを紹介します。
インデックス付きWhereのオーバーロード
まず、Whereを見てみます。
まずインデックスを使わないWhere
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var result = names.Where (name => name.Length < 5).ToList ();
result.ForEach(Console.WriteLine);
上記のようなWhereはよく使うかと思います。
List<T>(IEnumelable<T>)の要素自体を条件判定に用いて、条件を満たしたものだけ抽出しています。
上記のコードでは、5文字未満の文字列要素だけ抽出していて、Taro、Jiro、Goroと表示されます。
インデックスを使うWhere
Whereにはオーバーロードがあって、要素とインデックスの両方を抽出の条件判定に使えます。(リファレンスはこちら)
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var result = names.Where ( (name, index) => index % 2 == 1).ToList ();
result.ForEach(Console.WriteLine);
この例では、namesの要素の内、インデックスが奇数の要素を抽出していて、Jiro、Shiro、Rokuroと表示されます。
ここでは、抽出の条件にindexしか使おらず、nameは無視しています。もちろん条件にindexとname(要素)の両方を使うことも可能です。
Whereを使わずインデックスで抽出(Skip, Take)
インデックスを用いて抽出したい場合は、次のようなものがよくあるパターンだと思います。
-
最初の20個だけ抽出
-> OrderByメソッドなどで並び替え済みで、上位20件が欲しい
-
10個飛ばして5個を抽出(インデックスが10から14)
-> 1ページに表示できるのが5件ずつで、3ページ目(インデックスは2)を表示
これら場合、Whereを用いることももちろん可能なのですが、SkipとTakeを使う方が読みやすくなり、処理も早く終わることがあります。
最初の20個の抽出は,
int displayCount = 20;
var result = list.Take(displayCount);
10個飛ばして5個を抽出(インデックスが10から14)は
int countPerPage = 5;
int pageIndex = 2;
var result = list.Skip(countPerPage * pageIndex).Take(countPerPage);
のようになります。
じゃあ、いつ使う?
では、このようなインデックス付きでWhereは、いつ使うか考えてみます。何か思いついたら、コメントしていただけると嬉しいです。正直あまりいい例が思い浮かびません。
ひねり出すとしたら、
- インデックスが偶数、奇数、もしくはNの倍数のものを抽出
- ランダムに作ったインデックスのセット、そのセットにインデックスが含まれているものは抽出
などでしょうか。
インデックス付きSelectのオーバーロード
次に、射影を行うメソッドSelectです。
まずインデックスを使わないSelect
次は名前のリスト(List<String>型のnames)を、その名前の文字数に射影するコードです。
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var result = names.Select ( name => name.Length).ToList ();
result.ForEach(Console.WriteLine);
これはインデックスを使わない例です。
インデックスを使うSelect
この投稿の最初に挙げた例は、人名が入ったlistを、インデックスを文頭に付けた文字列のリストに射影していました。これをSelectのオーバーロードを使って書いてみます。
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var result = names.Select ( (name, index) => string.Format("index{0} {1}", index, name)).ToList ();
result.ForEach(Console.WriteLine);
このようにfor文を書く必要なくインデックスと共に要素を射影できました。
匿名型と一緒に使うことも
void SampleMethod(string name, int index){
//...略...
}
このようなメソッドがあったとします。
これをnamesというリストの要素とそのインデックスを引数に渡して呼び出したいです。
LINQを使わない場合こんな感じですね。
var names = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
for(int i = 0; i < names.Length; i++)
{
SampleMethod(names[i], i);
}
これ、こんな感じのバグが入り込む可能性があります。
var boys = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var girls = new List<string> {"Haruko", "Natsuko", "Akiko", "Huyuko"};
for(int i = 0; i < girls.Length; i++)
{
SampleMethod(boys[i], i);
}
ちょっとわざとらしい例でしたね。本当は、
for(int i = 0; i < boys.Length; i++)
とする必要がありました。
このような場合バグはForeach文を使えば防げますが、ここではindexが必要です。ここでSelectのインデックスも使うオーバーロードが役に立ちます。また、匿名型も使います。(匿名型はめちゃくちゃ便利でLINQで活躍する場面が多いです。以前、C#の匿名型について調べてみたという投稿をしたので興味がある方は見てください。)
foreach(var element in boys.Select((name, index) => new {name, index})) {
SampleMethod(element.name, element.index);
}
このようにSelectで匿名型(ここではstring型のnameとint型のindexというプロパティを持つ匿名型)に射影して、それをforeach文で回せば、SampleMethodを呼び出すことが可能です。
こんな場合はSelectはいらない、Zipでいい
二つのリストがあり、同じindex同士でペアを作ります。LINQを使わずまずやってみます。
var boys = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var girls = new List<string> {"Haruko", "Natsuko", "Akiko", "Huyuko"};
for (int i = 0; i < boys.Count; i++) {
Console.WriteLine (string.Format ("pair : {0} and {1}", boys [i], girls [i]));
}
foreach文でなくて、for文を用いており、2つのリストにインデクサーでアクセスしています。おかしところがありますね。が、1回置いておきます。
これをforeach文に書き換えてみます。boysをイテレートすればいいのですが、同じindexがgirlsの要素にアクセスするためにインデックスが必要です。そのためindex付きSelect使ってみます。
var boys = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var girls = new List<string> {"Haruko", "Natsuko", "Akiko", "Huyuko"};
foreach (var element in boys.Select((boy, index) => new {boy, index})) {
var boy = element.boy;
var girl = girls[element.index];
Console.WriteLine (string.Format ("pair : {0} and {1}", boy, girl));
}
いまいちイケていませんね。むしろ読みづらくなった気もします。
foreach文になりましたが、girlsの要素にはインデクサーでアクセスしています。
さて、この二つのプログラム、とてもまずいです。
boysの要素数が6、girlsの要素数が4なので、girlsの5番目の要素にアクセスした際、System.ArgumentOutOfRangeException例外が投げられてしまいます。
このように、二つのリストがあり同じインデックスの要素同士でペアを作ったり、同じインデックスの要素同士で同時に処理をしたりする際に便利なメソッドがあります。上記の2つの例のように、リストの要素にインデクサーでアクセスする必要もなく、System.ArgumentOutOfRangeExceptionも心配する必要ありません。
Zipメソッドを使います。
var boys = new List<string>{"Taro", "Jiro", "Saburo", "Shiro", "Goro", "Rokuro"};
var girls = new List<string> {"Haruko", "Natsuko", "Akiko", "Huyuko"};
foreach (var pair in boys.Zip(girls, (boy, girl) => new { boy, girl })) {
var boy = pair.boy;
var girl = pair.girl;
Console.WriteLine (string.Format ("pair : {0} and {1}", boy, girl));
}
スッキリしましたね。インデクサーでのアクセスも無くなりました。この例ではShiroとHuyukoのペアが最後のペアです。(GoroとRokuroはあぶれてしまいました。)Zipで合体したペア数は要素の少ないペア数に勝手になってくれます。System.ArgumentOutOfRangeExceptionが発生しないように、リスト長の大小比較などは必要ありません。
この用に2つのリスト(実際はIEnumerable<T>)を同じインデックスの要素同士でペアを作って何か処理したい場合、for文やインデクサーを使う必要も、インデックスを得るためにSelectをインデックスを使えるオーバーロードを使う必要もありません。Zipメソッド、便利ですね。
いつ使う?
この節は完全な個人的な意見です。
インデックス付きのWhereメソッドよりは、インデックス付きのSelectメソッドは使う機会が多いかなと思います。
IEnumerable<T>型を実装したList<T>型などのインスタンスを、GroupByメソッドなどで並び替えます。その結果のIEnumerable<T>中の要素のインデックスは、シーケンス中の順位として、とても価値があるもので、利用シーンも多いとと思います。
ただそれ以外は、あまり多くはないかとも思います。
他の言語はどうだろう?
GroovyとScalaとJava8を少し調べてみました。インデックス付きの抽出や射影の似たメソッドはあるのでしょうか。各言語でのコレクション型と関数オブジェクトを用いた、インデックス付きの抽出や射影があるのか、ないならば実現できるのかなどを少し調べてみました。
それぞれの言語でのコレクション、関数オブジェクトの実装や設計思想、文化が違うので、単純に比較してもあまり意味は無いかもしれませんが。
※ それぞれの言語に詳しい方、間違いの指摘やご意見、不正確な点の指摘ありましたらよろしくお願いします。
Groovy
Groovyで射影はcollectメソッドなどで行うようです。ですがcollectには、C#のSelectのようにインデックスも使えるオーバーロードは無いようです。
同様に抽出はfindAllで行えるようですが、これもインデックス付きのものは無いようです。
しかし射影・抽出を行うメソッドではないですが、C#のListクラスのForEachメソッドに似たコレクションの要素をイテレートできる、eachメソッドとeachWithIndexというメソッドがあるようです。eachWithIndexは、要素とインデックスを両方扱えます。繰り返しになりますが、これは射影・抽出を行うメソッドではないです。(C#のListのForEachは返り値void、GroovyのeachとeachWithIndexはそのメソッドを呼び出したインスタンス自身のようです。)
一方で、Groovyはメタプログラミングが非常にやりやすく、自分でcollectWithIndexのようなメソッドを定義して、それを使えるようにすることもできるようです。
こちらのStackOverFlowの解答でコードが載っています。
リンク先のものを用いれば、1つのメソッドでインデックス付きの射影を行うことが可能だと思います。
[追記]
GroovyのListのtransposeメソッドで、zipとかcollectionWithIndexとかというタイトルで、インデックスも使えるSelectに該当するcollectWithIndexやScalaのzipWithIndexを実装できる、ということをまとめました。
Scala
scala.collection.immutable.Listクラスについて調べました。
抽出はfilterメソッド、射影はmapメソッドで行うようです。
C#のWhereやSelectのように、インデックスを同時に扱えるオーバーロードは無いようです。
ListにはzipWithIndexというメソッドがあるようです。Listでこのメソッドを呼び出すことで、要素とそのインデックスのタプルを要素とするListを新たに作れるようです。
C#のSelectやWhereのように1つのメソッドではできませんが、
zipWithIndexで作ったListにmapやfilterを使えば同様なことができると思います。
Java8
分かる方、よかったらコメントをお願いします!
Java8で加わったStream APIでできるか調べてみました
射影はmap、抽出はfilterでできるようです。
ですが、インデックス付きは見つけられませんでした。
こちらのStackOverFlowの質問への解答で、Utilityクラスを実装して、インデックス付きのマップや、ScalaのListのzipWithIndexのようなメソッドを実装している方がいました。
標準APIいくつかの組み合わせでは実現できないのでしょうかね?
個人的な意見とまとめ
さて、そもそもインデックス付きの射影や抽出は必要でしょうか?C#の場合ですが、射影も抽出もそれぞれインデックスつきで行えるオーバーロードが標準で用意されていますね。
インデックス付きの抽出(Where)の方は、あまり利用するシーンは多くはないかなと感じます。
GroupByメソッドで並び替えたあと、そのインデックスを用いて射影(Select)することは利用価値も利用頻度も高いかと思います。
ですが、他のメソッドで代用できたり、他のメソッドの方が読みやすいことも多いと思います。
また、インデックス付きのWhereやSelectがオーバーロードで用意されていますが、個人的にはWhereWithIndexやSelectWithIndexという別のメソッドの名前の方がわかりやすいかと思います。
Groovy、Scala、Java8のそれぞれのコレクション・関数オブジェクトのAPIで、C#のインデックス付きのWhereとSelectに該当するものは無いようです。ですが、メソッドの組み合わせクラス・メソッドを自作することで同様な処理ができると思います。そもそも各言語でコレクション・関数オブジェクトの実装方針や文化、そして理念がそれぞれ違うということを忘れてはいけないと思います。
「どうしても、インデックスが必要だ。」となった時にWhere・Selectにはオーバーロードがあるということは覚えておかなくては、と思いました。