Want to learn about other C# 6 features? Check out the full list of articles & source code on GitHub
C# 6 adds in a new feature which is called Dictionary Initialisers or Index Initialisers, because of how they work internally. In fact, the C# team can’t get it straight with roadmap page referring to dictionary while the feature description document, referring to index. While we wait for the name to be decided, what is an index initialiser and how does it work internally?
Note: This is based on VS 2015 CTP 6 and thus may change.
Adding to collections in C# 2
In C# 2 the only way we could add to a collection was with the methods provided by the collection, normally add. For example, below we are adding to a dictionary and an list using the C# 2 way.
public static void CSharp_Two_Dictionary() { var config = new Dictionary<int, string>(); config.Add(1, "hi"); config.Add(2, "how are you"); config.Add(3, "i am fine"); config.Add(4, "good bye"); foreach (var item in config) { Console.WriteLine("{0} = {1}", item.Key, item.Value); } } public static void CSharp_Two_List() { var config = new List<string>(); config.Add("hi"); config.Add("how are you"); config.Add("i am fine"); config.Add("good bye"); foreach (var item in config) { Console.WriteLine(item); } }
Collection Initialisers in C# 3
C# 3 added support for collection initialisers, where we can use braces to add the items. When using collection initialisers, the parentheses for the constructor become optional - for the example below I have omitted them.
This is just syntactic sugar, i.e. the compiler is rewriting this to use the Add method of the collection. In short it ends up the same as the example above when it is run - it is just easier and less error prone to type using collection initialisers.
The requirements collection initialisers to work are::
- The collection class must implement IEnumerable.
- The collection class must have an add method. Almost uniquely for .NET there is no interface required, it is merely the name of the method and that it must take, at least, one parameter.
public static void CSharp_Three_Dictionary() { var config = new Dictionary<int, string> { { 1, "hi" }, { 2, "how are you" }, { 3, "i am fine" }, { 4, "good bye" }, }; foreach (var item in config) { Console.WriteLine("{0} = {1}", item.Key, item.Value); } } public static void CSharp_Three_List() { var config = new List<string> { "hi", "how are you", "i am fine", "good bye", }; foreach (var item in config) { Console.WriteLine(item); } }
Index Initialisers in C# 6
C# 6 adds a new syntax option to add items to collections, where we can add items to collections in a slightly different way:
public static void CSharp_Six_Dictionary() { var config = new Dictionary<int, string> { [1] = "hi", [2] = "how are you", [3] = "i am fine", [4] = "good bye", }; foreach (var item in config) { Console.WriteLine("{0} = {1}", item.Key, item.Value); } } public static void CSharp_Six_List() { // note: this will error var config = new List<string> { [1] = "hi", [2] = "how are you", [3] = "i am fine", [4] = "good bye", }; foreach (var item in config) { Console.WriteLine(item); } }
In the above example you’ll note that we use similar syntax to the collection initialiser (use of brace, optional use of constructor parenthesis) but the way we add items is different. We use brackets to define the index, followed by equals and the value. Internally this is different to collection initialisers since the generated code does NOT use the add method, rather it uses the collections indexer. For example, this is what the generated code the above index initialiser example would look like:
public static void CSharp_Six_Dictionary_Generated() { var config = new Dictionary<int, string>(); config[1] = "hi"; config[2] = "how are you"; config[3] = "i am fine"; config[4] = "good bye"; foreach (var item in config) { Console.WriteLine("{0} = {1}", item.Key, item.Value); } } public static void CSharp_Six_List_Generated() { // note: this will error var config = new List<string>(); config[1] = "hi"; config[2] = "how are you"; config[3] = "i am fine"; config[4] = "good bye"; foreach (var item in config) { Console.WriteLine(item); } }
Since the compiler is generating code with indexers, the ONLY requirement for the class being added is that the class it must have an indexer and the indexer has which has a setter method. This means it can be used on anything which supports indexers. There is NO requirement that this can only be used with collections or things which support IEnumerable.
In the above example the List example will fail and the Dictionary will pass because of the subtle differences in how their indexers work. A dictionary uses the setter on the indexer as the key and will add the item if needed, while a list uses the setter on its indexer as a position and if the list doesn’t have a collection with that size it will fail with an exception.
Example non-collection usage
As mentioned this can be used outside a collection, so let us build a bit of code to assign the four roles to a team.
NOTE: This is an example, it is not meant as guidance for how you should do it. Personally, I think the end example is better handled by a constructor.
In C# 2 we may have done it like this:
public static void C_Sharp_Two_Set_Members() { // code to add team members var team = new Team(); team.ScrumMaster = new Member("Jim"); team.ProjectOwner = new Member("Sam"); team.DevLead = new Member("Phil"); team.Dev = new Member("Scott"); } // we will exclude role from future examples since it doesn't change in the other examples enum Role { ScrumMaster, ProjectOwner, DevLead, Dev } class Team { public Member ScrumMaster { get; set; } public Member ProjectOwner { get; set; } public Member DevLead { get; set; } public Member Dev { get; set; } }
// we will exclude member from future examples since it doesn’t change in the other examples class Member { public Member(string name) { Name = name; } public string Name { get; } }
With C# 3 we could change this to use an object initialiser, which ultimately generates the exact same code as in C# 2:
// rest of example code remains the same public static void C_Sharp_Three_Object_Initialiser() { var team = new Team { ScrumMaster = new Member("Jim"), ProjectOwner = new Member("Sam"), DevLead = new Member("Phil"), Dev = new Member("Scott"), }; }
The one requirement for both examples is that the properties have accessible setter methods, or it will not work, so we cannot use either option with private setters for example. We could use an indexer in this scenario by adding it to the Team class and then being able to use Index initialiser syntax to assign the roles.
public static void C_Sharp_Six_Index_Initialiser() { var team = new Team { [Role.ScrumMaster] = new Member("Jim"), [Role.ProjectOwner] = new Member("Sam"), [Role.DevLead] = new Member("Phil"), [Role.Dev] = new Member("Scott"), }; } class Team { public Member this[Role role] { get { switch (role) { case Role.ScrumMaster: return this.ScrumMaster; case Role.ProjectOwner: return this.ProjectOwner; case Role.DevLead: return this.DevLead; case Role.Dev: return this.Dev; } throw new IndexOutOfRangeException(); } set { switch (role) { case Role.ScrumMaster: { this.ScrumMaster = value; break; } case Role.ProjectOwner: { this.ProjectOwner = value; break; } case Role.DevLead: { this.DevLead = value; break; } case Role.Dev: { this.Dev = value; break; } } } } public Member ScrumMaster { get; private set; } public Member ProjectOwner { get; private set; } public Member DevLead { get; private set; } public Member Dev { get; private set; } }
Hopefully this explains the new syntax in C# 6 and gives you good ideas on where and how it works. If you have any ideas of how Index Initialisers could be used, let me know in the comments below!