平々毎々(アーカイブ)

はてなダイアリーのアーカイブです。

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拡張メソッドにはintlongfloatdoubledecimalなどをとるオーバーロードが用意されているので、キャストは不要です。

数値を合計するとか平均をとる場合には、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#では全ての例外が非検査例外です。JavaIOExceptionはもちろん検査例外ですが、そういえば元コードではどのメソッドが例外を投げるんでしょうね?

ところで、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

いくつかのリストを組み合わせてひとつのリストを作りたい場合に使えるのは、JavaScalaでは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とかがサポートされてから結構長く経ちました。いまさらコーディング規約で禁止したりするのは頭が悪いとしか。