【C#】R3でスレッドセーフな値変更通知プロパティを実装する

OSSを探してみたのですが (1)プロパティを排他制御で保護しつつ、(2)変更したら通知を緩く出す、のような条件に一致するいい感じのが見つからなかったので自分で作ってみました。

確認環境

  • Visual Studio 2026
  • .NET 8
  • Windows11(マルチプラットフォームは未考慮)
  • OSS: R3 (1.3.1)

実装例

IObservableLockingProperty<T>

実装内容は環境によって色々バリエーションが発生する可能性があると思うので念のためインターフェースを作成しました。

UI スレッドの操作が入るような Subscribe をされるがプロパティの実装内で吸収してくれとか、例外が出たら値を元に戻すとか、通知順序も保証してくれとか、状況によって変わると思いますが統一して扱えるようにインターフェースを作成してます。

using R3;

/// <summary>
/// 単一のプロパティをスレッドセーフに管理し値の変更をObservableで通知する機能を提供します。
/// </summary>
public interface IObservableLockedProperty<T> : IDisposable
{
    /// <summary>
    /// <see cref="Value"/> が変更されたときに発火する <see cref="Observable{T}"/> を取得します。
    /// </summary>
    Observable<T> ValueChanged { get; }

    /// <summary>
    /// 現在保持している値を取得または設定します。
    /// </summary>
    T Value { get; set; }
}

ObservableSynchronizedProperty<T>

以下が実装です。

注意点として、プロパティへの読み書きはスレッドセーフに保護されますが、競合が激しいと通知順が順不同になるかも?

using R3;

public sealed class ObservableLockedProperty<T> : IObservableLockedProperty<T>
{
    // ----- Fields -----

    readonly Subject<T> _subject = new();
    readonly Observable<T> _observable;
    readonly object _lockObj = new();

    T _value;

    // 破棄されたかどうか
    // true: 破棄済み / false: まだ
    bool _isDisposed;

    // ----- Properties -----

    public Observable<T> ValueChanged => _observable;

    public T Value
    {
        // プロパティの読み書きはスレッドセーフで行う
        get { lock (_lockObj) { return _value; } }
        set
        {
            lock (_lockObj)
            {
                ObjectDisposedException.ThrowIf(_isDisposed, this);
                if (EqualityComparer<T>.Default.Equals(_value, value))
                {
                    return;
                }
                _value = value;
            }
            // 通知はロックの外で実行する -> もしかしたらset順じゃないかも
            _subject.OnNext(value);
        }
    }

    // ----- Constructors -----

    public ObservableLockedProperty() : this(default!)
    {
        // nop
    }

    public ObservableLockedProperty(T value)
    {
        _value = value;
        _observable = _subject.AsObservable();
    }

    // ----- Public Methods -----

    public void Dispose()
    {
        lock (_lockObj)
        {
            if (_isDisposed)
            {
                return;
            }
            _isDisposed = true;
        }
        _subject.OnCompleted();
        _subject.Dispose();
    }
}

使い方

まず以下のようなクラスをサンプルとして作成します。

public sealed class Example : IDisposable
{
    public ObservableLockedProperty<string> Name { get; } = new();

    public ObservableLockedProperty<int> ID { get; } = new();

    public void Dispose()
    {
        Name.Dispose(); // 必ず破棄する
        ID.Dispose();
    }
}

利用するときは以下のように実装します。

プロパティ

static void Main(string[] args)
{
    // クラスを作成して通知を受け取りたいプロパティをSubscribeする
    using Example example = new();
    using IDisposable d1 = 
        example.Name.ValueChanged.Subscribe(str => Console.WriteLine($"Name: {str}"));
    using IDisposable d2 = 
        example.ID.ValueChanged.Subscribe(str => Console.WriteLine($"ID: {str}"));
    
    // プロパティを変更するとイベントが呼ばれてコンソールに表示される
    example.Name.Value = "Takap";
    example.ID.Value = 100;
    // >Name: Takap
    // >ID: 100
}