SL/WP7のSilverlight Unit Test Frameworkについて少し深く

の、前に少し。DynamicJsonAnonymousComparerをNuGetに登録しました。どちらも.csファイル一個のお手軽クラスですが、NuGetからインストール可能になったことで、より気楽に使えるのではかと思います。機能説明は省略。

そして、昨日の今日ですがChaining AssertionSilverlight Unit Test Frameworkに対応させました。リリースのバージョンは1.6.0.1ということで。NuGetではChainingAssertion-SLChainingAssertion-WP7になります。

Silverlight Unit Test Framework

Silverlightで使う場合は(WP7じゃなくてね、という意味です)、一応Silverlight Toolkitに同梱という話ではあるのですが、テンプレートなどの用意が面倒くさいので、NuGet経由で入れるのが最も楽のようです。Install-Package Silverlight.UnitTestで。

まず、Silverlightアプリケーションを新規作成。Webサイトでのホストはなしでいいです。それとブラウザで実行させる必要もないので、プロジェクトのプロパティからOut of Browserに変更してしまいましょう。次に、NuGetからInstall-Package Silverlight.UnitTest。これでライブラリの参照と、ApplicationExtensions.cs(イニシャライズ用拡張メソッド)、UnitTest.cs(テスト用テンプレ)が追加されているはずです。次にApp.xaml.csのStartupを以下のように書き換えます。

private void Application_Startup(object sender, StartupEventArgs e)
{
    // this.StartTestRunnerDelayed();
    this.StartTestRunnerImmediate();
}

StartTestRunnerDelayedはテストランナー起動時に実行オプション(指定属性のもののみ実行するなど)を選択可能にするもの、Immediateはすぐに全テストを実行する、というものです。どちらかを選択すればOK。それで、とりあえず実行(Ctrl+F5)してみれば、テストランナーが立ち上がって、デフォテンプレに含まれるUnitTest.csのものが実行されているんじゃないかしらん。あとは、それを適宜書き換えていけばよし。なお、テンプレのテストクラスはSilverlightTestを継承していますが、これは必ずしも継承する必要はありません。後述しますが、Asynchronousのテストを行いたいときは必須ですが、そうでないならば、普通にMSTestでの場合と同じように、[TestClass]と[TestMethod]属性がついているものがテスト対象になっています。

なお、MainPage.xaml/.xaml.csは不要なので削除してしまってOK。StartTestRunnerによって、参照DLLのほうに含まれるxamlが呼ばれているためです。

WP7の場合。

一応NuGetにも用意されてるっぽい(silverlight.unittest.wp7)んですが、動きませんでした。ので、今のところ手動で色々用意する必要があります。詳しくはWindows Phone 7用の単体テストツール? その2「使ってみた」 - かずきのBlog@Hatenaに全部書いてあるのでそちらを参照のことということで。参照するためのDLLを拾ってくる→App.xaml.cs、ではなくてMainPage.xaml.csを書き換える、という、Silverlight版とやることは一緒なのですけどね。こういう状況なのはMangoのSDKがベータだったからとかなんとかのせいだとは思うので、近いうちに解決するのではかと、楽観視したいところです。

Chaining Assertionを使ってみる

Chaining Assertion ver 1.6.0.0の解説で紹介した失敗結果が丁寧に表示されるよー、をチェックしてみませう。

// こんなクラスがあるとして
public class Person
{
    public int Age { get; set; }
    public string FamilyName { get; set; }
    public string GivenName { get; set; }
}
 
[TestClass]
public class ToaruTest
{
    [TestMethod]
    public void PersonTest()
    {
        // こんなPersonがあるとすると
        var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" };
        // こんな風にメソッドチェーンで書ける(10歳以下でYamadaTarouであることをチェックしてます)    
        // 実際の値は50歳でYamamotoTasukeなので、このアサーションは失敗するでしょう
        person.Is(p => p.Age <= 10 && p.FamilyName == "Yamada" && p.GivenName == "Tarou");
    }
}

はい、ちゃんと表示されます。Chaining Assertionを使うと、メソッドチェーンスタイルで、実際の値.Is(期待値の条件)というように、 簡潔な記述でテストを書くことが出来るのがうりです。また、失敗時には、この場合personの値を詳細に出力してくれるので、何故失敗したのかが大変分かりやすい。もし、普通に書くと以下のようになりますが、

// もし普通に書く場合
var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" };
Assert.IsTrue(person.Age <= 10);
Assert.AreEqual("Yamada", person.FamilyName);
Assert.AreEqual("Tarou", person.GivenName);

まず、Assert.IsTrueでは失敗時にperson.Ageの値を出してくれないので、確認が面倒です。また、この場合、Personが正しいかをチェックしたいわけなので、FamilyNameやGivenNameも同時に判定して欲しいところですが、Ageを判定した時点で失敗のため、そこでテストは終了してしまうため、FamilyNameやGivienNameの実際の値を知ることは出来ません。

などなどの利点があるので、Chaining Assertionはお薦めです!この記事はSilverlight Unit Test Frameworkの紹介の体をとっていますが、実態はChaining Assertionの宣伝記事ですからね(キリッ

非同期テストをしてみる

Silverlightといったら非同期は避けて通れない。というわけで、Silverlight Unit Test Frameworkには非同期をテストできる機構が備わっています。[Asynchronous]というように、Asynchronous属性をつければそれだけでOK。と、思っていた時もありました。実際に試してみると全然違って、独特なシステムのうえにのっかっていて、かなり面倒くさかった……。

準備。まず、非同期テストをしたいクラスはSilverlightTestクラスを継承します。そしてAsynchronous属性をつけます。すると、そのテストメソッドはTestCompleteが呼ばれるか例外を検知するまでは、終了しなくなります。というわけで、こんな感じ。

[TestClass]
public class ToaruTest : SilverlightTest
{
    [TestMethod]
    [Asynchronous]
    public void AsyncTest()
    {
        var req = WebRequest.Create("http://www.google.co.jp/");
        req.BeginGetResponse(ar =>
        {
            try
            {
                req.EndGetResponse(ar)
                    .ResponseUri.ToString()
                    .Is("http://www.google.co.jp/");
            }
            catch (Exception ex)
            {
                EnqueueCallback(() => { throw ex; }); // 例外はテスト用スレッドに投げる必要がある
                return;
            }

            // ↓は定型句なので、EnqueueTestComplete(); という単純化されたのが用意されている
            EnqueueCallback(() => TestComplete()); // 何事もなければ終了でマーク
        }, null);
    }
}

このUnitTestの非同期は、独自のスレッドモデル(のようなもの)で動いていて、Dispatcherのようなキューにたいしてアクションを放り投げてあげる必要があります。別スレッドからUIスレッドは触れないように、「成功(TestComplete)」か「失敗(例外発生)」を伝えるには、EnqueueCallbackを経由しなければなりません。この辺はDispatcher.BeginInvokeするようなもの、と考えるといいかもしれません。

上のは少し原理に忠実にやりすぎた。まるごとEnqueueCallbackしてしまえばスレッドを意識する必要性は少しだけ減ります。

[TestMethod, Asynchronous]
public void AsyncTest()
{
    var req = WebRequest.Create("http://www.google.co.jp/404"); //404なので例外出してくれる
    req.BeginGetResponse(ar =>
    {
        EnqueueCallback(() => req.EndGetResponse(ar)
            .ResponseUri.ToString()
            .Is("http://www.google.co.jp/"));

        EnqueueTestComplete();
    }, null);
}

といっても、これは非常に単純なケースなだけであって、複雑なケースを書くとどんどん泣きたくなっていくでしょう……。一応、Enqueueには他にEnqueueConditionalという、条件式がtrueになるまで待機し続けるというものが用意されているので、若干制御はできなくもないんですが、あんまりできるとは言い難い仕組みがあります。詳しくは述べませんというか、別に使いやすいシステムじゃないのでどうでもいいです。

Rxを使ってみる

結果・もしくは例外を別のスレッドシステムに投げる。どこかで聞いたことあるような。ここでティンと来るのはReactive ExtensionsのObserveOnDispatcherです。Dispatcher.BeginInvokeのかわりにEnqueueCallback。丸っきりそっくり。なので、ObserveOnTestQueueのようなメソッドが作れれば、非常に使い勝手がいいんじゃないか。と思い浮かぶわけです。

と、浮かんだ人は実に素敵な発想力を持っていますね。浮かんだのは私じゃなくて海外の人です。はい。Writing asynchronous unit tests with Rx and the Silverlight Unit Testing Framework | Richard Szalayに、実装が書かれています。

そのRxによるScheduler実装を使うと(WP7版なのでSystem.ObservableとMicrosoft.Phone.Reactiveも参照してください)

[TestMethod, Asynchronous]
public void AsyncTest()
{
    var req = WebRequest.Create("http://www.google.co.jp/");
    Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse,req.EndGetResponse)()
        .ObserveOnTest(this)
        .Subscribe(r => 
            r.ResponseUri.ToString().Is("http://www.google.co.jp/"),
            () => TestComplete());
}

EnqueueCallbackの管理がなくなり、非常に簡単に記述できました。Rxのスケジューラのシステムの柔軟さの賜物ですね。これはRxの素晴らしい応用例だと本当に感動しました。Richard Szalayさんに乾杯。それと、私がこの記事を知ったのはInfoQ: Rx と Silverlight で非同期テストを記述するからなので、紹介したInfoQと、そして翻訳した勇 大地さんにも大変感謝します。

Silverlightの場合

Richard SzalayさんのコードはWP7のMicrosoft.Phone.Reactiveのためのものなので、Silverlight用Rxの場合はそのままでは動きません。はい。残念ながら、WP7版RxとDataCenter版Rxとでは、互換性がかなり崩壊しているので、そのまま動くことなんてないんです。悲しいですねえ……。これに関しては銀の光と藍い空: 「Rx と Silverlight で非同期テストを記述する」をWeb版にも使えるようにしたい!に書かれていますが、Silverlight用に移植してあげればよいようです。

既に、上記記事で田中さんが移植されているのですが、二番煎じに書いてみました(と、※欄で書いたものを流用です、毎回、流用させてもらっていてすみません……)

public static class TestHarnessSchedulerObservableExtensions
{
    public static IObservable<T> ObserveOnTestHarness<T>(this IObservable<T> source, WorkItemTest workItemTest)
    {
        return source.ObserveOn(new TestHarnessScheduler(workItemTest));
    }

    public static IDisposable RunAsyncTest<T>(this IObservable<T> source, WorkItemTest workItemTest, Action<T> assertion)
    {
        return source.ObserveOnTestHarness(workItemTest).Subscribe(assertion, () => workItemTest.TestComplete());
    }
}

public class TestHarnessScheduler : IScheduler, IDisposable
{
    readonly WorkItemTest workItemTest;
    readonly CompositeDisposable subscriptions;

    public TestHarnessScheduler(WorkItemTest workItemTest)
    {
        var completionSubscription =
            Observable.FromEventPattern<TestMethodCompletedEventArgs>(
                h => workItemTest.UnitTestHarness.TestMethodCompleted += h,
                h => workItemTest.UnitTestHarness.TestMethodCompleted -= h)
            .Take(1)
            .Subscribe(_ => Dispose());

        this.subscriptions = new CompositeDisposable(completionSubscription);
        this.workItemTest = workItemTest;
    }

    public void Dispose()
    {
        subscriptions.Dispose();
    }

    public DateTimeOffset Now
    {
        get { return DateTimeOffset.Now; }
    }

    public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
    {
        return Schedule(state, dueTime - Now, action);
    }

    public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
    {
        if (subscriptions.IsDisposed) return Disposable.Empty;

        workItemTest.EnqueueDelay(dueTime);
        return Schedule(state, action);
    }

    public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
    {
        if (subscriptions.IsDisposed) return Disposable.Empty;

        var cancelToken = new BooleanDisposable();

        workItemTest.EnqueueCallback(() =>
        {
            if (!cancelToken.IsDisposed) action(this, state);
        });

        subscriptions.Add(cancelToken);
        return Disposable.Create(() => subscriptions.Remove(cancelToken));
    }
}

Richard Szalayさんのコードが非常に素晴らしく、あらゆるケースへのキャンセルに対して完全に考慮されているという感じなので、そのまま持ってきました。実際のところ、テスト用なので「例外発生/TestCompleteが呼ばれる」で実行自体が終了してしまうわけなので、こうもギチギチに考えなくてもいいのではかなー、とか緩いことを思ってしまいますが、まあ、よく出来ているならよく出来ているままに使わさせてもらいます。

メソッド名は、ObserveOnTestHarnessに変更しました。ObserveOnTestだけだと何かイマイチかなー、と思いまして。それと、時間のスケジューリングは、NotSupportedではなくて、EnqueueDelayというのものがあるので、それを使うことにしてみました。それと、ObserveOn -> Subscribe -> onCompletedにTestCompleteが定形文句なので、それらをひとまとめにしたRunAsyncTestを追加。こんな風に書けます。

var req = WebRequest.Create("http://www.google.co.jp/444");
Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
    .RunAsyncTest(this, res => 
        res.ResponseUri.ToString().Is("http://www.google.co.jp/"));

定形文句が減る、つまりうっかりミスで書き忘れて死亡というのがなくなる、というのはいいことです。

通常のMSTestの場合

ところで、もしSilverlight/WP7固有の機能は使っていなくて、WPFでも利用出来るようなコードならば、コードをリンク共有の形でWPF側に持っていってしまって、そこでテスト実行してしまうと非常に楽です。まず第一に、MSTestやNUnitなどの通常のテストフレームワークが使えるため、Visual Studio統合やCIが簡単に行えます。第二に、非同期のテストが(Rxを使った場合)更に簡単になります。

[TestMethod]
public void AsyncTest()
{
    var req = WebRequest.Create("http://www.google.co.jp/");
    var result = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
        .First(); // First()で同期的に待機して値が取れる。複数の場合はToEnumerable().ToArray()で。

    result.ResponseUri.ToString().Is("http://www.google.co.jp/");
}

FirstやToEnumerable.ToArrayにより、同期的に待機することが出来るので、簡単にテストすることができます。通常のコードは同期的待機はすべきではないのですが、こうしたユニットテストの場合は便利に使えます。

じゃあSilverlightのユニットテストでも待機できるのはないか?というと、それはできません。理由はWindows Phone 7で同期APIを実現するたった つの冴えないやり方で書いたのですが、WebRequestなどのネットワーク問い合わせは、一度Dispatcherに積まれて、現在のメソッドを抜けた後に実行開始されるので、テスト実行スレッドで同期的に待って値を取り出すことは不可能なのです。

こういった細部の違いもあるので、コード共有してMSTestでチェックするのは楽でいいのですが、やはりSilverlight/WP7の実際の環境で動かされるユニットテストのほうも必要不可欠かなー、と。どこまでやるか、にもよりますが。

まとめ

Chaining Assertionは便利なので是非試してみてね!

なお、Rxを使うとTestScheduler(時間を好きなように進められる)やITestableObserver(通知の時間と値を記録できる)といった、イベント/非同期のテストを強力に支援する仕組みが備わっているので、それらと併用することで、より簡単に、もしくは今までは不可能だったことを記述できるようになります。それはまた後日そのうち。

SL/WP7のテストは、本当はIDE統合されてるといいんですけどねー。まあ、エミュレータ動かさなければならないので、しょうがないかな、というところもありますけれど。その辺も次期VisualStudioでは改善されるのかされないのか、怪しいところです。現在DeveloperPreviewで出ているVS11は、特に何も手をつけられてる感じがしないので、そのままな可能性はなきにしもあらず。どうなるかしらん。async/awaitが入ることだし、色々変わってくるとは思うんですけれど。

Chaining Assertion ver 1.6.0.0

Chaining Assertionというメソッドチェーンスタイルでユニットテストを書くことの出来るテスト用補助ライブラリをver.1.6に更新しました。内容はAssertEx.ThrowsContractExceptionの追加と、ラムダ式を使った判定の失敗時メッセージが親切になりました。

ThrowsContractException

まず、契約失敗でスローされる例外を厳密に検出することができるということについて。以前に基礎からのCode Contractsというスライドに書きましたが、Contract.Requires(など)で発生する、契約の条件に合っていない時にスローされる例外は、ContractExceptionというリライト時にアセンブリに埋め込まれる型のため、型を判別してのcatchは不可能です。

そのため、従来は大雑把にExceptionがスローされるか否か、でしか判定できませんでした。そこでThrowsContractExceptionを使うと、厳密に、契約失敗の例外のみを判定することができます。

// こんなContractなクラスがあるとして
public class QB
{
    public void Homu(string s)
    {
        Contract.Requires(s != null);
    }
}

// こういう風に契約違反の例外を捉えることができる
[TestMethod]
public void QBTest()
{
    AssertEx.ThrowsContractException(() =>
        new QB().Homu(null));
}

Code Contractsを使ったコードを書いている場合は、便利に使えるのではないでしょうかー。

ラムダ式によるアサーション

で、Chaining Assertionって、こんな感じに書けます。

// こんなクラスがあるとして
public class Person
{
    public int Age { get; set; }
    public string FamilyName { get; set; }
    public string GivenName { get; set; }
}

// こうして判定することが出来ます
[TestMethod]
public void PersonTest()
{
    // GetPersonメソッドでPersonインスタンスを取得するとして、
    // こんな風にメソッドチェーンで書ける(10歳以下でYamadaTarouであることをチェックしてます)
    new HogeService().GetPerson().Is(p =>
        p.Age <= 10 && p.FamilyName == "Yamada" && p.GivenName == "Tarou");
}

今回追加したのは、失敗した時のメッセージをより分かりやすくしました。

[TestMethod]
public void PersonTest()
{
    // こんなPersonがあるとすると
    var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" };
    // このアサーションは失敗します
    person.Is(p => p.Age <= 10 && p.FamilyName == "Yamada" && p.GivenName == "Tarou");
}

分かりやすいですよね!値は全部のダンプじゃなくて、ラムダ式の中で使われているプロパティ/フィールドのみを出すことにしているので、メッセージ欄が極度に爆発することもないです。今はまだ一階層の値しか出力してないのですが、いずれはもう少し複雑に解析して表示できるようにしたいところ。理想はGroovyのPowerAssertのようなグラフィカルな表示ですね。Expressionにより、データはあるので、解析をがんばれば作ること自体は可能だ、というのがC#のポテンシャルです。活かすか殺すかは、努力次第。まだ、活かしきれてはいません。

まとめ

MSでSilverlight周りのチームにいるJafar Husain氏(Silverlight Toolkitに入ってるという話で最初にRxを世界に紹介した人ですね!)は、unfold: Better Unit Tests with Test.Assert() for NUnit/VSTT/SUTFという記事で.NETはずっとパワフルなのに、いつまでJUnitスタイルの古いAPIを引きずってるんだ?と問題提起し、Expressionを解析して適切なAssertに差し替えるという、Queryable的な実装を示しました。Chaining Assertionでは、もっと野蛮に、拡張メソッドとラムダ式により、C#らしいスタイルで軽快に記述することを可能にしました。

最近少し刺され気味なので若干弁解しておきますが、別にスタイルは自由ですよ。でも他人に使わせるものは、より良いものであるべきだし、そうして他人が使ったりリファレンスとして参照されるものが、あんまりな出来だったら、そりゃ一言あって然りでしょう。本当に多くの人が参照するものだったら、なおのことです。いやまあ、度を超えた発言は刺されてもしょうがないですが。

NuGetからも入れられるのと、MSTestの他にNUnit, MbUnit, xUnit.NETにも対応しているので、試してもらえると嬉しいです。

Rxにおける並行非同期実行とリソース処理の問題

非同期(Asynchronous)だの並列(Parallel)だの並行(Concurerrent)だの、よくわからない単語が並びます。ParallelがやりたければPLINQ使うべし、と思うわけですがそれはさておき、Rxを使うと、意図しても意図しなくても、並行な状態にはなります。そして、その意図していないという状態は危うい線を踏んでいるので、きちんと認識しておく必要があります。また、危ういと同時に、Rxはその並行をうまくコントロールするメソッドが揃っているので、覚えておくと世界が一気に広がります。

例えば、こういう非同期メソッドがあるとして。

IObservable<T> AsyncModoki<T>(T value, int second)
{
    // second秒後にvalueを返す非同期処理をシミュレート
    return Observable.Return(value)
        .Delay(TimeSpan.FromSeconds(second));
}

static void Main(string[] args)
{
    // 1,2,3,4という入力をすぐに送り込む
    new[] { 1, 2, 3, 4 }
        .ToObservable()
        .SelectMany(x => AsyncModoki(x, 3))
        .Subscribe(x => Console.Write(x + "->"));

    Console.ReadLine();
}

Microsoft Silverlight を入手

1~4は、全て同時にリクエストが開始されます。だから、3秒後に同時に結果が表示されます。

同時に実行が開始されているということは、非同期の結果が完了する時間にズレがある場合、結果が前後することがあります。実際、上のものも何度か実行すると毎回結果が変わると思います(Delayは(デフォルトだと)値をThreadPoolに投げて遅延させます。ThreadPoolに入った時点で、順序の保証が消滅する)。というわけで、基本的にSelectManyを使った場合1:1で渡していくわけではなければ、順序は壊れると考えてください。さて、それだと困る場合もあるのではと思いますので、結果の順序を制御する方法が幾つかあります。

Switch

Switchは実に有意義なメソッドで、分かると、SelectMany以上に多用することが多くなるのではと思います。

clickEventObservable // クリック毎に
    .Select(x => AsyncModoki(x, 1)) // 何らかの非同期処理をするとする
    .Switch() // IObservable<IObservable<T>>の状態なので、Switch
    .Subscribe(Console.WriteLine);

Microsoft Silverlight を入手

クリックすると1秒遅延(非同期処理でもしていると考えてください)して、値が表示されます。しかし、1秒以内に次の値がクリックされた場合はキャンセルされ、表示しません。

つまり、最新の値だけを返すことを保証します。それ以前のものはキャンセル(Disposeが呼ばれる)されます。どういう時に使うかというと、例えばインクリメンタルサーチ。L, LI, LIN, LINQと入力が変わる度に非同期リクエストを発生させますが、欲しい結果は最後の一件のみで、次のキー入力があった場合は以前のものはキャンセルして欲しい。キャンセルはともかく、非同期実行だと結果が前後してしまうことだってあります。LINQと入力したのにLIの結果一覧が表示されてしまったら困る。そんな場合に、まさに、うってつけです。そして存外、こういったシチュエーションは多いのではないかと思われます。例えば私の以前作ったUtakotohaというWP7用歌詞表示アプリケーションも、曲のスキップに応じて最新のものだけを表示するために、Switchを利用しました。(コードが激しく酷いのと機能貧弱っぷりなので、そろそろ書き直したい)

Merge/Concat

Switch以外にも色々あります。

new[] { 1, 2, 3, 4 }
    .ToObservable()
    .SelectMany(x => AsyncModoki(x, 1)) // 全て並行実行(最初の例です)
    .Subscribe(x => Console.Write(x + "->"));

new[] { 1, 2, 3, 4 }
    .ToObservable()
    .Select(x => AsyncModoki(x, 1)) // IO<IO<T>>
    .Merge() // こちらも全て並行実行、SelectMany(xs => xs)と同じ
    .Subscribe(x => Console.Write(x + "->"));

new[] { 1, 2, 3, 4 }
    .ToObservable()
    .Select(x => AsyncModoki(x, 1))
    .Merge(2) // 2件ずつ並行実行する(並行実行数の指定が可能)
    .Subscribe(x => Console.Write(x + "->"));


new[] { 1, 2, 3, 4 }
    .ToObservable()
    .Select(x => AsyncModoki(x, 1))
    .Concat() // 1件ずつ実行する(Merge(1)と同じ)
    .Subscribe(x => Console.Write(x + "->"));

Microsoft Silverlight を入手

ネストはSelectManyで一気に崩してしまうケースが一般的でしょうけれど、IObservable<IObservable<T>>といったネストした状態にすると、選択肢がSwitchもそうですが、更に、MergeとConcatを選択することができます。ちなみに、このintで並行実行数が指定可能なMergeはWP7同梱版のRxには存在しません。残念。(もう一つ余談ですが、SelectManyはRx内部ではSelect(selector).Merge()という実装になっていたりします)

実行タイミングの問題

上のSilverlight、Merge2とMerge2Exの二つを用意しましたが、Merge2Exのほうは4つ同時に表示されるのが確認出来るはずです。コードはほぼ同一なのですが、AsyncModokiを似たようで別なものに差し替えました。

// Merge(2):Ex
new[] { 1, 2, 3, 4 }
    .ToObservable()
    .Select(x => AsyncModoki2(x, 1)) // これが差分
    .Merge(2)
    .Subscribe(x => Console.Write(x + "->"));

// スレッドプール上で非同期実行(結果は指定秒数後に返る)のシミュレート
// second秒後にネットワーク問い合わせが返る、的なものをイメージしてみてください
static IObservable<T> AsyncModoki2<T>(T value, int second)
{
    var subject = new AsyncSubject<T>();

    ThreadPool.QueueUserWorkItem(_ =>
    {
        Thread.Sleep(TimeSpan.FromSeconds(second)); // 指定秒数待機
        subject.OnNext(value);
        subject.OnCompleted(); // 完了(2つでワンセット)
    });

    return subject; // これ自体はすぐに返す(FromAsyncPatternの中身はこんな感じ)
}

このAsyncModoki2は、このメソッドを通ると即座にThreadPoolに送り込んで「実行」しています。Subscribeされるかどうかとは関係なく、Subscribeの「前に」。対してAsyncModokiはSubscribeされないと実行が開始されません。同じようで違う、この二つの状態をRxでは「Hot」と「Cold」と呼んで区別しています。HotはSubscribeとは関係なく動いているもの、イベントなんかはそうですね。ColdはSubscribeされて初めて動き出すもの、Observable.ReturnであったりRangeであったりと、Rxからの生成子の場合は、こちらのパターンが多いです。

実はFromAsyncPatternはHotなので、Subscribeとは関係なく即座に(といっても戻り値はFuncなのでInvokeしたら、ですが)非同期実行が開始されたりします。これは、あまり都合が良くなく(例えば上の例で見たように、MergeはSubscribeのタイミングによって実行数をコントロールしている)、Coldに変換したほうが扱いやすいです。そのためのメソッドがDefer。

static IObservable<WebResponse> AsyncModoki3<T>(WebRequest req)
{
    return Observable.Defer(()=>
        Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse,req.EndGetResponse)());
}

こちらのほうが、大抵の利用シーンにはマッチするかと思われます。

キャンセル時のリソース処理の問題

Switchは実に有意義なのですが、それの行っていることは、次の値を検知すると前の値をキャンセルする、ということです。普段はあまりキャンセルはしないと思うのですが、Switch内部では大量のキャンセルが発生しています。さて、どのような問題が発生するか、というと、例えば……。

using System;
using System.Net;
using System.Reactive.Linq;

class Program
{
    static void Main(string[] args)
    {
        // ネットワークの最大接続数。通常、デフォルトは2になっているはず。
        ServicePointManager.DefaultConnectionLimit = 2;

        // テキストボックスのTextChangedイベントをイメージした、インクリメンタルサーチで来る文字列群
        new[] { "w", "wi", "wik", "wiki", "wikip", "wikipe", "wikiped", "wikipedi", "wikipedia" }
            .ToObservable()
            .Select((word, id) =>
            {
                // wikipediaのAPIにリクエスト飛ばす
                var url = "http://en.wikipedia.org/w/api.php?action=opensearch&search=" + word + "&format=xml";
                var req = (HttpWebRequest)WebRequest.Create(url);
                req.UserAgent = "test";

                return Observable.FromAsyncPattern<WebResponse>((ac, state) =>
                    {
                        Console.WriteLine("ASYNC START:" + id);
                        return req.BeginGetResponse(ac, state);
                    }, ar =>
                    {
                        Console.WriteLine("ASYNC END:" + id);
                        return req.EndGetResponse(ar);
                    })()
                    .Select(res =>
                    {
                        using (res) // ここのセクションが呼ばれることはない
                        {
                            Console.WriteLine("CALLED NEXT:" + id);
                            return "response string:" + id;
                        }
                    });
            })
            .Switch()
            .ForEach(Console.WriteLine); // 終了を待機する形でのSubscribe
    }
}

// ConsoleApplication用のコードですが、是非実行してみてください。結果は以下のようになります。

ASYNC START:0
ASYNC START:1
ASYNC START:2
ASYNC START:3
ASYNC START:4
ASYNC START:5
ASYNC START:6
ASYNC START:7
ASYNC START:8
ASYNC END:0
ASYNC END:1
// そしてフリーズ...

これは、フリーズします。何故かというと、まず8件の非同期処理が一斉に開始されます(ASYNC STARTの表示)。一斉に開始はされますが、ネットワークの最大接続数は2なので、それ以外のものは内部的には待機されています。そして、Switchによる切り替えは最新のものだけを通すようにするため、7件はキャンセルされます。その後、最初の二件分のネットワークリクエストが終了し(ASYNC ENDの表示)、キャンセルされているためメソッドチェーンの続きであるSelectは呼ばれません。そして、フリーズ。

何故フリーズしてしまうかというと、EndGetResponseで取得した最初の二件のWebResponseが解放されていないためです。キャンセルが呼ばれなければ、Selectを通り、そこでusingにより利用+解放されるのですが、そのセクションを通らなければ何の意味がありません。使われることなく虚空に放り出されたWebResponseが、永遠にネットワーク接続を握ったままになってしまっています。

当然、大問題。

Switchを諦めてSelectMany(全件キャンセルせずに並行実行、どうせネットワーク自体の最大接続数で制限かかっているし)というのも手ではあります。大体の場合は結果は問題ないでしょう。けれど、Switchの利点は何でしたっけ、と。結果が前後しないことです。LINQを検索しようとしていたのに、検索結果が前後したせいでLINQ→LINの順番に結果が得られた結果、表示されるのがLINの結果では困ってしまいます。Switchなら、後に実行したものが必ず最後に来ると保証されるので、そのようなことにはなりません。反面、SelectManyは並行実行のため、前後する可能性が出てきます。Switchはこの例で挙げたような、インクリメンタルサーチのようなものと相性がとても良いんですね。

ではどうするか?

WebResponseのDispose(Close)を呼べれば解決するので、FromAsyncPatternのEnd部分に少し細工を加えてやる、ということが考えられます。

// こんなFromAsyncPatternを用意して
public static IObservable<TResult> SafeFromAsyncPattern<TResult>(Func<AsyncCallback, object, IAsyncResult> begin, Func<IAsyncResult, TResult> end)
    where TResult : IDisposable
{
    // WP7版ではCreateWithDisposableで(この辺の細かな差異が割とウザい)
    return Observable.Create<TResult>(observer =>
    {
        var disposable = new BooleanDisposable();

        Observable.FromAsyncPattern<TResult>(begin, ar =>
        {
            var result = end(ar);
            if (disposable.IsDisposed) result.Dispose(); // キャンセルされてたらDispose
            return result;
        })().Subscribe(observer);

        return disposable; // Disposeが呼ばれるとIsDisposedがtrueになる
    });
}

// こんな風に使うとか
public static IObservable<WebResponse> GetResponseAsObservable(this WebRequest req)
{
    return ObservableEx.SafeFromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse);
}

これにより、キャンセルされたかどうかをEnd部分で判定することが出来ます。よってEnd時にキャンセルされていたらリソースをDisposeしてしまう(ここでreturnしたオブジェクトは、チェーンは切れているので別に使われることなく虚空を彷徨うだけ)。これにより、FromAsyncPatternがリソースを返し、かつ、いつキャンセルされても問題なくなります。

他にも色々なアプローチが考えられます。CompositeDisposable/MutableDisposable/SingleAssignDisposableなどを使い、Disposeが呼ばれたら同時に管理下のリソースをDisposeしてしまう、といった手法。これは、リソースのDisposeされる瞬間が逆にコントロールしにくくなって、例えばWebResponseですと、その後のStreamを呼んでる最中にWebResponseがDisposeされてしまうなどの自体も起こりうるので、少し厄介に思えました。。リソースを後続に渡すまでは責任を持つ。それ以降はノータッチなので好きにやらせる、利用も解放も後続側が責任を。その方が自然だし、素直な動きになるので、いいかな。

他には、キャンセルを伝搬しないようなメソッドを作り、Disposeが呼ばれてもリソースを受け取れるようにし、後続でリソースをDisposeする、などの手段も考えられます。そうすればSafeFromAsyncPatternなどといった、独自のFromAsyncPatternを作る必要はなく、全てに適用できて汎用性は高いのですが、チェーンでの保証が途切れてしまうのが若干微妙かな、と……。この辺は悩ましいところです。

そもそもWebRequestなら、DisposeでAbortしてしまったほうが、キャンセルらしくていいかもしれない。

public static IObservable<WebResponse> GetResponseAsObservable(this WebRequest request)
{
    return Observable.Create<WebResponse>(observer =>
    {
        Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse,
            ar =>
            {
                try
                {
                    return request.EndGetResponse(ar); // Abort後の場合は例外発生
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.RequestCanceled) return null;
                    throw; // キャンセル時以外は再スロー
                }
            })()
            .Subscribe(observer);
        return () => request.Abort(); // Dispose時にこのActionが呼ばれる
    });
}

Disposeが呼ばれるとwebRequest.Abortが呼ばれます。その後にEndGetResponseを呼ぶとRequestCanceledなWebExceptionが発生するので、キャンセルされていたならnullを(どちらにせよ、Dispose済みなので、ここでreturnしたものは次のメソッドチェーンで使われることはない)、そうでない例外ならば再スローを、という方針です。悪くなさそうですが、どうでしょうか。私的にはこれを採用するのがベストかなー、と考え中です。

まとめ

SwitchやMergeなどで、従来扱いにくかった並行処理時の非同期のコントロールが簡単になりました。単純に一本の非同期をSelectManyで摩り替えるだけもアリですけれど、せっかくの多機能なのだから、並行にリクエストなどを飛ばして、より速いアプリケーション作りを目指してもいいかもしれません。同期リクエストをTask.Factory.StartNewで包んで振り回すよりかは、ずっと楽です。また、現在行われているMSのイベントBUILDで発表されたWinRTなどは、完全に非同期主体です。C#5.0でasync/awaitが入り、非同期がより扱いやすくなることで、それに併せてModelの有り様も、同期から非同期へと変わっていき、それにあわせてVMなどの書き方も変わってくるのではかと思われます。

ただ、リソースの問題にだけは気をつけて!上で挙げた問題は、本質的にはFromAsyncPatternに限らず、リソース処理が引き離されている場合の全てで該当します。リソースを扱うのは難しい。とはいえ、全面的に問題になるのは、このFromAsyncPatternぐらいな気はします。Observable.Usingなども用意されているので、不用意にリソースをチェーン間で渡したりしなければ原則的には起こらない。けれど、そのFromAsyncPatternこそがリソースを扱うシチュエーションで最も使われるものなんですよね、とほほほ。

キャンセル(Dispose)を不用意に呼ばなければ問題は起こらないといえば起こらないんですが(そのため、不適切に書いてしまっていても、多くのケースで問題が表面化することはないでしょう)、Switchのようなアプローチが取れなくなるのがどうにも。現状だと、とりあえず気をつけましょう、としか言いようがないので、気をつけましょう。もし何かうまい具合に動かないなあ、と思ったら、この辺を疑ってみると良いかもしれません。

その辺難しいなあ、という場合は、近いうちに私の出すRx拡張ライブラリを使いましょう。特に考えなくても済むよう、色々配慮してあります。いつ出るの?というと、はい、最近ゴリゴリと書いてますんで(ブログがちょっと放置気味だった程度には)、必ず近いうちに出します。

Re:FromEvent vs FromEventPattern

現在の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#の壁を縮める!とかなんとかかんとか。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive