かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Reactive Extensions再入門 その12「Finallyメソッドとリソース解放」

はじめに

前回は、例外処理のtry catchと同じ要領で使えるCatchメソッドを紹介しました。今回はtry catchときたらfinallyということでFinallyメソッドの紹介を行います。

Finallyメソッド

Finallyメソッドは名前の通り例外処理のfinally句と同じ動きをします。Reactive ExtensionsのIObservableを起点とする一連のシーケンスの処理が終わったタイミングで呼び出される処理を記述することが出来ます。引数にはActionを渡すだけのシンプルなシグネチャです。

IObservable<TSource> Finally(Action action)

まずは、正常にシーケンスの処理が終わる場合のコード例を下記に示します。

var source = new Subject<string>();
source
    // 終了時に必ず呼ばれる処理
    .Finally(() => Console.WriteLine("Finally"))
    // 購読
    .Subscribe(
        s => Console.WriteLine("OnNext {0}", s),
        ex => Console.WriteLine("OnError {0}", ex),
        () => Console.WriteLine("OnCompleted"));

// 2つ値を発行したあと、終了する。
source.OnNext("A");
source.OnNext("B");
source.OnCompleted();

実行結果を下記に示します。

OnNext A
OnNext B
OnCompleted
Finally

Finallyが、全ての処理の最後に呼ばれていることが確認出来ます。次に、Catchメソッドを使用して例外を処理したケースのコードを下記に示します。

var source = new Subject<string>();
source
    // sourceから例外が発生したらErrorという文字列を後続へ流す
    .Catch(Observable.Return("Error"))
    // 終了時に必ず呼ばれる処理
    .Finally(() => Console.WriteLine("Finally"))
    // 購読
    .Subscribe(
        s => Console.WriteLine("OnNext {0}", s),
        ex => Console.WriteLine("OnError {0}", ex),
        () => Console.WriteLine("OnCompleted"));

// 2つ値を発行したあと、例外を発生させる。
source.OnNext("A");
source.OnNext("B");
source.OnError(new Exception("例外"));

実行結果を下記に示します。

OnNext A
OnNext B
OnNext Error
OnCompleted
Finally

この場合もCatchメソッドでエラーから復帰しているため、OnCompletedの後にFinallyが呼ばれていることが確認できます。次に、Catchメソッドを使用せずにSubscribeのOnErrorで例外処理をした場合の動作確認をするコードを下記に示します。

var source = new Subject<string>();
source
    // 終了時に必ず呼ばれる処理
    .Finally(() => Console.WriteLine("Finally"))
    // 購読
    .Subscribe(
        s => Console.WriteLine("OnNext {0}", s),
        ex => Console.WriteLine("OnError {0}", ex),
        () => Console.WriteLine("OnCompleted"));

// 2つ値を発行したあと、例外を発生させる。
source.OnNext("A");
source.OnNext("B");
source.OnError(new Exception("例外"));

実行結果を下記に示します。

OnNext A
OnNext B
OnError System.Exception: 例外
Finally

この場合も、OnErrorなどのSubscribeで行われる処理の後にFinallyで指定した処理が実行されていることがわかります。最後に、OnErrorでもCatchメソッドでも例外を処理しなかった場合の動作を確認するコードを下記に示します。

var source = new Subject<string>();
var subscriber = source
    // 終了時に必ず呼ばれる処理
    .Finally(() => Console.WriteLine("Finally"))
    // 購読(エラー処理無し)
    .Subscribe(
        s => Console.WriteLine("OnNext {0}", s));

// 2つ値を発行したあと、例外を発生させる。
source.OnNext("A");
source.OnNext("B");
try
{
    // Reactive Extensionsのシーケンスの処理内で例外が処理されないため例外がスローされる
    source.OnError(new Exception("例外"));
}
catch
{
    // 例外が発生した場合はログを出して握りつぶす
    Console.WriteLine("catch句");
}
// 購読を解除する
Console.WriteLine("subscriber.Dispose()");
subscriber.Dispose();

今回は、OnErrorでもCatchメソッドでも例外を処理していないためsource.OnError(new Exception(“例外”))の部分で例外が発生します。そのためtry catchで例外処理を記載しています。また、例外処理のあとで、SubscribeしたものをDisposeして購読を解除しています。このコードの実行結果を下記に示します。

OnNext A
OnNext B
catch句
subscriber.Dispose()
Finally

上記の実行結果からわかるように、未処理の例外がある場合は、何もしないとFinallyメソッドで渡したアクションが実行されません。明示的にコードでDisposeをすることでFinallyで渡したアクションが実行されます。

Observable.Usingメソッド

Finallyメソッドでリソースの解放を行う処理を記述している場合にちらっと頭をよぎってほしいメソッドがあります。Observable.Usingメソッドです。Usingメソッドは、名前の通りusingブロックのようにリソースを確実に解放するために使用するメソッドになります。メソッドのシグネチャは、第一引数にFuncを渡して、リソースを確保するオブジェクトを作成する処理を指定します。TResource型はIDisposableを実装している必要があります。第二引数に、Func>を渡して、リソースを使ってIObservableを取得する処理を指定します。実際には、外部リソースに依存するものを使用する場合のコード例が適しているのですが、ここでのサンプルは、ダミーのIDisposableを実装した下記のようなクラスを使用して動作確認を行います。

// サンプルのダミーリソースクラス
class SampleResource : IDisposable
{
    // データを取得する
    public IObservable<string> GetData()
    {
        return Observable.Create<string>(o =>
        {
            o.OnNext("one");
            o.OnNext("two");
            o.OnNext("three");
            o.OnCompleted();
            return Disposable.Empty;
        });
    }

    // 解放処理
    public void Dispose()
    {
        Console.WriteLine("Resource.Dispose called");
    }
}

このクラスを使用してUsingメソッドの動作確認を行います。Usingメソッドの使用例を下記に示します。

// SampleResource(IDisposableを実装)を使用してデータを取得する
var source = Observable.Using(
    () => new SampleResource(),
    sr => sr.GetData());
// 購読
source.Subscribe(
    i => Console.WriteLine("OnNext({0})", i),
    ex => Console.WriteLine("OnError({0})", ex.Message),
    () => Console.WriteLine("Completed()"));

UsingメソッドでSampleResourceクラスの作成と、SampleResourceクラスを使ってIObservableを取得しています。このコードの実行結果を下記に示します。

OnNext(one)
OnNext(two)
OnNext(three)
Completed()
Resource.Dispose called

実行結果からもわかるように、最後に、SampleResourceクラスのDisposeが呼ばれていることがわかります。Usingメソッドを使用できる場合はFinallyよりもこちらを使った方が漏れが無くて良いと思います。