Archive - C#
T4による不変オブジェクト生成のためのテンプレート
- C# - 10.08/30
不変欲しい!const欲しい!readonlyをローカル変数にもつけたい!という要望をたまに見かけるこの頃。もし、そういった再代入不可というマークがローカル変数に導入されるとしたら、readonlyの使い回しだけは勘弁です。何故って、ローカル変数なんて大抵は再代入しないので、readonly推奨ということになるでしょう、そのうちreadonly付けろreadonly付けろというreadonly厨が出てくるのは目に見えています。
良いことなら付ければいいじゃない、というのはもっともですが、Uglyですよ、視覚的に。readonly var hoge = 3 だなんて、見たくはない。頻繁に使うほうがオプションで醜く面倒くさいってのは、良くないことです。let hoge = 3 といったように、let、もしくはその他のキーワード(valとか?)を導入するならば、いいかな、とは思いますが。
それに、ただ単にマークしただけじゃあ不変を保証するわけでもない……。例えばListなんてClearしてAddRangeしたのと再代入とは、どう違うの?的な。難しいねえ。そんなimmutableの分類に関してはufcppさんのimmutableという記事が、コメント欄含め参考になりました。
そうはいっても、そんなにガチに捉えなくても、不変にしたいシチュエーションはいっぱいあります。実はオブジェクト指向ってしっくりきすぎるんです! 不変オブジェクトのすゝめ。 - Bug Catharsis。おお、すすめられたい。ところでしかし、こういう時にいつも疑問に思っているのは、生成どうすればいいのだろう、ということ。今のところ現実解としてあるのはreadonly、つまり、コンストラクタに渡すしかないのですが……
public Hoge(int a, int b, int c, string d, string e, DateTime f, .....
破綻してる。こんなクソ長いコンストラクタ見かけたら殺していいと思う。全くもって酷い。さて、どうしましょう。こういう場合はビルダーを使いましょう、とはEffective Javaが言ってますので(私、この本あんま好きじゃないんだよねー、とかはどうでもいいんですがー)とりあえずストレートに従ってみます。
// あまり行数使うのもアレなので短くしますが、実際は10行ぐらいあると思ってください Hoge hoge = new HogeBuilder() .Age(10) .Name("hogehoge") .Build();
まあ、悪くない、ですって?いえいえ、これはBuilder作るの面倒くさいし、第一Java臭い。メソッドチェーンだからモダンで素敵、と脳が直結してる人は考えが一歩足らない。むしろ古臭い。最近は流れるようなインターフェイスとかも割と懐疑的で、私は。頂くのはアイディアだけであって、書き方に関しては、各言語にきっちり馴染ませるべき。先頭の大文字小文字だけ整えて移植だとか、愚かな話。というわけで、C#ならオブジェクト初期化子を使おう。
var hoge = new HogeBuilder { Age = 10, Name = "hogehoge" }.Build(); // 暗黙的な型変換を使えばBuildメソッドも不要になる(私はvarのほうが好みですが) Hoge hoge = new HogeBuilder { Age = 10, Name = "hogehoge" };
ええ、これなら悪くない。オブジェクト初期化子は大変素晴らしい(本当にそろそろModern C# Designをですね……)。ビルダーを作る手間もJava方式に比べ大幅に軽減されます(set専用の自動プロパティを用意するだけ)。それにIntelliSenseのサポートも効きます。
未代入のもののみリストアップしてくれる(Ctrl+Space押しだと全部出てきたりする、バグですかね、困った困った)。そういえばで、これは、不変である匿名型の記法とも似ています。余分なのは.Build()だけで、書く手間的にはそんな変わらない。
前説が長ったらしくなりました。本題は「匿名型のような楽な記法で不変型を生成したい」が目標です。C#の現在の記法では、それは無い。欲しいなあ。名前付き引数使えば似たような雰囲気になると言えばなるんですが、アレ使うと「省略可」な雰囲気が出てダメ。ビルダーで作りたいのは、原則「省略不可」なので。
なければ作ればいいじゃない、オブジェクト初期化子を使って.Buildで生成させるビルダーを作れば似たような感じになる。あとは、手動でそれ定義するの非常に面倒なので、そう、T4で自動生成しちゃえばいいぢゃない。
以下コード。例によってパブリックドメインで。別にブログにベタ貼りなコードは自明でいいんじゃないかって気もするんですが、宣言は一応しておいたほうがいいのかなー、と。
<#@ assembly Name="System.Core.dll" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ output extension="Generated.cs" #> <# // 設定:クラス名はそのまま文字列で入力 // クラスの持つ変数は、コンストラクタに書くみたいにdeffinesに // "string hoge","int huga" といった形で並べてください // usingとnamespaceは、直下の出力部を直に弄ってください // partial classなので、これをベースにメソッドを足す場合は別ファイルにpartialで定義することを推奨します // Code Contractsに関わる部分は(ContractVerification属性とContract.EndContractBlock())は、 // 対象がWindows Phone 7などContractが入っていない環境下では削除してください(通常の.NET 4環境では放置で大丈夫) var className = "Person"; var deffines = new DeffineList { "string name", "DateTime birth", "string address" }; #> using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Contracts; namespace Neue.Test { [DebuggerDisplay(@"<#= deffines.DebuggerDisplay #>", Type = "<#= className #>")] public partial class <#= className #> : IEquatable<<#= className #>> { <# foreach(var x in deffines) {#> [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly <#= x.TypeName #> <#= x.FieldName #>; public <#= x.TypeName #> <#= x.PropName #> { get { return <#= x.FieldName #>; } } <# } #> private <#= className #>(<#= deffines.Constructor #>) { <# foreach(var x in deffines) {#> this.<#= x.FieldName #> = <#= x.FieldName #>; <# } #> } [ContractVerification(false)] public static implicit operator Person(Builder builder) { return builder.Build(); } public bool Equals(<#= className #> other) { if (other == null || GetType() != other.GetType()) return false; if (ReferenceEquals(this, other)) return true; return EqualityComparer<<#= deffines.First().TypeName #>>.Default.Equals(<#= deffines.First().FieldName #>, other.<#= deffines.First().FieldName #>) <# foreach(var x in deffines.Skip(1)) {#> && EqualityComparer<<#= x.TypeName #>>.Default.Equals(<#= x.FieldName #>, other.<#= x.FieldName #>) <# } #> ; } public override bool Equals(object obj) { var other = obj as <#= className #>; return (other != null) ? Equals(other) : false; } public override int GetHashCode() { var hash = 0xf937b6f; <# foreach(var x in deffines) {#> hash = (-1521134295 * hash) + EqualityComparer<<#= x.TypeName #>>.Default.GetHashCode(<#= x.FieldName #>); <# } #> return hash; } public override string ToString() { return "{ " + "<#= deffines.First().PropName #> = " + <#= deffines.First().FieldName #> + <# foreach(var x in deffines.Skip(1)) {#> ", <#= x.PropName #> = " + <#= x.FieldName #> + <# } #> " }"; } public class Builder { <# foreach(var x in deffines) {#> public <#= x.TypeName #> <#= x.PropName #> { private get; set; } <# } #> public <#= className #> Build() { <# foreach(var x in deffines) {#> if ((object)<#= x.PropName #> == null) throw new ArgumentNullException("<#= x.PropName #>"); <# } #> Contract.EndContractBlock(); return new <#= className #>(<#= string.Join(", ", deffines.Select(d => d.PropName)) #>); } } } } <#+ class Deffine { public string TypeName, FieldName, PropName; public Deffine(string constructorParam) { var split = constructorParam.Split(' '); this.TypeName = split.First(); this.FieldName = Regex.Replace(split.Last(), "^(.)", m => m.Groups[1].Value.ToLower()); this.PropName = Regex.Replace(FieldName, "^(.)", m => m.Groups[1].Value.ToUpper()); } } class DeffineList : IEnumerable<Deffine> { private List<Deffine> list = new List<Deffine>(); public void Add(string constructorParam) { list.Add(new Deffine(constructorParam)); } public string DebuggerDisplay { get { return "\\{ " + string.Join(", ", list.Select(d => string.Format("{0} = {{{1}}}", d.PropName, d.FieldName))) + " }"; } } public string Constructor { get { return string.Join(", ", list.Select(d => d.TypeName + " " + d.FieldName)); } } public IEnumerator<Deffine> GetEnumerator() { return list.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } #>
以下のようなのが出力されます(長いねー)
using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Contracts; namespace Neue.Test { [DebuggerDisplay(@"\{ Name = {name}, Birth = {birth}, Address = {address} }", Type = "Person")] public partial class Person : IEquatable<Person> { [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly string name; public string Name { get { return name; } } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly DateTime birth; public DateTime Birth { get { return birth; } } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly string address; public string Address { get { return address; } } private Person(string name, DateTime birth, string address) { this.name = name; this.birth = birth; this.address = address; } [ContractVerification(false)] public static implicit operator Person(Builder builder) { return builder.Build(); } public bool Equals(Person other) { if (other == null || GetType() != other.GetType()) return false; if (ReferenceEquals(this, other)) return true; return EqualityComparer<string>.Default.Equals(name, other.name) && EqualityComparer<DateTime>.Default.Equals(birth, other.birth) && EqualityComparer<string>.Default.Equals(address, other.address) ; } public override bool Equals(object obj) { var other = obj as Person; return (other != null) ? Equals(other) : false; } public override int GetHashCode() { var hash = 0xf937b6f; hash = (-1521134295 * hash) + EqualityComparer<string>.Default.GetHashCode(name); hash = (-1521134295 * hash) + EqualityComparer<DateTime>.Default.GetHashCode(birth); hash = (-1521134295 * hash) + EqualityComparer<string>.Default.GetHashCode(address); return hash; } public override string ToString() { return "{ " + "Name = " + name + ", Birth = " + birth + ", Address = " + address + " }"; } public class Builder { public string Name { private get; set; } public DateTime Birth { private get; set; } public string Address { private get; set; } public Person Build() { if ((object)Name == null) throw new ArgumentNullException("Name"); if ((object)Birth == null) throw new ArgumentNullException("Birth"); if ((object)Address == null) throw new ArgumentNullException("Address"); Contract.EndContractBlock(); return new Person(Name, Birth, Address); } } } }
これで、どれだけ引数の多いクラスであろうとも、簡単な記述でイミュータブルオブジェクトを生成させることが出来ます。しかも、普通にクラス作るよりも楽なぐらいです、.ttをコピペって、先頭の方に、コンストラクタに並べる型を書くだけ。後は全部自動生成任せ。もし積極的に使うなら、Generated.csのほうを消して.ttのみにした状態で、ファイル→テンプレートのエクスポートで項目のエクスポートをすると使い回しやすくて素敵と思われます、項目名はImmutableObjectとかで。
// 書くときはこんな風にやります var person1 = new Person.Builder { Name = "hoge", Birth = new DateTime(1999, 12, 12), Address = "Tokyo" }.Build(); // 暗黙的な型変換も実装されているので、.Buildメソッドの省略も可 Person person2 = new Person.Builder { Name = "hoge", Birth = new DateTime(1999, 12, 12), Address = "Tokyo" }; // 参照ではなく、全てのフィールドの値の同値性で比較される Console.WriteLine(person1.Equals(person2)); // true
匿名型の再現なので、EqualsやGetHashCodeもオーバーライドされて、フィールドの値で比較を行うようになっています。この辺はもう手動だと書いてられないですよね。ReSharperなどを入れて生成をお任せする、という手はありますが。
==はオーバーライドされていません。これもまた匿名型の再現なので……。Tupleもされてないですしね。これは、フィールドをreadonlyで統一しようと「変更可能」な可能性が含まれるので==は不適切、というガイドライン的なもの(と解釈しました)に従った結果です。変更可能云々は、下の方で解説します。
Code Contracts
更に、Code Contractsを入れれば、値の未代入に対するコンパイラからの静的チェックまで得られます!下記画像のは、Addressが未入力で、通常は実行時に例外が飛ぶことで検出するしかないですが、Code Contractsが静的チェックで実行前にnullだと警告してくれています。
ビルダーの欠点は未代入の検出が実行時まで出来なかったりすること。インターフェイスで細工することで、順番を規定したり、必ず代入しなければならないものを定義し終えるまでは.BuildメソッドがIntelliSenseに出てこないようにする。などが出来ますが、手間がかかりすぎて理想論に留まっている気がします。
簡単であることってのはとても大事で、過剰な手間暇や複雑な設計だったりってのは、必ず無理が生じます。手間がかかること、複雑であることは、それ自体が良くない性質の一つであり、メリットがよほど上回らない限りは机上の空論にすぎない。
今回、Code Contractsのパワーにより、シンプルなオブジェクト初期化子を使ったビルダーでも未代入の静的チェックをかませる、という素敵機構が実現しました。残念ながらCode Contractsは要求環境が厳しいです。アドインを入れてない/入れられない(Express)場合はどうなるのか、というと、.NET 4にクラス群は入っているので、コンパイル通らないということはありません。普通にArgumentNullExceptionがthrowされるという形になります。
私が考えるに、.NET 4でクラスが入ったのって、Code Contractsのインストールの有無に関係なくコードが共有出来るように、という配慮でしかない予感。ExpressでContractクラスを使う意味は、あまりなさそうですね。Windows Phone 7環境など、Contractクラスそのものがないような場合では、T4のBuilderクラスBuildメソッドのContract.EndContractBlock();の一行とimplict operatorのContractVerification属性を削除してください。自分で好きに簡単に書き換えられるのもT4の良さです。
今回はnullチェックしかしていないので、つまり値型の未代入には無効です。何とかして入れたいとは思ったんですが、例えば対策として値型をNullableにするにせよType情報が必要で、そのためにはAssembly参照が必要で、と設定への手間が増えてしまうので今回は止めました(このT4はただ文字列を展開しているだけで、完全にリフレクション未使用)
Code Contractsに関しては、事前条件のnullチェックにしか使っていなくて真価の1%も発揮されていないので、詳しくはとある契約の備忘目録。契約による設計(Design by Contract)で信頼性の高いソフトウェアを構築しよう。 - Bug Catharsisなどなどを。不変オブジェクトに関してもそうだけれど、zeclさんの記事は素晴らしいです。
Code Contracts自体は、メソッド本体の上の方で、コントラクトの記述が膨れ上がるのは好きでないかも。従来型の、if-then-throwでの引数チェックも、5行を超えるぐらいになるとウンザリしますね。ご丁寧に{で改行して、if-then-throwの一個のチェックに4行も使って、それが5個ぐらい連なって20行も使いやがったりするコードを見ると発狂します。そういう場合に限ってメソッド本体は一行で他のメソッド呼んでるだけで、更にその、他のメソッドの行頭にも大量の引数チェックがあったりすると、死ねと言いたくなる。コードは視覚的に、横領域の節約も少しは大事だけど、縦も大事なんだよ、分かってよね……。メソッド本体が1000行とか書く人じゃなく、100行超えたら罰金(キリッ とか言ってる人だけど、それならガード句が10行超えたら罰金だよこっちとしては。
話が脱線した。つまるところ、コントラクトはライブラリレベルで頑張るよりも、言語側でのサポートが必要な概念ですね、ということで。実際rewriterとか、ライブラリレベル超えて無茶しやがって、の領域に踏み込んでいますし<Code Contracts。
プラスアルファ
partial classで生成されるので(デフォルトではクラス名.Generated.cs)、別ファイルにクラスを作ることで、フィールドの増減などでT4を後で修正しても、影響を受けることなくメソッドを追加することができます。
// Person.csという形で別ファイルで追加 using System; namespace Neue.Test { public partial class Person { public int GetAge(DateTime target) { return (target.Year - birth.Year); } } }
それと、nullチェックだけじゃなくきっちりBuildに前提条件入れたい(もしくはnullを許容したい)場合は、T4のBuildメソッドの部分に直に条件を書いてしまうか、それも何だか不自然に感じる場合は生成後のファイルをT4と切り離してしまうのも良いかもですね。自由なので好きにどうぞですます。
で、本当に不変なの?
何をもってどこまでを不変というのかはむつかしいところですが、Equalsが、GetHashCodeが変化するなら、可変かしら? 単純に全ての含まれる型のゲッターが常に同一の値を返さなければ不変ではない、でも良いですが。冒頭でも言いましたが、そう見るとreadonlyだけでは不変を厳密には保証しきれていません。匿名型で例を出すと
class MyClass { public int i; public override int GetHashCode() { return i; } } static void Main(string[] args) { var anon = new { MC = new MyClass { i = 100 } }; var hashCode1 = anon.GetHashCode(); anon.MC.i = 1000; // 変更 var hashCode2 = anon.GetHashCode(); Console.WriteLine(hashCode1 == hashCode2); // false Console.WriteLine(hashCode1); Console.WriteLine(hashCode2); }
参照しているMyClassのインスタンスの中身が変化可能で、それが変化してしまえば、違う値になってしまいます。厳密に不変であるためには、中のクラス全てが不変でなければなりません。これは今の言語仕様的には制限かけるのは無理かなー、といったところ。T4なのでリフレクションで全部バラして、参照している型が本当の意味で不変なのかどうか検証して、可変の型を含む場合はジェネレートしない、という形でチェックかけるのは原理的には可能かもしれません、が、やはり色々無理があるかなあ。
まとめ
プログラミングの楽しさの源は、書きやすく見た目が美しいことです。私はLinq to Objects/Linq to Xmlでプログラミングを学んだようなものなので、Linqの成し遂げたこと(究極のIntelliSenseフレンドリーなモデル・使いづらいDOMの大破壊)というのが、設計の理想と思っているところが相当あります。C#は言語そのものが素晴らしいお手本。匿名型素晴らしいよ(一年ぐらい前は匿名型も可変ならいいのに、とか口走っていた時期があった気がしますが忘れた、いやまあ、可変だと楽なシチュエーションってのもそれなりにいっぱいあるんですよね)。
T4の標準搭載はC#にとって非常に大きい。T4標準搭載によって、出来る事の幅がもう一段階広がった気がします。partial class素晴らしい。自動生成って素敵。T4はただのテキストテンプレートじゃなくて「VSと密接に結びついていて」「なおかつ標準搭載」「もはやC#の一部といってもいい」ことが、全く違った価値をもたらしていると思います。自動生成前提のパターンを作っても/使ってもいいんだよ、と。言語的に足らない部分の迂回策が、また一つ加わった。
見た目上若干Uglyになっても自動生成でなんとかする、というのはJava + Eclipseもそうですが、それと違うのはpartialでUglyな部分を隔離出来る(隔離によって自動生成の修正が容易になることも見逃せない)ことと、自動生成部分をユーザーが簡単に書けること、ですね。Eclipseの自動生成のプラグインを書くのは敷居が高すぎですが、T4を書く、書くまではしなくても修正する、というのは相当容易でしょう。
最近本当にT4好きですねー。色々と弄ってしまいます。こーどじぇねれーと素晴らしい。あとは、T4自体のUglyさが少し軽減されればな、といったところでしょうか。テンプレートエンジンとしてRazorに切り替えられたりを望みたいなあ。
Reactive ExtensionsのFromEventをT4 Templateで自動生成する
Rxで面倒くさいのが、毎回書かなければならないFromEvent。F#ならイベントがファーストクラスで、そのままストリーム処理に流せるという素敵仕様なのですが、残念ながらC#のeventはかなり雁字搦めな感があります。しかし、そこは豊富な周辺環境で何とか出来てしまうのがC#というものです。F#では form.MouseMove |> Event.filter と書けますが、 form.MouseMoveAsObservable().Where と書けるならば、似たようなものですよね?
というわけで、T4です。FromEventを自動生成しましょう!と、いうネタは散々既出で海外のサイトにも幾つかあるし、日本にもid:kettlerさんがFromEventが面倒なので自動生成させてみた2として既に書かれているのですが、私も書いてみました。書くにあたってid:kettlerさんのコードを大変参考にさせていただきました、ありがとうございます。
私の書いたもののメリットですが、リフレクションを使用しないFromEventで生成しているため、実行コストが最小に抑えられています。リフレクションを使わないFromEventは書くのが面倒でダルいのですが、その辺自動生成の威力発揮ということで。それと、命名規則をGetEventではなくEventAsObservableという形にしています。これは、サフィックスのほうがIntelliSenseに優しいため。
んね?この命名規則は、RxJSのほうで公式に採用されているものなので(例えばrx.jQuery.jsのanimateAsObservable)、俺々規則というわけじゃないので普通に従っていいと思われます。
以下コード。利用改変その他ご自由にどうぞ、パブリックドメインで。
<#@ assembly Name="System.Core.dll" #> <#@ assembly Name="System.Windows.Forms.dll" #> <#@ assembly Name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\System.Xaml.dll" #> <#@ assembly Name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\PresentationCore.dll" #> <#@ assembly Name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\PresentationFramework.dll" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ import namespace="System.Reflection" #> <# // 設定:ここに生成したいクラス(のTypeをFullNameで)を足してください(以下の4つは例) // クラスによってはassemblyの増減が必要です、WPF/Silverlightなどはフルパス直書きしてください var types = new[] { typeof(System.Collections.ObjectModel.ObservableCollection<>), typeof(System.Windows.Forms.Button), typeof(System.Windows.Controls.Primitives.TextBoxBase), typeof(System.Windows.Controls.Primitives.ButtonBase) }; #> using System.Linq; using System.Collections.Generic; <# foreach(var x in GenerateTemplates(types)) {#> namespace <#= x.Namespace #> { <# foreach(var ct in x.ClassTemplates) {#> internal static class <#= ct.Classname #>EventExtensions { <# foreach(var ev in ct.EventTemplates) {#> public static IObservable<IEvent<<#= ev.Args #>>> <#= ev.Name #>AsObservable<#= ct.GenericArgs #>(this <#= ct.Classname #><#= ct.GenericArgs #> source) { return Observable.FromEvent<<#= ev.Handler + (ev.IsGeneric ? "<" + ev.Args + ">" : "") #>, <#= ev.Args #>>( h => <#= ev.IsGeneric ? "h" : "new " + ev.Handler + "(h)" #>, h => source.<#= ev.Name #> += h, h => source.<#= ev.Name #> -= h); } <# } #> } <# }#> } <# }#> <#+ IEnumerable<T> TraverseNode<T>(T root, Func<T, T> selector) { var current = root; while (current != null) { yield return current; current = selector(current); } } IEnumerable<ObservableTemplate> GenerateTemplates(Type[] types) { return types.SelectMany(t => TraverseNode(t, x => x.BaseType)) .Distinct() .GroupBy(t => t.Namespace) .Select(g => new ObservableTemplate { Namespace = g.Key, ClassTemplates = g.Select(t => new ClassTemplate(t)) .Where(t => t.EventTemplates.Any()) .ToArray() }) .Where(a => a.ClassTemplates.Any()) .OrderBy(a => a.Namespace); } class ObservableTemplate { public string Namespace; public ClassTemplate[] ClassTemplates; } class ClassTemplate { public string Classname, GenericArgs; public EventTemplate[] EventTemplates; public ClassTemplate(Type type) { Classname = Regex.Replace(type.Name, "`.*$", ""); GenericArgs = type.IsGenericType ? "<" + string.Join(",", type.GetGenericArguments().Select((_, i) => "T" + (i + 1))) + ">" : ""; EventTemplates = type.GetEvents(BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.DeclaredOnly | BindingFlags.Instance) .Select(ei => new { EventInfo = ei, Args = ei.EventHandlerType.GetMethod("Invoke").GetParameters().Last().ParameterType }) .Where(a => a.Args == typeof(EventArgs) || a.Args.IsSubclassOf(typeof(EventArgs))) .Select(a => new EventTemplate { Name = a.EventInfo.Name, Handler = Regex.Replace(a.EventInfo.EventHandlerType.FullName, "`.*$", ""), Args = a.Args.FullName, IsGeneric = a.EventInfo.EventHandlerType.IsGenericType }) .ToArray(); } } class EventTemplate { public string Name, Args, Handler; public bool IsGeneric; } #>
// こんなのが生成されます namespace System.Collections.ObjectModel { internal static class ObservableCollectionEventExtensions { public static IObservable<IEvent<System.Collections.Specialized.NotifyCollectionChangedEventArgs>> CollectionChangedAsObservable<T1>(this ObservableCollection<T1> source) { return Observable.FromEvent<System.Collections.Specialized.NotifyCollectionChangedEventHandler, System.Collections.Specialized.NotifyCollectionChangedEventArgs>( h => new System.Collections.Specialized.NotifyCollectionChangedEventHandler(h), h => source.CollectionChanged += h, h => source.CollectionChanged -= h); } } } namespace System.ComponentModel { internal static class ComponentEventExtensions { public static IObservable<IEvent<System.EventArgs>> DisposedAsObservable(this Component source) { return Observable.FromEvent<System.EventHandler, System.EventArgs>( h => new System.EventHandler(h), h => source.Disposed += h, h => source.Disposed -= h); } } // 以下略
使い方ですが、RxGenerator.ttとか、名前はなんでもいいのですがコピペって、上の方のvar typesに設定したい型を並べてください。一緒に並べたものの場合は、全て継承関係を見て重複を省くようになっています。WPFとかSilverlightのクラスから生成する場合は、assembly Nameに直にDLLのパスを書いてやってくださいな。コード的には、例によってLinq大活躍というかLinqなかったら死ぬというか。リフレクションxLINQxT4は鉄板すぎる。
一つ難点があって、名前空間をそのクラスの属している空間にきっちりと分けたせいで、例えばWPFのbutton.ClickAsObservableはSystem.Windows.Controls.Primitivesをusingしないと出てこないという、微妙に分かりづらいことになっちゃっています……。これ普通にHogeHogeExtensionsとかいう任意の名前空間にフラットに配置したほうが良かったのかなあ。ちょっと悩ましいところ。
T4の書き方
漠然と書いてると汚いんですよね、T4。読みにくくてダメだし読みにくいということは書きにくいということでダメだ。というわけで、今回からは書き方を変えました。ASP.NETのRepeater的というかデータバインド的にというかで、入れ物クラスを作って、パブリックフィールド(自動プロパティじゃないのって?そんな大袈裟なものは要りません)を参照させるという形にしました。foreachや閉じカッコ(”}”)は一行にする。<% %>で囲まれる範囲を最小限に抑えることで、ある程度の可読性が確保出来ているんじゃないかと思います。
といったようなアイディアは
よく訓練されたT4使いは 「何を元に作るか」 「何を作るか」 だけを考える。
何を元に作るかはきっと from … select になるでしょう。 何を作るかの中では <#=o.Property#> で値を出力する事ができます。
csproj.user を作るための T4 テンプレート
からです。「何を元に作るか」 「何を作るか」 。聞いてみれば当たり前のようだけれど、本当にコロンブスの卵というか(前も同じこと書いた気がする)、脳みそガツーンと叩かれた感じで、うぉぉぉぉぉ、と叫んで納得でした。はい。それと、T4は書きやすいと言っても書きにくい(?)ので、囲む範囲を最小にするってことは、普通のコードでじっくり書いてからT4に移植しやすいってことでもあるんですね。
まとめ
最近F#勉強中なのです。Expert F# 2.0買ったので。と思ったらプログラミングF#が翻訳されて発売されるだとー!もうすぐ。あと一週間後。くぉ、英語にひいこらしながら読んでいるというのにー。
F#すげーなー、と知れば知るほど確かに思うわけですが、しかし何故か同時に、C#への期待感もまた高まっていきます。必ずや「良さ」を吟味して取り込んでくれるという信頼感があります、C#には。そしてまた、ライブラリレベルで強烈に何とか出来る地力がある、例えばイベントをストリームに見立てた処理には、Reactive Extensionsが登場してC#でも実現出来ちゃったり。Scalaと対比され緩やかに死んでいくJavaと比べると、F#と対比しても元気に対抗していくC#の頼もしさといったらない。
といっても、F#も全然まだ表面ぐらいしか見えてないし、突っつけば突っつくほど応えてくれる奥の深い言語な感じなので、今の程度の知識で比較してどうこうってのはないです。Java7のクロージャにたいし、Javaにそんなものはいらない、とか頑な態度を取っている人を見るとみっともないな、と思うわけですが、いつか私もC#に拘泥してC#にそんなものはいらない、的なことを言い出すようだと嫌だなー、とかってのは思ってます。進化を受け入れられなくなったら、終わり。
マルチパラダイム言語の勝利→C++/CLI大勝利ですか?→いやそれは多分違う。的なこともあるので何もかもを受け入れろ、ひたすら取り込んで鈍重な恐竜になれ(最後に絶滅する)、とは言いません。この辺のバランス感覚が、きっと言語設計にとって難しいことであり、そして今のC#は外から見れば恐竜のようにラムダ式だのdynamicだのを取り入れてるように見えるでしょうが、決してそうではなく、素晴らしいバランスに立っています。機能の追加が恐竜への道になっていない。むしろ追加によって過去の機能を互換性を保ちつつ捨てているんですよね、例えば、もうdelegateというキーワードは書くどころか目にすることもほとんどない←なのでC#を学習する場合、C#1.0->2.0->3.0->4.0という順番を辿るのは良くなくて、最新のものから降りていったほうがいい。
何が言いたいかっていったらC#愛してるってことですな。うはは。5.0にも当然期待していますし、Anders Hejlsbergの手腕には絶対的に信頼を寄せています。4.0は言語的な飛躍はあまりなかっただけに、5.0は凄いことになるに違いない。
テストを簡単にするほんの少しの拡張メソッド
- C# - 10.08/02
テストドリブンしてますか?私は勿論してません。え……。別に赤が緑になっても嬉しくないし。コード先でテスト後のほうが書きやすくていいなあ。でもそうなると、テスト書かなくなってしまって、溜まるともっと書かなくなってしまっての悪循環。
そんな普段あまりテスト書かないクソッタレな人間なわけですが(レガシーコード殺害ガイドが泣いている)、普段テスト書かないだけに書こうとすると単純なものですらイライライライラしてしまって大変よくない。しかし、それはそもそもテストツールが悪いんじゃね?という気だってする。言い訳じゃなく、ふつーにバッチイですよ、テストコード。こんなの書くのはそりゃ苦痛ってものです。
Before
例えば、こういうどうでもいいクラスがあったとします。
public class MyClass { public string GetString(string unya) { return (unya == "unya") ? null : "hoge"; } public IEnumerable<int> GetEnumerable() { yield return 1; yield return 2; yield return 3; } }
ウィザードで生成されたのをベースに書くとこうなる(MSTestを使っています)
[TestMethod()] public void GetStringTest() { MyClass target = new MyClass(); string unya = "unya"; string expected = null; string actual; actual = target.GetString(unya); Assert.AreEqual(expected, actual); expected = "hoge"; actual = target.GetString("aaaaa"); Assert.AreEqual(expected, actual); } [TestMethod()] public void GetEnumerableTest() { MyClass target = new MyClass(); IEnumerable<int> expected = new[] { 1, 2, 3 }; IEnumerable<int> actual; actual = target.GetEnumerable(); CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray()); }
何だこりゃ。超面倒くさい。むしろテストがレガシーすぎて死にたい。CollectionAssertはIEnumerableに対応してないし。泣きたい。こんなの書いてられない。吐き気がする。
After
JavaScriptのQUnitは、大抵EqualとDeepEqualで済む簡単さで、それがテストへの面倒くささを大いに下げてる。見習いたい。シンプルイズベスト。ごてごてしたAssert関数なんて悪しき伝統にすぎないのではなかろうか?と思ったので、もうアサート関数なんてIsだけでいいぢゃん、ついでにactualの後ろに拡張メソッドでそのままexpected書けると楽ぢゃん、と開き直ることにしました。
[TestMethod()] public void GetStringTest() { // 1. 全オブジェクトに対して拡張メソッドIsが定義されててAssert.AreEqualされる // 2. ラムダ式も使えるので、andやorや複雑な比較などはラムダ式でまかなえる // 3. nullはIs()で(本当はIs(null)でやりたかったのだけど、都合上断念) new MyClass().GetString("aaaaa").Is("hoge"); new MyClass().GetString("aaaaa").Is(s => s.StartsWith("h") && s.EndsWith("e")); new MyClass().GetString("unya").Is(); } [TestMethod()] public void GetEnumerableTest() { // 対象がIEnumerableの場合はCollectionAssert.Equalsで比較されます // 可変長配列を受け入れることが出来るので直書き可 new MyClass().GetEnumerable().Is(1, 2, 3); }
すんごく、すっきり。メソッドはIsだけ、ですがそれなりのオーバーロードが仕込まれているので、ほとんどのことが一つだけで表現出来ます。IsNullはIs()でいいし(表現的には分かりにくくて嫌なのですが、Is(null)だとオーバーロードの解決ができなくてIs((型)null)と書かなくて面倒くさいので、泣く泣く引数無しをIsNullとしました)し、IsTrueはIs(true)でいい。複雑な条件で比較したいときはラムダ式を渡せばいい。Is.EqualTo().Within().And() とか、全然分かりやすくないよね。流れるようなインターフェイスは悪くないけれど、別に自然言語的である必要なんて全然なくて、ラムダ一発で済ませられるならそちらのほうがずっと良い。.Should().Not.Be.Null()なんてまで来ると、もう馬鹿かと思った。
大事なのはシンプルに気持良く書けることであって、形式主義に陥っちゃいけないのさあ。
コレクション比較もIsだけですませます。IEnumerableを渡すことも出来るし、可変長引数による値の直書きも出来る。なお、Isのみなのでコレクション同士の参照比較はありません。コレクションだったら有無をいわさず要素比較にします。だって、別に参照比較したいシーンなんてほとんどないでしょ?そういう例外的な状況は素直にAssert.AreEqual使えばいい。また、CollectionAssertには色々なメソッドがありますが、それ全部Linqで前処理すればいいよね?例えばCollectionAsert.IsEmptyはAny().Is(false)で済ませられるので不要。他のも大体はLinqで何とかできるので大概不要です。
ところで、このぐらいだとウィザードが冗長というだけで
Assert.AreEqual(new MyClass().GetString("aaaaa"), "hoge");
って書けるじゃないかって突っ込みは、そのとおり。でも、少し長くなると、引数に押し込めるの大変になってきますよね。そうなると
var expected = "hoge"; var actual = new MyClass().GetString("aaaaa") Assert.AreEqual(expected, actual);
といった具合に、変数名が必要になって大変かったるい。ので、余計な一時変数なしで流し込める方が圧倒的に楽です。そもそもに、Assert.AreEqualだと、毎回どっちがactualでどっちがexpectedだか悩むのがイライラしてしまって良くない。まあ、逆でもいいんですが。よくないんですが。
パラメータ違いのテストケース
ついでに面倒くさいのは、パラメータが違うだけにすぎない、同じようなAssertの量産。テストなんてとっとと書いてナンボなので大体コピペで取り回しちゃうわけですが、どう考えてもクソ対応です本当にありがとうございました。そういうことやると、テストの書き直しが出来なくなって身重になってしまって良くない。コードはサクッと書き直せるべきだし、テストもサクッと書き直せるべきだ。といったわけで、NUnitには属性を足すだけでパラメータ違いのテストを実行出来るそうですがMSTestにはなさそう。うーん、でも、Linqがあれば何でも出来るよ?Linq万能神理論。ということで、Linqをベースにしてパラメータ違いを渡せるクラスを書いてみました。
// コレクション初期化子を使ってパラメータを生成します new Test.Case<int, int, int> { {1, 2, 3}, {100, 200, 500}, {10000, 20, 30} } .Select(t => t.Item1 + t.Item2 + t.Item3) .Is(6, 800, 10050);
複数の値はTupleに突っ込めばいい。あとはSelectでactualを作って、最後にIsの可変長引数使って期待値と比較させれば出来上がり。Tupleは、C#には匿名型があるため、あまり活用のシーンがないのですが、こういうところでは便利。このTest.Caseは7引数のTupleまで対応しています(それ以上?そもそも標準のTupleの限界がそれまでなので)。使い方はnewしてコレクション初期化子でパラメータを並べるだけ。
つまるところTest.CaseクラスはただのTupleCollectionです。Tupleの配列を作るには、普通だと new[]{Tuple.Create, Tuple.Create…} と書かなければならず、死ぬほど面倒。そこで出てくるのがコレクション初期化子。これなら複数引数を受け入れるのが楽に記述できる。というわけで、コレクション初期化子を使いたいがためだけに、クラスを立てました。唯一の難点はnewしなければならない、つまりジェネリクスの型引数を書かなければならない、ということでしょうか。
そうそう、コレクション初期化子のおさらいをすると、IEnumerable<T>かつAddメソッド(名前で決め打ちされてる)があると呼び出せます。複数引数時も、波括弧で要素をくくることで対応できます(Dictionaryなどで使えるね)。
ソースコード
長々と長々してましたがソースを。Test.CaseのTupleの量産が面倒なのでT4 Templateにしました。Test.ttとかって名前にしてテストプロジェクトに突っ込んでください。中は完全に固定だから、取り回すなら生成後のTest.csを使っていくと良いかもですね。ご利用はご自由にどうぞ。パブリックドメインで。
<#@ assembly Name="System.Core.dll" #> <#@ import namespace="System.Linq" #> using System; using System.Collections; using System.Collections.Generic; using System.Linq; namespace Microsoft.VisualStudio.TestTools.UnitTesting { public static class Test { // extensions /// <summary>IsNull</summary> public static void Is<T>(this T value) { Assert.IsNull(value); } public static void Is<T>(this T actual, T expected, string message = "") { Assert.AreEqual(expected, actual, message); } public static void Is<T>(this T actual, Func<T, bool> expected, string message = "") { Assert.IsTrue(expected(actual), message); } public static void Is<T>(this IEnumerable<T> actual, IEnumerable<T> expected, string message = "") { CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray(), message); } public static void Is<T>(this IEnumerable<T> actual, params T[] expected) { Is(actual, expected.AsEnumerable()); } public static void Is<T>(this IEnumerable<T> actual, IEnumerable<Func<T, bool>> expected) { var count = 0; foreach (var cond in actual.Zip(expected, (v, pred) => pred(v))) { Assert.IsTrue(cond, "Index = " + count++); } } public static void Is<T>(this IEnumerable<T> actual, params Func<T, bool>[] expected) { Is(actual, expected.AsEnumerable()); } // generator <# for(var i = 1; i < 8; i++) { #> public class Case<#= MakeT(i) #> : IEnumerable<Tuple<#= MakeT(i) #>> { List<Tuple<#= MakeT(i) #>> tuples = new List<Tuple<#= MakeT(i) #>>(); public void Add(<#= MakeArgs(i) #>) { tuples.Add(Tuple.Create(<#= MakeParams(i) #>)); } public IEnumerator<Tuple<#= MakeT(i) #>> GetEnumerator() { return tuples.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } <# } #> } } <#+ string MakeT(int count) { return "<" + String.Join(", ", Enumerable.Range(1, count).Select(i => "T" + i)) + ">"; } string MakeArgs(int count) { return String.Join(", ", Enumerable.Range(1, count).Select(i => "T" + i + " item" + i)); } string MakeParams(int count) { return String.Join(", ", Enumerable.Range(1, count).Select(i => "item" + i)); } #>
オプション引数のお陰で、こういうちょっとしたのが書くの楽になりましたね(C#4.0 からの新機能)。あとは、可変長引数が配列だけじゃなくてIEnumerableも受け付けてくれれば、AsEnumerableで渡すだけの余計なオーバーロードを作らないで済むんだよね。C# 5.0に期待しますか。
まとめ
テストのないコードはレガシーコード。と、名著が言ってる(1/4ぐらいしかまだ読んでませんが!)のでテストは書いたほうがいいっす。
でも、コード書きってのは気持良くなければならない。気持ち良ければ自然に書くんです。書かない、抵抗感があるってのは、環境が悪いんです。「テスト書きは苦痛だけど良いことだから、赤が緑に変わると嬉しいから書こうぜ!」とかありえない。そんな自己啓発っぽいのは無理。というわけで、拡張メソッドで環境を変えて、気持よく生きましょうー。
JsUnit(非常にイマイチ)もそうだったんだけど、Java由来(xUnitはSmalltalkのー、とかって話は分かってます)のライブラリとかは、Java的な思考に引き摺られすぎ。もっと言語に合わせたしなやかなAPIってものがあると思うんですよね。MSTestはVS2010で、色々刷新してLinqや拡張メソッドを生かしたものを用意すべきだったと思います。C#2.0的なコードは読むのも書くのも、もう苦痛。レガシーコードとは何か?C#2.0的なコードです。いやほんと。生理的な問題で。
Reactive Extensions入門 + メソッド早見解説表
Silverlight Toolkitに密かに隠された宝石”System.Reactive.dll”が発見されてから1年。Reactive FrameworkからReactive Extensionsに名前が変わりDevLabsでプロジェクトサイトが公開され、何度となく派手にAPIが消滅したり追加されたりしながら、JavaScript版まで登場して、ここまで来ました。IObservable<T>とIObserver<T>インターフェイスは.NET Framework 4に搭載されたことで、将来的なSP1での標準搭載は間違いなさそう。Windows Phone 7にはベータ版の開発キットに搭載されているように、間違いなく標準搭載されそう。
現在はAPIもかなり安定したし、Windows Phone 7の登場も迫っている。学ぶならまさに今こそベスト!そんなわけで、Rxの機能の簡単な紹介と導入コード、重要そうなエッセンス紹介、そして(ほぼ)全メソッド一行紹介をします。明日から、いや、今日からRxを使いましょう。
その前にRxとは何ぞや?ですが、Linq to EventsもしくはLinq to Asynchronus。イベントや非同期処理をLinqっぽく扱えます。
Rxの出来る事
まずReactive Extensions for .NET (Rx)からインストール。そして、System.CoreEx、System.Reactiveを参照に加え(Rxにはもう一つ、System.Interactiveが含まれていて、これはEnumerableの拡張メソッド群になります)れば準備は終了。
// Rxの出来る事その1. イベントのLinq化 var button = new Button(); // WPFのButton Observable.FromEvent<RoutedEventArgs>(button, "Click") .Subscribe(ev => Debug.WriteLine(ev.EventArgs)); // Rxの出来る事その2. 非同期のLinq化 Func<int, int> func = i => i * 100; // intを100倍する関数 Observable.FromAsyncPattern<int, int>(func.BeginInvoke, func.EndInvoke) .Invoke(5) // Invokeで非同期関数実行開始(Invokeは任意のタイミングで可) .Subscribe(i => Debug.WriteLine(i)); // 500 // Rxの出来る事その3. 時間のLinq化 Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(5)) .Subscribe(l => Debug.WriteLine(l)); // 5秒毎に発火 // Rxの出来る事その4. Pull型のPush型への変換 var source = new[] { 1, 10, 100, 1000 }; source.ToObservable() .Subscribe(i => Debug.WriteLine(i));
それぞれ一行でIObservable<T>に変換出来ます。あとは、LinqなのでSelectやWhereなどお馴染みのメソッドが、お馴染みなように使えます。そして最後にSubscribe。これは、まあ、foreachのようなものとでも捉えてもらえれば(今はね!)。
イベントをLinq化して何が嬉しいの?
合成出来るのが嬉しいよ!クリックしてクリックイベントが発動する、程度なら別にうまみはありません。でも、イベントは切り目をつけられないものも多数あります。例えばドラッグアンドドロップは「マウスダウン→マウスムーブ→マウスアップ」の連続的なイベント。従来は各関数の「外」で状態管理する変数を持ってやりくりしていましたが、Rxならば、スムーズにこれらを結合して一本の「ドラッグアンドドロップ」ストリームとして作り上げることが出来ます。逆に言えば、ただたんにイベントをLinq化しても嬉しいことはあまりありません。合成して初めて真価を発揮します。そのためには合成系のメソッド(SelectMany, Merge, Zip, CombineLatest, Join)を知る必要がある、のですがまだ当サイトのブログでは記事書いてません。予定は未定じゃなくて近日中には必ず紹介します……。
非同期をLinq化して何が嬉しいの?
それはもう自明で、単純にBeginInvoke/EndInvokeで待ち合わせるのは面倒くさいから。たった一行でラッピング出来る事の素晴らしさ!でも、同期的に書いてBackgroundWorkerで動かせばいいぢゃない。というのは、一面としては正しい。正しくないのは、Silverlightや、JavaScriptは非同期APIしか用意されていません。なので、クラウド時代のモダンなウェブアプリケーションでは、非同期と付き合うより道はないのです。
RxではBeginXxx/EndXxxという形で.NETの各メソッドにある非同期のパターンが簡単にラップ出来るようになっています。ジェネリクスの型として、引数と戻り値の型を指定して、あとはBeginInvokeとEndInvokeを渡すだけ。あの面倒くさい非同期処理がこんなにも簡単に!それだけで嬉しくありませんか?
Pull型をPush型に変えると何が嬉しいの?
分配出来るようになります。え?具体的には、C#とLinq to JsonとTwitterのChirpUserStreamsとReactive Extensionsという記事で紹介しました。そもそもPullとPushって何?という場合はメソッド探訪第7回:IEnumerable vs IObservableをどうぞ。
Rxを使うのに覚えておきたい大切な3つのこと
あまり深く考えなくても使えるけれど、少しポイントを押さえると、驚くほど簡単に見えてくる。「HotとColdの概念を掴むこと」「Schedulerが実行スレッドを決定すること」「Subjectでテストする」。この3つ。まあ、後の二つは実際のとここじつけみたいなもので、本当に大事なのはHotとColdです。あまりにも大事なのだけど、それに関して書くには余白が狭すぎる。ではなくて、以前にメソッド探訪第7回:IEnumerable vs IObservableとして書いたのでそちらで。とりあえず、ColdはSubscribeしたら即座に実行される、HotはSubscribeしても何もしないでイベント待ち。ぐらいの感覚でOKです。
Scheduler
Schedulerを使うと「いつ」「どこで」実行するかを決定することが出来ます。Rx内部でのメソッド実行は大抵このSchedulerの上に乗っかっています。
// 大抵の生成メソッドはISchedulerを受けるオーバーロードを持つ // それに対してSchedulerを渡すと、実行スレッドを決定出来る Observable.Range(1, 10, Scheduler.CurrentThread); Observable.Interval(TimeSpan.FromSeconds(1), Scheduler.ThreadPool);
基本的には引数に渡すだけ。「いつ」「どこで」ですが、「いつ」に関してはRxの各メソッドが受け持つので、基本的には「どのスレッドで」実行するかを決めることになります。なお、当然デフォルト値もあるわけですが、RangeはCurrentThreadでTimerはThreadPoolだったりと、各メソッドによって若干違ったりすることに注意(但しTimerでCurrentThreadを選ぶと完全にブロックされてTimerというかSleepになるので、挙動として当然といえば当然のこと)
生成メソッドに渡す以外に、まだ使う場所があります。
// WPFでbutton1というボタンとtextBlock1というtextBlockがあるとする Observable.FromEvent<RoutedEventArgs>(button1, "Click") .ObserveOn(Scheduler.ThreadPool) // 重い処理をUIスレッドでするのを避けるためThreadPoolへ対比 .Do(_ => Thread.Sleep(3000)) // 猛烈に重い処理をすることをシミュレート .ObserveOnDispatcher() // Dispatcherに戻す .Subscribe(_ => textBlock1.Text = "clickした"); // UIスレッドのものを触るのでThreadPool上からだと例外
UIスレッドのコントロールに他のスレッドから触れると例外が発生します。でも、重たい処理をUIスレッド上でやろうものなら、フリーズしてしまいます。なので、重たい処理は別スレッドに退避し、コントロールの部品を弄る時だけUIスレッドに戻したい。という場合に、ObserveOnを使うことで簡単に実行スレッドのコントロールが可能になります。もうDispatcher.BeginInvokeとはサヨナラ!
Subjectって何?
SubjectはIObservableでありIObserverでもあるもの。というだけではさっぱり分かりません。これは、イベントのRxネイティブ表現です。なので、C#におけるeventと対比させてみると理解しやすいはず。eventはそのクラス内部からはデリゲートとして実行出来ますが、外からだと追加/削除しか出来ませんよね?Subjectはこれを再現するために、外側へはIObservableとして登録のみ出来るようにし、内部からのみ登録されたものへ実行(OnNext/OnError/OnCompleted)して値を渡します。なお、ただキャストしただけでは、外からダウンキャストすればイベントを直接実行出来るということになってしまうので、Subjectを外に公開する時は AsObservableメソッド(IObservableでラップする)を使って隠蔽します。
どんな時に使うかというとRx専用のクラスを作るとき、もしくはObservableの拡張メソッドを作る時、に有効活用出来るはずです。もしくは、メソッドを試すときの擬似的なイベント代わりに使うと非常に便利です。
// Buttonのイベントをイメージ var buttonA = new Subject<int>(); var buttonB = new Subject<int>(); // Zipの動きを確認してみる…… buttonA.Zip(buttonB, (a, b) => new { a, b }) .Subscribe(a => Console.WriteLine(a)); buttonA.OnNext(1); // ボタンClickをイメージ buttonA.OnNext(2); // Subscribeへ値が届くのはいつ? buttonB.OnNext(10); // デバッグのステップ実行で一行ずつ確認 buttonA.OnCompleted(); // 片方が完了したら buttonB.OnNext(3); // もう片方にClickが入ったときZipはどういう挙動する?
動きがよくわからないメソッドも、この方法で大体何とか分かります。Subjectには他に非同期実行を表現したAsyncSubjectなど、幾つか亜種があるのでそちらも見ると、Rxのメソッドの動きがよりイメージしやすくなります。例えばFromAsyncPatternは中ではAsyncSubjectを使っているので、AsyncSubjectの動き(OnCompletedの前後でHotとColdが切り替わる、OnNextはOnCompletedが来るまで配信されず、OnCompleted後に最後の値をキャッシュしてColdとして配信し続ける)を丁寧に確認することで、FromAsyncPatternの挙動の理解が簡単になります。
メソッド分類早見表
決して全部というわけではなく、幾つか飛ばしていますが簡単に各メソッドを分類して紹介。
生成系メソッド雑多分類
イベント(hot) FromEvent - 文字列で与える以外のやり方もありますよ 非同期系(hot/cold) Start - ToAsync().Invoke()の省略形 ToAsync - 拡張メソッドとしてじゃなくそのまま使うのが型推論効いて素敵 FromAsyncPattern - ToAsyncも結局これの省略形なだけだったりする ForkJoin - 非同期処理が全て完了するのを待ち合わせて結果をまとめて返す Enumerableっぽいの系(cold) Range - いつもの Return - ようするにRepeat(element, 1) Repeat - 無限リピートもあるよ ToObservable - pull to push Generate - ようするにUnfold(と言われても困る?) Using - 無限リピートリソース管理付き Timer系(cold) Timer - 実はcold Interval - Timer(period, period)の省略形なだけだったり GenerateWithTime - 引数地獄 空っぽ系(cold) Empty - OnCompletedだけ発動 Throw - OnErrorだけ発動 Never - 本当に何もしない その他 Defer - 生成の遅延 Create - 自作したい場合に(戻り値はDispose時の実行関数を返す) CreateWithDisposable - 同じく、ただし戻り値はIDisposableを返す
こうしてみるとColdばかりで、Hotなのってイベントだけ?的だったりしますねー。では、IObservableの拡張メソッドも。
合成系 SelectMany - Enumerableと同じ感じですが、Rxでは合成のように機能する Zip - 左右のイベントが揃ったらイベント発行(揃うまでQueueでキャッシュしてる) CombineLatest - 最新のキャッシュと結合することで毎回イベント発行 Merge - Y字みたいなイメージで、左右の線を一本に連結 Join(Plan/And/Then) - Joinパターンとかいう奴らしいですが、Zipの強化版みたいな Concat - 末尾に接続 StartWith - 最初に接続 時間系 Delay - 値を一定時間遅延させる、coldに使うと微妙なことになるので注意 Sample - 一定時間毎に、通過していた最新の値だけを出す Throttle - 一定時間、値が通過していなければ、その時の最新の値を出す TimeInterval - 値と前回の時間との差分を包んだオブジェクトを返す RemoveTimeInterval - 包んだオブジェクトを削除して値のみに戻す Timestamp - 値と通過した時間で包んだオブジェクトを返す RemoveTimestamp - 包んだオブジェクトを削除して値のみに戻す Timeout - 一定時間値が来なければTimeoutExceptionを出す Connectable系(ColdをHotに変換する、細部挙動はSubjectでイメージするといい) Publish - Subjectを使ったもの(引数によってはBehaviorSubject) Prune - AsyncSubjectを使ったもの Replay - ReplaySubjectを使ったもの Enumerableに変換系(Push to Pull、使い道わかりません) Next - MoveNext後に同期的にブロックして値が来るまで待機 Latest - 値を一つキャッシュするNext(キャッシュが切れると待機) MostRecent - ブロックなしでキャッシュを返し続ける 例外ハンドリング系 OnErrorResumeNext - 例外来たら握りつぶして予備シーケンスへ移す Catch - 対象例外が来たら握りつぶして次へ Finally - 例外などで止まっても最後に必ず実行するのがOnCompletedとの違い 実行スレッド変更系 SubscribeOn - メソッド全体の実行スレッドを変える ObserveOn - 以降に流れる値の実行スレッドを変える クエリ系 Select - 射影(SelectManyはこっちじゃないのって話ももも) Where - フィルタリング Scan - Aggregateの経過も列挙するバージョン、一つ過去の値を持てるというのが重要 Scan0 - seed含む GroupBy - グルーピング、なのだけどIGroupedObservableは扱いが少し面倒かなあ BufferWithCount - 個数分だけListにまとめる BufferWithTime - 一定時間内の分だけListにまとめる BufferWithTimeOrCount - そのまんま、上二つが合わさったの DistinctUntilChanged - 連続して同じ値が来た場合は流さない すっとばす系 Skip - N個飛ばす SkipWhile - 条件に引っかかる間は飛ばす SkipLast - 最後N個を飛ばす(Lastを除いたTakeという趣向) SkipUntil - 右辺のOnNextを察知する「まで」は飛ばす Take - N個取る TakeWhile - 条件に引っかかる間は取る TakeLast - 最後N個だけを取る TakeUntil - 右辺のOnNextを察知する「まで」は取る Aggregate系 AggregateとかAllとかSumとかEnumerableにもある色々 - 値が確定したとき一つだけ流れてくる 変換系 ToEnumerable - 同期的にブロックしてIEnumerableに変換する、Hotだと一生戻ってこない ToQbservable - IQueryableのデュアルらしい、完全にイミフすぎてヤバい Start - ListなんだけどObservableという微妙な状態のものに変換する その他 Materiallize - OnNext,OnError,OnCompletedをNotificationにマテリア化 Dematerialize - マテリア化解除 Repeat - OnCompletedが来ると最初から繰り返し Let - 一時変数いらず Switch - SelectMany書かなくていいよ的なの AsObservable - IObservableにラップ、Subjectと合わせてどうぞ
疲れた。間違ってるとかこれが足りない(いやまあ、実際幾つか出してないです)とか突っ込み希望。
JavaScript版もあります
RxJSというJavaScript版のReactive Extensionsもあったりします。ダウンロードは.NET版と同じところから。何が出来るかというと、若干、というかかなりメソッドが少なくなってはいるものの、大体.NETと同じことが出来ます。SchedulerにTimeout(JavaScriptにはスレッドはないけどsetTimeoutがあるので、それ使って実行を遅らせるというもの)があったりと、相違も面白い。
JavaScriptは、まずAjaxが非同期だし、イベントだらけなのでRxが大変効果を発揮する。強力なはず、なのですが注目度はそんなに高くない。うむむ?jQueryと融合出来るバインディングも用意されていたりと、かなりイケてると思うのですがー。日本だとJSDeferredがあるね、アレの高機能だけど重い版とかとでも思ってもらえれば。
ところでObservableがあるということはEnumerableもありますか?というと、もちろんありますよ!linq.js - LINQ for JavaScriptとかいうライブラリを使えばいいらしいです!最近Twitterの英語圏でも話題沸騰(で、ちょっと浮かれすぎて頭がフワフワタイムだった)。RxJSと相互に接続できるようになっていたり、jQueryプラグインになるバージョンもあったりと、jQuery - linq.js - RxJSでJavaScriptとは思えない素敵コードが書けます。
JavaScriptはIEnumerableとIObservableが両方そなわり最強に見える。
Over the Language
Linqとは何ぞや。というと、一般にはLinq=クエリ構文=SQLみたいなの、という解釈が依然として主流のようで幾分か残念。これに対する異論は何度か唱えているけれど、では実際何をLinqと指すのだろう。公式の見解はともあれ勝手に考えると、対象をデータソースとみなし、Whereでフィルタリングし、Selectで射影するスタイルならば、それはLinqではないだろうか。
Linq to ObjectsはIEnumerableが、Linq to XmlではXElementが、Linq to SqlではExpression Treeが、Reactive ExtensionsにはIObservableの実装が必要であり、それぞれ中身は全く違う。昔はExpression Treeを弄ること、QueryProviderを実装することがLinq to Hogeの条件だと考えていたところがあったのだけど、今は、Linqの世界(共通のAPIでの操作)に乗っていれば、それはLinqなのだと思っている。
だからLinqは言語にも.NET Frameworkにも依存していない。Linqとは考え方にすぎない。例えば、Linq to Objectsはクロージャさえあればどの言語でも成り立つ(そう……JavaScriptでもね?)。むしろ重要なのは「Language INtegrated」なことであり、表面的なスタイル(SQLライクなシンタックス!)は全く重要ではない。言語に統合されていれば、異物感なく自然に扱え、IDEやデバッガなど言語に用意されているツールのサポートが得られる。(例えば……JavaScriptでガリガリと入力補完効かせてみたりね?)
言語を超えて共有される、より高い次元の抽象化としてのLinq。私はそんな世界に魅せられています。RxはLinqにおけるデータソースの概念をイベントや非同期にまで拡張(まさにExtension)して、更なる可能性を見せてくれました。次なる世界はDryad? まだまだLinqは熱い!
まとめ
ていうか改めてHaskellは偉大。でも、取っ付きやすさは大事。難しげなことを簡単なものとして甘く包んで掲示したLinqは、凄い。Rxも、取っ付きづらいFunctional Reactive Programmingを、Linqというお馴染みの土台に乗せたことで理解までの敷居を相当緩和させた。素晴らしい仕事です。
難しいことが簡単に出来る、というのがLinqのキモで、Rxも同じ。難しかったこと(イベントの合成/非同期)が簡単にできる。それが大事だし、その事をちゃんと伝えていきたいなあ。そして、Realworldでの実践例も。そのためにはアプリケーション書かなければ。アプリケーション、書きたいです……。書きます。
そういえばついでに、Rx一周年ということで、大体一年分の記事が溜まった(そしてロクに書いてないことが判明した)のと、少し前にlinq.jsのRT祭りがあった熱に浮かされて、応募するだけならタダ理論により10月期のMicrosoft MVPに応募しちゃったりなんかしました。恥ずかしぃー。分野にLinqがあれば!とか意味不明なことを思ったのですが、当然無いのでC#です、応募文句は、linq.js作った(DL数累計たった1000)と、Rx紹介書いてる、の二つだけって無理ですね明らかに。これから割と詳細に活動内容を書いて、送らなきゃいけないのですが、オール空白状態。応募したことに泣きたくなってきたよ、とほほ。
Windows Phone 7 + Reactive ExtensionsによるXml取得
- C# Rx Silverlight WindowsPhone - 10.07/19
Windows Phone 7にはReactive Extensionsが標準搭載されていたりするのだよ!なんだってー!と、いうわけで、Real World Rx。じゃないですけれど、Rxを使って非同期処理をゴニョゴニョとしてみましょう。ネットワークからデータ取って何とかする、というと一昔前はRSSリーダーがサンプルの主役でしたが、最近だとTwitterリーダーなのでしょうね。というわけで、Twitterリーダーにします。といっても、ぶっちゃけただたんにデータ取ってリストボックスにバインドするだけです。そしてGUI部分はSilverlightを使用してWindows Phone 7でTwitterアプリケーションを構築 - @ITのものを丸ごと使います。手抜き!というわけで、差分としてはRxを使うか否かといったところしかありません。
なお、別に全然Windows Phone 7ならでは!なことはやらないので、WPFでもSilverlightでも同じように書けます。ちょっとしたRxのサンプルとしてどうぞ。今回は出たばかりのWindows Phone Developer Tools Betaを使います。Windows Phone用のBlendがついていたりと盛り沢山。
Xmlを読み込む
とりあえずLinq to XmlなのでXElement.Load(string uri)ですね。違います。そのオーバーロードはSilverlightでは使えないのであった。えー。なんでー。とはまあ、つまり、同期系APIの搭載はほとんどなくて、全部非同期系で操作するよう強要されているわけです。XElement.Loadは同期でネットワークからXMLを引っ張ってくる→ダウンロード時間中はUI固まる→許すまじ!ということのようで。みんな大好きBackgroundWorkerたん使えばいいぢゃない、みたいなのは通用しないそうだ。
MSDNにお聞きすれば方法 : LINQ to XML で任意の URI の場所から XML ファイルを読み込むとあります。ネットワークからデータを取ってくるときはWebClient/HttpWebRequest使えというお話。
では、とりあえず、MainPage.xamlにペタペタと書いて、MessageBox.Showで確認していくという原始人な手段を取っていきましょう。XElementの利用にはSystem.Xml.Linqの参照が別途必要です。
public MainPage() { InitializeComponent(); var wc = new WebClient(); wc.OpenReadCompleted += (sender, e) => { var elem = XElement.Load(e.Result); // e.ResultにStreamが入ってる MessageBox.Show(elem.ToString()); // 確認 }; wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml")); // 非同期読み込み呼び出し開始 }
別に難しいこともなくすんなりと表示されました。簡単なことが簡単に書けるって素晴らしい。で、WebClientのプロパティをマジマジと見ているとAllowReadStreamBufferingなんてものが。trueの場合はメモリにバッファリングされる。うーん、せっかくなので完全ストリーミングでやりたいなあ。これfalseならバッファリングなしってことですよね?じゃあ、バッファリング無しにしてみますか。
var wc = new WebClient(); wc.AllowReadStreamBuffering = false; // デフォはtrueでバッファリングあり、今回はfalseに変更 wc.OpenReadCompleted += (sender, e) => { try { var elem = XElement.Load(e.Result); // ここで例外出るよ! } catch (Exception ex) { // Read is not supporeted on the main thread when buffering is disabled. MessageBox.Show(ex.ToString()); } };
例外で死にました。徹底して同期的にネットワーク絡みの処理が入るのは許しません、というわけですね、なるほど。じゃあ別スレッドでやるよ、ということでとりあえずThreadPoolに突っ込んでみた。
wc.OpenReadCompleted += (sender, e) => { ThreadPool.QueueUserWorkItem(_ => { try { var elem = XElement.Load(e.Result); MessageBox.Show(elem.ToString()); // 今度はここで例外! } catch(Exception ex) { // Invalid cross-thread access. Debug.WriteLine(ex.ToString()); } }); };
読み込みは出来たけど、今度はMessageBox.Showのところで、Invalid Cross Thread Accessで死んだ。そっか、MessageBoxもUIスレッドなのか。うーむ、世の中難しいね!というわけで、とりあえずDispatcher.BeginInvokeしますか。
wc.OpenReadCompleted += (sender, e) => { ThreadPool.QueueUserWorkItem(_ => { var elem = XElement.Load(e.Result); Dispatcher.BeginInvoke(() => MessageBox.Show(elem.ToString())); }); };
これで完全なストリームで非同期呼び出しでのXmlロードが出来たわけですね。これは面倒くさいし、Invoke系の入れ子が酷いことになってますよ、うわぁぁ。
Rxを使う
というわけで、非Rxでやると大変なのがよく分かりました。そこでRxの出番です。標準搭載されているので、参照設定を開きMicrosoft.Phone.ReactiveとSystem.Observableを加えるだけで準備完了。
var wc = new WebClient { AllowReadStreamBuffering = false }; Observable.FromEvent<OpenReadCompletedEventArgs>(wc, "OpenReadCompleted") .ObserveOn(Scheduler.ThreadPool) // ThreadPoolで動かすようにする .Select(e => XElement.Load(e.EventArgs.Result)) .ObserveOnDispatcher() // UIスレッドに戻す .Subscribe(x => MessageBox.Show(x.ToString())); wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml"));
非常にすっきり。Rxについて説明は、必要か否か若干悩むところですが説明しますと、イベントをLinq化します。今回はOpenReadCompletedイベントをLinqにしました。Linq化すると何が嬉しいって、ネストがなくなることです。非常に見やすい。更にRxの豊富なメソッド群を使えば普通ではやりにくいことがいとも簡単に出来ます。今回はObserveOnを使って、どのスレッドで実行するかを設定しました。驚くほど簡単に、分かりやすく。メソッドの流れそのままです。
FromAsyncPattern
WebClientだけじゃなく、ついでなのでHttpWebRequestでもやってみましょう。(HttpWebRequest)WebRequest.Create()死ね、といつも言ってる私ですが、SilverlightにはWebRequest.CreateHttpでHttpWebRequestが作れるじゃありませんか。何ともホッコリとします。微妙にこの辺、破綻した気がしますがむしろ見なかったことにしよう。
var req = WebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; req.BeginGetResponse(ar => { using (var res = req.EndGetResponse(ar)) using (var stream = res.GetResponseStream()) { var x = XElement.Load(res.GetResponseStream()); Dispatcher.BeginInvoke(() => MessageBox.Show(x.ToString())); } }, null);
非同期しかないのでBeginXxx-EndXxxを使うのですが、まあ、結構面倒くさい。そこで、ここでもまたRxの出番。BeginXxx-EndXxx、つまりAPM(Asynchronus Programming Model:非同期プログラミングモデル)の形式の非同期メソッドをラップするFromAsyncPatternが使えます。
var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() // 非同期実行開始(Invoke()じゃなくて()でもOKです、ただのDelegateなので) .Select(res => XElement.Load(res.GetResponseStream())) .ObserveOnDispatcher() .Subscribe(x => MessageBox.Show(x.ToString()));
ラップは簡単で型として戻り値を指定してBeginXxxとEndXxxを渡すだけ。あとはそのまま流れるように書けてしまいます。普通だと面倒くさいはずのHttpWebRequestのほうがWebClientよりも素直に書けてしまう不思議!FromAsyncPatter、恐ろしい子。WebClient+FromEventは先にイベントを設定してURLで発動でしたが、こちらはURLを指定してから実行開始という、より「同期的」と同じように書ける感じがあって好き。WebClient使うのやめて、みんなHttpWebRequest使おうぜ!(ふつーのアプリのほうでは逆のこと言ってるのですががが)
ところで、非同期処理の実行開始タイミングはInvokeした瞬間であって、Subscribeした時ではありません。どーなってるかというと、ぶっちゃけRxは実行結果をキャッシュしてます。細かい話はまた後日ちゃんと紹介するときにでも。
バインドする
GUIはScottGu氏のサンプルを丸々頂いてしまいます。リロードボタンを押したらPublicTLを呼ぶだけ、みたいなのに簡略化してしまいました。
<Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Button Grid.Row="0" Height="72" Width="200" Content="Reload" Name="Reload"></Button> <ListBox Grid.Row="1" Name="TweetList" DataContext="{Binding}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Source="{Binding Image}" Height="73" Width="73" VerticalAlignment="Top" /> <StackPanel Width="350"> <TextBlock Text="{Binding Name}" Foreground="Red" /> <TextBlock Text="{Binding Text}" TextWrapping="Wrap" /> </StackPanel> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid>
あとは、ボタンへのイベント設定と、Twitterのクラスを作る必要があります。
public class TwitterStatus { public long Id { get; set; } public string Text { get; set; } public string Name { get; set; } public string Image { get; set; } public TwitterStatus(XElement element) { Id = (long)element.Element("id"); Text = (string)element.Element("text"); Name = (string)element.Element("user").Element("screen_name"); Image = (string)element.Element("user").Element("profile_image_url"); } } public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); Reload.Click += new RoutedEventHandler(Reload_Click); // XAMLに書いてもいいんですけど。 } void Reload_Click(object sender, RoutedEventArgs e) { var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() .Select(res => XElement.Load(res.GetResponseStream())) .Select(x => x.Descendants("status").Select(xe => new TwitterStatus(xe))) .ObserveOnDispatcher() .Subscribe(ts => TweetList.ItemsSource = ts); } }
実行するとこんな具合に表示されます。簡単ですねー。ただ、これだとリロードで20件しか表示されないので、リロードしたら継ぎ足されるように変更しましょう。
イベントを合成する
継ぎ足しの改善、のついでに、一定時間毎に更新も加えよう。基本は一定時間毎に更新だけど、リロードボタンしたら任意のタイミングでリロード。きっとよくあるパターン。Reload.Click+=でハンドラ足すのはやめて、その部分もFromEventでObservable化してしまいましょう。そして一定時間毎のイベント発動はObservable.Timerで。
// 30秒毎もしくはリロードボタンクリックでPublicTimeLineを更新 Observable.Merge( Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(30), Scheduler.NewThread).Select(_ => (object)_), Observable.FromEvent<RoutedEventArgs>(Reload, "Click").Select(_ => (object)_)) .SelectMany(_ => { var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)(); }) .Select(res => XStreamingReader.Load(res.GetResponseStream())) .SelectMany(x => x .Descendants("status") .Select(xe => new TwitterStatus(xe)) .Reverse()) // 古い順にする .Scan((before, current) => before.Id > current.Id ? before : current) // 最後に通した記事よりも古ければ通さない(で、同じ記事を返す) .DistinctUntilChanged(t => t.Id) // 同じ記事が連続して来た場合は何もしないでreturn .ObserveOnDispatcher() .Subscribe(t => TweetList.Items.Insert(0, t)); // Insertだって...
流れるようにメソッド足しまくるの楽しい!楽しすぎて色々足しすぎて悪ノリしている感が否めません、とほほ。解説しますと、まず一行目のMerge。これは複数本のイベントを一本に統一します。統一するためには型が同じでなければならないのですが、今回はTimer(long)と、Click(RoutedEventArgs)なのでそのままでは合成出来ません。どちらも発火タイミングが必要なだけでlongもRoutedEventArgsも不必要なため、Objectにキャストしてやって合流させました。
こういう場合、Linq to Objectsなら.Cast<object>()なんですよね。Castないんですか?というと、一応あるにはあるんですが、実質無いようなもので。というわけで、今のところキャストしたければ.Select(=>(object))を使うしかありません。多分。もっとマシなやり方がある場合は教えてください。
続いてSelectMany。TimerもしくはClickは発火のタイミングだけで、後ろに流すのはFromAsyncPatternのデータ。こういった、最初のイベントは発火タイミングにだけ使って、実際に流すものは他のイベントに摩り替える(例えばマウスクリックで発動させて、あとはマウスムーブを使うとか)というのは定型文に近い感じでよく使うことになるんじゃないかと思います。SelectMany大事。
XMLの読み込み部は、せっかくなので、こないだ作ったバッファに貯めこむことなくXmlを読み込めるXStreamingReaderを使います。こんな風に、XMLを読み取ってクラスに変換する程度ならXElement.Loadで丸々全体のツリーを作るのも勿体無い。XStreamingReaderなら完全ストリーミングでクラスに変換出来ますよー。という実例。
その下は更にもう一個SelectMany。こっちはLinq to Objectsのものと同じ意味で、IEnumerableを平たくしています。で、ScanしたDistinctUntilChangedして(解説が面倒になってきた)先頭にInsert(ちょっとダサい)。これで古いものから上に足される = 新しい順番に表示される、という形になりました。XAML側のListBoxを直に触ってInsertとか、明らかにダサい感じなのですが、まあ今回はただのサンプルなので見逃してください。
RxのMergeに関しては、後日他のイベント合流系メソッド(CombineLatest, Zip, And/Then/Plan/Join)と一緒に紹介したいと思っています。合流系大事。
まとめ
驚くほどSilverlightで開発簡単。っぽいような印象。C#書ける人ならすぐにとっかかれますねー。素晴らしい開発環境だと思います。そして私は同時に、Silverlight全然分かってないや、という現実を改めて突きつけられて参ってます。XAMLあんま書けない。Blend使えない。MVVM分からない。モバイル開発云々の前に、基本的な技量が全然欠けているということが良く分かったし、それはそれで良い収穫でした。この秋なのか冬なのかの発売までには、ある程度は技術を身につけておきたいところです。
そしてそれよりなにより開発機欲すぃです。エミュレータの起動も速いし悪くないのですが、やっぱ実機ですよ、実機!配ってくれぇー。
XStreamingReader - Linq to Xml形式でのストリーミングXml読み込み
- C# - 10.07/16
CodePlex : XStreamingReader - Streaming for Linq to Xml
1クラスライブラリシリーズ。もしくはストリーミングをIEnumerableに変換していこうシリーズ。またはシンプルだけど小粒でピリッと隙間にぴったりはまるシリーズ(を、目指したい)。といったわけで、100行程度ではあるのですが、表題の機能を持つコードをCodePlexに公開しました。それとおまけとして、XMLファイルからC#クラス自動生成T4 Templateも同梱。
Linq to Xml風にXmlを読み込めるけれど、ツリーを構築せずストリームで、完全遅延評価で実行します。Linq to Xmlには、書き込み用にXStreamingElementというものがあるため、それと対比した読み込み用という位置付けのつもりです。メモリの厳しいモバイル機器や、巨大なXMLを読み込む際に使えるんじゃないかと思っています。
利用例
ぶっちゃけまるっきりXElementと同じです。例としてYahoo!天気情報のRSSから京都と東京を取り出し。
// XElement var kyoto = XElement.Load(@"http://rss.weather.yahoo.co.jp/rss/days/6100.xml") .Descendants("item") .Select(x => new { Title = (string)x.Element("title"), Description = (string)x.Element("description"), PubDate = (DateTime)x.Element("pubDate") }) .Where(a => !a.Title.StartsWith("[PR]")) // itemが広告の場合は除外 .ToArray(); // XStreamingReader var tokyo = XStreamingReader.Load(@"http://rss.weather.yahoo.co.jp/rss/days/4410.xml") .Descendants("item") .Select(x => new { Title = (string)x.Element("title"), Description = (string)x.Element("description"), PubDate = (DateTime)x.Element("pubDate") }) .Where(a => !a.Title.StartsWith("[PR]")) // itemが広告の場合は除外 .ToArray();
Load/Parseで生成し、ElementsやDescendantsで抽出。あとは、IEnumerable<XElement>となっているので、SelectしたりWhereしたり。完全にLinq to Xmlと同じAPIです。同じすぎてこれだけだと利点がさっぱり見えませんが、100%遅延評価+ストリーミング読み込みで逐次生成という違いがあります。詳しくは次のセクションで。
バックグラウンド
Androidでは性能のためにDOMじゃなくてSAXでXML扱うんだ。という話を良く聞いて、確かにただデータ取るためだけにDOM構築ってのは嫌だし、そりゃ避けたい。対象がDOMなら素直にそう思いますが、しかし、もしそれがLinq to Xmlならどうだろう?Windows Phone 7だったらLinq to Xml使うに決まってるよ、と言いたいのですが、これってDOMと同じく、すぐに(LoadなりParseなりした直後)ツリーを構築しています。Elements()なりDescendants()なりの戻り値がIEnumerableなため、遅延評価かと思ってしまうわけですが、遅延評価されるのはツリーの探索が、というだけであって、構築自体は即時でされています。
DOMに比べて軽量(という謳い文句)であることと、非常に軽々と書けるため抵抗感がないわけですが、考えてみれば Load.Descendants.Select みたいな、API叩いて何らかのクラスなり匿名型なりに変換するという程度の、しかしよくある定型作業は、わざわざツリー作る必要はなくストリーミングで取れるし、それならばストリーミングで取るべきではある。しかし、今時XmlReaderを直で触るなんて、時代への逆行のようなことはやりたくない。
ストリームはIEnumerableに変換するのがLinq以降のC#の常識。というのを日々連呼しているので、今回はXmlReaderをIEnumerable<T>に変換しなければなりません。しかし、困ったのが、<T>のTを何にすればいいのか、ということ。ファイル読み込みなら一行のString。データベースなら、IDataRecord(DbExecutorというライブラリとしてCodePlexに公開しています)を用いましたが、XmlReaderだと適当なのが見当たらない。XmlReaderを直接渡すのは危なっかしいし、そもそも渡したところで面倒くさいことにかわりなくてダメだ。何か適切なコンテナが……。
と、考えたり考えなかったりで、Twitterでもにょもにょと言っていたら
@neuecc Linq to Xml を使うにしても XmlReaderからReadSubtreeで切り出した断片に対してかなー、XML全体をオンメモリさせる必然性がなければStreamから読んで処理した端からGCに捨てて貰えるようにしておきたいだけだけど
http://twitter.com/kazuk/status/18193188205
うぉ!うぉぉぉぉぉ!なるほど、断片をXElementに変換してそれを渡せば、操作しやすいし感覚的にもXElement.Loadなどと変わらないしでベストだ!言われてみればそりゃそうだよねー、ですが全然頭になかった、まさにコロンブスの卵。こういうことがサラッと出てくることこそが、凄さだよね。
と、感嘆しつつ、それそのまま頂き、というわけで、TをXElementにするという形で解決しました。
public IEnumerable<XElement> Descendants(XName name) { using (var reader = readerFactory()) { while (reader.ReadToFollowing(name.LocalName, name.NamespaceName)) { yield return XElement.Load(reader.ReadSubtree()); } } }
Descendantsの実装はこんな感じで、断片から都度XElement生成しているという、それだけの単純明快な代物です。そのため挙動はXElement.Load.Descendantsと完全同一というわけじゃありません。例えばサブツリー中に同名の要素がある場合、XElementでDescendantsの場合はサブツリー中の要素も列挙しますが、XStreamingReaderではトップ要素のものだけが拾われます。
他に注意点としては、それぞれのXElementは完全に独立しているため、ParentやNextNodeなどは全てnullです。よってAncestorsで先祖と比較しながらの複雑な抽出、などといったことも出来ません。TwitterのAPIのような、ウェブサービスとして用意されているXMLなら素直な構造なので問題はありませんが、SGMLReaderでLinq to HTMLなどといった場合は、結構複雑なクエリで抽出することになるため使えないでしょう。その場合は素直にXElement.Loadを使うのが良いと思います。
おまけ(Xml→自動クラス生成)
Xmlから人力でClass作るのって定型作業で面倒だよねー。ということで、自動生成するT4 Templateも同梱しました。プロパティ定義だけではなく(ちゃんとPascalCaseに直します)、コンストラクタにXElementを投げるとマッピングもしてくれます。つまりは、XStreamingReaderの仕様に合わせたものです。
.ttの上の方にある3つの項目を適当に書き換えると
string XmlString = new WebClient().DownloadString("http://twitter.com/statuses/public_timeline.xml"); const string DescendantsName = "status"; // select class root const string Namespace = "Twitter"; // namespace
namespace Twitter { public class Status { public string CreatedAt { get; set; } public string Id { get; set; } // snip... public User User { get; set; } public string Geo { get; set; } public Status(XElement element) { this.CreatedAt = (string)element.Element("created_at"); this.Id = (string)element.Element("id"); this.User = new User(element.Element("user")); this.Geo = (string)element.Element("geo"); } } public class User { public string Id { get; set; } public string Name { get; set; } public string ScreenName { get; set; } // snip... public string FollowRequestSent { get; set; } public User(XElement element) { this.Id = (string)element.Element("id"); this.Name = (string)element.Element("name"); this.ScreenName = (string)element.Element("screen_name"); this.FollowRequestSent = (string)element.Element("follow_request_sent"); } } }
こんなのが生成されます。型は全部stringになるので、手動で直してください。半自動生成。T4で生成→新しいクラスファイル作って生成結果をコピペ→型を直す。みたいな使い方をイメージしています。完全自動生成じゃないと変更に対する自動追随ってのが出来ないので、自動生成する意味が半減。しかし、型かあ、スキーマないと無理ですな。まあ、ウェブサービスのAPIなどは基本的には固定で変化がないでしょうから、ある程度は手間を省けるんじゃないかと思われます。
まとめ
断片とはいえ、XElement作るのは無駄じゃないの?というと、無駄ではあります。抽出したらすぐ用済みでポイなわけなので、純粋にパフォーマンスの観点から言えばXmlReaderを直で触ったほうが良いに決まっています。しかし、さすがにそこまで来ると無視して良いと思うわけです。例えばLinqで一時的な匿名型は使わないって?ああ、むしろLinqなんてやめて全部forループにでもします?言いだいたらキリがない。
今回で大事なのは、ストリーミング化しても、決して使いやすさは損なわれていないということです。ツリー構築型と全く同じように快適に書ける。それが何より大切。「性能のために書きやすさが犠牲になるぐらいなら性能なんていらない!」と、現実は言えなくても心では言ってしまいます。ユーザー視点だと逆ですが……。ただ、中長期的には、スパゲティコードは開発者を幸せにしない→機能追加速度低下/洗練が鈍る→ユーザーも不幸せになる、のループが回るので綺麗さは重要。勿論、そこが性能上本当にボトルネックになっているならば気合入れて叩く必要がありますが、気分的に、もしくはマイクロベンチマーク的にちょっと性能Downな程度でパフォーマンスチューニングとか言い出すのならシバいてよし。
といったわけかで、私なりにWindows Phone 7プログラミングへの準備を進めています。これで、準備になってる?……だと?ご冗談を。ですね、はい、すみません。開発キットのベータ版が出たので、次回はWindows Phone 7で何か作ろう紹介でも書く予定は未定。
IEnumerableのCastを抹殺するためのT4 Templateの使い方
- C# - 10.07/07
.NET Framework 1.0の負の遺産、HogeCollection。それらの大半はIEnumerable<T>を実装していない、つまるところ一々Cast<T>しなければLinqが使えない。ほんとどうしょうもない。大抵のHogeCollectionは実質Obsoleteみたいなもので、滅多に使わないのだけれど、ただ一つ、RegexのMatchCollectionだけは頻繁に使うわけで、Castにイラつかされるので殺害したい。RegexにはMatchCollection、GroupCollection、CaptureCollectionという恐怖の連鎖が待っているので余計に殺したい。(ところで全く本題とは関係ないのですが、Captureは今ひとつ使い道がわからな……)
// わざとらしい例ですが var q = Regex.Matches("aag0 hag5 zag2", @"(.)ag(\d)") .Cast<Match>() .SelectMany(m => m.Groups.Cast<Group>().Skip(1).Select(g => g.Value)) .ToArray(); // a0h5z2
おお、何というCast地獄!つーか.NET 4でBCL書き直したとか言うんなら、その辺も少し融通聞かせてIEnumerable<T>にしてくれてもさー。あ、要望出さないのが悪いとかなのでしょうか……。それなら自己責任ですね、ちゃんと出していかないと。なのはともかく、自己責任ならば自己責任なりに、文句だけ言っててもしょうがないので自前で何とかしましょう。
ようするに.Cast<Hoge>()を自動で挟めばいいわけですよね。んー、ぴこーん!T4でジェネレートすればいいんじゃね?というわけで、T4 Templateを使ってみました。実際のところT4試してみたかったんだけどネタがなかったので、ネタが出てきて万歳!が本音だったりはします。
何もないところからテンプレートじゃあ作りようもないので、ひとまず完成系を書いてみる。
public static class MatchCollectionExtensions { public static IEnumerable<TResult> Select<TResult>(this MatchCollection source, Func<Match, TResult> selector) { return source.Cast<Match>().Select(selector); } // Where, Aggregate, .... }
こんな形。グッとイメージしやすくなります。型引数のTSourceを消して、Castを挟んで……。やるべき事が大体見えてきました。まずは、Enumerableの拡張メソッドの抽出を。
var extMethods = typeof(Enumerable) .GetMethods() .Where(mi => Attribute.IsDefined(mi, typeof(ExtensionAttribute)));
特にBindingFlagsは設定しませんが、ExtensionAttributeが指定されているものがあれば拡張メソッド、という判定で問題なく取り出すことが出来ます。続いて戻り値を抽出。
var returnType = extMethods .Select(mi => mi.ReturnType) .Select(mi => Regex.Replace(mi.Name, "`.*$", "") + (mi.IsGenericType ? ("<" + string.Join(", ", mi.GetGenericArguments().Select(t => t.Name)) + ">") : ""));
IEnumerable<T>のNameはIEnumerable1になっているので1を正規表現で削除。そして引数を並べる。ただまあ、これだけだとジェネリック引数がネストしたものに対応出来ていなかったりTSourceが除去できてなかったりダメなのですが、それはそれ(最終的なコードは下記の実例のほうを見てください)。
といったわけで、相変わらずリフレクション+Linqは鉄板ですね。というかLinqなしのリフレクションとかやりたくない……。こんな感じにポチポチと素材集めをしたら、T4化します。
<#@ template language="C#" #> <#@ output extension="cs" #> <#@ assembly Name="System.Core.dll" #> <#@ import namespace="System" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Runtime.CompilerServices" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ import namespace="System.Reflection" #> <# var target = new Dictionary<string, string> { {"MatchCollection", "Match"}, {"GroupCollection", "Group"}, {"CaptureCollection", "Capture"} }; #> <# var ignoreMethods = new HashSet<string> { "Max", "Min", "Average", "Sum", "Zip", "OfType", "Cast", "Join", "GroupJoin", "ThenBy", "ThenByDescending", "LongCount" }; #> using System; using System.Collections.Generic; using System.Linq; namespace System.Text.RegularExpressions { <# foreach (var kvp in target) { #> public static class <#= kvp.Key.Replace(".","") #>Extensions { <# foreach (var methodInfo in typeof(Enumerable).GetMethods().Where(mi => Attribute.IsDefined(mi, typeof(ExtensionAttribute)))) { if(ignoreMethods.Contains(methodInfo.Name)) continue; #> public static <#= MakeReturnType(methodInfo, kvp.Value) #> <#= methodInfo.Name #><#= MakeGenericArguments(methodInfo) #>(this <#= kvp.Key #> source<#= MakeParameters(methodInfo, kvp.Value) #>) { return source.Cast<<#= kvp.Value #>>().<#= MakeMethodBody(methodInfo) #>; } <#}#> } <#}#> } <#+ const string TSource = "TSource"; static string ConstructTypeString(Type type, string castType) { var result = type.Name.Contains(TSource) ? type.Name.Replace(TSource, castType) : Regex.Replace(type.Name, "`.*$", ""); if (type.IsGenericType) { result += string.Format("<{0}>", string.Join(", ", type.GetGenericArguments().Select(t => ConstructTypeString(t, castType)))); } return result; } static string MakeReturnType(MethodInfo info, string castType) { return ConstructTypeString(info.ReturnType, castType); } static string MakeGenericArguments(MethodInfo info) { var types = info.GetGenericArguments().Select(t => t.Name).Where(s => s != TSource); return types.Any() ? string.Format("<{0}>", string.Join(", ", types)) : ""; } static string MakeParameters(MethodInfo info, string castType) { var param = info.GetParameters() .Skip(1) .Select(pi => new { pi.Name, ParameterType = ConstructTypeString(pi.ParameterType, castType) }); return param.Any() ? ", " + string.Join(", ", param.Select(a => a.ParameterType + " " + a.Name)) : ""; } static string MakeMethodBody(MethodInfo info) { var args = info.GetParameters().Skip(1).Select(pi => pi.Name); return string.Format("{0}({1})", info.Name, args.Any() ? string.Join(", ", args) : ""); } #>
上のほうの、ディクショナリ(target)の初期化子を弄ることで対象の型を増減できます。namespaceはテンプレートに埋め込みなので変える場合は適当に変えてください。ハッシュセット(ignoreMethods)はその名の通り、除外したい拡張メソッドを指定します。今回はMax,Minなどと、Zip,Join,GroupJoin(これらは若干弄らないと対応出来ないので見送り)を除外しています。あとLongCountも外してます、理由はRxのSystem.InteractiveがLongCountで競合するから(多分、Rxチームのミスだと思うのでそのうち直ると思います)。
どんなクラスにも対応出来る(はず)ので、もしキャストが必要なウザいHogeCollectionがあったら、このテンプレートを使ってみると良いかもしれません。WinFormsのControl.ControlCollectionとかWPFのUIElementCollectionとか(そういうのは、元よりごった煮で詰め込むの前提なので、UIElementでSelect出来ても嬉しくはないかなー)。ともあれ、利用はご自由にどうぞ。
こんな感じに、MatchCollection, GroupCollection, CaptureCollectionだと合計1100行ぐらいのコードが生成されます。これで、CastいらずにLinqが書けるようになりました。メデタシメデタシ。
T4 Template
T4 Templateはかなり良いですね。VisualStudioと密接に動作して、生成出来ないようならエラーですぐ知らせてくれるのが嬉しい。これ大事。超大事。それがないと書けません。C#もそうだけれど、とりあえず書く→コンパイラエラー→直す、をリアルタイムで繰り返せるのは素晴らしい。現代のプログラミング環境はこうでないと、な良さに溢れてます。アドインを入れれば入力補完やシンタックスハイライトも付いてくるので非常に快適。
T4 Templateは標準搭載の機能だし実に強力なので、積極的に使っていきたいものです。MSDNだとコード生成とテキスト テンプレート辺りかな。例によって、読んでもさっぱり意味がわかりません(笑) 今のところオフィシャルだとこんなドキュメントしかないのかなあ、少し厳しめ。いやまあ、T4自体は構文がシンプルなので、ただ書くだけならサンプル改変で何とかなる、というか、私もサンプル改変以上の機能は知らないのですががが。
Rx(Reactive Extensions)を自前簡易再実装する
という表題でUstreamやりました。Reactive Extensions for .NET (Rx)のSelect, Where, ToObservable, FromEventを実装することで、挙動を知ろうという企画。結果?酷いものです!
Shift+Alt+F10はお友達。それにしたってぐだぐだ。想像以上に頭が真っ白。セッションやライブコーディングしてる人は凄いね、と実感する。プレゼンどころか人と話すのも苦手です、な私には敷居が高かった。とにかく説明ができない。デバッガで動かせば分かりやすいよねー、なんてやる前は思ってたんですが、人がデバッガ動かしてるの見ててもさっぱり分かりやすくないよ!ということに途中で気づいて青ざめる。
まあ、こういうのも経験積まないとダメよね、と考えると、リスクゼロ(見てくれた人には申し訳ないですが)で練習出来るので、これからもネタがあればやっていきたいとは思います。反省は活かして。ネタはあまりないのでリクエストあればお願いします。Ustreamの高画質配信については、去年に書いた高画質配信するためのまとめ記事が自分で役に立ったぜ、経験が活きたな、的な。私自身の環境はちょっと、というかかなり変わったのですが、配信の基本的部分に関しては今も昔も(といっても1年前か)変わってなかったね。
さて、そんなUstreamはともかくとして、Rxの基本的な拡張メソッド「Select, Where」と、基本的な生成メソッド「ToObservable, FromEvent」を自前で実装してみる/デバッガで追ってみましょう。自分の手で動かして追うと理解しやすくなります。なので、以下に出すソースはコピペでもいいので、実際にVisualStudio上で動かしてもらえればと思います。
IEnumerableで考える
IObservableの拡張メソッド実装、の前に復習を兼ねてIEnumerableの拡張メソッドを実装してみましょう。
public static IEnumerable<TR> Select<T, TR>(IEnumerable<T> source, Func<T, TR> selector) { foreach (var item in source) { yield return selector(item); } }
恐ろしく簡単です。こんなにも簡単に書けるのは、yield returnのお陰。裏では、コンパイラが自動で対応するIEnumerable, IEnumeratorを生成してくれます。もしこれを教科書通りに自前で書くとしたら
public static IEnumerable<TR> Select<T, TR>(IEnumerable<T> source, Func<T, TR> selector) { return new SelectEnumerable<TR>(); // 本当は引数も必要ですが省略 } class SelectEnumerable<T> : IEnumerable<T> { public IEnumerator<T> GetEnumerator() { return new SelectEnumerator<T>(); } // 以下略 // IEnumerator IEnumerable.GetEnumerator() } class SelectEnumerator<T> : IEnumerator<T> { // Current, Dispose, MoveNextが必要ですが略 }
ああ、長い。やってられない。こんなものがオブジェクト指向だなどと言うならば、クソったれだと唾を吐きたくなる。そこで、AnonymousHogeパターンを用いれば……
public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> source, Func<T, TR> selector) { return new AnonymousEnumerable<TR>(() => { var enumerator = source.GetEnumerator(); return new AnonymousEnumerator<TR>( () => enumerator.MoveNext(), () => selector(enumerator.Current), () => enumerator.Dispose() ); }); }
驚くほどスッキリ。デザインパターンの本はC#でラムダ式全開でやり直すと、考え方はともかく、コードは全然違った内容になるんじゃないかなあ、とか思いつつ。この突然出てきたAnonymousEnumerableに関しては.NET Reactive Framework メソッド探訪第二回:AnonymousEnumerableを参照にどうぞ。去年の9月ですか……。AnonymousObservableも紹介する、といって10ヶ月後にようやく果たせている辺りが、やるやる詐欺すぎて本当にごめんなさい。
簡単に説明すれば、コンストラクタにラムダ式で各メソッドの本体を与えてあげることで、その場でクラスを作ることが出来るという代物です。クロージャによる変数キャプチャにより、引数を渡し回す必要もないため非常にすっきり書く事ができます。
これってようするにJavaの無名クラスでしょ?と言うと、その通り。おお、Java、大勝利。なんてこたぁーない。大は小を兼ねない、むしろこれは、小は大を兼ねる事の証明。
AnonymousObservable
IObservableはIEnumerableのようなコンパイラサポートはないので、自前で書かなければなりません。が、普通に書くと面倒なので、AnonymousObservableを使って書くことにしましょう。
public class AnonymousObservable<T> : IObservable<T> { Func<IObserver<T>, IDisposable> subscribe; public AnonymousObservable(Func<IObserver<T>, IDisposable> subscribe) { this.subscribe = subscribe; } public IDisposable Subscribe(IObserver<T> observer) { return subscribe(observer); } } public class AnonymousObserver<T> : IObserver<T> { Action<T> onNext; Action<Exception> onError; Action onCompleted; public AnonymousObserver(Action<T> onNext, Action<Exception> onError, Action onCompleted) { this.onNext = onNext; this.onError = onError; this.onCompleted = onCompleted; } public void OnCompleted() { onCompleted(); } public void OnError(Exception error) { onError(error); } public void OnNext(T value) { onNext(value); } } public class AnonymousDisposable : IDisposable { Action dispose; bool isDisposed = false; public AnonymousDisposable(Action dispose) { this.dispose = dispose; } public void Dispose() { if (!isDisposed) { isDisposed = true; dispose(); } } }
そのまま書き出すだけなので、難しいことは何一つありませんが、面倒くさい……。なお、今回はRx抜きでの実装のためこうして自前で定義していますが、RxにはObservable.Create/CreateWithDisposable、Observer.Create、Disposable.Createというメソッドが用意されていて、それらは今回定義したAnonymousHogeと同一です。new ではなくCreateメソッドで生成するため型推論が効くのが嬉しい。
Observable.Select/Where
下準備が済んだので実装していきましょう。まずはSelect。
public static IObservable<R> Select<T, TR>(this IObservable<T> source, Func<T, TR> selector) { return new AnonymousObservable<TR>(observer => source.Subscribe( new AnonymousObserver<T>( t => observer.OnNext(selector(t)), observer.OnError, observer.OnCompleted))); }
Enumerableと似ているようで非常に分かりにくい。AnonymousObservableの引数のラムダ式は、Subscribeされた時に実行されるもの。というわけで、突然出てきているかのような引数のobserverは、Subscribeによって一つ後ろのメソッドチェーンから渡されるものとなります。
Observable.Range(1, 10) // これがsource .Select(i => i * i) .Subscribe(i => Console.WriteLine(i)); // これがobserver
こんな前後関係の図式になっています。ドットの一つ前のメソッドがsource、一つ後ろのメソッドがobserver。 最終的な目的としては元ソースからOnNext->OnNext->OnNextと値を伝搬させる必要があるわけですが、元ソースは末端どころか次に渡す先すら知りません。そのため、まず最初(Subscribeされた時)にsource.Subscribeの連鎖で元ソースまで遡ってやる必要がある、というわけです。非常に説明しづらいのでデバッガで追ってみてください。
public static IObservable<T> Where<T>(this IObservable<T> source, Func<T, bool> predicate) { return new AnonymousObservable<T>(observer => source.Subscribe( new AnonymousObserver<T>( t => { if (predicate(t)) observer.OnNext(t); }, observer.OnError, observer.OnCompleted))); }
WhereはSelectのOnNext部分が違うだけのもの。コピペ量産体制。
ToObservable
Selectなどと同じくreturn new AnonymousObservableですが、もうSubscribeはしません(そもそもIObservable sourceがないので出来ないですが)。ここからは、末端から伝達されてきたobserverに対して値をPushしてやります。
public static IObservable<T> ToObservable<T>(this IEnumerable<T> source) { return new AnonymousObservable<T>(observer => { var isErrorOccured = false; try { foreach (var item in source) { observer.OnNext(item); } } catch (Exception e) { isErrorOccured = true; observer.OnError(e); } if (!isErrorOccured) observer.OnCompleted(); return new AnonymousDisposable(() => { }); }); }
Subscribeされると即座にforeachが回ってOnNext呼びまくる。ToObservableはHot or ColdのうちColdで、Subscribeされるとすぐに値が列挙されるわけです。Coldってのは、なんてことはなく、ようはすぐforeachされるからってだけの話でした。
戻り値のIDisposableは、FromEventではイベントのデタッチなどの処理がありますが、ToObservableでは何もする必要がないので何も無し。
FromEvent徹底解剖
Coldだけでは、別にEnumerbaleと全然変わらなくて全く面白くないので、Hot Observableも見てみます。Hotの代表格はFromEvent。そんなFromEventには4つのオーバーロードがあります。せっかくなので、細かく徹底的に見てみましょう。
public class EventSample { public event EventHandler BlankEH; public event EventHandler<SampleEventArgs> GenericEH; public event SampleEventHandler SampleEH; } public class SampleEventArgs : EventArgs { } public delegate void SampleEventHandler(object sender, SampleEventArgs e); static void Main(string[] args) { var sample = new EventSample(); // 1. EventHandlerに対応するもの Observable.FromEvent( h => sample.BlankEH += h, h => sample.BlankEH -= h); // 2. EventHandler<EventArgs>に対応するもの Observable.FromEvent<SampleEventArgs>( h => sample.GenericEH += h, h => sample.GenericEH -= h); // 3. 独自EventHandlerに対応するもの Observable.FromEvent<SampleEventHandler, SampleEventArgs>( h => new SampleEventHandler(h), h => sample.SampleEH += h, h => sample.SampleEH -= h); // 4. リフレクション Observable.FromEvent<SampleEventArgs>(sample, "GenericEH"); Observable.FromEvent<SampleEventArgs>(sample, "SampleEH"); }
FromEventと言ったら文字列で渡して―― という感じだったりですが、むしろそれのほうが例外的なショートカットで、基本はeventをadd/removeする関数を渡します。3つもありますが、基本的には三番目、conversionが必要なものが最も多く出番があるでしょうか。ただのEventHandlerなんて普通は使わないし、ジェネリクスのEventHandlerもほとんど見かけないしで、どうせみんな独自のEventHandlerなんでしょ、みたいな。もしEventHandler<T>で統一されていれば、こんな面倒くさいconversionなんて必要なかったのに!もしくは、みんなAction<object, TEventArgs>で良かった。名前付きデリゲートの氾濫の弊害がこんなところにも……。
実際のとこ文字列渡しで良いよねー、と思います。リフレクションのコストはどうせ最初の一回だけだし。リファクタリング効かないといっても、別にイベントの名前なんて変更しないっしょっていうか、フレームワークに用意されてるイベントは固定だし、って話ですし。
FromEventの作成
そんなわけで、今回は3引数のFromEventを作ります。FromEventの戻り値はIEventなので、IEventの定義も一緒に。
public interface IEvent<TEventArgs> where TEventArgs : EventArgs { object Sender { get; } TEventArgs EventArgs { get; } } public class AnonymousEvent<TEventArgs> : IEvent<TEventArgs> where TEventArgs : EventArgs { readonly object sender; readonly TEventArgs eventArgs; public AnonymousEvent(object sender, TEventArgs eventArgs) { this.sender = sender; this.eventArgs = eventArgs; } public object Sender { get { return sender; } } public TEventArgs EventArgs { get { return eventArgs; } } } public static IObservable<IEvent<TEventArgs>> FromEvent<TDelegate, TEventArgs>( Func<EventHandler<TEventArgs>, TDelegate> conversion, Action<TDelegate> addHandler, Action<TDelegate> removeHandler) where TEventArgs : EventArgs { return new AnonymousObservable<IEvent<TEventArgs>>(observer => { var handler = conversion((sender, e) => { observer.OnNext(new AnonymousEvent<TEventArgs>(sender, e)); }); addHandler(handler); return new AnonymousDisposable(() => removeHandler(handler)); }); }
感覚的にはToObservableの時と一緒。Subscribeされたら実行される関数を書く。Subscribe時に実際に実行されるのはaddHandlerだけ。つまりイベント登録。そしてイベントが発火した場合は、conversionのところのラムダ式に書いたものが呼び出される、つまり次のobserverに対してOnNextでIEventを送る。そして、DisposeされたらremoveHandlerの実行。
これが、Hotなわけですね。つまりSubscribeだけではOnNextが呼ばれず、もう一段階、奥から実行される。
// 実行例としてObservableCollectionなどを用意。 var collection = new ObservableCollection<int>(); var collectionChanged = Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( h => new NotifyCollectionChangedEventHandler(h), h => collection.CollectionChanged += h, h => collection.CollectionChanged -= h) .Select(e => (int)e.EventArgs.NewItems[0]); // attach collectionChanged.Subscribe(new AnonymousObserver<int>(i => Console.WriteLine(i), e => { }, () => { })); collectionChanged.Subscribe(new AnonymousObserver<int>(i => Console.WriteLine(i * i), e => { }, () => { })); collection.Add(100); // 100, 10000 collection.Add(200); // 200, 40000
利用時は大体こんな感じになります。いたって普通。
まとめ
というわけで実装を見ていきましたが、意外と簡単です。リフレクタでToObservable見たけどこんな簡単じゃなかったぞ!と言われると、そうですね、実際のRxはScheduler(カレントスレッドで実行するかスレッドプールで実行するか、などなどが選べる)が絡むので実装はもう少し、というかもうかなり複雑です。だからこそ惑わされてしまうというわけで、基本的な骨格部分にのみ絞ってみれば十二分にシンプル、というのを掴むのが肝要じゃないかと思います。
次回は前回予告の通りに、後回しにしちゃってるけれど結合周りを紹介できればいいなあ。あと、FromAsyncか、Timer周辺か、Schedulerか……。RxJSもちゃんと例を出したいし、例を出したいといえば、そう、メソッド紹介だけじゃなく実例も出していきたいなあ、だし。うーん。まあ、ボチボチとやっていきます。最近ほんとRxの知名度・注目度が高まってるような気がしてます。ぐぐる検索で私のへっぽこ記事が上位に出てしまうという現状なので、申し訳ない、じゃなくて、それ相応の責任を果たすという方向で頑張りたいと思います。つまりは記事をちゃんと充実させよう。
Reactive Extensions for .NET (Rx) メソッド探訪第7回:IEnumerable vs IObservable
物凄く期間を開けてしまいましたが、Reactive Extensions for .NET (Rx)紹介を再開していきます。もはやRxってなんだっけ?という感じなので、今回は最も基本である、IObservableについて扱います。ボケーッとしている間にIQbservable(IQueryableのデュアル)とか出てきてて置いてかれちゃってるし。
そんなこんなで、IObservableはIEnumerableのデュアルなんだよ、とか言われてもぶっちゃけさっぱり分かりません。なので、その辺のことはスルーして普通にコードで対比させながら見ていくことにします。
// IEnumerable (RunはForEachです、ようするに) Enumerable.Range(1, 10) .Where(i => i % 2 == 0) .Select(i => i * 2) .Run(Console.WriteLine, () => Console.WriteLine("completed!")); // IObservable Observable.Range(1, 10) .Where(i => i % 2 == 0) .Select(i => i * 2) .Subscribe(Console.WriteLine, () => Console.WriteLine("completed!"));
ボタンを押して確認する、までもなく同じ結果です。1から10までを偶数だけ通して二倍して出力。見た目は同じですが、中身は丸っきり違います。見た目が一緒すぎて言葉で表現出来ないので図に表してみました。
何という下手っぴな図、さっぱり伝わらん。……。というのはおいておいて、矢印の向きに注目。IEnumerableの連鎖は、列挙を消費する時にIEnumeratorの伝搬に変わります。Run->Select->Where->RangeとMoveNextが駆け上がったら、今度はRange->Where->Select->RunとCurrentが降りていきます。末尾(Run)が値を要求(MoveNext)して値(Current)を取り出すという連鎖。末端から根元の値を引っ張ってくる(Pull)ようなイメージ。
IObservableは、根元自体が値を押し出していく(Push)ようなイメージ。こちらはIObserverの連鎖になっていて、根元からOnNextで値を伝えていきます。
Pushのメリット
Observable.Rangeのような、もしくはEunmerableに対してToObservableした時のような、普通のPull型シーケンスをPush型に変換することのメリットは?イベントや非同期など、他の形式から生成されたIObservableと連携出来る、というのは当然一番の話ですが、もう一つ、要素を分配出来るようになります。
このイミフな図の言わんとしていることが伝わる、わけはないので説明。Pull型はソースと1対1の関係である必要があるため、複数の列挙の消費者(RunだったりCountだったりSumだったりLastだったり)がいる場合、接続した回数だけ列挙が最初から回ることになります。かたやPush型は、1対多の関係を持つことが出来るため、一度の列挙で全ての消費者に値を配分することが可能です。
Hot vs Cold
同じように見えるIObservableにも、HotとColdという性質があります。それはyield returnで作る遅延評価のIEnumerableと、配列のように既に値が生成済みのIEnumerableとの違い、のようなものかもしれません。
var seq = Observable.Range(1, 5) .Do(i => Console.WriteLine("source -> " + i)); button1.Click += (sender, e) => seq.Subscribe(i => Console.WriteLine("button1 -> " + i)); button2.Click += (sender, e) => seq.Subscribe(i => Console.WriteLine("button2 -> " + i));
Doは、列挙に通ったものを取り出しつつも素通しします。つまり、 Select(i => { action(i); return i; }) です。今回は列挙がその箇所を通ったかどうかを書き出しています。余談ですが、IEnumerableならNyaRuRuさんの作成されたAchiralにはHookというメソッドがあって、細かい列挙中のモニタリングが出来るようになっています。
実行結果を見てみると、ボタンを押す=Subsribeを繋げると、即座に列挙が開始されていて、これだとIEnumerableのforeachと何も変わません。よって、このIObservableはColdです。もう値は生成され終わっているので。Subscribeの度に即座に全ての値をPushします。
ではHotは?
// FromEvent(canvas,"MouseMove")は手軽ですが、丁寧にこう書くほうが理想的かしら Func<IObservable<Point>> GetMouseMovePosition = () => Observable.FromEvent<MouseEventHandler, MouseEventArgs>( h => (sender, e) => h(sender, e), h => canvas.MouseMove += h, h => canvas.MouseMove -= h) .Select(e => e.EventArgs.GetPosition(canvas)); // ICollection<IDisposable>です。 var disposables = new CompositeDisposable(); evenButton.Click += (sender, e) => { disposables.Add( GetMouseMovePosition() .Where(p => p.X % 2 == 0 && p.Y % 2 == 0) .Subscribe(p => Console.WriteLine("Even -> " + p.X + ":" + p.Y))); }; oddButton.Click += (sender, e) => { disposables.Add( GetMouseMovePosition() .Where(p => p.X % 2 != 0 && p.Y % 2 != 0) .Subscribe(p => Console.WriteLine("Odd -> " + p.X + ":" + p.Y))); }; disposeButton.Click += (sender, e) => { // Disposeでイベントのデタッチ + 再登録不可 // Clearでイベントのデタッチ + 再登録可 disposables.Clear(); };
例えばマウスイベント。クリックの度にOnNextに値を送る、ムーブの度に値を送るといったイベントをIObservable化するFromEventはHot。無限リスト状態になっているものは、接続しただけでは値が送られてこないとも言えるので、幾つでもSubscribeすることが出来ます。サンプルでは、ボタンをクリックすればしただけ、右側のログ表示に同内容のものが連続して表示されるのが確認出来ます。
両者が混ざったような挙動をするIObservableもあります(例えばReplaySubject)ので、HotなのかColdなのか両方なのか。というのを意識してみると理解が深まるかもしれません。また、メソッドの動作確認などの際にHotとColdを区別せずにいると、思わぬ挙動で混乱するかもしれないので注意。というか、私はよくやります……。Observable.Rangeばかりで確認していてイミフ!と思ったら、FromEventでチェックしたら何て分かりやすいこと!というのが何度も。
CompositeDisposable
本題と離れますがTips。イベントのデタッチが簡単なのもRxのメリットの一つです。さて、複数イベントをデタッチする場合はどうしましょうか?List<IDisposable>に格納してforeachで列挙してDispose、というのも悪くないですが、そういう用途で使うためのCompositeDisposableというICollection<IDisposable>なクラスが用意されているので、そちらを使ったほうがよりスマートに書けます。
上のSilverlightのHotのサンプルコードでは、ボタンを押す(=Subscribeする=イベントを登録する)度にCompositeDisposableにAdd。そしてDisposeAllボタンでまとめてデタッチしています。
var subject = new Subject<int>(); var d1 = subject.Subscribe(i => Console.WriteLine(i)); var d2 = subject.Subscribe(i => Console.WriteLine(i * i)); using (new CompositeDisposable(d1, d2)) { subject.OnNext(2); // 2, 4 subject.OnNext(3); // 3, 9 } subject.OnNext(2); // usingを抜けデタッチ済みなので何も起こらない
List<IDisposable>に対するCompositeDisposableのメリットは、Disposeで解除出来るということ。つまり、using構文に放りこむことが可能です。多段Usingよりも綺麗に見えるのでお薦め。
上の例にコソッと出したSubjectクラスはPush型シーケンスの大本で、OnNextやOnCompletedを後続に送ることが出来ます。イベントのラップじゃなく、Rxネイティブなクラスを作る場合に使います。Subjectはちゃんと詳しく書かなきゃいけない大事なクラスの一つなので、また次にでもきっちり紹介する予定は未定。
列挙の分配
Pushのメリットとして分配可能なことを挙げたのに、Coldなので分配出来ません。以上終了。で終わるわけは当然ないわけで、Cold to Hot変換メソッドが使えます。Publishです。Publishの戻り値はIConnectableObservable。
public interface IConnectableObservable<out T> : IObservable<T> { IDisposable Connect(); }
IObservableなのでメソッドチェインを繋げることが出来ます。そして、Subscribeしても列挙は始まりません。Connectを呼んだ時に、一度だけ列挙することが出来ます(二度以降Connectを呼んでも何もしない)
私はダムの堰止をイメージしています。何もしないとドバドバと水が流れてしまうのでPublishで一時的に止めて、Connectで放水。放水後は空っぽ。みたいな。
Max/Sumなど集計系
インターフェイスを挙げただけじゃよく分からないので実例を。SumやMaxといった集計系メソッドと合わせて使ってみます。そこら中にモニタリング用のDoが入っていてコードが若干分かりづらいですが、実行結果で、どのタイミングで値が通過するのかを確認してみてください。
var source = Enumerable.Range(1, 5) .Do(i => Console.WriteLine("Source -> " + i)); enumerableButton.Click += (sender, e) => { var sum = source.Sum(); var max = source.Max(); var all = source.All(i => i < 3); Console.WriteLine("sum = " + sum); Console.WriteLine("max = " + max); Console.WriteLine("all = " + all); }; observableButton.Click += (sender, e) => { var connectable = source.ToObservable().Publish(); connectable.Subscribe(_ => { }, () => Console.WriteLine("OnCompleted")); var sum = default(int); connectable .Do(i => Console.WriteLine("BeforeSum -> " + i)) .Sum() .Do(i => Console.WriteLine("AfterSum -> " + i)) .Subscribe(i => sum = i); var max = default(int); connectable .Do(i => Console.WriteLine("BeforeMax -> " + i)) .Max() .Do(i => Console.WriteLine("AfterMax -> " + i)) .Subscribe(i => max = i); var all = default(bool); connectable .Do(i => Console.WriteLine("BeforeAll -> " + i)) .All(i => i < 3) .Do(b => Console.WriteLine("AfterAll -> " + b)) .Subscribe(b => all = b); connectable.Connect(); Console.WriteLine("sum = " + sum); Console.WriteLine("max = " + max); Console.WriteLine("all = " + all); };
値が確定した時、Allならば全ての列挙が完了した(OnCompletedを受信する)か、条件がfalseのものが見つかったときに、1つだけSubscribeに値が届きます。SumやMaxは、全ての列挙が完了しないと算出出来ないので、全て完了したとき。こういった結果の確定するタイミングは、Enumerableでの場合と変わりません。
このような動作(戻り値が長さ1のIObservable)をするものには、 Aggreagte, Count, Any… 、ようするにIEnumerableにもあって戻り値がIEnumerableじゃないメソッドは全てそうです。全部似たりよったりなので具体的な紹介は省きます。
Pushのデメリット
IObservable便利すぎてIEnumerableいらなくネ? と、言いたいところですが、例えばこれら集計系メソッドは全て長さ1のIObservableになります。Sumの場合、欲しいのはintであってIObservableではありません。長さ1のIObservableは、いつConnectされるか分からないのでSubscribeで外の値に受け渡してやらなければならないわけですが、見た目が美しくなく宣言も冗長になる。
また、集計するのに複数回列挙は確かに格好悪いな!よし、そういう場合はRx使おう。と思った場合はとりあえず待った。ただの配列からの列挙程度の場合は、ふつーに複数回列挙したほうがPublishで分岐させるよりも遥かに速かったりします。元ソースが複雑にLinqで繋いであって重たかったり、ファイルやネットワーク経由だったりで複数呼び出しを避けたい、副作用があって複数回呼びだすと内容が変化している、という場合はRxです。が、一旦ToListしてキャッシュすれば済むシーンならば、キャッシュした方が分かりやすく速い場合が多かったりします。
Publishの具体的な使い処としては、以前に、TwitterのStreamAPIをRxを使って分配するという記事で紹介しました。
まとめ
PullとPushは、むしろ動作的にはPushのほうが素直で分かりやすい雰囲気。難解だと思って避けていたそこのアナタ、さあ、Rxを使おう! しかしColdとHotは大いなる罠。初見ではきっとつまづく。この区別は本当に大事。Rxが難解っぽいとしたら、Cold/Hotのせい。挙動がまるっと変わるんだもの。でも、ゆっくり紐解けば全然大丈夫。さあ、Rxを使おう!Publishや集計系はそんなには使わないかもですが、覚えておくと便利な時も割とある。さあ、Rxを(ry
個人的にRxの特色・使いどころは「イベントの合成」「タイマー・ネットワーク・スレッドなど非同期処理の一元化」「シーケンスの分配」の3つだと思っているのですが、このブログでは、延々とシーケンス分配という、3つの中で一番どうでもいい機能しか紹介していない!という酷い事実に気がつきました。そんなんじゃRxのポテンシャルを全然伝えられない。
というわけで、次回はタイマー辺りを紹介したいと思います予定は未定。というか計画ではObservableの合流周りとMarble Diagramについてを書く予定。Rxの知名度も徐々に上がってきているようなので、しっかり紹介していきたいですし、他の人も書いて欲すぃ。
Linq雑話
- C# - 10.06/08
ここ数日Twitterで見た/出したLinqネタまとめ。私の広くない観測範囲(@neuecc)での話ですが。
SelectManyとクエリ構文でUsing
ネタ元、コード元はCode, code and more code.: SelectMany; combining IDisposable and LINQから。
static void Main(string[] args) { var firstLines = from path in new[] { "foo.txt", "bar.txt" } from stream in File.OpenRead(path) from reader in new StreamReader(stream) select path + "\t" + reader.ReadLine(); } public static IEnumerable<TResult> SelectMany<TSource, TDisposable, TResult>( this IEnumerable<TSource> source, Func<TSource, TDisposable> disposableSelector, Func<TSource, TDisposable, TResult> resultSelector) where TDisposable : IDisposable { foreach (var item in source) { using (var disposableItem = disposableSelector(item)) yield return resultSelector(item, disposableItem); } }
自前定義の拡張メソッドはメソッド構文だけのものと思っていませんでしたか?私はそう思っていました。でも、クエリ構文でも同名のものがあれば拡張メソッドが使用されるんです、というお話。それを利用してusingのネストをクエリ構文で華麗に表現してやったぜー、というサンプルで、確かにこれはクール!素晴らしすぎる。
でも、クエリ構文使いたいかというと、そんなことはなく変わらずメソッド構文派です、私は。クエリ構文自体は悪いとは思わないし、良さがあるのも分かるんですが、他の拡張メソッドに繋げる時に前後にカッコで括ると途端に書き/読みにくくなることと、拡張性の乏しさが如何ともし難い。クエリ構文とメソッド構文のちゃんぽんになるぐらいなら、メソッド構文だけで書いたほうが美しいよね、と思ってしまう。あと、クエリ構文の存在が「LINQ = SQLみたいなの」という図式を産んでしまっているくさいのも、憎んでしまいますね……。
ラムダ式の引数の名前とシャッフルについて
お馴染み感溢れるOrderByでのシャッフル。
var rand = new Random(); var shuffle = Enumerable.Range(1, 10).OrderBy(_ => rand.Next());
それはそれとして、ラムダ式の引数の名前どうする?というお話が。私は、引数を使わない場合は _ を、使う場合は型の1~2文字(i(Int32)とかs(String)とかa(AnonymousType)とか、考えるの面倒なときはx、配列系はarかxs)という自分ルールを敷いています。以前にラムダ式の引数の名前という記事を書いたのですが、その時から変わっていません。ですが、最近ネットで見かけるコードでは全部でまかなう例もよく見るね、と。Scalaでは匿名関数の引数としてが使える(プレースホルダ構文って言うんですね、名前知らなかった)ようなので、_をダメとは言い辛いのですけど、私はちょち苦手(linq.jsで$をゴリゴリ使ってるくせに、って話ではあるけど) 。
C#にもプレースホルダ構文みたいなの欲しいね、というのは、若干ある。プロパティの「value」とか最初からそこにある良く分からない変数、みたいなのはあるし。ただ、IntelliSenseとの兼ね合いもあるし、そういうのが入れられるか、入って本当に幸せになれるのかどうかの判断は保留。短絡的に欲しい!って言うのは簡単だけど、それの及ぼす影響となると分からないものだ。
それともう一つ。OrderByの引数は比較関数ではなくキーセレクターにすぎないのでちゃんとシャッフルされる、とか言ったりなどした私ですが、そうじゃなくてシャッフルの精度はランダムの範囲に影響されるね(実際上は問題ないとしても)、という話が。完全に頭から抜け落ちていて、かつ、全くもってその通りで恥ずかしかったりしたのですが確認できてよかったです、感謝。
OrderByのComparison
全然使わないけどOrderByの第二引数。
class MyClass { public int Hoge { get; set; } public int Fuga { get; set; } } static void Main(string[] args) { var array = new[] { new MyClass(), new MyClass() }; // コンパイルは通るけど例外出る var ordered = array.OrderBy(x => x).ToArray(); // 上のはこれに等しい(当然、例外出る) array.OrderBy(x => x, Comparer<MyClass>.Default); // AnonymousComparerを使えばComparisonを使った比較が出来る array.OrderBy(x => x, (x, y) => x.Fuga - y.Hoge); }
OrderByついでですが、キーセレクターは制約かかってないので別にIComparableじゃなくても動いたりします。そういう時はComparer<T>.Defaultが指定されることになって、例外出て死ぬだけです。意味ナイネ。
DescendingとThenByがあるので滅多に使わないであろう第二引数はIComparer。一々クラス作ってnewですってよ、C#らしくないですね。Comparisonじゃないなんて!大変ウザい。そんな人のためのAnonymousComparer。ラムダ式でIEqualityComparer/IComparerを作ることが出来ます。また、Linq標準演算子への拡張メソッドとしてOrderBy/ThenByのオーバーロードとしてComparisonが使えるようになります。便利ですね!是非使ってください、という宣伝。
Empty -> Sum
Empty.Sum()は0。言われてみれば当たり前といえば当たり前なのですが……。
// SumはAggregateで表現出来る var sum = Enumerable.Range(1, 10).Aggregate((x, y) => x + y); // 55 // でもEmptyで例外出るから表現出来ない(キリッ sum = Enumerable.Empty<int>().Sum(); // 0 sum = Enumerable.Empty<int>().Aggregate((x, y) => x + y); // 例外 // 実はseed与えればおk sum = Enumerable.Empty<int>().Aggregate(0, (x, y) => x + y); // 0
SumやMax, Minなどは全てAggregateで表現出来ます。でも、Sumは空シーケンスの時はゼロ出すけど(MaxやAverageは例外)Aggregateを使うと例外が出てしまうので表現出来ない、とか言ったのですが0を最初に与えとけばいいよね、という話が。ぬお、そうでした!
発端はlinq.jsでこの問題(というかC#と互換が取れてないこと)に気づいたことで、linq.jsではAggregateでやってるため、AggregateはScan.LastだからScan.LastOrDefault(0)にするー、なんて考えてたんですが、初項0で済むというシンプルさを完全に失念。標準演算子外のメソッドを大量に用意してあるので、そっち側で解決しちゃおうとしてしまう姿勢は、ちょっと頭硬直化しちゃってる、全くもってよろしくない。
シャッフルの話といい、Aggregateの話といい、最近はLinqに慣れすぎて逆に見方が定型的になりすぎていると実感したので、少し気を引き締めないと。あ、で、そんなこんなでlinq.jsの空シーケンスでのSumの問題は次のリリースで直します。他にもバグがあったり(MemoizeAllが少しマズい)、加えたいことが数点あったりするので、もう少し先になりますが。
世の中の主流はまだVS2005ですか?
開発言語としてのJavaとC#を10の視点から比較
共通点が多いが、今後は違いが大きくなるかも
しかし近年のC#はLINQ(Language Integrated Query:言語統合クエリ)プロジェクトが重視されています。これはクエリ、集合操作、変換、および型推測などのデータ指向機能の多くを直接的にC#言語に統合しようとするものです。今後は違いがさらに大きくなっていくかもしれません。
プログラマが知っておきたいJavaと.NETの違い (3/4) - @IT
Linqは、VS2008出たのは3年前だよね(プレビュー版から言えばどれだけ前なのかしら)。今後は違いが大きくなるかも、じゃなくて既に違いは大きすぎるような。そして10の比較というけれど、最大の違いはデリゲートの有無では?特に、匿名メソッド/ラムダ式の有無。A.R.N [ Top > 書庫 > Microsoftの「Delegate」について ]にある、Javaには無名クラスがあるからdelegateは不要、とは10年以上前のSunの言で、さすがに10年以上も前のを持ち出してどうこう言ってもしょうがないのですが(比較対象に匿名メソッドないし)、価値観は移り変わっていくものなのだと思わずにはいられない。匿名クラスで代用出来るって、いやまあ出来なくもないのは分かりますがUglyすぎ。今、クロージャなんて不要、とか言ったらフルボッコなはず。
言語面で見ると、Java5から進化の足を止めている(そしてJava7延期しすぎ)ように見えるJavaと、ひたすら貪欲に(無節操に)取り込み続けるC#。スタート時には似たようなものだったとして、今はもうコードの見た目からして全然似てるようには見えない。Java畑の人は、今でもC#はJavaに似たようなもの、という認識なのかしら。
確かに、古典的に書けば似てますが……。そして、他の言語を考えれば、やっぱ似てるといえば似てるのですが。しかし……。ふむ。そろそろModern C# Designが出版されるべき。 Bart De Smetが書くC# 4.0 Unleashed
には超期待。
.NET(C#)におけるシリアライザのパフォーマンス比較
- C# - 10.05/29
ちょっとしたログ解析(細々としたのを結合して全部で10万件ぐらい)に書き捨てコンソールアプリケーションを使って行っていたのですが(データ解析はC#でLinqでコリっと書くのが楽だと思うんです、出力するまでもなく色々な条件を書いておいてデバッガで確認とか出来るし)、実行の度に毎回読んでパースして整形して、などの初期化に時間がかかってどうにも宜しくない。そこで、データ丸ごとシリアライズしてしまえばいいんじゃね?と思い至り、とりあえずそれならバイナリが速いだろうとBinaryFormatterを使ってみたら異常に時間がかかってあらあら……。
というしょうもない用途から始まっているので状況としては非現実的な感じではありますが、標準/非標準問わず.NET上で実装されている各シリアライザで、割と巨大なオブジェクトをシリアライズ/デシリアライズした時間を計測しました。そんなヘンテコな状況のパフォーマンスなんてどうでもいー、という人は、各シリアライザの基本的な使いかたの参考にでもしてください(いやまあ、どれもnewしてSerializeメソッド呼ぶだけですが)。ソースは後で出しますが、具体的に計測に使ったオブジェクトはテスト用クラスが10万件含まれたListです。まずは結果のほうを。
Serialize BinaryFormatter 00:00:06.4701421 53MB Serialize XmlSerializer 00:00:07.7035246 59MB Serialize DataContractSerializer 00:00:02.1545153 149MB Serialize DataContractSerializer Binary 00:00:01.4706517 78MB Serialize DataContractJsonSerializer 00:00:02.6021908 47MB Serialize DataContractJsonSerializer Binary 00:00:02.5019512 75MB Serialize NetDataContractSerializer 00:00:09.1802584 183MB Serialize NetDataContractSerializer Binary 00:00:08.1960399 99MB Serialize Formatter`1 - Protocol Buffers 00:00:00.7043000 13MB Serialize MsgPackFormatter - MessagePack 00:01:33.3844083 46MB Serialize JsonSerializer - JSON.NET 00:00:07.4997295 38MB Serialize JsonSerializer - JSON.NET BSON 00:00:11.7767353 44MB Deserialize BinaryFormatter 00:00:59.1693980 Check => OK Deserialize XmlSerializer 00:00:02.7073623 Check => NG Deserialize DataContractSerializer 00:00:06.3459340 Check => OK Deserialize DataContractSerializer Binary 00:00:03.5622500 Check => OK Deserialize DataContractJsonSerializer 00:00:10.5392504 Check => OK Deserialize DataContractJsonSerializer Binary 00:00:07.2658857 Check => OK Deserialize NetDataContractSerializer 00:00:09.1020073 Check => OK Deserialize NetDataContractSerializer Binary 00:00:07.4024345 Check => OK Deserialize Formatter`1 - Protocol Buffers 00:00:00.9176016 Check => OK Deserialize MsgPackFormatter - MessagePack 00:00:29.8292134 Check => OK Deserialize JsonSerializer - JSON.NET 00:00:11.7517757 Check => OK Deserialize JsonSerializer - JSON.NET BSON 00:00:12.0099519 Check => OK
対象シリアライザ、かかった時間、シリアライズ時は出力ファイルサイズ、デシリアライズ時は正しく復元できたかを表示しています。
.NET標準ライブラリからはBinaryFormatter, XmlSerializer, DataContractSerializerとそのバイナリ出力, DataContractJsonSerializerとそのバイナリ出力, NetDataContractSerializerとそのバイナリ出力。オープンソースライブラリからは、GoogleのProtocol Buffersの.NET移植protobuf-net、国産のMessagePackのC#実装、Json.NETのJSONシリアライズとBSON(Mong DBで使われているバイナリ形式のJSON)のシリアライズを出力しました。
BinaryFormatter遅くね?が発端であり裏付けるように、BinaryFormatterのデシリアライズが異常に遅い。他が数秒なのに1分かかってます。テスト用データの詳細は後で述べますが、どうもObjectの配列が含まれていると遅くなるようです。テストデータにはKeyValuePair[]を含んでいるので、それが引っかかって激遅に。シリアライザする対象によって速度が変化するのは分かりますが、幾らなんでも限度を超えた速度低下。バグじゃないかしら?と言いたい。DataContractSerializerでもバイナリが吐ける昨今、もはやObsoleteにしてもいい雰囲気すら漂う。
XmlSerializerが速い・サイズ少ないという感じですがデシリアライズでCheck => NGと表記されているように、シリアライズに失敗しています。テストデータにKeyValuePair[]が含まれているのですが、それがシリアライズ出来なくて空配列になってしまったため。Dictionaryがシリアライズ出来なかったりと、XmlSerializerは割と使いにくいところがあります。今だとガイドライン的にも、XmlのAttributeの設定とか出力するXMLを細かく制御するならXmlSerializer、そうでないならDataContractSerializerのほうを推奨、とのことです。
そのDataContractSerializerはファイルサイズが嵩んでいるのが難点。属性などを付与していないため、テストデータ中の自動プロパティが <x003C_MyProperty1_x003E_k_BackingField>hoge1</x003C_MyProperty1_x003E_k_BackingField> といったような、とんでもなく長ったらしいタグになってしまっているのが原因。DataMember属性で名前を振ってあげればマシになりますが、今回は属性未使用で計測としました。そんなDataContractSerializerですが、バイナリXMLとして保存するとファイルサイズが縮むので気になる場合はそちらを使えばいいのかも。パフォーマンスも良くなります。
DataContractJsonSerializerは、シリアライズ・デシリアライズの速度はXMLよりも劣っていますが、ファイルサイズに関しては(JSONなので当然とはいえ)比較にならないほど小さい。バイナリXMLよりも小さくて、中々優秀のようです。しかし、バイナリ化して保存すると逆にファイルサイズが膨らむという罠が。
NetDataContractSerializerはシリアライザ作る時に型指定がいらなかったりと利便性はちょびっと○。また、循環参照が含まれていても問題なくシリアライズ出来たりと、DataContractSerializer(素の状態だと循環参照が含まれると例外)とは若干毛色が違います。といった点から言っても、BinaryFormatterの後継はこれになるのでしょう。
Protocol Buffersは爆速な上に非常に縮んで素晴らしい!さすがGoogle。ということだけじゃなく、protobuf-netの実装も良いのでしょうね。APIも非常に練られていて使いやすいし、C#でのクラスから.protoファイルの生成とかも出来て大変便利。Silverlightで使えるし、Windows Phone 7でも動かせるよう調整中、とのことでかなり気に入りました。
MsgPackFormatterはシリアライズ、デシリアライズ共にかなり時間が……。これは、MessagePackが、というよりも、実装の方でオブジェクトグラフの生成に時間がかかってるようです。
JSON.NETは中々優秀な結果を出しています。しかし、Performance ComparisonでDataContractSerializerよりも速いぜ!と謳っていたけれど、そんなことはなく。こういうのは計測する内容によって変わってくるので一概にどうこう言えないんですねー、というのを知るなど。バイナリ化で逆にサイズが膨らむのはDataContractJsonSerializerと同様。なお、JSON.NETはAPIが正直使いにくくてどうにかならないのかねー、と思うので個人的には好きじゃありません。使いやすいAPI設計ってセンスが必要ですよね……。
コード
割と長ったらしいので、分割して。追試したい方はこちらに元ソース置いておくので使ってみてください。VS2010用。ライブラリ全部揃えるのも大変だと思うので、測定を省きたい項目はMainメソッドのリスト初期化子の部分で該当するものをコメントアウトすれば省けます。
[ProtoContract] [Serializable] public class TestClass : IEquatable<TestClass> { [ProtoMember(1)] public string MyProperty1 { get; set; } [ProtoMember(2)] public int MyProperty2 { get; set; } [ProtoMember(3)] public DateTime MyProperty3 { get; set; } [ProtoMember(4)] public bool MyProperty4 { get; set; } [ProtoMember(5)] public KeyValuePair<string, string>[] MyProperty5 { get; set; } public bool Equals(TestClass other) { return this.MyProperty1 == other.MyProperty1 && this.MyProperty2 == other.MyProperty2 && this.MyProperty3 == other.MyProperty3 && this.MyProperty4 == other.MyProperty4 && this.MyProperty5.SequenceEqual(other.MyProperty5); } public override int GetHashCode() { return this.MyProperty1.GetHashCode() + this.MyProperty2.GetHashCode(); } }
これがテスト用クラスの中身です。string, int, DateTime, bool, KeyValuePair<string,string>[] とそこそこ満遍なく散りばめて、それとデシリアライズがちゃんと出来たか確認出来るようIEquatable<TestClass>を実装して値比較出来るようにしています。&&や||や三項演算子の:は前置にする派。綺麗に揃う感じが好き。属性はProtocol Buffersはつけないと動かないのでProtoMemberを指定していますが、それ以外は無指定です。指定した方が縮んだりとかありそうですが、私的には無指定の状態で調べたいなあ、と思ったのでなし。最適じゃない状態からのそれぞれのシリアライズ形式への生成、書き戻しの具合を見たいと思ったので。いや、単純に各実装で使う属性を調べるのが面倒だったという手抜きな理由もありますが。
public abstract class SerializerBenchmark { public static readonly List<TestClass> TestData; static SerializerBenchmark() { TestData = Enumerable.Range(1, 100000) .Select(i => new TestClass { MyProperty1 = "hoge" + i, MyProperty2 = i, MyProperty3 = new DateTime(1999, 12, 11).AddDays(i), MyProperty4 = i % 2 == 0, MyProperty5 = Enumerable.Range(1, 10) .ToDictionary(x => x.ToString(), _ => i.ToString()).ToArray() }) .ToList(); } public static SerializerBenchmark<T> Create<T>(T serializer, Func<T, Action<Stream, Object>> serializeSelector, Func<T, Func<Stream, Object>> deserializeSelector, string optional = null) { return new SerializerBenchmark<T>(serializer, serializeSelector, deserializeSelector, optional); } public abstract void Serialize(); public abstract void Deserialize(); }
ベンチマークで重複コードのコピペ(ストップウォッチを前後に挟んだり)を省くために抽象クラスを作りました。このList<TestClass> TestDataが実際にシリアライズ/デシリアライズに使ったデータになります。Enumerable.Range(1, 100000)のToListで10万件のリスト生成。KeyValuePairの配列は、全て長さ10で、DictionaryのToArrayで作っています。
public class SerializerBenchmark<T> : SerializerBenchmark { private T serializer; private string name; Action<Stream, Object> serialize; Func<Stream, Object> deserialize; private string FileName { get { return name + ".temp"; } } public SerializerBenchmark(T serializer, Func<T, Action<Stream, Object>> serializeSelector, Func<T, Func<Stream, Object>> deserializeSelector, string optional = null) { this.serializer = serializer; this.name = serializer.GetType().Name + ((optional == null) ? "" : " " + optional); this.serialize = serializeSelector(serializer); this.deserialize = deserializeSelector(serializer); } private void Bench(string label, Action action) { GC.Collect(); Console.WriteLine(label + " " + name); var sw = Stopwatch.StartNew(); action(); Console.WriteLine(sw.Elapsed); } private void OpenAndExecute(string path, Action<FileStream> action) { using (var fs = File.Open(path, FileMode.OpenOrCreate)) { action(fs); } } public override void Serialize() { Bench("Serialize", () => OpenAndExecute(FileName, fs => serialize(fs, TestData))); Console.WriteLine(new FileInfo(FileName).Length / 1024 / 1024 + "MB"); } public override void Deserialize() { List<TestClass> data = null; Bench("Deserialize", () => OpenAndExecute(FileName, fs => data = (List<TestClass>)deserialize(fs))); Console.Write("Check => "); Console.WriteLine(TestData.SequenceEqual(data) ? "OK" : "NG"); } }
こちらがベンチマークの中身。処理を共通化したかったのですが、各シリアライザがIFormatter(Serialize,Deserializeメソッドを持つインターフェイス)を実装している、なんてことは全くなくて共通化しようがなくてうぎゃー。せめてメソッド名が全てSerializeならdynamic使うという最終手段もあるけれど、DataContractSerializerのメソッド名はWriteObjectだし、ダメだこりゃ。
で、気づいたのがSerializeはAction<Stream, Object>、DeserializeはFunc<Stream, Object>だということ。というわけで、各メソッドそのものをコンストラクタで渡してあげる形にすることで共通化できた。めでたしめでたし。もはやFuncやActionのない世界は考えられませんね!え、Javaのことですか知りません。
Deserialize時には、正しくデシリアライズ出来たかのチェックを仕込んでいます。データがListなのでLinqのSequenceEqualで元データと値比較。Listの要素であるTestClassもIEquatableなので、全部値比較で確認。
static class Program { static void Main(string[] args) { var bench = new List<SerializerBenchmark> { SerializerBenchmark.Create(new BinaryFormatter(), x => x.Serialize, x => x.Deserialize), SerializerBenchmark.Create(new XmlSerializer(typeof(List<TestClass>)), x => x.Serialize, x => x.Deserialize), SerializerBenchmark.Create(new DataContractSerializer(typeof(List<TestClass>)), x => x.WriteObject, x => x.ReadObject), SerializerBenchmark.Create(new DataContractSerializer(typeof(List<TestClass>)), x => (s, data) => XmlDictionaryWriter.CreateBinaryWriter(s).Using(xw => x.WriteObject(xw, data)), x => s => XmlDictionaryReader.CreateBinaryReader(s,XmlDictionaryReaderQuotas.Max).Using(xr => x.ReadObject(xr)), "Binary"), SerializerBenchmark.Create(new DataContractJsonSerializer(typeof(List<TestClass>)), x => x.WriteObject, x => x.ReadObject), SerializerBenchmark.Create(new DataContractJsonSerializer(typeof(List<TestClass>)), x => (s, data) => XmlDictionaryWriter.CreateBinaryWriter(s).Using(xw => x.WriteObject(xw, data)), x => s => XmlDictionaryReader.CreateBinaryReader(s,XmlDictionaryReaderQuotas.Max).Using(xr => x.ReadObject(xr)), "Binary"), SerializerBenchmark.Create(new NetDataContractSerializer(), x => x.Serialize, x => x.Deserialize), SerializerBenchmark.Create(new NetDataContractSerializer(), x => (s, data) => XmlDictionaryWriter.CreateBinaryWriter(s).Using(xw => x.WriteObject(xw, data)), x => s => XmlDictionaryReader.CreateBinaryReader(s,XmlDictionaryReaderQuotas.Max).Using(xr => x.ReadObject(xr)), "Binary"), SerializerBenchmark.Create(ProtoBuf.Serializer.CreateFormatter<List<TestClass>>(), x => x.Serialize, x => x.Deserialize, "- Protocol Buffers"), SerializerBenchmark.Create(new MsgPackFormatter(), x => x.Serialize, x => x.Deserialize, "- MessagePack"), SerializerBenchmark.Create(new JsonSerializer(), x => (s, data) => new StreamWriter(s).Using(sw => new JsonTextWriter(sw).Using(tw=> x.Serialize(tw, data))), x => s => new StreamReader(s).Using(sr => new JsonTextReader(sr).Using(tr=> x.Deserialize<List<TestClass>>(tr))), "- JSON.NET"), SerializerBenchmark.Create(new JsonSerializer(), x => (s, data) => new BsonWriter(s).Using(bw => x.Serialize(bw, data)), x => s => new BsonReader(s){ReadRootValueAsArray = true}.Using(br => x.Deserialize<List<TestClass>>(br)), "- JSON.NET BSON") }; bench.ForEach(b => b.Serialize()); Console.WriteLine(); bench.ForEach(b => b.Deserialize()); Console.ReadKey(); } // IDisposable extensions static void Using<T>(this T disposable, Action<T> action) where T : IDisposable { using (disposable) action(disposable); } static TR Using<T, TR>(this T disposable, Func<T, TR> func) where T : IDisposable { using (disposable) return func(disposable); } }
Listに突っ込んで、まとめてForEachで計測。でも何だかゴチャゴチャしててキタナイですね……。Listの最初の3つまでは良いんです。それぞれ一行で。SerializeとDeserialize登録するだけで。その次から想定外でして……。そう、Serializeは決して必ずしもAction<Stream, Object>じゃあなかった!例えばDataContractSerializerでバイナリXML化を行うには、WriteObjectにStreamじゃなくてXmlDictionaryWriterを渡さなきゃいけない。と、いうわけで、そういった特別対応が必要なメソッドには入れ子のラムダ式を作って処理しています。そのせいでゴチャゴチャと。もはや可読性の欠片もない。がくり。入れ子のラムダが出てくると読むのシンドイんですよね、可能な限り避けたいとは思ってるんですが……。
Using拡張メソッドは、using構文使うと式じゃなくて文になってラムダ式で書きづらくてウザいので無理やり式にするためのシロモノ。そのせいで入れ子が更に入れ子になって可読性落としてる気がする。何事もやりすぎはいけない。
まとめ
DataContractSerializer良いよね。XmlDictionaryWriter.CreateBinaryWriter突っ込めばバイナリ化も出来るし。あとprotobuf-netはこうして数字見ると本当に凄いね。と、いったことことぐらいしか言うことがなく。
それとは関係あったりなかったりで、Twitter素晴らしい。毎回思うけれど、やっぱTwitterは素晴らしい。もともとBinaryFormatterなんか遅くね?とTwitterに愚痴ったところからスタートして、色々な人にアドバイスを受けて最終的にこんな記事(斜め上なだけな気はする)になったわけで、Twitterなかったら「遅くね?以上終了」で終わってた。
私がTwitterをはじめたのはTwitterはじめましたとかいう記事で確認出来るところでは2008/01/04(CoD4について書いてますけど、CoD4のシングルはあんま好きじゃなかった、でもその後マルチプレイヤーにはまったので結局MW2買ってますね!)。しかも、この後も当分は何もポストしないままでいて、同年9月の三度目の正直とかいう記事でTwitter→はてダへの転送ツールを作ってからようやく始めてました。コードの一部が載ってますけど、postList.Addとかいうのが初々しいなあ。今だとforeach使ったら負け、.ToList()だろ常識的に考えて。ですね。あと、TrimStartの使い方を間違えてるという。1年半前の私のド素人っぷりが伺えて面白い。ていうか、考えてみると物凄く成長しましたね……。
そして今ではすっかりTwitter中毒です。あらあらうふふ。
アドバイス受けてばかりというのもアレですので、私でも応えられそうなのがTLに流れてきたり検索(キーワード「Linq」を割と頻繁に眺めてる)にかかった時は答えるようにしています。いや、あんましてないかも。積極的に答えるようにしていきたいなあ、と。そんなわけで、@neueccでTwitterやってるので気が向いたらフォローしてやってください。
DynamicJson ver 1.2.0.0
- C# - 10.05/21
DynamicJsonをver1.2.0.0に更新しました。おうぁぁぁぁ。Dynamicのまま配列をばらせないことに気づいた!foreachで列挙出来るのはいいけれど、Linqで、例えばSelectManyに渡せないぢゃん!ヤバいヤバい。バグではないけど、そんなんじゃ使い物にならない。ということでその辺のを修正しました。
何言ってるのかよくわからないですね、なわけで例を。
// page1から5までを取得して平らにする(で、古い順に並び替える) var wc = new WebClient(); var statuses = Enumerable.Range(1, 5) .Select(i => wc.DownloadString("http://twitter.com/statuses/user_timeline/neuecc.json?page=" + i)) .SelectMany(s => (dynamic[])DynamicJson.Parse(s)) .OrderBy(j => j.id); foreach (var status in statuses) { Console.WriteLine(status.text); }
neueccさん(私だ、私)の投稿を取得するわけですが、一回に20件しか取得出来ないので(countを指定すれば200件まで取れますけど)、複数ページ分取得して結合することにします。そういう場合はforでグルグル?ご冗談を、Modern C#(そんな定義ない)ではLINQを使います。
1ページのJSONは[{text:nantoka},{text:kantoka}] といった形に配列にデータが入った形で取得出来ます。それが5ページ分。[[{},{}],[{},{}]]といった感じなので、これを真平らにしてやりましょう。[{},{},{},{}]といった風に。それがSelectManyです。SelectManyは微妙にわかりにくいけれど、Linqの中でもかなり重要なんですよー。図解 SelectMany - NyaRuRuの日記なども参考に。
さて、この「.SelectMany(s => (dynamic[])DynamicJson.Parse(s))」って部分が、前は出来なかったのです、とほほほ。IEnumerable<T>を実装していれば(IEnumerable<dynamic>)で済んだのだけどなあ。そうすれば配列に変換しなくても済むので効率的にも良いし。Dynamic Viewと結果ビューが共存出来さえすれば……。
というわけですが、今回の修正で無事、出来るようになりました。配列変換は気にくわないけどしょうがない。IEnumerableを実装しているけれどDynamic Viewが使えるやり方募集中です。方法あったら誰か教えてくださいお願いします。
あと、キャストついでに、読み取り専用プロパティに対しても値をセットしようとして例外出るのを修正しました。これは、まあ、バグですね。仕様とか言ったら怒る。ていうかすみません。
C# DynamicObjectの基本と細かい部分について
- C# - 10.05/06
DynamicJsonをver.1.1に更新しました。ver.1.0の公開以降に理解したDynamicObjectについての諸々を反映させてあります。具体的には、IsDefinedやDeleteといったメソッド名を書かずに、それらが呼び出せるようにし、また、foreach時にキャストが不要になりました。DynamicはIntelliSenseが効かないので、メソッド名を書かせたら負け。大まかに分けて「プロパティ」「名前付きメソッド」「名前無しメソッド」「キャスト」の4つをうまいこと振り分けて、不自然ではなく使えるようにすれば、DynamicObjectとして良い設計となるのではないかな、と思っています。
というわけで、今回は微妙にはまったDynamicObjectの挙動の色々について書きますが、その前に1.1の更新事項を。DynamicJson自体については、ver1.0のリリース時の記事を参照ください。
// ただの入れ物クラスなのであまり気にしないで。 public class FooBar { public string foo { get; set; } public int bar { get; set; } } // こんなJSONがあったとするとする var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }"); var arrayJson = DynamicJson.Parse(@"[1,10,200,300]"); var objectJson = DynamicJson.Parse(@"{""foo"":""json"",""bar"":100}"); // .プロパティ名()はIsDefined("プロパティ名")と同じになります var b1_1 = json.IsDefined("foo"); // true var b2_1 = json.IsDefined("foooo"); // false var b1_2 = json.foo(); // true var b2_2 = json.foooo(); // false; // .("プロパティ名")はDelete("プロパティ名")と同じになります json.Delete("foo"); json("bar"); // キャストはDeserialize<T>()と同じになります var array1 = arrayJson.Deserialize<int[]>(); var array2 = (int[])arrayJson; // array1と一緒 int[] array3 = arrayJson; // こう書いてもDeserialize呼び出しと同じだったりする // 配列だけではなく、パブリックプロパティ名で対応を取るマッピングも可能です var foobar1 = objectJson.Deserialize<FooBar>(); var foobar2 = (FooBar)objectJson; FooBar foobar3 = objectJson; // 勿論、配列+オブジェクトでも可。Linqに繋げる時はキャストで囲みましょう(asはダメ) var objectJsonList = DynamicJson.Parse(@"[{""bar"":50},{""bar"":100}]"); var barSum = ((FooBar[])objectJsonList).Select(fb => fb.bar).Sum(); // 150 var hoge = objectJsonList as FooBar[]; // これはnullになる、asとキャストは挙動が違う // array状態のDynamicJsonにforeachはdynamicが渡る // 中の型が分かっている場合は、varではなく型名指定するといいかも // ちなみに、数字はdynamicのままだと全てdoubleです foreach (int item in arrayJson) { Console.WriteLine(item); // 1, 10, 200, 300 } // オブジェクト状態のDynamicJsonへのforeachはKeyValuePair // .Key、.Valueなのは分かってる、というならdynamicで受けると楽かも foreach (KeyValuePair<string, object> item in objectJson) { Console.WriteLine(item.Key + ":" + item.Value); // foo:json, bar:100 }
foreachを自然に使えるようにしたのと、IsDefined、Remove、DeserializeをDynamicとして自然に呼び出せるようにした、というのが更新内容になります。IsDefined(”name”)が.name()で自然なのか?というと、どうなんでしょうねー、という感じですが、しかしDynamicはIntelliSenseが効かないのです!なので、多少ややこしくても、こうして使える方が便利だと思われます。
ところで、キャストとasの関係は、DynamicObjectだとより正確に意識する必要が出てきます。キャストとasは例外が飛ぶかnullになるかの違いしかない、と思っていたりしたのは私なのですが、そんなことはなくて、asはユーザ定義の変換演算子を呼ばないという性質があります。DynamicObjectでもそれが反映されているため、キャストするとTryConvertが呼ばれるのですが、asは純粋にそのクラスの継承関係しか見ません。
DynamicObjectとは
では本題。まずは基本から。型宣言がdynamicの場合に、挙動が変わるオブジェクトの作成方法は、DynamicObjectを継承して、挙動を変えたいメソッド(Tryなんたら)をオーバーライドするだけです。
override->スペースでIntelliSenseに候補が出てきますね、素敵。Try何とかかんとかの第一引数はbinderですが、とりあえずbinder.Nameだけで何とかなります。成否はboolで返し(falseの場合は、呼び出しを解決するため更に連鎖が重なったりする)、trueの場合はresultにセットした値が呼び出し元に返る。といった仕組みになっています。
呼び出しの解決
簡単な例、ということで呼び出し名を返す、というだけの単純なTryInvokeMemberを定義してみます。
// MyDynamic:DynamicObjectのオーバーライド、メソッド呼び出し名を文字列としてそのまま返す、引数がある場合はfalse public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result = binder.Name; return (args.Length > 0) ? false : true; } dynamic d = new MyDynamic(); var t1 = d.Hoge(); // Hoge - dynamic(string) var t2 = d.ToString(); // ToStringにはならない、MyDynamicのToStringが呼ばれる
さて、この場合本来あるメソッドであるToStringやEquals、その他自分で定義したメソッドがあれば、それらを呼んだ場合はどちらが優先されるでしょうか、というと、定義されたメソッドが優先です。なので、TryInvokeMemberにだけ挙動を定義しておくと、どうやっても呼べないメソッドが出てきます。例えばDynamicJsonで言えば、プロパティ名がToStringのJSONに対してToString()で定義されているか確認は出来ません。そのための回避策として、TryInvokeMemberはIsDefinedの簡易記法としています。IsDefined(”ToString”)ならば問題なく呼べますので。
引き続いて、TryInvokeMemberがfalseの場合も見てみます。
public override bool TryGetMember(GetMemberBinder binder, out object result) { result = new MyDynamic(); return true; } public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) { result = ((string)args[0]).ToUpper(); return true; } dynamic d = new MyDynamic(); var t = d.Hoge("aaa"); // AAA(InvokeMemberが失敗したらGetMemberが呼ばれ、それのTryInvokeが呼ばれる)
d.Hoge(”aaa”)は、まずTryInvokeMemberが呼ばれます。今回は引数がある場合はfalseとしているので、失敗します。すると、呼び出しの解決のためTryGetMemberが呼ばれます。ここでtrueの場合は、引数を持ってTryInvokeが呼ばれます。TryInvokeはd(”aaa”)のような、メソッド名なしでの関数呼び出しです。C#にはない記法となるので、TryInvokeMemberからの失敗の連鎖でTryGetMemberでDynamicObject以外を返すと、TryInvokeに失敗という形で例外が出ます。
ややこしいですね!この失敗の連鎖はDynamicJsonではオブジェクトがネストしている際の呼び出しの解決に利用しています。
var json = DynamicJson.Parse(@"{""tes"":10,""nest"":{""a"":0}"); json.nest(); // これはjson.IsDefined("nest") json.nest("a"); // これはjson.nest.Delete("a")
json.nest(”a”)は、まずTryInvokeMemberが呼んでいます。これは原則IsDefinedと等しいのですが、引数がある場合はfalseにしています。そのためTryGetMemberが呼ばれて.nestを取得。そして、TryInvoke(これはDeleteに等しい)を呼ぶという流れになっています。真面目にDynamicObjectを使って構造を作る場合、呼び出し解決順序などを意識する必要は、間違いなくあります。ややこしいですけどねー。
DynamicObjectとforeach
DynamicObjectがIEnumerableならば、foreachはそれを呼びます。ていうかforeach可能なものはIEnumerableにするでしょ常識的に考えて。と、言いたいのですが世の中そうもいかなかったりします。
Dynamicの変数はデバッガで見るとDynamic Viewというものが用意されていて、展開すると全てのプロパティ名と値を表示してくれます(これに対応させるにはGetDynamicMemberNamesをオーバーライドする必要がある、DynamicObject作るなら必須!)。大変便利なのですが、これ、クラスがIEnumerableを実装している場合はIEnumerableの結果ビューに置き換わってしまい、Dynamic Viewがなくなってしまいます。
Dynamic Viewがないと非常に不便極まりないので、DynamicJsonではIEnumerableの実装は断念しました。しかし、foreachでそのまま呼べないのは不便だ。どうする?となって思い浮かんだのは、IEnumerableじゃなくてもGetEnumeratorがあればforeachって呼ばれるんだよねー、普通のクラスは。という仕様がC#にはあるので、Dynamicでも行けるかな?と思いきやそんなことはなく、呼ばれませんでした。じゃあ、かわりにforeach時に何が呼ばれていたか、というと、TryConvertが呼ばれます。
public override bool TryConvert(ConvertBinder binder, out object result) { // foreachで呼ばれた時はbinder.TypeがIEnumerable if (binder.Type == typeof(IEnumerable)) { result = Enumerable.Range(1, 10); // resultはIEnumerableとIEnumerator、どちらでも可 return true; } // 通常のキャストは適当に分岐させるか、キャストの演算子オーバーロードでもどちらでもいい // 演算子オーバーロードがある場合は、そちらが優先されます result = (binder.Type == typeof(string)) ? "hogehoge" : null; return true; } dynamic d = new MyDynamic(); foreach (var item in d) { Console.WriteLine(item); // 1,2,3,4,5,6,7,8,9,10 } var ie = (IEnumerable)d; // これは失敗する、インターフェイスへの明示的型変換は不可!
C#ではインターフェイスの演算子オーバーロードは定義出来ないし、Dynamicでも呼び出しは不可能になっています。が、foreach呼び出し時のみ、TryConvertにTypeがIEnumerableとして渡るようになっているので、そこでtrueを返せば、IEnumerableではないDynamicObjectでもforeachで列挙出来ます。
正直なところ、セコいハックに過ぎないです。本当はデバッガでDynamic Viewと結果ビューが共存できればいいんですよ、ていうか出来るべき。あと、Dynamic Viewは今のところ値がnullのものは表示しないようになっているのですが、これも不便な仕様ですね、改善して欲しいところ。とはいえ、IEnumerableじゃないから不便になってる!ということは無いと思われます。どちらにせよIEnumerableじゃなくてもdynamicはキャストしないと拡張メソッドが呼べない(つまりそのままではLinqが使えない)ため、利用感は犠牲にしていません。
そういえばExpandoObjectはIEnumerableなのにdynamic viewが出るので、何か方法はあるかもしれませんね。
まとめ
dynamicは当初思っていたよりも、遥かに使いがいのある仕組みでした。dynamicはDSL。だと思います。そして、DSLとして便利に使わせるならば、メソッド名で呼ばせるのは厳禁。用意された機構を上手く使ってIntelliSenseレスでも快適に操作出来るようにしなければならない。というか、それで操作出来ないならば普通にC#で組んでIntelliSense効かせた方がずっと良い。
と、まあ、そんなわけでDynamicJsonは400行程度の小さいコードですが、割と色々考えて作ってありますので、是非使ってみてください。お手製ライブラリにありがちなJSON解析の出来が怪しい、といった問題を、解析部を.NET FrameworkのJsonReaderWriterFactoryに丸投げしているため避けられている、というのも大きな利点かと思われます。
ソースコード本体は、ArrayとObjectを一つのクラスに統合しているため、各メソッド行頭でifで分ける、というのが若干怪しいのですが(オブジェクト指向的にはポリモーフィズム、ていうかDynamicならその辺考えずに分けても問題ない、だろうけど400行のコードですからねえ、分割したほうが手間かつ分かりにくくなるだろうしで、まだリファクタリングの出番ではない、と思いますです、これ以上規模が膨れるなら別ですが)、全体的にはDynamicObjectの機能を満遍なく使っているので、参考になるかと思います。あと、JSON書き出し時のLinq to XmlでのXML組み立て部分は若干トリッキーなものの、割とよく出来ているかな? Deserializeはもう少し気合入れて実装し直さないとダメな感じですがががが。
DynamicJson - C# 4.0のdynamicでスムーズにJSONを扱うライブラリ
- C# - 10.04/30
C#4.0の新機能といったらdynamic。外部から来る型が決まってないデータを取り扱うときは楽かしら。とはいえ、実際に適用出来る範囲はそんなに多くはないようです。例えばXMLをdynamicで扱えたら少し素敵かも、と一瞬思いつつもElementsもDescendantsも出来なくてAttributeの取得出来ないXMLは、実際あんまり便利じゃなかったりする。ただ、ちょうどジャストフィットするものがあります。それは、JSONですよ、JSON。というわけで、dynamicでJSONを扱えるライブラリを書いてみました。ライブラリといっても300行程度のクラス一個です。
使い方は非常にシンプルで直感的。まずは、文字列JSONの読み込みの例を。DynamicJson.Parseメソッド一発です。
// Parse (from JsonString to DynamicJson) var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }"); var r1 = json.foo; // "json" - dynamic(string) var r2 = json.bar; // 100 - dynamic(double) var r3 = json.nest.foobar; // true - dynamic(bool) var r4 = json["nest"]["foobar"]; // インデクサでのアクセスも可 // 定義されてるかチェック var b1 = json.IsDefined("foo"); // true var b2 = json.IsDefined("foooo"); // false
Parseしたら、あとはJavaScriptと同じ感じにプロパティ名をドット打つだけで値が取り出せます。dynamicとJSONは相性が良いですね、JavaScriptと全く同じ感覚です。注意点としては、存在しないプロパティ名を読むと例外が出ます。Dictionaryみたいなものだと思ってください。さすがにそれだと使いにくいところもあるので、IsDefinedメソッドであるかないかチェック出来ます。dynamicなため、IntelliSenseには出てこないということは注意してください。もう一つ気をつけなきゃいけないのは、数字は全てdoubleです。JSONでは数値類は全部一緒くたにNumberなので、適宜自分でキャストしてください。
オブジェクトからJSON文字列への変換
dynamicとは関係ないのですが、JSON文字列へのシリアライズも可能です。こちらもDynamicJson.Serializeメソッド一発。
// Serialize (from Object to JsonString) var obj = new { Name = "Foo", Age = 30, Address = new { Country = "Japan", City = "Tokyo" }, Like = new[] { "Microsoft", "Xbox" } }; // {"Name":"Foo","Age":30,"Address":{"Country":"Japan","City":"Tokyo"},"Like":["Microsoft","Xbox"]} var jsonStringFromObj = DynamicJson.Serialize(obj);
匿名型でサクッとJSONを作り上げられます。非常にお手軽。DataContractJsonSerializerはちょっと大仰すぎなのよねえ、という時にはこれでサクサクッと作ってやってください。匿名型だけじゃなく、普通のオブジェクトでも大丈夫です(その場合はパブリックプロパティからKeyとValueを生成します)。
JSONオブジェクトの再編集・作成
生成したDynamicJsonは可変です。自由に編集して再シリアライズとか出来ます。
var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }"); // 追加、編集、削除が出来ます json.Arr = new string[] { "NOR", "XOR" }; // Add json.foo = 5000; // Replace json.Delete("bar"); // Delete // DynamicJsonから文字列へのシリアライズはToStringを呼ぶだけ var reJson = json.ToString(); // {"foo":5000,"nest":{"foobar":true},"Arr":["NOR","XOR"]} // 配列はちょっと特殊で、foreachなので扱いたい場合はobject[]にキャストしてください Console.WriteLine(json.Arr[1]); // XOR foreach (var item in (object[])json.Arr) Console.WriteLine(item); // NOR XOR // 新しく作成することも出来ます dynamic root = new DynamicJson(); // ルートのコンテナ root.obj = new { }; // 空のオブジェクトの追加は匿名型をどうぞ root.obj.str = "aaa"; root.obj.@bool = true; // C#の予約語と被る場合は@を先頭につけるとアクセス出来るよ! root.array = new[] { 1, 200 }; // 配列の追加 root.obj2 = new { str2 = "bbbb", ar = new object[] { "foobar", null, 100 } }; // オブジェクトの追加と初期化 // {"obj":{"str":"aaa","bool":true},"array":[1,200],"obj2":{"str2":"bbbb","ar":["foobar",null,100]}} Console.WriteLine(root.ToString());
追加は存在しないプロパティ名に直接突っ込めばOK。編集はそのまま上書き。型名とか関係ないので、元から入っているものの型に合わせる必要はありません。削除はDeleteメソッドを呼べば出来ます。配列はちょっと扱いが特殊でして、foreachしたかったりLinqメソッド使いたい場合はobject[]にキャストする必要があります。この辺は仕様です。諸事情によりIEnumerableじゃないんです。ごめんなさい。ちなみにobjectってものなあ、intが欲しいんすよ、っていう時は.Cast
一から新しいDynamicJsonオブジェクトを作成することも出来ます。普通にnewするだけ。注意点としては、変数はdynamicで受けてください。varで受けても何の嬉しいこともありませんので。あとは、普通にぽこぽこ足すだけ。オブジェクトを作る場合は空の匿名型でやります。決してDynamicJsonを足したりしないでください、がっかりなことになりますので。
実装の裏側
300行のクラス一個、ということで、勿論自前でパーサー書いてるわけがありません。ていうか、その手のは自分で書きたくないんだよね、ソートアルゴリズムとかもそうだけど、こういうのはちゃんと検証されてるものを使うべき。(そしてそもそも、ちゃんとしたのが書けるかというと、書けません……)。で、何を使っているかというと先日の記事でLinq to Jsonとか言ってたように、JsonReaderWriterFactoryを使用しています。
ようするに、ただのJsonReaderWriterFactoryのラッパーです。内部ではJSONの構造をXMLとして保持していて、書き出しの際にJsonReaderWriterFactoryを通しています。ただですね、Readerのほうは使い易くてJsonReaderWriterFactoryお薦め!なのですが、Writerのほうは結構厳しいです。ルールに則って書いたXMLを通すとJSONになる、という仕組みなのですが、ルールに則ってないと即弾かれるということでもあって、かなり面倒くさいです。
// 例えばこんなDynamicJSONは dynamic root = new DynamicJson(); root.Hoge = "aiueo"; root.Arr = new[] { "A", "BC", "D" }; // 内部ではこんなコードに変換されています new XElement("root", new XAttribute("type", "object"), new XElement("Hoge", new XAttribute("type", "string"), "aiueo"), new XElement("Arr", new XAttribute("type", "array"), new XElement("item", new XAttribute("type", "string"), "A", new XElement("item", new XAttribute("type", "string"), "BC", new XElement("item", new XAttribute("type", "string"), "D")))));
このXElementを素で書いていくのは地獄でしょう。DynamicJsonはこの変換を自動で行います。dynamicでラッピングすることで、煩わしい部分を完全に包み隠すことができました。ここまで簡略化出来ると、DSLの域です。C#は大変素晴らしいデスネ。いや、マジで。
まとめ
クラス一個なので、csファイルをコピペって使ってもいいですし(その場合は追加でSystem.Runtime.Serializationの参照を)、DLLを参照設定に加えても、どちらでもお好きな方をどうぞ。数あるJSONライブラリの中でも、使いやすさはトップクラスなのではないでしょうか(自画自賛)。いや、これは、単純にdynamicの威力の賜物ですね。これを作るまではdynamicについて割と勘違いしていたところもあったのですが、なんというか、DSL向けだと思います。で、DSL指向で行くなら全部プロパティだけで組まないとダメですねえ。IntelliSenseが動かないのでメソッドを使うのは今ひとつ。そういう意味で、IsDefinedじゃなくて.property? とかって感じに、末尾に?をつけるとかどうかな!とか考えてみたんですが、コンパイル通らないのでダメでした、残念。「.あいうえお」なら行けるので、日本語プログラミングDSLが待たれるところです。嘘。
static void Main() { var publicTL = new WebClient().DownloadString(@"http://twitter.com/statuses/public_timeline.json"); var statuses = DynamicJson.Parse(publicTL); foreach (var status in (dynamic[])statuses) { Console.WriteLine(status.user.screen_name); Console.WriteLine(status.text); } }
最後に例として、Twitterのpublic_timeline.jsonを引っこ抜くコードを。凄まじく簡潔です。C#はどこをどう見てもLightWeightですね、本当にありがとうございました。
C#とLinq to JsonとTwitterのChirpUserStreamsとReactive Extensions
何か盛り沢山になったのでタイトルも盛り沢山にしてみました。SEO(笑)
最近話題のTwitterのChirpUserStreamsを使ってみましょー。ChirpUserStreamsとは、自分のタイムラインのあらゆる情報がストリームAPIによりリアルタイムで取得出来る、というもの。これを扱うには、まずはストリームをIEnumerable化します。そのまま扱うよりも、一度IEnumerable化すると非常に触りやすくなる、というのがLinq時代の鉄則です。C#でのストリームAPIの取得方法は以前にも記事にしましたが、かなり汚かったのでリライト。WebClient愛してる。
public static IEnumerable<XElement> ConnectChirpStream(string username, string password) { const string StreamApiURL = "http://chirpstream.twitter.com/2b/user.json"; var wc = new WebClient() { Credentials = new NetworkCredential(username, password) }; using (var stream = wc.OpenRead(StreamApiURL)) using (var reader = new StreamReader(stream)) { var query = reader.EnumerateLines() // 1行に1JSONなのです .Where(s => !string.IsNullOrEmpty(s)) // 空文字が来るので除去 .Select(s => // 文字列JSONからXElementへ変換 { using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.Default.GetBytes(s), XmlDictionaryReaderQuotas.Max)) return XElement.Load(jsonReader); }); foreach (var item in query) yield return item; // 無限列挙 } } // StreamReaderの補助用拡張メソッド(あると大変便利) public static IEnumerable<string> EnumerateLines(this StreamReader streamReader) { while (!streamReader.EndOfStream) { yield return streamReader.ReadLine(); } }
中々シンプルに書けます。C#もLLと比べても全然引けを取らないでしょう(誰に言ってる)。ChirpUserStreamsはJSONでしか取れないのですが、StreamAPIではXmlよりもJSONのほうが使い易いので、JSONだけでも全然問題ありません。とはいえ、C#でJSONはちょっと扱いにくいんだよねー?と思いきや、意外に普通に標準ライブラリだけで何とかなりました。
参照設定にSystem.Runtime.Serializationを加えます(注:VS2008ではSystem.ServiceModel.Webを参照設定に加えてください)。この参照で通常使うのはDataContractJsonSerializerだと思いますが、もう一つ、JsonReaderWriterFactoryというクラスが用意されていて、このReaderはJSONをXmlReaderとして扱うことが出来ます。この図式は以前のLinq to HtmlのためのSGMLReader利用法と同じです。そのままXElementに流しこめば、JSONをXmlとして扱って、Linq to Jsonが成り立ちます。
さて、StreamAPIなのでEndOfStreamは来ない。繋ぎっぱなし、つまりはConnectChirpStreamメソッドは無限リストになります。この中には色々な情報が混ぜこぜになって来ます。投稿した、という他にも、誰かをフォローした、何かをふぁぼった、何かをリツイートした、などなどなどなど。クライアントソフト作るなら、当然どの情報も漏れ無く扱いたいわけですが、どうしましょう?foreachでグルッと回して、ifで分ける、しか、ない、かもですね? しかしそれは敗北です。退化です。foreachを使ったら負けだと思っている。
ところで突然に、今のところ思うReactive Extensions for .NET (Rx)を使うメリットは3つ。「複雑になりがちな複数イベントの合成」「同じく複雑になりがちな非同期処理のLinq化」そして、「列挙の分配」。従来型のLinqでは、一回の列挙には一個の処理しか挟めませんでした。例えば、MaxとCountを同時に取得する方法はなかった。MaxとCountを別々に二度列挙するか、または旧態依然なやり方、つまりforeachでグルグルと回してMaxとCountを手動で計算するかしかなかった。それはIEnumerableがPullモデルなためで、PushモデルのIObservableならば、出来ないこともない。
では、Rxでこのストリームを分配してみましょう。
static void Main(string[] args) { // IEnumerableをIObservableに変換し、Publish(Connectするまで列挙されない(ので分配が可能になる)) var connecter = ConnectChirpStream("username", "password") .ToObservable() .Publish(); // 1件目は必ず自分のフレンドのIDリストが来るらしいっぽいのでまるっと保存 HashSet<int> friendList; connecter.Take(1).Subscribe(x => friendList = new HashSet<int>( x.Element("friends").Elements().Select(id => (int)id))); // どんなのが来るのかよく分からないのでモニタ用にテキストにまるっと保存 var sw = new StreamWriter("streamLog.txt") { AutoFlush = true }; connecter.Subscribe(x => sw.WriteLine(x)); // userがあるなら普通の投稿(ってことにしておく) connecter.Where(x => x.Element("user") != null) .Select(x => new { Text = x.Element("text").Value, Name = x.Element("user").Element("screen_name").Value }) .Subscribe(a => Console.WriteLine(a.Name + ":" + a.Text)); // favoriteとかretweetは "event":"favorite" というJSONが来る var events = connecter.Where(x => x.Element("event") != null); // favoriteの場合の処理 events.Where(x => x.Element("event").Value == "favorite") .Subscribe(x => Console.WriteLine(x)); // favorite用の何か処理 // retweetの場合の処理 events.Where(x => x.Element("event").Value == "retweet") .Subscribe(x => Console.WriteLine(x)); // retweet用の何か処理 // 同期か非同期かは、ToObservableの引数で変わる。デフォルトは同期 // Scheduler.ThreadPoolを引数に入れるとThreadPoolで非同期になる connecter.Connect(); // 列挙開始 }
ID一覧取得、テキスト保存、投稿時処理、Fav時処理、リツイート時処理の5つへの分配が非常にスマートに出来ました。ToObservable、Publish、Connect。たったこれだけで一つストリームを複数に分配することが出来ます。普通にそれぞれをWhereだのSelectだの、独立してLinqでコネコネ出来ました。で、何が嬉しいかっていうと、それぞれが完全に独立していて見やすいってのは勿論あります。あと、部品化されてるので外部に分割しやすくなるんですね、物凄く。組み合わせたりもしやすいし。
結論
Rxヤバい。というわけで、みんなRx触ろう! .NET Framework 4.0ではRxで使うIObservableとIObserverインターフェイスが搭載されています。インターフェイスだけでどうすんだよボケ、っていうと、実際のところどうにもなりませんね、たはー。それでもインターフェイスだけ先行搭載ということは、RxはDevLabs内だけで終わる実験的プロジェクトではなく、必ず標準搭載するから安心しろよ!というメッセージだと受け取ることにしました。きっと.NET Framework 4.0 SP1には標準搭載されます。される、と、いいなあ。ちなみにRxは初期の頃と結構変わってますし、まだ変わるかも。でも、だからこそ、それに付き合うのも楽しいってものですよ?
ああ、あと、ChirpUserStreamsもヤバいですね。リアルタイムでゴリゴリ迫ってくる感覚は素敵というか、なんか別次元のメディアになった感じでもあります。今、新規にTwitterクライアント作るならStreamAPI完全対応すれば差別化出来て良いですね!私は作りませんが。ただ、ストリームAPIとRxは相性良いと思うので、ストリームAPI時代の到来と同時にRx時代も到来!する、かなあ?
ToDo
linq.jsがようやく一段落したので、Rxの紹介もまたやっていきたいですねー(すみません、かなり長いこと放置していて)。ToObservableによる列挙の分配は中々に強烈なので、linq.jsとRxJSを橋渡しするようなコードというかJSというかも用意したいなあ(軽く作ってみたんですが、RxJSでもPublishで分配出来て中々に威力ありそうでした)。うーん、考えてみるとやることはいっぱいあるねえ。適当に待っていてください。