Friday, January 27, 2012

System.Collections.IEnumerable and collection initializer syntax

I'd like to make a post about something that I think is one of the least utilized aspects of C#: custom collection initializers. Implementing a custom collection initializer for your class can make your code much more declarative and concise.

It's common to have a collection of things that you set up at design time: drop down list items, configurations, items in a game, a list of mappings, all kinds of things. These are often stored in external xml files or in a sql table. If you're like me then you'd rather have this in code and have it as concise and easy to get right as possible. Here's how.

Look at this example of setting up a list of Armor types for a game:

public List<Armor> ArmorsWithListExample()
{
 List<Armor> armors = new List<Armor>();
 armors.Add(new Armor("leather armor",  1,  250, 10));
 armors.Add(new Armor("chain mail",     4,  500, 14));
 armors.Add(new Armor("splint mail",    7,  750, 16));
 armors.Add(new Armor("plate armor",   10, 1000, 18));
 return armors;
}

Here is the same thing using the collection initializer syntax:

public List<Armor> ArmorsWithInitializedListExample()
{
 return new List<Armor>() {
  new Armor("leather armor",  1,  250, 10),
  new Armor("chain mail",     4,  500, 14),
  new Armor("splint mail",    7,  750, 16),
  new Armor("plate armor",   10, 1000, 18),
 };
}

There are several advantages with the collection initializer syntax. You don't have to have the temporary variable, it's less code with fewer redundancies, and it's a nice declarative way to show that all you are doing is creating the collection. Small advantages, I admit, but they add up. But if you look at some C code, you see that the code can be even more concise by declaring the objects themselves inline. Here's an example from the game Brogue:

const itemTable armorTable[NUMBER_ARMOR_KINDS] = {
 {"leather armor", "", "", 10, 250,  10, {30,30,0},  true, false, "This lightweight armor offers basic protection."},
 {"scale mail",  "", "", 10, 350,  12, {40,40,0},  true, false, "Bronze scales cover the surface of treated leather, offering greater protection than plain leather with minimal additional weight."},
 {"chain mail",  "", "", 10, 500,  13, {50,50,0},  true, false, "Interlocking metal links make for a tough but flexible suit of armor."},
 {"banded mail",  "", "", 10, 800,  15, {70,70,0},  true, false, "Overlapping strips of metal horizontally encircle a chain mail base, offering an additional layer of protection at the cost of greater weight."},
 {"splint mail",  "", "", 10, 1000,  17, {90,90,0},  true, false, "Thick plates of metal are embedded into a chain mail base, providing the wearer with substantial protection."},
 {"plate armor",  "", "", 10, 1300,  19, {120,120,0}, true, false, "Emormous plates of metal are joined together into a suit that provides unmatched protection to any adventurer strong enough to bear its staggering weight."}
};


It's a small improvement but I want to do that in C# as well. With custom collection initializers, you can.

public List<Armor> ArmorsWithCustomInitializerExample()
{
 return new ArmorCollection() {
  { "leather armor",  1,  250, 10 },
  { "chain mail",     4,  500, 14 },
  { "splint mail",    7,  750, 16 },
  { "plate armor",   10, 1000, 18 },
 }.List;
}

Look at that! Concise and easy to get right. Here's the code for the ArmorCollection; notice that it's just a wrapper around a List. The only additional functionality it has is the Add method.

class ArmorCollection : IEnumerable
{
 public List<Armor> List { get; set; }
 
 public ArmorCollection()
 {
  List = new List<Armor>();
 }
  
 IEnumerator IEnumerable.GetEnumerator(){
  return List.GetEnumerator();
 }
  
 public void Add(string name, int weight, int cost, int ac)
 {
  List.Add(new Armor(name, weight, cost, ac));
 }
}

The Add method is what makes it work. According to section 7.6.10.3 of the C# specs, if a class implements the IEnumerable interface then the compiler converts the initializer syntax into a series of calls to the Add method, even though the Add method isn't part of the IEnumerable interface. The Add method can have any number of parameters and the compiler will make it work.

So when you have a collection of things that isn't going to change, instead of using a built-in collection with a series of Add calls, loading from an external xml file, or loading a sql table, consider creating an in memory collection by using a custom initializer: it may be easier, faster, and more concise than the alternatives.

5 comments:

  1. Great read as always! On a side note, are you planning on doing more C# room generation posts? Guess i have grown too addicted to your blog :)

    ReplyDelete
  2. @Anon, thanks! My metroidvania is on hold but March has the 7DRL challenge and I've been thinking about roguelikes again. I'll probably have some posts about that soon. Is there anything specific you're interested in?

    ReplyDelete
  3. Your metroidvania version where you implemented a "loot table" that distributed pickups through the map was of particular interest. It seemed really well implemented regarding density distribution. But hey, I have learned so much from your approaches on programming and design that I'm sure whatever comes next will be a great read! Keep up the awesome work and best of luck for the upcoming 7DRL, will be sure to play it!

    -Tiago

    ReplyDelete
  4. I realise this post is a year old, but a tip better comes late than never. :-)
    Your ArmerCollection can be made simpler:
    (the preview doesn't show angle brackets so I'll replace them with |)

    public class ArmorCollection : List|Armor|
    {
    public void Add(string name, int weight, int cost, int ac)
    {
    Add(new Armor(name, weight, cost, ac));
    }
    }

    ReplyDelete
    Replies
    1. Good point Tom! If it extends List then there's no need to have a List member. That also means it can be used anywhere a List is used.

      Delete