なぜ var d = new Dictionary<string, string> { { "a", "b" }, { "c", "d" } } と書けるのか ―― コレクション初期化子

執筆日時:

Dictionary ってその場で初期化できるんだね。

private Dictionary<string, string> AllowedFileType =
new Dictionary<string, string>()
{
{ "image/jpeg", "jpg" },
{ "image/png" , "png" },
{ "image/gif" , "gif" },
};

こっちのほうがいいや。

これまでのサンプルを NuGet パッケージにしてみました - だるろぐ

簡単に書けるのはとっても素晴らしいのだけれど、イマイチこうやって書ける理由がわからなかったので調べてみました。

C# 3.0 のコレクション初期化子*1

とりあえず、基本となるコレクション初期化子(配列初期化子)の復習から。

List は、次のように作成して初期化することができます。

var contacts = new List<Contact> {
new Contact {
Name = "Chris Smith",
PhoneNumbers = { "206-555-0101", "425-882-8080" }
},
new Contact {
Name = "Bob Harris",
PhoneNumbers = { "650-555-0199" }
}
};

これは、以下と同じ効果を持ちます。

var contacts = new List<Contact>();
var __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
contacts.Add(__c1);
var __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
contacts.Add(__c2);

__c1 と __c2 は、このような方法を使用しなければ隠蔽されアクセスできないテンポラリ変数です。

Overview of C# 3.0 | Microsoft Docs

つまり、

var x = List<string>() { "a", "b" };

は、

var x = List<string>();
x.Add("a");
x.Add("b");

とほぼ同じ。ただし、配列初期化子を利用して初期化できるクラスは、 IEnumerable が実装されていること、メンバーとして Add() が実装されていることの二つが条件となります。逆に言えば、それさえ満たしていれば内容は問われません(後述)。

足りない条件

さて、 List の場合はだいたいわかりました。けれど、 Dictionary の場合はそれではまだ説明が足りないと思います。

以下のコードで言えば、なぜ { "a", "b" } なんて書けるのか、ちょっとわかりません。

var d = new Dictionary<string, string>
{
{ "a", "b" }, // <-- この {} って何さ!
{ "c", "d" }
}

すると、こんな助言をいただくなど。

そういうわけで少し試してみました。

f:id:daruyanagi:20120822235621p:plain

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1 { using System.Collections; using System.Diagnostics;

class SampleClass : IEnumerable<string> { public IEnumerator<string> GetEnumerator() { throw new NotImplementedException(); }

IEnumerator GetEnumerator() { throw new NotImplementedException(); }

public void Add(string arg1, string arg2) { Debug.WriteLine("{0}, {1}", arg1, arg2); } }

class Program { static void Main(string[] args) { var a = new List<string>() { "a", "b" }; a.ForEach(() => Debug.WriteLine());

var b = new List<string>() { { "c" }, { "d" } }; b.ForEach(() => Debug.WriteLine());

// var c = { 1 };

var d = new SampleClass() { { "o", "p" }, { "q", "r" } };

Console.WriteLine("何かキーを押してください。"); Console.ReadLine(); } } }

f:id:daruyanagi:20120822235856p:plain

少しずつカラクリが見えてきました。

{ x } で Add(x) が呼ばれる

var b = new List<string>() { { "c" }, { "d" } };

全然知らなかったのだけれど、こんな書き方もいけるのですね。つまり、

var a = new List<string>() { "a", "b" };

において、 "a" は { "a" } の省略記法と見なせそう。

var c = { 1 };

さすがにこれはダメですね。 { } ではなく ( ) ならばそのままコンパイルできるのですが。

f:id:daruyanagi:20120822235244p:plain

ゴメンナサイ。

中身はどうでもいい。とりあえず IEnumerable を実装して Add() 書いとけ

SampleClass は IEnumerable の出来損ないです。しかも、要素に一つの値しか取れないくせに Add() の引数は二つもある!

class SampleClass : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator()
{ throw new NotImplementedException(); }

IEnumerator GetEnumerator() { throw new NotImplementedException(); }

public void Add(string arg1, string arg2) { Debug.WriteLine("{0}, {1}", arg1, arg2); } }

それでも、これが動くんですね。

var d = new SampleClass() {
{ "o", "p" }, { "q", "r" }
};

ただ単に Add() してるのを簡易記法で隠蔽しているだけだとわかれば、何も難しくありません。

var d = new SampleClass();
d.Add("o", "p"); // <-- 実際にはコレクションに値を追加していない!
d.Add("q", "r");

こういうのを糖衣構文(シンタックスシュガー)というのですね。ちなみに、引数の数を間違えると静的にチェックされ、さすがにコンパイルエラーになります。

f:id:daruyanagi:20120822235203p:plain

var d = new SampleClass() {
{ "o", "p" }, { "q", "r", "s" } // <-- アウチ!
};

ちょっと興味深かったのでまとめてみました。

*1:.NET Framework 3.5