.NET Reactive Framework メソッド探訪第一回:FromEvent

まず、リアクティブフレームワークとは何ぞや、ということなのですが今のところInfoQ: .NETリアクティブフレームワーク(Rx)がLINQ to Eventsを可能にするの記事ぐらいしか情報はありません。.NET 4.0に含まれる(かもしれない)ということ、現在のところSilverlight Toolkitの単体テストのところにこっそりと配置されていること。それだけです。紹介も、記事中にもリンクされていますがunfold: Introducing Rx (Linq to Events)の一連の記事ぐらいしかありません。これの前文が中々に素敵です。

Buried deep in the bin folder of the Silverlight Toolkit Unit Tests is a hidden gem: The Rx Framework (System.Reactive.dll). If you glanced quickly you’d miss it altogether but it’s one of the most exciting additions to the .NET framework since Linq.

今のところ微妙にパッとしない(Parallelは簡単に使えるがゆえにインパクトが足らない)4.0の隠し玉はコレですね、間違いない。軽く触ってみたのですが、中々に感動的。Linq to Objects好きならば間違いなく琴線に触れます。C#3.0がコレクションの操作をforeachからLinqに変えてしまったように、.NET4.0はイベントもLinqに変わる。まさにLinq to Everywhere! Functional Reactive Programming!

How to use

Silverlight ToolkitをダウンロードしてSource/Binaries/System.Reactive.dllを頂けば完了。ただし、これはそのままだとSilverlightのプロジェクトでしか動作しないので、その他ので利用したい場合はここの記事に示されているように、githubに公開されているコードを実行(Cecilのdllが必要、記事文中にリンクされています)して変換する必要があります。今回はとりあえず、Silverlightで試してみたいと思います。こちらはこちらで、Silverlight 3 Toolsのダウンロードが必要ですけれど。

実例

Microsoft Silverlight を入手

マウスの動きに円が追随する、という単純なものをSilverlightで作ってみました。移動は完全に追随するのではなく、座標が15で割り切れる位置の場合のみ移動としました。スナップすることをイメージしたつもりなのですが、動きがガクガクです。これは、マウス移動に完全に追随して全ての座標でイベントが発生するわけではない = 15で割り切れる座標を通過してもイベントが発生しない場合がある = 動きがガクガク。というわけで、スナップしたい場合はWhereで間引くのではなく、Selectで近傍座標に寄せるべきです、が、いやまあ、例なので……。

// XAMLではなく全部コード上に書いたのは両方を張るのが面倒だから……
// 内容はCanvasとEllipseを配置するというもので、本筋とは関係ありません
InitializeComponent();
var canvas = new Canvas { Background = new SolidColorBrush(new Color {A=255, R = 100, G = 100, B = 100 }) };
var ellipse = new Ellipse { Height = 30, Width = 30, Fill = new SolidColorBrush(Colors.Orange) };
canvas.Children.Add(ellipse);
this.Content = canvas;

// FromEventはイベント発火がトリガとなってLinq発動
// 後段に送られるのはEvent<T>というもので、
// SenderとEventArgsという読み取り専用プロパティを持つクラス
var canvasMove = Observable.FromEvent<MouseEventArgs>(canvas, "MouseMove")
    .Select(e => e.EventArgs.GetPosition(canvas))
    .Where(p => (p.X % 15 == 0) || (p.Y % 15 == 0))
    .Subscribe(p =>
    {
        ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2);
        ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2);
    });

// Subscribeの戻り値の型はIDisposable
// Disposeを呼ぶと登録したイベントをデタッチすることが出来る
// デタッチしないなら取得する必要は特にはない
// canvasMove.Dispose();

MouseMoveでイベントが発火する度にLinqを通る。なるほど、イベントがリストに、見える。イベントを無限リスト生成として捉えることで、イベントに対してLinq操作が可能になった。ObserverパターンとIteratorパターンは同じだったんだよ!なんだってー!みたいなノリがある。もう少し丁寧に見ると、Observable.FromEventでイベントをPush型の無限リストに変換。戻り値はIObservable<T>。イベント発火時に後段に流れてくるのはEvent<T>。これは通常のイベント登録時に使うsenderとeventArgsをラップしただけの単純なもの。あとはIObservableに用意されているメソッド(Select, Where, TakeWhileなどお馴染みのものから、Delay, WaitUntilなどイベント用の目新しいメソッドなど多数)を繋げて、最後にSubscribe。このSubscribeは、つまり通常のイベント登録時のメソッド本文の役割を果たす。Linqで言ったらForEachのようなもの。Subscribeのオーバーロードも幾つかあるのですが、それはまた後日。

// つまるところ、以下のコードと同じだったりはする
// ただ、IObservable<T>は通常のイベント登録では無理な複雑な操作が簡単、
// そして何よりも、このような単純なコードでもそんなに複雑になっていない!
canvas.MouseMove += (sender, e) =>
{
    var pos = e.GetPosition(canvas); // Select
    if (!(pos.X % 15 == 0 || pos.Y % 15 == 0)) return; // Where
    ellipse.SetValue(Canvas.LeftProperty, pos.X - ellipse.Width / 2);
    ellipse.SetValue(Canvas.TopProperty, pos.Y - ellipse.Height / 2);
};

通常のイベント登録と対比してみると分かりやすいかしらん。FromEventではMouseEventArgsという型を明示する必要があるのがカッタルイ。推論は偉大。が、しかし、IObservableが複雑な操作が可能なのに対し、イベントに追加では直球なものしか書けない。また、複雑な操作が可能なわりには、FromEventは驚くほどシンプルに書ける。シンプルな操作でも(記述するのに)重たくない、というのは特筆すべきことじゃあないでしょうか。

// stringを避けたこういう登録方法もあるけれど、面倒なうえに警告出る
Observable.FromEvent((EventHandler<MouseEventArgs> h) => new MouseEventHandler(h),
        h => canvas.MouseMove += h, // addHandler
        h => canvas.MouseMove -= h) // removeHandler
    .Subscribe(e => Debug.WriteLine(e.EventArgs.GetPosition(canvas)));

ところで、イベント名をstringで書くのはどうよ、ていうかJavaScriptのaddEventHandlerみたいで嫌だよね?ね?リファクタリング効かないわ、IntelliSenseも動かないわでロクなことがない。というわけで、FromEventのオーバーロードを見ると、ちゃんと普通に登録する方法も用意されてはいる。一応、用意、されては、いる。が、しかし、あんまりだー。あんまりすぎるー。流れてくるEventhandler<MouseEventArgs>をMouseMoveが受け取ってくれないので、第一引数でMouseEventHandlerに変換する(ところで警告が消せないのですが、警告無しで処理する方法ってあるのかしらん)。あとは、addとremoveの登録。長ったらすぎてこれはダメぽ。確かに、こんなんなら、stringでいいです……。

Microsoft Silverlight を入手

// 普段あまり書かないMouseEventArgsとかいう型定義は書きにくいし
// メソッド名もstringで書くのはミスが出がち、ということで
// 拡張メソッドでイベント取り出し用のメソッドを予め作っておくと良い
public static IObservable<Event<MouseEventArgs>> GetMouseMove(this UIElement elem)
{
    return Observable.FromEvent<MouseEventArgs>(elem, "MouseMove");
}

// マウスの軌跡を1秒後に描画します
canvas.GetMouseMove()
    .Select(e => e.EventArgs.GetPosition(canvas))
    .Delay(1000)
    .Subscribe(p =>Dispatcher.BeginInvoke(()=>
    {
        ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2);
        ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2);
    }));

汚い部分は隔離!ということで、拡張メソッドに退避してやると、美しく書ける。いやまあ、この辺は全部unfold: The Joy of Rx: Extension Eventsに書いてあることなのですけど。んで、デモ的にもう少し面白げがあったほうがいいかな、と思ったのでDelayを足してみました。1秒後にマウス移動の軌跡を描画します。グルグルーっとマウス動かして止めてみてください。スムーズ、とは言い難いですね、しょんぼり。記述も、Delayを足すだけ。と言いたかったんですがBeginInvokeかあ、これどーにかなる方法ないかなあ。

次回

全10回ぐらいで、全部のメソッドを紹介するつもりです。私が理解できればの話ですが。ちょこちょこと実例的なものも交えていきたいと思います。私が使いこなせればの話ですが。というわけで、次回はSubscribeのオーバーロードの紹介にしたいと思います。Reactive Frameworkならではの魅力、に関してはもう少し先になってしまいそう。少し飛ばして、非同期連結の話なんかを先に持ってきた方が良いかなあ。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive