Java8のStreamを使いこなすをC#で
さて、C#で関数型っぽいことをやって遊んでみたわけですが、恥ずかしくなって猫の写真に置き換えました。
C#で実際に使うのは、LINQです。
ということで、LINQの使い方をひととおり見てみます。
基本
LINQの中核となるのはIEnumerable<T>
インタフェースです。これがJava8のStream
に相当します。
さて、IEnumerable<T>
インタフェースにはForEach
拡張メソッドが用意されていません。これはFAQで、意図的に入れていないということです。
LINQとはズレますが、List<T>
クラスにはForEach
メソッドがありますから、こっちを使いましょう。ええ、クラスです。インターフェースではありません。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}.ToList(); names.ForEach(s => Console.WriteLine(s));
これで次のように表示されます。
hoge hoge foo bar naoki kishida
foreach
文だと次のように書きます。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}.ToList(); foreach(var s in names) { Console.WriteLine(s); }
C#ではラムダを使わなくても型推論が効くので、わかっている型を書く必要がありません。これはなかなか便利です。
また、今回のように、値をそのまま別のメソッドに渡すという場合にはメソッド参照が使えます。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}.ToList(); names.ForEach(Console.WriteLine);
シーケンスにしてmapやfilter
さて、上記のForEach
は、すべての値にたいしてそのまま処理をする場合には使えるのですが、実際には値を加工したり、条件によって処理する値を絞ったりということが必要になります。
そのような場合、Java8ではList
などをStream
に変換して処理する必要がありますが、LINQの場合は配列やジェネリックコレクションはIEnumerable<T>
インタフェースを実装していますから、普通は変換しないでそのまま拡張メソッドを呼び出します。
たとえば、値を加工する場合にはSelect
拡張メソッドを使って次のようになります。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; names.Select(s => "[" + s + "]") .ToList() .ForEach(Console.WriteLine);
これで次のように表示されます。
[hoge hoge] [foo bar] [naoki] [kishida]
条件によって処理する値を絞る場合は次のようにWhere
拡張メソッドを使います。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; names.Where(s => s.length() > 5) .Select(s => "[" + s + "]") .ToList() .ForEach(Console.WriteLine);
こうすると、長さが5文字より長い値だけ、「[〜]」で囲まれて表示されます。
[hoge hoge] [foo bar] [kishida]
コレクションの判定
プログラムを書いていると、コレクションのすべての要素が条件をみたしてるかとか、逆に条件をみたした要素がひとつもないかとか、ひとつでもあるかとかを判定することがあります。
C# 1.2まででは、フラグを用意してループをまわしてとめんどうな記述が必要でしたが、C# 2.0にはList<T>
クラスに[http://msdn.microsoft.com/ja-jp/library/kdxe4x4w(v=vs.80).aspx:title=TrueForAll]
メソッドや[http://msdn.microsoft.com/ja-jp/library/bfed8bca(v=vs.80).aspx:title=Exists]
メソッドが……
はいはい、クラスじゃなくてインターフェースを対象とするんですね。C# 3.0以降でできるようになりました。
すべての要素が条件をみたしているかどうか判定する場合にはAll
拡張メソッドを使います。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; Console.WriteLine(names.All(s => !string.IsNullOrEmpty(s))); //True
ひとつでも条件を満たす要素があるかどうか判定するにはAny
拡張メソッドを使います。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; Console.WriteLine(names.Any(s => s.Length > 7)); //True
条件を満たす要素がひとつもないことを判定するにはAny
拡張メソッドを使えばいいと思います。論理の初歩です。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; Console.WriteLine(!names.Any(s => s.StartsWith("A"))); //True
コレクター
シーケンスからひとつのオブジェクトを得る場合にコレクターは用意されていません。というか、コレクターという概念はありませんが、同じことはやれます。
たとえば、すべての文字列を連結した文字列を得るには、String.Concat
静的メソッドやString.Join
静的メソッドが使えます。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; Console.WriteLine(String.Join(":", names));
次のようにすると、文字数の合計がとれます。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; Console.WriteLine(names.Sum(s => s.Length)); //28
Sum
拡張メソッドにはint
、long
、float
、double
、decimal
などをとるオーバーロードが用意されているので、キャストは不要です。
数値を合計するとか平均をとる場合には、int
などの数値型シーケンスに変換してもいいですが、実際には変換しないほうが便利です。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; Console.WriteLine(names.Where(s => s.Length).Sum()); //28
シーケンスをList<T>
クラスに変換する必要があることも多いと思いますが、その場合にはToList
拡張メソッドを使うというのは最初から見てますね。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; var converted = names.Where(s => s.Length > 5) .Select(s => "(" + s + ")") .ToList(); Console.WriteLine(converted); // System.Collections.Generic.List`1[System.String]
List<T>
クラスはToString
をオーバーライドしていません。残念でした!(じゃあやるなよ)
便利そうなのがGroupBy
拡張メソッドです。GroupBy
で得た値をキーにして、値を格納したシーケンスを割り当てたMapのようなものが作成されますが、実態はMapではなく、[http://msdn.microsoft.com/ja-jp/library/bb344977.aspx:title=IGrouping<K,V>]
インターフェースのシーケンスです。
var names = new[] { "hoge hoge", "foo bar", "naoki", "kishida" }; names.GroupBy(s => s.Length) .ToList() .ForEach(x => Console.WriteLine(x.Key + ":" + string.Join(",", x)));
これは次のようになります。
9:hoge hoge 7:foo bar,kishida 5:naoki
Java8の例はなんで逆順になるんでしょうね?
ファイルとシーケンス
ファイルを扱うにはFileStream
クラスがあったわけですが、C#でテキストファイルの読み書きをするときには別のクラスを使うことの方が多いでしょう。
実際、[http://msdn.microsoft.com/ja-jp/library/3saad2h5.aspx:title=System.IO.File]
クラスという静的ユーティリティクラスがあるので、ちょっとした処理であればこのクラスのメソッドで事足ります。
例えば、次のようなlines.txt
というファイルがデスクトップにあるとします。
SCREEN 2; FOR I=0 TO 100 LINE (RND(1)*128,RND(1)*96)-(RND(1)*128+128,RND(1)*96+96),15 NEXT I GOTO 50
これを表示しようとすると、次のようになります。
var path=@"C:\User\naoki\Desktop\lines.txt"; Console.Write(File.ReadAllText(path));
今回はサンプルなので行数の短いファイルですが、一般にはファイルの行数はためしに表示するには結構長いので、最初の10行などを表示したくなります。その場合も、File
ユーティリティクラスのメソッドが使えます。
var path=@"C:\User\naoki\Desktop\lines.txt"; File.ReadLines(path) .Take(10) .ToList() .ForEach(Console.WriteLine);
C#では全ての例外が非検査例外です。JavaのIOException
はもちろん検査例外ですが、そういえば元コードではどのメソッドが例外を投げるんでしょうね?
ところで、FileStream
などのStream
派生クラスにも、FileReader
などのTextReader
派生クラスにも、シーケンス(IEnumerable<T>
インターフェース)に変換するメソッドは用意されていません。
zipでふたつのシーケンスを統合する
プログラムを組んでいると、ふたつのリストの同じ位置の値を統合して処理するということが、たまに必要になりました。シーケンスを使っていると、ふたつのシーケンスを統合したいということも多くなります。
そこで使えるのが、IEnumerable<T>
インターフェースのZip
拡張メソッドです。こいつはC# 4.0から使えるようになりました。
例えば次のように使います。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; var indexes = new[] {10, 20, 30, 40}; indexes.Zip(names, (idx, line) => string.Format("{0,3} {1}", idx, line)) .ToList() .ForEach(Console.WriteLine);
結果は次のようになります。
10 hoge hoge 20 foo bar 30 naoki 40 kishida
このような場合、Enumerable.Range
が使えないこともないですが、Range
メソッドは引数をふたつしか取らないので、ちょっと不便です。
インデックスが欲しいだけなら、Select
拡張メソッドで十分でしょう。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; names.Select((idx, line) => string.Format("{0,3} {1}", idx*10+10, line)) .ToList() .ForEach(Console.WriteLine);
上の例のidx
は0 Originです。
無限シーケンス
ところで、今回はnames
の要素数がわかっているので、あらかじめ範囲を決めたインデックス配列を用意することができました。けれども、ファイルから読み込んだシーケンスと対応づける場合などは、要素数がいくつあるかわからないので、あらかじめ範囲を決めておくことができません。そんな時でもさっきのSelect
拡張メソッドなら問題ありませんが、麻疹にかかっているラムダ厨にとってはちょっとかっこ悪いです。
そこでむりやり使いたいのが無限シーケンスです。Iterate
メソッドはyield
を使って自分で実装しましょう。
次のように、Take
などと組み合わせて使います。
static IEnumerable<T> Iterate<T>(T seed, Func<T, T> f) { for(var i=seed; ; i=f(i)) { yield return i; } } static void Main() { Iterate(10, i => i + 10).Take(5).ToList().ForEach(Console.WriteLine); }
このような表示になります。
10 20 30 40 50
Zip
拡張メソッドはどちらかのシーケンスが終われば処理を終わるので、有限なシーケンスと無限シーケンスを組み合わせると便利です。そうすると、ファイルから読み込んだシーケンスとの結合も要素数を気にすることなく次のように書けます。
var path=@"C:\User\naoki\Desktop\lines.txt"; Iterate(10, idx => idx + 10) .Zip( File.ReadLines(path), (idx, line) => string.Format("{0,3} {1}", idx, line)) .ToList() .ForEach(Console.WriteLine);
次のような表示になります。
10 SCREEN 2; 20 FOR I=0 TO 100 30 LINE (RND(1)*128,RND(1)*96)-(RND(1)*128+128,RND(1)*96+96),15 40 NEXT I 50 GOTO 50
あと、無限シーケンスでは次のような遊びをすることも多いですね。
Iterate(321, i => (i * 211 + 2111) % 1999) .Take(10) .ToList() .ForEach(Console.WriteLine);
ランダムっぽい値が出力されます。
321 1876 146 933 1073 628 686 930 440 998
flatMap
いくつかのリストを組み合わせてひとつのリストを作りたい場合に使えるのは、JavaやScalaではflatMap
ですが、C#ではSelectMany
拡張メソッドです。
たとえば、要素の文字列をスペースで分割してそれぞれを要素としたリストを作る、という場合には次のように書きます。
var names = new[] {"hoge hoge", "foo bar", "naoki", "kishida"}; names.SelectMany(s => s.Split(' ')) .ToList() .ForEach(Console.WriteLine);
次のような表示になります。
hoge hoge foo bar naoki kishida
reduceでひとつにまとめる
ここまでで、たとえばAny
拡張メソッドやSum
拡張メソッドなど、シーケンスの値をまとめてひとつの値を返すメソッドがありました。
このようなメソッドを一般化したものがJavaではreduce
メソッドですが、C#ではAggregate
拡張メソッドです。
たとえば、Any
拡張メソッドをAggregate
拡張メソッドで置き換えると次のように書けます。
var names = new[] { "hoge hoge", "foo bar", "naoki", "kishida" }; Console.WriteLine(names.Select(s => s.Length > 7).Aggregate((l, r) => l || r));
戻り値は普通にbool型になっているので、そのまま値を取り出せます。
もちろん、Any
拡張メソッドはtrue
が最初に来た時点で処理を打ち切るはずですが、結果としては同じものになります。
ところで、Aggregate
拡張メソッドは引数を1つとるもの、2つとるもの、3つとるものがあります。引数を1つとるものはサンプルをあげましたが、2つとるものもほぼ同様の書き方になります。戻り値は値が直接返ります。
var names = new[] { "hoge hoge", "foo bar", "naoki", "kishida" }; Console.WriteLine(names.Select(s => s.Length > 7).Aggregate(false, (l, r) => l || r));
ここでAggregate
拡張メソッドの最初の引数には、「単位元」となるものを指定する必要があります。「単位元」というのは、続く演算の片方の引数に与えたとき、もう片方の引数がそのまま返るような値です。
たとえば、足し算の場合は0が単位元になります。掛け算の場合は1が単位元になります。ここでは論理和のor演算を行っているので、false
が単位元になります。
引数を3つとるAggregate
拡張メソッドも別に使い方は同じです。
引数3つのAggregate
拡張メソッドの最初の引数には、引数2つのときと同じく単位元を渡します。
2つ目のメソッドには、途中過程のオブジェクトとシーケンスの要素オブジェクトが渡されるので、これらから新たな途中過程を生成します。ここで、渡された途中過程のオブジェクトの状態を変えても問題はありません。
3つ目のメソッドには、シーケンスのすべての要素が反映された途中過程のオブジェクトが渡されるので、これから最終的な演算結果を返します。
並列シーケンス
並列シーケンスに対して並列処理を行う Parallel LINQというのが用意されていますが、それを書き始めるとAggregate
だけの話じゃすまなくなるので、今回は省略したいと思います。
一つ注意しなければいけないのは、並列化すれば速くなるというものではなく、むしろ遅くなるケースも多いこと。
並列シーケンスで何ができるかをざっくり知りたい人は「データ並列パターンと PLINQ」を、どういうときに遅くなってしまうのかを知りたい人はPLINQ での高速化についてとPLINQ の非利便性を読むといいでしょう。
まとめ
Java 8がきたら、C#/Scala/Python/Ruby/F#でデータ処理はどう違うのか? (1/3):特集:人気言語でのデータ処理の比較 - @ITに追加することができますね。
ところで、C#でラムダ式とかLINQとかがサポートされてから結構長く経ちました。いまさらコーディング規約で禁止したりするのは頭が悪いとしか。