xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Rx入門 (28) – Disposables名前空間まとめ

Rx入門を書いてから早2年。当時はVersion 1だったRxも今ではVersion 2。考え方は変わらないものの、いくつかObsoleteになってしまった機能もあり、参考にして頂いている方には多少なり申し訳なさがあります...。あれからC#自体にも言語機能として非同期処理 (async/await) が搭載され、Rxの出番は書いた当初よりは少なくなったと思われますが、もともとの思想が違えば用途も違うということで棲み分けは十分に可能です。なのでまだまだ学習する価値はあります!最近でもSignalRをRxで楽しようとReactiveSignalRを作ったりしましたし、やっぱりRxは便利です。

RxやSignalRは接続状態をIDisposableなオブジェクトに持たせ、Disposeメソッドで接続を解除するという動きをします。このIDisposableなオブジェクトをどう扱うかはアプリケーション仕様ですが、様々なバリエーションがあると思います。この扱いを少しでも楽にするため、System.Reactive.Disposables名前空間に補助機能が用意されています。だいぶ今さらですが、今一度これらの挙動を確認してみました。

前準備

Dispose処理の挙動を調べるため、Disposeされたときにコンソール出力するだけの単純なクラスを用意しておきます。

class DisposableObject : IDisposable
{
    private readonly string name = null;

    public DisposableObject(string name)
    {
        this.name = name;
    }

    public void Dispose()
    {
        Console.WriteLine("{0} - Disposed", this.name);
    }
}

BooleanDisposableクラス

BooleanDisposableは非常に単純で、DisposeしたらIsDisposedのフラグを立てるだけです。

var d = new BooleanDisposable();
Console.WriteLine(d.IsDisposed);
d.Dispose();  //--- Disposeしたらフラグ立てるだけ
Console.WriteLine(d.IsDisposed);

/*
False
True
*/

CancellationDisposableクラス

CancellationDisposableCancellationTokenSourceをラップしただけのもので、Dispose時にCancelメソッドが呼び出されます。IsDisposedプロパティはIsCancellationRequestedを返します。

var source = new CancellationTokenSource();
source.Token.Register(() => Console.WriteLine("Cancelled"));
var d = new CancellationDisposable(source);
Console.WriteLine(d.IsDisposed);
d.Dispose();
Console.WriteLine(d.IsDisposed);

/*
False
Cancelled
True
*/

CompositeDisposableクラス

CompositeDisposableはIDisposableなオブジェクトをグループ化し、まとめてDisposeを呼び出します。追加はコンストラクタ、もしくはAddメソッドで行います。Clearメソッドは内部に格納しているIDisposableオブジェクトをDisposeしつつコレクションから削除します。DisposeメソッドもClearメソッドと同様の動きをしますが、こちらは1度しか呼び出せません。一旦Disposeメソッドを呼び出すと、その後にAddメソッドで追加しようとしても即座にDisposeされます。

var d = new CompositeDisposable();
d.Add(new DisposableObject("#1"));
d.Add(new DisposableObject("#2"));
d.Add(new DisposableObject("#3"));
d.Clear();    //--- コレクションから削除するときにDisposeが呼び出される
Console.WriteLine("----------");
d.Add(new DisposableObject("#4"));
d.Add(new DisposableObject("#5"));
d.Add(new DisposableObject("#6"));
d.Dispose();  //--- CompositeDisposable自体もDisposeされる
Console.WriteLine("----------");
d.Add(new DisposableObject("#7"));  //--- Dispose後に追加すると即座にDisposeが呼び出される

/*
#1 - Disposed
#2 - Disposed
#3 - Disposed
----------
#4 - Disposed
#5 - Disposed
#6 - Disposed
----------
#7 - Disposed
*/

ContextDisposableクラス

ContextDisposableは、コンストラクタで指定されたSynchronizationContext上で指定されたIDisposableオブジェクトのDisposeメソッドを呼び出します。SynchronizationContextには同期的にメッセージを送信するSendメソッドと非同期的に送信するPostメソッドがありますが、Dispose時に利用されるのはPostメソッドです。同期コンテキストの対象となるスレッドが暇になったときに実行される、ということだけ押さえておきましょう。

var context = SynchronizationContext.Current;
var obj     = new DisposableObject("#1");
var d       = new ContextDisposable(context, obj);
d.Dispose();  //--- 指定されたContext上で非同期的にDisposeを実行

/*
#1 - Disposed
*/

ScheduledDisposableクラス

ScheduledDisposableは先のContextDisposableと似ていて、コンストラクタで与えられたIScheduler上で指定されたIDisposableオブジェクトのDisposeメソッドを呼び出します。

var scheduler = ThreadPoolScheduler.Instance;
var obj       = new DisposableObject("#1");
var d         = new ScheduledDisposable(scheduler, obj);
d.Dispose();

/*
#1 - Disposed
*/

SingleAssignmentDisposableクラス

SingleAssignmentDisposableはDisposableプロパティにIDisposableオブジェクトを後付けで指定します。このset操作は一度しかできません。DisposableプロパティにすでにIDisposableオブジェクトが指定されている (=すでに一度setしたことがある) 状態で別のインスタンスを設定しようとすると「すでに設定されてるよ」の旨の例外が発生します。また、SingleAssignmentDisposable自体がすでにDisposeされている状態でDisposableプロパティにインスタンスを設定しようとすると、即座にDisposeが呼び出されます。

var d        = new SingleAssignmentDisposable();
d.Disposable = new DisposableObject("#1");
try
{
    d.Disposable = new DisposableObject("#2");  //--- 例外発生
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
}
d.Dispose();
d.Disposable = new DisposableObject("#3");

/*
Disposable has already been assigned.
#1 - Disposed
#3 - Disposed
*/

MultipleAssignmentDisposableクラス

MultipleAssignmentDisposableSingleAssignmentDisposableと同様、DisposableプロパティにIDisposableオブジェクトを後付けで指定します。SingleAssignmentDisposableと異なるところは、Disposableプロパティへのset操作を複数回行えることです。DisposableプロパティにすでにインスタンスAが設定されている状態で別のインスタンスBを設定すると、AのDisposeは呼び出されずBに差し替えられます。また、MultipleAssignmentDisposable自体がすでにDisposeされている状態での挙動は同様です。

var d        = new MultipleAssignmentDisposable();
d.Disposable = new DisposableObject("#1");
d.Disposable = new DisposableObject("#2");
d.Dispose();
d.Disposable = new DisposableObject("#3");

/*
#2 - Disposed
#3 - Disposed
*/

SerialDisposableクラス

SerialDisposableもDisposableにプロパティにインスタンスを後付けで設定します。MultipleAssignmentDisposableは先に設定されていたインスタンスに手を加えることなくインスタンスの差し替えを行いますが、SerialDisposableは先に設定されていたインスタンスをDisposeしてから差し替えを行います。また、SerialDisposable自体がDisposeされている状態での挙動は同様です。

var d = new SerialDisposable();
Console.WriteLine("#1 - Set");
d.Disposable = new DisposableObject("#1");
Console.WriteLine("#2 - Set");
d.Disposable = new DisposableObject("#2");
Console.WriteLine("Dispose");
d.Dispose();
Console.WriteLine("#3 - Set");
d.Disposable = new DisposableObject("#3");

/*
#1 - Set
#2 - Set
#1 - Disposed
Dispose
#2 - Disposed
#3 - Set
#3 - Disposed
*/

RefCountDisposableクラス

RefCountDisposableはコンストラクタで指定されたインスタンスを参照カウントで管理します。参照カウントの初期値は0で、GetDisposableメソッドでインスタンスを取得する度に参照カウントをインクリメント (+1) します。GetDisposableメソッドで取得したインスタンスをDisposeすることで参照カウントをデクリメント (-1) します。RefCountDisposableをDisposeする際に参照カウントが0であれば、コンストラクタで指定したインスタンスを同時にDisposeします。

var d    = new RefCountDisposable(new DisposableObject("#1"));
var ref1 = d.GetDisposable();  //--- 参照カウントのインクリメント [Count = 1]
var ref2 = d.GetDisposable();  //--- 参照カウントのインクリメント [Count = 2]
Console.WriteLine("ref2 - Dispose");
ref2.Dispose();                //--- 参照カウントのデクリメント [Count = 1]
Console.WriteLine("ref1 - Dispose");
ref1.Dispose();                //--- 参照カウントのデクリメント [Count = 0]
Console.WriteLine("Dispose");
d.Dispose();                   //--- RefCountDisposable本体のDispose

/*
ref2 - Dispose
ref1 - Dispose
Dispose
#1 - Disposed
*/

参照カウントが0でない状態で先にRefCountDisposable自体をDisposeした場合、参照カウントが0になったタイミングでコンストラクタで設定したインスタンスも自動的にDisposeされます。

var d    = new RefCountDisposable(new DisposableObject("#1"));
var ref1 = d.GetDisposable();  //--- 参照カウントのインクリメント [Count = 1]
var ref2 = d.GetDisposable();  //--- 参照カウントのインクリメント [Count = 2]
Console.WriteLine("Dispose");
d.Dispose();                   //--- 参照カウントが0になったときにDisposableObjectをDisposeするようにマーク
Console.WriteLine("ref2 - Dispose");
ref2.Dispose();                //--- 参照カウントのデクリメント [Count = 1]
Console.WriteLine("ref1 - Dispose");
ref1.Dispose();                //--- 参照カウントのデクリメント [Count = 0] (DisposableObjectも同時にDisposeされる)

/*
Dispose
ref2 - Dispose
ref1 - Dispose
#1 - Disposed
*/

Disposable.Emptyプロパティ

Dispose時に何もしないインスタンスを取得します。以下のようなものが取得されるということです。

var d = Disposable.Empty;
d.Dispose();  //--- 何も起こらない

Disposable.Createメソッド

Dispose時に実行する処理をデリゲートとして与え、インスタンスを生成します。よくあるAnonymousDisposableで、継承したクラスを作る必要がないので大変重宝します。

var d = Disposable.Create(() =>
{
    Console.WriteLine("Disposed");
});
d.Dispose();

/*
Disposed
*/