C# Immutableオブジェクト 導出項目のキャッシュ機能

例として下記のようなVector2クラスを考えます。

Lengthプロパティは、呼び出される毎に計算を行っています。

処理が軽い場合は特に問題ありませんが、導出に時間がかかるプロパティは結果をキャッシュしてあるほうが好ましいです。

今回はキャッシュを行う機能を実装してみたいと思います。

public class Vector2
{
    public int X { get; }
    public int Y { get; }
    public double Length => Math.Sqrt(X * X + Y * Y);
    public Vector2(int x, int y)
    {
        X = x;
        Y = y;
    }
}


Cache構造体

簡単のため、入力はValueTupleを利用します。(ValueTupleを利用しない場合は、TArgsを増やした複数のCache構造体を定義してください。)

入力に変化がない場合は、前回の値を返すように実装しています。

構造体である理由は、Immutableクラスが変更・複製されたときに、キャッシュを共有してしまわないようにするためです。

struct Cache<TArgs, TResult>
{
    bool _initialized;
    TArgs _args;
    TResult _result;
    Func<TArgs, TResult> _f;
    public Cache(Func<TArgs, TResult> f)
    {
        _initialized = false;
        _args = default(TArgs);
        _result = default(TResult);
        _f = f;
    }
    public TResult Get(TArgs args)
    {
        if (_initialized && args.Equals(_args))
            return _result;
        _initialized = true;
        _args = args;
        _result = _f(args);
        return _result;
    }
}


Vector2書換え

新たにキャッシュのフィールドを追加し、LengthプロパティではGetを呼び出しています。

public class Vector2
{
    public int X { get; }
    public int Y { get; }
    Cache<(int, int), double> _length = new Cache<(int x, int y), double>(args =>
        Math.Sqrt(args.x * args.x + args.y * args.y));
    public double Length => _length.Get((X, Y));
    public Vector2(int x, int y)
    {
        X = x;
        Y = y;
    }
}


動作確認

同じオブジェクトに対し、二回目以降のLength呼出ではキャッシュが返るようになります。

Withで複製した場合でも問題なく動作していることも確認できます。(Withに関しましては、下記記事を参照してください。)
ネストしたImmutableオブジェクトの更新(C#) - 何でもプログラミング

Vector2 v = new Vector2(1, 1);
v.Length;
v.Length;  // キャッシュが返る

Vector2 v2 = v.With(x => x.Y, 2);
v.Length;  // キャッシュが返る
v2.Length;
v2.Length; // キャッシュが返る