Rxでのイベント変換まとめ - FromEvent vs FromEventPattern

Reactive Extensionsの機能の一つに.NETにおけるイベントをIObservable<T>に変換する、というものがあります。Bridging with Existing .NET Events。そして、そのためのメソッドがFromEventでした。ところが最近のRxでは二つ、FromEventとFromEventPatternが用意されています。この差異は何なのでしょうか?

結論としては、過去のRx(このサイトの古い記事や他のサイトの過去の記事などで触れられている)やWindows Phone 7でのFromEventはFromEventPatternに改名されました。後続にEventPatternという(object Sender, TEventArgs EventArgs)を持つ.NETのイベントの引数そのものを渡すものです。そして、空席になったFromEventに新しく追加されたFromEvent(紛らわしい!)は、EventArgsだけを送ります。それ以外の差異はありません。

つまるところFromEventは FromEventPattern.Select(e => e.EventArgs) ということになります。なら、それでいいぢゃん、何も混乱を生む(WP7のFromEventがFromEventPatternである、というのは致命的よねえ)ことはないよ、とは思うのですが、パフォーマンスの問題でしょうかね。確かに、Senderは必要なく使うのはEventArgsだけの場合が多い。それなのに、毎回EventPatternを生成していたり、Selectというメソッド呼び出しが入るのは無駄です。

そもそもインスタンスに対してFromEventで包むということは、クロージャでsenderは変数としていつでもどこでも使えてしまうのですよね、そもそも、そもそも。そういう意味でも送られてくるのはEventArgsだけでいいのであった。というわけで、基本的にはFromEventでいいと思います。

FromEventPatternについて

では、改めてFromEventPatternを復習します(WP7の人はFromEventで考えてください)。Observable.FromEventPattern(TEventArgs) Method (Object, String) (System.Reactive.Linq)にサンプルコードがあるのですけれどね。そうそう、MSDNのリファレンスには、一部のメソッド/一部のオーバーロードにはサンプルコードがあります。全部ではないのがミソです、見て回って発掘しましょう。まあ、というわけで、とりあえずそのFileSystemWatcherで。

// FileSystemWatcherは指定フォルダを監視して、変化があった場合にイベントを通知します
// 例えばCreatedイベントはファイルが作成されたらイベントが通知されます
var fsw = new FileSystemWatcher(@"C:\", "*.*") { EnableRaisingEvents = true };

// FromEventPatternその1、文字列でイベント名指定
Observable.FromEventPattern<FileSystemEventArgs>(fsw, "Created")
    .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));

// FromEventPatternその2、静的なイベントをイベント名指定(WP7にはない)
Observable.FromEventPattern<ConsoleCancelEventArgs>(typeof(Console), "CancelKeyPress")
    .Subscribe(e => Console.WriteLine(e.EventArgs.SpecialKey));

一番馴染み深いと思うのですが、文字列でイベント名を指定するものです。その2のほうはあまり見ないかもしれませんが、静的イベントに対しての指定も可能です。これら文字列指定によるメリットは、比較的シンプルであること。デメリットは、リフレクションを使うので若干遅い・スペルミスへの静的チェックが効かない・リファクタリングが効かない、といった、リフレクション系のデメリットそのものとなります。

リフレクションしかないの?というと、勿論そんなことはありません。

// FromEventPatternその3、EventHandlerに対する変換
var current = AppDomain.CurrentDomain;
Observable.FromEventPattern(h => current.ProcessExit += h, h => current.ProcessExit -= h)
    .Subscribe(e => Console.WriteLine(e.EventArgs));

// FromEventPatternその4、EventHandler<T>に対する変換
Observable.FromEventPattern<ContractFailedEventArgs>(
        h => Contract.ContractFailed += h, h => Contract.ContractFailed -= h)
    .Subscribe(e => Console.WriteLine(e.EventArgs.Message));

// FromEventPatternその5、独自イベントハンドラに対する変換
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => new FileSystemEventHandler(h),
        h => fsw.Created += h,
        h => fsw.Created -= h)
    .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));

イベントの登録と削除を行うためのラムダ式を渡してやります。その3とその4は比較的分かりやすいのではないでしょうか。その5の第一引数が謎いのですが、これはconversionです。C#の型システムの都合上、そのまんまだと独自イベントハンドラを処理出来ないので、型を変換してやる必要があるという定型句。

数あるFromEventPatternのオーバーロードの中で、一番多く使うのはその5だと思います。何故なら、C#のイベントは独自イベントハンドラになっていることが多いから。はっきしいって、最低です。EventHandler<T>を使ってくれてさえいれば、こんな苦労はしなくて済むというのに。独自イベントハンドラは100害あって一利なし。え、WPFとか.NET標準がイベントハンドラは独自のものを使ってる?それは、WPFが悪い、.NET設計の黒歴史、悪しき伝統。

それと、もはや独自デリゲートも最低です。FuncやActionを使いましょう。C#のデリゲートはメソッドの引数や戻り値が一致していようが、型が違ったら別のものとして扱われます。そのことによる不都合は、↑で見たように、あるんです。極力ジェネリックデリゲートを使いましょう。そうすれば、こんな腐った目に合わなくても済みます。

ところで、その5は、もう少しだけ記述が短くなります。

// FromEventPatternその5、第一引数別解、こう書くと短くて素敵
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => h.Invoke,
        h => fsw.Created += h,
        h => fsw.Created -= h)
    .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));

h.Invoke。というのは、割とhそのものなわけですが、しかしInvokeと書くことで型が変換されます。この辺はコンパイラの都合上のマジックというか何というか。そういうものだと思えばいいのではかと。その5のスタイルで書くときは、この書き方をすると良いと思います。で、まだオーバーロードがあって

// その6
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => fsw.Created += h, h => fsw.Created -= h)
    .Subscribe(e => Console.WriteLine(e.EventArgs.FullPath));

conversionが不要で書けたりもします。一見素晴らしい、のですが、これ、中でなにやってるかというとconversionに相当するものをリフレクションで生成してるだけだったりして。そのため、なるべくconversionを使うオーバーロードのほうを使ったほうがよいでしょう。h => h.Invokeを書くだけですしね。このオーバーロードは紛らわしいだけで存在意義が不明すぎる。

FromEventについて

と、長々と見てきましたが、ではFromEventのほうも。

// FromEvent
Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
        h => (sender, e) => h(e),
        h => fsw.Created += h,
        h => fsw.Created -= h)
    .Subscribe(e => Console.WriteLine(e.FullPath));

// FromEventPatternその5(比較用)
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => (sender, e) => h(sender, e),
        h => fsw.Created += h,
        h => fsw.Created -= h)
    .Select(e => e.EventArgs)
    .Subscribe(e => Console.WriteLine(e.FullPath));

というわけで、FromEventPatternのその5に近いわけですが、conversionでEventArgsしか渡していない、という点が差異ですね。なので、後続にはsenderが伝わってこず、EventArgsしか通りません。まあ、senderは、↑の例ですとfswでどこでも使えるので、そもそも不要なわけで、これで良いかと思います。

ところでFromEventも色々なオーバーロードがあるにはあるんですが、私の頭では存在意義が理解できなかったので無視します。挙動とかは理解したんですが、なんというか、存在する必要性、有効な利用法がさっぱり分からなかったのです……。まあ、多分、あんま意味ないと思うので気にしないでもいいかと。

拡張メソッドに退避させよう

FromEventにせよFromEventPatternにせよ、長いです。長い上に定型句です。なので、拡張メソッドに退避させると、スッキリします。例えば、今まで見てきたFileSystemWatcherだったら

// .NETのFromEventなら IObservable<TEventArgs>
// .NETのFromEventPatternなら IObservable<EventPattern<TEventArgs>>
// WP7のFromEventなら IObservable<IEvent<TEventArgs>>
// を返す拡張メソッド群を用意する。
// 命名規則はイベント名AsObservableがIntelliSenseの順序的にお薦め
public static class FileSystemWatcherExtensions
{
    public static IObservable<FileSystemEventArgs> CreatedAsObservable(this FileSystemWatcher watcher)
    {
        return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
            h => (sender, e) => h(e), h => watcher.Created += h, h => watcher.Created -= h);
    }

    public static IObservable<FileSystemEventArgs> DeletedAsObservable(this FileSystemWatcher watcher)
    {
        return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
            h => (sender, e) => h(e), h => watcher.Deleted += h, h => watcher.Deleted -= h);
    }

    public static IObservable<RenamedEventArgs> RenamedAsObservable(this FileSystemWatcher watcher)
    {
        return Observable.FromEvent<RenamedEventHandler, RenamedEventArgs>(
            h => (sender, e) => h(e), h => watcher.Renamed += h, h => watcher.Renamed -= h);
    }

    public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher watcher)
    {
        return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>(
            h => (sender, e) => h(e), h => watcher.Changed += h, h => watcher.Changed -= h);
    }
}
var fsw = new FileSystemWatcher(@"C:\", "*.*") { EnableRaisingEvents = true };

// 例えば、ただ変更をロギングしたいだけなんだよ、という場合の結合
// FromEventを外出ししていることによって、すっきり書ける
Observable.Merge(
        fsw.CreatedAsObservable(),
        fsw.DeletedAsObservable(),
        fsw.ChangedAsObservable(),
        fsw.RenamedAsObservable())
    .Subscribe(e => Console.WriteLine(e.ChangeType + ":" + e.Name));

といった形です。また、普通に+-でのイベント以外のものへの登録も可能です。例えば

// LayoutRootはWPFの一番外枠の<Grid Name="LayoutRoot">ということで。
Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
        h => (sender, e) => h(e),
        h => LayoutRoot.AddHandler(UIElement.MouseDownEvent, h),
        h => LayoutRoot.RemoveHandler(UIElement.MouseDownEvent, h))
    .Subscribe(e => Debug.WriteLine(e.ClickCount));

こんな形のものもObservable化が可能です。

イベントの解除

Subscribeの戻り値はIDisposableで、Disposeを呼ぶことでイベントが解除されます。

// アタッチ
var events = Observable.Merge(
        fsw.CreatedAsObservable(),
        fsw.DeletedAsObservable(),
        fsw.ChangedAsObservable(),
        fsw.RenamedAsObservable())
    .Subscribe(e => Console.WriteLine(e.ChangeType + ":" + e.Name));

// デタッチ(合成などをしていて、元ソースが複数ある場合も、すべて解除されます)
events.Dispose();

Rxのこの仕組みは、従来に比べて圧倒的にイベントの解除がやりやすくなっていると思います。

まとめ

非同期の説明ばかりしてきていて、イベントはすっかり置き去りだったことを、まずはゴメンナサイ。少し前からFromEvent周りは大きな仕様変更が入ったわけですが、ようやくまともに解説できました。基本中のキの部分であるここが、過去のリソースがそのまま適用出来ないという最悪の自体に陥っていたので、とりあえずこれで何とか、でしょうかどうでしょうか。

小さなこととはいえ、WP7との互換性が絶えているのが痛いのですが、その辺どうにかならなかったのかねー、とは思います。けれど、このEventArgsだけ送るFromEvent自体は良いと思います。 .Select(e => e.EventArgs) が定型句だったので、こういった変更は喜ばしい限り。それと、今まで思っていた、ぶっちゃけラムダ式とかRxでイベント登録するならsenderって不要じゃね?に対する答え(その通りで、完全に不要)を出してくれたのが嬉しい。

さて、変換できるのはいいけれど、じゃあどこで使うのがいいの?という話がいつもありません。次回は、時間周りと絡めて、その辺のお話が出来ればと思いますが、いつも次回予告が達成されたことはないので、別のことを書くでしょう←ダメぢゃん。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive