前にも書いた話です
C# のオブジェクト初期化子で readonly なデータを設定したい

C# ではオブジェクト初期化子で↓のように new するときにプロパティも一緒に見やすく設定できます

var value = new Class1
{
prop1 = 1,
prop2 = 2,
};

ですが 初期化時にしか使えないのに 後からプロパティ設定しているのと一緒の動きになります

それが原因で

  • private は設定できない
  • const プロパティにできない

という使いづらさがあります


メソッドの実行で内部状態としてプロパティの値がどんどん変わっていくオブジェクトならともかく ただの値として変更するつもりのない場合は const な値にしておきたいものです

前回は コンストラクタ側で頑張って似た書き方をする方法にしましたが 今回はオブジェクト初期化子はそのまま使って const にしてみました

namespace Project1
{
    public class Program
    {
        /// <summary>
        /// エントリポイント
        /// </summary>
        public static void Main()
        {
            var p = new Point
            {
                x = 100,
                y = 200,
            };
            p.x = 1; // exception
        }
    }
    public abstract class ConstPropertyBase
    {
        protected Dictionary<string, object> property_entities = new Dictionary<string, object>();
        protected T getConstProperty<T>(string name)
        {
            if (this.property_entities.ContainsKey(name))
            {
                return (T)this.property_entities[name];
            }
            else
            {
                throw new Exception($"'{name}' is not initialized.");
            }
        }
        protected void setConstProperty<T>(string name, T value)
        {
            if (this.property_entities.ContainsKey(name))
            {
                throw new Exception($"Cannot assign to constant variable: '{name}'.");
            }
            else
            {
                this.property_entities[name] = value;
            }
        }
    }
    public class Point : ConstPropertyBase
    {
        public int x
        {
            get { return this.getConstProperty<int>(nameof(x)); }
            set { this.setConstProperty(nameof(x), value); }
        }
        public int y
        {
            get { return this.getConstProperty<int>(nameof(y)); }
            set { this.setConstProperty(nameof(y), value); }
        }
    }
}


プロパティの getter/setter を使って一度値を設定すると 変更しようとしたときにエラーを出すようにしています
readonly や const や {get;} を使うのではなく自分で const となるような処理を作っています


C# は trait や 2 つのクラスを継承する機能はないので ConstPropertyBase を継承しないといけなくて他に継承したいクラスがあると困ります

なので 外部の static メソッドとして呼び出すパターンも作ってみました

    public class ConstPropertyManager
    {
        static private ConditionalWeakTable<object, Dictionary<string, object>> data
            = new ConditionalWeakTable<object, Dictionary<string, object>>();
        static public T get<T>(object _this, string name)
        {
            var dict = data.GetValue(_this, key => new Dictionary<string, object>());
            if (dict.ContainsKey(name))
            {
                return (T)dict[name];
            }
            else
            {
                throw new Exception($"'{name}' is not initialized.");
            }
        }
        static public void set<T>(object _this, string name, T value)
        {
            var dict = data.GetValue(_this, key => new Dictionary<string, object>());
            if (dict.ContainsKey(name))
            {
                throw new Exception($"Cannot assign to constant variable: '{name}'.");
            }
            else
            {
                dict[name] = value;
            }
        }
    }
    public class Point
    {
        public int x
        {
            get { return ConstPropertyManager.get<int>(this, nameof(x)); }
            set { ConstPropertyManager.set(this, nameof(x), value); }
        }
        public int y
        {
            get { return ConstPropertyManager.get<int>(this, nameof(y)); }
            set { ConstPropertyManager.set(this, nameof(y), value); }
        }
    }


getter/setter が自動プロパティにできず定義する側では行数は増えるものの 使う側は普通にオブジェクト初期化子で使えばいいだけなのでまぁまぁ満足できる使いやすさです


唯一の問題は初期値を設定できないこと

最初に値を入れると変更できなくしています
なので コンストラクタ等で初期値を入れることはできません
先に入れてしまうと初期化子での代入でエラーです
逆にあとで入れようにも 初期化子での代入はコンストラクタのようなものが呼ばれるわけではないので自動的に行うことができません

var value = new Class1
{
prop1 = 100,
prop2 = 200,
}.init();

のようにメソッドを呼び出して そこで初期化されてないところにデフォルト値入れることはできますが 使う側で追加の処理必要になるのであんまりやりたくない方法です

const にしたい場合ってデフォルト初期値を使わないことが多そうなのでとりあえず初期値問題は見なかったことにしておきます

一番いいのは 言語レベルで初期化子がコンストラクタ扱いの動きしてくれることなんだけどなー