コレクション初期化子でオブジェクト初期化

オブジェクト初期化子とコレクション初期化子は便利なんだが、同時に使えないという欠点がある。文法的には書けてもよいと思うのだけど、なんらかの問題があるのだろう。*1

オブジェクト初期化子は、コード上にプロパティ設定を行うためだけの短命なローカル変数を作成しなくてよいのが、可読性を上げる*2 というのが大きな利点だと思うんだが、event 型プロパティの add ハンドラを呼び出せないのが微妙なところである。対するコレクション初期化子は、階層構造を表現する時なんかには便利なんだが、それを前提にオブジェクトの初期化をやろうとすると、オブジェクト初期化子がないためにコンストラクタに多量のパラメータとオーバーロードが発生するのが難点だ。

というわけで、コレクション初期化子を悪用してオブジェクトを初期化するとか、どうだろうか? いや、悪用なんて書いてる時点でダメダメなのはわかってるんですが...

※ 以下は、hatena の日記に直書きコードなので、動作チェックどころか文法チェックすらしていません!

public class MenuItem : List<MenuItem>
{
    public Menu() { }

    public string Text { get; set; }
    public Image Image { get; set; }
    public string TooltipText { get; set; }

    public event EventHandler Click;
}

new MenuItem()
{
  new MenuItem()     // + ファイル
  { new MenuItem(),  // |-- 新規作成
    new MenuItem(),  // |-- 開く
    new MenuItem(),  // |-- 保存
    new MenuItem(),  // |-- 終了
  },
  new MenuItem()     // + 編集
  { new MenuItem(),  // |-- 切り取り
    new MenuItem(),  // |-- コピー
    new MenuItem(),  // |-- 張り付け
  },
}

こういった感じでツリーを表現していると、末端はオブジェクト初期化子を利用して、

  new MenuItem() { Text = "新規作成", Image = Resources.CreateNew.png },
  new MenuItem() { Text = "開く", Image = Resources.OpenFile.png },

などと記載することができるかもしれないが、最初に書いたように Click イベントの設定に困るのと、コレクション初期化子で子オブジェクトを初期化している上位ノードは、コンストラクタで初期化することになり、プロパティの個数や設定の組み合わせに応じて非常にたくさんのコンストラクタを準備しなければならないという状態になる。
これを、コレクション初期化子を悪用して、

public class MenuItem : List<string, MenuItem>
{
  // 略

  public void Add(string text) { this.Text = text; }
  public void Add(Image image) { this.Image = image; }
}

とかすれば、

new MenuItem()
{
  new MenuItem()
  { 
    "ファイル", Resources.File.png,
    new MenuItem()
    {
        "新規作成", Resources.CreateNew.png,
    },
    new MenuItem()
    {
        "開く", Resources.OpenFile.png,
    },

などと書けるようになる。ちょっと見た目が悪くなったが、括弧やインデントを調整すればそれなりに見やすくもなるだろう。
たとえば、私は Visual Studio の自動整形でブロックの開閉を独立行、開閉がペアのときは同一行を許可としているので、

new MenuItem()
{
  new MenuItem()   
  {
                     "ファイル",    Resources.File.png,
    new MenuItem() {    "新規作成", Resources.CreateNew.png, }
    new MenuItem() {    "開く",     Resources.OpenFile.png,  }
    new MenuItem() {    "保存",     Resources.Save.png,      }
    new MenuItem() {    "終了",     Resources.Exit.png,      }
  },
  new MenuItem()   
  {
                     "編集",        Resources.Edit.png,
    new MenuItem() {    "切り取り", Resources.Cut.png,       }
    new MenuItem() {    "コピー...

というような記述なら、Visual Studio も文句をいわないで、ツリー構造を一見できるかもしれない。メニュー構造と対応するアイコンが一見できる。また、イベントハンドラの設定も void Add(EventHandler onclick) というようなメソッドを作れば設定できる。
問題は、Add() のオーバーロードを利用して設定できるプロパティの数に限界があるところで、Add(string) では、Text プロパティと TooltipText プロパティを同時に設定することはできない。回避策としては、

public class MenuItem : List<MenuItem>
{
    public void Add(Action<MenuItem> initializer)
    {
        initializer(this);
    }

    public static Action<MenuItem> SetText(string text)
    {
       return mi => mi.Text = text;
    }

    public static Action<MenuItem> SetTooltip(string text)
    {
       return mi => mi.TooltipText = text;
    }
}

new MenuItem()
{
    new Action<MenuItem>(mi => mi.Image = Resources.Save.png),
    MenuItem.SetText("保存..."),
    MenuItem.SetTooltip("名前を付けてファイルを保存します。"),
    new MenuItem(),
    new MenuItem(),
}

みたいなかんじだろうか。new Action... が並ぶと見た目がいまいちになり、static method もクラス名が説明的で長くなるにつれいまいちになる。むしろ、プロパティ名を指定して

public class MenuItem : List<MenuItem>
{
    public void Add(string property, object value, params object[] index)
    {
        this.GetType().GetProperty(property).SetValue(this, value, index);
    }
}

new MenuItem()
{
    { "Text",  "保存..." }, 
    { "Image", Resources.Save.png }, 
    { "TooltipText", "名前を付けてファイルを保存します。" },
    new MenuItem(),
    new MenuItem(),
}

みたいに key=value な感じ? しかし、引数2つの Add() メソッドを使っちゃうと Dictionary みたいな型の場合に

IDictionary<string, TValue> dict = new Dict()
{
  { "Key1", Value1 },
  { "Key2", Value2 },
  { "Key3", Value3 },
}

となると、このコレクション初期化子は要素の追加用の Add(key, value) とかちあってしまいますね。最初の Action の延長として、

public class MenuItem : List<MenuItem>
{
    public void Add<T>(Expression<Func<object, T>> initializer)
    {
        var self = Expression.Parameter(typeof(MenuItem), "self");

        // Visitor とか使って真面目にやりましょう
        Expression.Lambda<MenuItem>(
            Expression.Assign(
                Expression.PropertyOrField(self, initializer.Parameters[0].Name),
                initializer.Body),
            self)
        .Compile()(this);
    }
}

new MenuItem()
{
    Text        => "保存...", 
    Image       => Resources.Save.png, 
    TooltipText => 名前を付けてファイルを保存します。",
    new MenuItem(),
    new MenuItem(),
}

とかすると、まるでプロパティを設定しているみたいに見えて、一番マシかもしませんね。

*1:オブジェクト初期化子が代入式の構文…つまり右辺値なので、混在させると機械的に処理しにくそうではある?

*2:または、落とさない