Re:FromEvent vs FromEventPattern
- 2011-09-10
現在のRxで最大の分かりにくいポイントになっているFromEventとFromEventPattern。以前にRxでのイベント変換まとめ - FromEvent vs FromEventPatternとして軽くまとめましたが、改めて、詳細に考えてみたいと思います。なお、ここでいうFromEventPatternはWP7版のRxではFromEventを指しています(ここも分かりにくいポイントです)。そして、ここでいうFromEventはWP7版のRxには未搭載です、あらあらかしこ。
ネタ元ですが、銀の光と藍い空: Silverlight 5 の新機能その3 番外編 DoubleClickTrigger をRxっぽくしてみたを拝見して、FromEventに関して実行時に例外が出るとのことなので、その部分の説明を書こうかと。最初コメント欄に書いたのですが、少しコメントにトチってしまったので、自分のブログに書かせていただきます、どうもすみません……。
FromEventとFromEventPatternの最大の違いは、FromEventがAction<EventArgs>を対象にしていて、FromEventPatternはAction<object, EventArgs>を対象にしているということです。Action<object, EventArgs>は、つまりEventHandler。ではAction<EventArgs>って何なんだよ、というと、通常は存在しません。というのもeventはデリゲートなら何でもアリということに仕様上はなっていますが、慣例としてobject sender, EventArgs eを引数に持つデリゲートを選択しているはずですから。
さて、デリゲート間には同じ引数の型・同じ戻り値の型を持っていても、型自体に互換性がないので(例えばEventHandlerとEventHandler<T>)、FromEventもFromEventPatternも、引数を3つ持つオーバーロードの第一引数は conversionという、デリゲートの型を変換するラムダ式を受け入れるようになっています。よって
Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
h => h.Invoke,
h => AssociatedObject.MouseLeftButtonDown += h,
h => AssociatedObject.MouseLeftButtonDown -= h);
これが、リフレクションなしで変換出来る形式になります。conversionが不要なオーバーロードもあるのですが(+=と-=を書くだけ)、それはリフレクションを使ってデリゲートの変換をしていて今一つ効率が悪いので、わざわざ+=, -=を書いているのだから、もう一手間かけて、 h => h.Invoke を書いておいたほうがお得です。(もし対象がEventHandler<T>の場合は事情が違ってconversionが不要なので+=と-=だけで済みます、この辺の事情の違いが面倒臭く混乱を招きがちなんですよね…… Rxチームには「便利そうだから」機能を追加する、とかやる前に、もう少し深く考えてくださいと苦情を言いたい)
h => h.Invoke だけで何故変換出来ているのかを詳しく説明します。これは正しく書くのならば
(EventHandler<MouseButtonEventArgs> h) => new MouseButtonEventHandler(h)
が正解です。(左側の型に関しては省略可能ですが、説明用には型を書いていたほうが分かりやすいので明記しておきます、また、この最初からEventHandler<T>であることが、対象がEventHandler<T>である場合はconversionが不要な理由になっています)。ただし、関数を直接渡すとコンパイラがデリゲートの型を変換してくれるため、h => h.Invokeを渡した場合は
h => new MouseButtonEventHandler(h.Invoke)
という風に内部的には自動で整形してくれます。そのため、new MouseButtonEventHandlerを書く手間が省けるということになっています。h と h.Invoke はやってることは完全一緒なのですけど、この辺はコンパイラの仕組みの都合に合わせるという感じで。むしろ仕組みの隙間をついたやり方といいましょうか。
では、FromEventなのですが、まず正しく変換出来る形を見ると
Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
h => (sender, e) => h(e),
h => AssociatedObject.MouseLeftButtonDown += h,
h => AssociatedObject.MouseLeftButtonDown -= h);
です。もし第一引数を省いた場合はAction<EventArgs>を探してリフレクションをかけるようになっていて、そして、通常はそんなイベントを使うことはないので、十中八九例外が出るのではかと思われます(だからこういう混乱を招くだけのオーバーロードを入れるなとRxチームには苦情を言いたい)。
型まで明記すれば
Action<MouseButtonEventArgs> h => (object sender, MouseEventArgs e) => h(e)
となっているわけで、senderを捨ててMouseEventArgsだけを引数に渡す独自のconversionを渡しています。これですが、FromEventPatternであっても
h => (sender, e) => h(sender, e)
とも書けるので(つまるところ h.Invoke って何かといえば (sender, e) => h(sender,e) なのです)、それのsender抜きバージョンを渡しているということになります。
わかりづらい?例えば
.Subscribe(Console.WriteLine)
.Subscribe(x => Console.WriteLine(x))
この二つはやってること一緒なんですよ、ということですね。ラムダ式が入れ子になるとワケガワカラナイ度が加速されるので、私は関数型言語erにはなれないな、と思ったり思わなかったり。
まとめ
ただたんに使うにあたっては、こんなことは知ってる必要はなくh => h.Invoke と h => (sender, e) => h(e) を定型句だと思って暗記してもらうだけで十分です。はい。本来は、こういう部分はちゃんと隠蔽されてたほうがいいんですけれど、まあ、C#の限界としてはそうはいかないというとこですね(F#だとイベントがもう少し扱いやすいんですが)。
また、FromEventにせよFromEventPatternにせよFromAsyncPatternにせよ、実際に使うコードに直接書いてくにはノイズが多すぎるので、Rxでのイベント変換まとめ - FromEvent vs FromEventPatternで書いたように、拡張メソッドに隔離するのを私はお薦めしています。そうこうして裏側で地道に努力することでF#とC#の壁を縮める!とかなんとかかんとか。