Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / Visual Basic 10

Having fun with custom collections!

Rate me:
Please Sign up or sign in to vote.
4.91/5 (71 votes)
14 Oct 2011CPOL44 min read 185.2K   2.9K   121   73
Creating custom collections from IEnumerable(T) to IDictionary(T) and everything in between!

Introduction

The .NET Framework has provided collections of different types since .NET 1.0 (located in the System.Collections namespace). With the introduction of Generics in .NET 2.0, Microsoft has included a generic version of almost every collection type in .NET too (included in the System.Collections.Generic namespace). Collections are an important feature of the .NET Framework. So important even that every .NET release includes at least a couple of new (Generic) collection types. Yes, every .NET developer will have to use some kind of collection sooner or later. Not everyone knows how to deal with these collections though (especially the Generic ones). Arrays (which are ultimately also collections) have been around much longer than, for example, List<T> (List(Of T) in VB) and many programmers still favour Arrays over .NET collections. However, Arrays have limitations and in many cases, they just cannot do what a List<T> can do. If you are not yet familiar with the basics of List<T>, please take a look at the ListTUsage project in the C# solution, or the ListOfTUsage project in the VB solution. Note that if you want to run this sample, you will have to manually set it as the Startup project. Also, do not forget to set the Startup project back to FunWithCollectionsCSharp or FunWithCollectionsVB if you do.

In this article, I want to look at some of the basic and most used collection types of the .NET Framework. I assume that the reader of this article has at least used some type of collection in .NET before and that he has some clue about how, why, and when to use Interfaces. I am going to explain how they work and how to make your own. These include IEnumerable<T>, ICollection<T>, IList<T>, and IDictionary<TKey, TValue> (replace <T> with (Of T) for VB). You might be wondering what the <T> and (Of T) stand for? This is called a Generic. Generics is a method to create type-safe classes using any Type. In pre-Generics time, you would use Objects. Objects are not type-safe, however, and requires boxing/unboxing and/or casting to specific types. Generics fixed these problems. For example, a collection can be a collection of Strings, Integers, Forms... You name it. The <T> or (Of T) syntax allows for any Type. You could create a List<Form> or List(Of Form). This means you can only put Forms in your list. It also means that if you get an item from the list, the compiler will know that it is a Form, no casting is required. Needless to say, it is (almost) always better to use Generic types of collections if they are available. This said, IEnumerable (the base for every .NET collection) would be quite the same as an IEnumerable<Object> (or (Of Object) in VB). I say "would be" because they are not. The Generic IEnumerable<T> has other advantages, as we will see later in this article.

Maybe you are wondering why I wrote yet another article about collections. There are many articles to be found on CP, and also MSDN has quite some documentation on the subject. I find the documentation on MSDN sufficient to help me create my own custom collection, I do not find them sufficient to get a deeper understanding of the internals of collections in .NET. After creating my first (very simple) custom collection using MSDN as example, I was rather scared off by all the Methods I had to implement using IList<T>. I also looked at CP for some explanations, but the articles I found got low ratings, were old, and made me wonder if they were worth reading or simply did not cover all I wanted to see covered (although I found some very good ones too!). After having done some research concerning collections in .NET, I did not find it all that scary and difficult anymore. I hope this article can help in understanding and creating collections of any type.

I have provided two sample projects. One in VB (because I am a VB programmer in daily life) and one in C# (because I want to learn and because I have a notch C# is better appreciated by the average CPian). Both projects (hopefully) do the exact same thing, so if you know both languages, you can pick either. I have used the .NET 4 version of the Framework. The basics of the article go back to .NET version 2.0, but I will be using some LINQ which is from more recent versions. As mentioned, I will be explaining how collections in .NET work and how to create your own from scratch. I will take it one step at a time, so I hope it will be easy to follow. For user interaction (and testing of my custom collections), I have created a WinForms user interface. I think everything is pretty self-explaining, but I will try and guide you through the code as much as possible. Most explanation is done in VB and C#. I will also be looking at some differences between the two. Enjoy!

"One is a tchotchke, two is chance, and three is a collection with persona."- Vicente Wolf

Why build a custom collection?

Seriously, why would you want to build your own collection? The classes List<T>, Dictionary<TKey, TValue>, ReadOnlyCollection<T>, ObservableCollection<T> etc., have really more functionality than you could ever wish for! While it is certainly true that .NET has some very advanced collection types that can be used in practically any situation, there simply are some situations that require a custom collection. Let us say, for example, that you have a class Person. Now you want to make a class People. Simply inheriting List<Person> would do the trick. But let us be honest. While List<T> is an amazing class that has certainly helped me out more times than I can count, it was not intended for customisation. The fact that it has no overridable members says a lot in that respect. So say, for example, you want a list of people, but only people that are older than 18 are allowed in the list. How would you solve this? The List<T> does not care about the age of a person to be added. You could simply check if a person's age is at least 18 before putting them into the list, but the next developer will not know your intents and simply create a new Person class, and put a child in your adult list! You could create an Adult class that inherits from Person, but you do not really want to go that way. Face it, the only option for you is to build a custom collection. Luckily, this is pretty easy once you get the hang of it. Before I am going to tell you all about which Interfaces to implement, you should know this... By far most of the custom collections you are going to make are simply going to be a wrapper around the already existing List<T> class. Phew! That certainly is good news. Microsoft has already done all the hard work for us when they created List<T> and other collection types. Let us use them gratefully!

Sweet collections are made of these!

IEnumerable

If you are reading this article, you have undoubtedly worked with some kind of collection. Either an Array, List<T>, or even a Dictionary<TKey, TValue> ((Of TKey, TValue) in VB). And undoubtedly, at some point in your code, you have used the foreach (For Each in VB) keyword to iterate through the items in your collection. Have you ever wondered why you can use the foreach keyword on collections such as Arrays or Lists? This is because every collection type in .NET is ultimately an IEnumerable (the non-Generic one). Try this for yourself. Create a new class and implement IEnumerable. It is not necessary to actually write code for the methods it provides (you are not going to run this example). Now create a new instance of your just created class and use foreach (For Each in VB) on your instance. Yes, it actually works!

C#:
C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace MyTest
{
    public class Test
    {
        public Test()
        {
            TestEnumerator te = new TestEnumerator();
            foreach (var o in te)
            {
                // Do stuff.
            }
        }
    }

    public class TestEnumerator : System.Collections.IEnumerable
    {
        public System.Collections.IEnumerator GetEnumerator()
        {
            throw new NotImplementedException(
                  "No need to implement this for our example.");
        }
    }
}
VB:
VB
Public Class Test

    Public Sub New()
        Dim te As New TestEnumerator
        For Each o In te
            ' Do stuff.
        Next
    End Sub

End Class

Public Class TestEnumerator
    Implements IEnumerable

    Public Function GetEnumerator() As System.Collections.IEnumerator _
           Implements System.Collections.IEnumerable.GetEnumerator
        Throw New NotImplementedException(_
              "No need to implement this for our example.")
    End Function

End Class

As you can see, the IEnumerable is the key to every collection in .NET. It provides support for the foreach keyword that is so important to collections. IEnumerable and IEnumerable<T> offer little in terms of usability though. It does not support adding or removing from the collection. It is really only a means to iterate through a collection. It does this by calling the only function the Interface provides: the GetEnumerator or GetEnumerator<T> function that returns an IEnumerator or IEnumerator<T>. It is the returned IEnumerator that moves through a collection of Objects and returns the item at that index. Luckily, this is not hard at all, as you will see later in this article. The .NET Framework does not include a Generic Enumerable or Enumerator base class that you can use. You have to implement it yourself. Of course, you could create your own base class or Generic Enumerator as I will also show you later in this article.

When you finish the little assignment I just gave you, you might notice that when you type 'te' in your project, IntelliSense will give you some additional methods that do not inherit from Object or are provided by IEnumerable. Here is some good news for you! Microsoft has created numerous Extension Methods for IEnumerable and derivatives. And this is where IEnumerable and IEnumerable<Object> differ from each other. In the example above, try making your TestEnumerator implement System.Collections.Generic.IEnumerable<int> or System.Collections.Generic.IEnumerable(Of Integer).

C#:
C#
public class TestEnumerator : System.Collections.Generic.IEnumerable<int>
{
    public System.Collections.IEnumerator 
           System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    System.Collections.Generic.IEnumerator<int> GetEnumerator()
    {
        throw new NotImplementedException(
              "No need to implement this for our example.");
    }
}
VB:
VB
Public Class TestEnumerator _
    Implements IEnumerable(Of Integer)

    Public Function GetEnumerator() As _
           System.Collections.Generic.IEnumerator(Of Integer) _
           Implements System.Collections.Generic.IEnumerable(Of Integer).GetEnumerator
        Throw New NotImplementedException(_
           "No need to implement this for our example.")
    End Function

    Private Function GetEnumerator1() As System.Collections.IEnumerator _
            Implements System.Collections.IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function

End Class

You should now notice a couple of things. In C#, I have explicitly implemented the non-Generic GetEnumerator. In VB, I have renamed the GetEnumerator that is implemented from the non-Generic IEnumerable to GetEnumerator1 (you can name it anything you like) and I have made it Private. This means that it is not exposed to other classes using TestEnumerator (unless you cast it to an IEnumerable). Also, this non-Generic GetEnumerator now calls the Generic GetEnumerator that is provided by the Generic IEnumerable(Of T). I have done this so that I only have to write code for one GetEnumerator, and if it changes, the non-Generic GetEnumerator will automatically call the changed code. Another interesting change is in your Test Class that uses TestEnumerator. If, before you changed IEnumerable to IEnumerable(Of Integer), you hovered over var in C# or o in VB in the following line of code:

C#:
C#
foreach(var o in te)
VB:
VB
For Each o In te

you could see that var or o was an Object. If you hover over it now, the compiler knows that var or o is an int in C# and an Integer in VB because te is an IEnumerable<int> in C# and an IEnumerable(Of Integer) in VB. Long live Generics! Now, what makes IEnumerable so different from IEnumerable<Object>? This, for reasons unknown to me, becomes clear only when you explicitly mention the IEnumerable type of your variable.

C#:
C#
System.Collections.IEnumerable te = new TestEnumerable();
// te. returns few Extension Methods.

IEnumerable<int> te = new TestEnumerable();
// te. returns lots of Extension Methods!
VB:
VB
Dim te As IEnumerable = New TestEnumerable
' te. returns few Extension Methods.

Dim te As IEnumerable(Of Integer) = New TestEnumerable
' te. returns lots of Extension Methods!

If you do not see any Extension Methods in C#, make sure you have

using 
<a title="System.Linq" href="http://msdn.microsoft.com/en-us/library/system.linq.aspx">System.Linq</a>
at the top of your document. In VB, you should see Extension Methods by default if you have not messed with the default project references. Perhaps now that you know that most of the methods that, for example List<T>, provides are Extension Methods, you do not have to worry so much about creating your own collection type. After all, most of the functionality is already implemented for you!

So to wrap it up, the IEnumerable is the base Interface for all collection types in .NET. It provides functionality for iterating through a collection (using foreach or For Each) by using the GetEnumerator function. IEnumerable<T> is the Generic version of the IEnumerable (and inherits from it). At this point, I should apologize for my sloppy copy/paste/paint skills...

IEnumerable.png

IEnumerable_Of_T_.png

IEnumerator

So, now that you globally know how and why collections can be iterated through, let us take a closer look at this oh so important IEnumerator. An IEnumerable calls GetEnumerator which returns an IEnumerator, and this IEnumerator takes care of looping through a collection and returning the item at the current index. Actually, when looking at the methods an IEnumerator provides, that makes pretty good sense.

IEnumerator.png

IEnumerator_Of_T_.png

If you use a foreach (For Each in VB) keyword on an IEnumerable, the GetEnumerator is called. The enumerator that is returned calls the MoveNext method, which should increment an Integer that represents an index value and return a Boolean specifying whether another MoveNext can be called (which is possible as long as you have not reached the last item of the collection). After that, the Current property is called which should return the item at the current index of your collection. Good to know here that if you use foreach, it will call MoveNext for the first item, twice for the next item, thrice for the next item, etc. This means that an IEnumerable does not have to 'remember' the index of the IEnumerator. It does not even have to remember its enumerator at all! Instead, you can return a new enumerator every time GetEnumerator is called. This also means the Reset method should never be called because every time the enumerator is fetched, it returns a new instance of the enumerator. In fact, the Reset method is used for compatibility with COM and should throw a NotImplementedException if you plan on not using or supporting COM. So let us look at how to implement all this in the simplest form. In the sample code, I have included a project called Aphabet. In this project, you will find two classes. One is an IEnumerable<String>, the other an IEnumerator<String>. I have excluded comments for readability.

C#:
C#
using System;
using System.Collections.Generic;
using System.Linq;

namespace Alphabet
{
    public class WesternAlphabet : IEnumerable<String>
    {

        IEnumerable<String> _alphabet;

        public WesternAlphabet()
        {
            _alphabet = new string[] { "A", "B", "C", 
               "D", "E", "F", "G", 
               "H", "I", "J", "K", 
               "L", "M", "N", "O", 
               "P", "Q", "R", "S", 
               "T", "U", "V", "W", 
               "X", "Y", "Z" };
        }

        public System.Collections.Generic.IEnumerator<String> GetEnumerator()
        {
            return new WesternAlphabetEnumerator(_alphabet);
        }

        System.Collections.IEnumerator 
               System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    internal class WesternAlphabetEnumerator : IEnumerator<String>
    {

        private IEnumerable<String> _alphabet;
        private int _position;
        private int _max;

        public WesternAlphabetEnumerator(IEnumerable<String> alphabet)
        {
            _alphabet = alphabet;
            _position = -1;
            _max = _alphabet.Count() - 1;
        }

        public string Current
        {
            get { return _alphabet.ElementAt(_position); }
        }

        object System.Collections.IEnumerator.Current
        {
            get { return this.Current; }
        }

        public bool MoveNext()
        {
            if (_position < _max)
            {
                _position += 1;
                return true;
            }
            return false;
        }

        void System.Collections.IEnumerator.Reset()
        {
            throw new NotImplementedException();
        }

        public void Dispose() { }
    }
}
VB:
VB
Public Class WesternAlphabet
    Implements IEnumerable(Of String)

    Private _alphabet As IEnumerable(Of String)

    Public Sub New()
        _alphabet = {"A", "B", "C", "D", "E", _
           "F", "G", "H", "I", "J", _
           "K", "L", "M", "N", "O", _
           "P", "Q", "R", "S", "T", _
           "U", "V", "W", "X", "Y", "Z"}
    End Sub

    Public Function GetEnumerator() As _
           System.Collections.Generic.IEnumerator(Of String) _
           Implements System.Collections.Generic.IEnumerable(Of String).GetEnumerator
        Return New AlphabetEnumerator({})
    End Function

    Private Function GetEnumeratorNonGeneric() As _
            System.Collections.IEnumerator Implements _
            System.Collections.IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function

End Class

Friend Class AlphabetEnumerator
    Implements IEnumerator(Of String)

    Private _alphabet As IEnumerable(Of String)
    Private _position As Integer
    Private _max As Integer

    Public Sub New(ByVal alphabet As IEnumerable(Of String))
        _alphabet = alphabet
        _position = -1
        _max = _alphabet.Count - 1
    End Sub

    Public ReadOnly Property Current As String Implements _
           System.Collections.Generic.IEnumerator(Of String).Current
        Get
            Return _alphabet(_position)
        End Get
    End Property

    Private ReadOnly Property Current1 As Object _
            Implements System.Collections.IEnumerator.Current
        Get
            Return Me.Current
        End Get
    End Property

    Public Function MoveNext() As Boolean Implements _
           System.Collections.IEnumerator.MoveNext
        If _position < _max Then
            _position += 1
            Return True
        End If
        Return False
    End Function

    Private Sub Reset() Implements System.Collections.IEnumerator.Reset
        Throw New NotImplementedException
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
    End Sub

End Class

Now I hear you thinking, where did that Dispose method come from? I have not talked about this yet, but it is a difference between IEnumerator and IEnumerator<T>. IEnumerator<T> derives from IDisposable. IDisposable is a .NET Interface that supports (as the name implies) disposing of resources (which is outside the scope of this article). If a class implements this Interface, you can declare it using the using statement (Using in VB) just like IEnumerator provides the foreach functionality. So why does IEnumerator<T> derive from IDisposable? In a rare case where an enumerator might enumerate through files or database records, it is important to properly close a connection or file lock. Microsoft chose to have IEnumerator<T> derive from IDisposable because it is slightly faster than opening and closing connections outside a foreach loop and having an empty Dispose method is not a very big deal. The Dispose method in an enumerator is called at the end of a loop (that means when MoveNext returns false). The Dispose method will be empty in the majority of cases.

Now let us take a look at frmAlphabet. This Form uses the WesternAlphabet class. When you press the top left button, the code loops through the items in the WesternAlphabet and puts them in a TextBox.

C#:
C#
private void btnGetAlphabet_Click(object sender, EventArgs e)
{
    txtAlphabet.Text = String.Empty;
    
    int count = _alphabet.Count();
    for (int i = 0; i <= count - 2; i++)
    {
        txtAlphabet.Text += _alphabet.ElementAt(i) + ", ";
    }
    // Omit the comma after the last character.
    txtAlphabet.Text += _alphabet.ElementAt(count - 1);
}
VB:
VB
Private Sub btnGetAlphabet_Click(ByVal sender As System.Object, _
       ByVal e As System.EventArgs) Handles btnGetAlphabet.Click
    txtAlphabet.Text = String.Empty

    Dim count As Integer = _alphabet.Count
    For i As Integer = 0 To count - 2
        txtAlphabet.Text += _alphabet(i) & ", "
    Next
    ' Omit the comma after the last character.
    txtAlphabet.Text += _alphabet(count - 1)
End Sub

That is pretty regular stuff, right? You probably already do this on a daily basis. But what is really going on here? As you can see, I call the Count method of the WesternAlphabet class. We did not implement this, but remember, this is an Extension Method. What Count actually does is it calls IEnumerable.GetEnumerator. It uses this IEnumerator to call MoveNext until it returns false. Finally, it calls IEnumerator.Dispose before returning how often it could call MoveNext. Not quite what you expected? You can imagine that calling the Count method numerous times on big collections can slow things down considerably. That is why I call it once and store the result in a variable. Although performance is not really an issue here, it is something you should be aware of.

So what about the code inside the loop? This code is quite different between C# and VB. This is because an IEnumerable<T> is not indexed. In VB, this does not matter and you can use () to get the item at the specified index on any collection. In C#, however, we are forced to use the ElementAt Extension Method. A non-Generic IEnumerable does not have that Extension Method either and you are forced to write your own item-at-index-fetcher function. IList and IList<T> are indexed, I will get back on them later.

I will ignore the txtIndex.Validating event because it is not relevant for this article. It simply checks if the input is a valid Integer. What is relevant is the txtChar.Click event.

C#:
C#
private void btnGetChar_Click(object sender, EventArgs e)
{
    if (txtIndex.Text != String.Empty)
    {
        txtChar.Text = 
          _alphabet.ElementAtOrDefault(Convert.ToInt32(txtIndex.Text) - 1);
    }
}
VB:
VB
Private Sub btnGetChar_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles btnGetChar.Click
    If txtIndex.Text <> String.Empty Then
        txtChar.Text = _alphabet(CInt(txtIndex.Text) - 1)
    End If
End Sub

What is interesting here is that the item at some index in the collection is fetched. However, unlike in the previous example, the index might not exist! For VB, this, again, is no problem. It will simply fetch a 'default' Object. String.Empty for a String, 0 for numeric types, etc. The C# ElementAt Extension Method that I used earlier throws an Exception if the index is out of range though. To get the same behaviour as in VB, you can use the ElementAtOrDefault Extension Method. Other than that, CInt is just a VB shortcut for Convert.ToInt32, which converts a String to an Integer. We know this is possible because we check for String.Empty and if it is not empty, it is already validated by the txtIndex.Validate event.

That sums it up for the basics. So far we have taken a look at the absolute basics of any collection type, IEnumerable and IEnumerator. Now there is a little secret I must share. All the work of implementing our own IEnumerator<T> was not really necessary... As you have seen, the WesternAlphabet class has an _alphabet variable that is actually already a collection type! We could have called _alphabet.GetEnumerator instead (this would have returned an enumerator that does the same as ours and maybe even better)! However, that would not have given us the understanding of IEnumerator that we have just gained. I do not recommend building your own enumerator for every custom collection though.

A collection of cultists...

Now that you know the basics, it is time to create a collection that has add and remove functionality. Of course, you could implement IEnumerable<T> and write your own Add and Remove methods. A better solution would be to implement ICollection<T>. Notice how I skip the non-Generic ICollection? This is because ICollection does not support adding and removing functionality. ICollection is more of a thread-safe base collection (or, arguably, because it does not support adding and removing functionality, not much of a collection at all). To make things even more confusing, ICollection<T> does not inherit ICollection (in contrast to IEnumerable<T> and IEnumerator<T> that both inherit their non-Generic ancestor). To add non-Generic adding and removing functionality, you would have to implement IList. We will implement IList<T> later in this article though.

So back to ICollection<T>. There is an implementation of this Interface in the System.Collections.ObjectModel namespace. The Collection<T> class also implements IList<T> though, so it is not an 'ICollection<T>-prototype' I guess. Why is this Collection<T> not in the System.Collections.Generic namespace? Because there is already a class called Collection in the Microsoft.VisualBasic namespace (C# does not have a Collection class like that). Since both the Microsoft.VisualBasic and System.Collections.Generic namespaces are referenced in VB projects by default, Microsoft decided to put the Collection<T> in a different namespace to prevent confusion. However, ICollection<T> is in the System.Collections.Generic namespace and ICollection<T> has everything you need to build your own collection that supports adding and removing of items.

ICollection_Of_T_.png

As you can see, ICollection<T> inherits from IEnumerable<T>. Next to the GetEnumerator function, ICollection<T> requires the Add, Remove, Clear, and Contains methods to be implemented. Also note that Count should be implemented even though this is already an Extension Method (you will learn why in a bit). The IsReadOnly property is to support ReadOnly collections (such as the System.Collections.ObjectModel.ReadOnlyCollection<T>). Last is the CopyTo method. This method is used by LINQ Extension Methods such as ToList.

Since I used to play a lot of Role Playing games, I have created a project called TheCult. A creepy cult always plays well in RPG's, so I thought I would introduce them to .NET. Also, there is something funny about people that join a cult. Basically, I see it like this: a person enters a cult and a cultist is born! For this reason, I have created two classes, Person and Cultist. You can look them up in the sample project. The Person class is pretty straightforward, so I am not going to say anything about it here. The Cultist class inherits from Person. That means a Cultist is still a Person, however a Cultist has some extra properties defined. A Cultist has some sort of ReadOnly Mark which proves he is in a cult. In this case, the Mark is simply a unique Integer within the cult. A cultist also has a Rank with an internal (Friend in VB) setter. Why is it not possible to set the Mark and Rank properties outside of the assembly? Simply because only the Cult class can decide what unique Mark and which Rank a new cult member gets. Your custom collection of Persons is actually a Cultist factory! That also means that while it is an ICollection<Person>, the GetEnumerator function actually returns an Enumerator that contains Cultists. So how does this all work? Let us take a quick look at some code. I have left out all the methods that are not required by the implemented Interfaces, as well as a lot of comments. You can study those by yourself.

C#:
C#
public class Cult : ICollection<Person>, IListSource
{
    private List<Cultist> _innerList;

    public Cult()
    {
        _innerList = new List<Cultist>();
    }

    public Cult(IEnumerable<Person> people)
        : this()
    {
        this.AddRange(people);
    }

    protected List<Cultist> InnerList
    {
        get { return _innerList; }
    }

    public void Add(Person item)
    {
        if (item == null)
            throw new ArgumentNullException("Item cannot be nothing.");

        // Check if the person is allowed to enter the cult.
        if (CanJoin(item))
        {
            // Create a new cultist with the current person.
            Cultist cultist = Cultist.CreateCultist(item, GetMark());
            // Add the new recruit to the cult!
            this.InnerList.Add(cultist);

            // Check how many members the cult currently has and
            // set the new recruits rank accordingly.
            int count = this.InnerList.Count;
            switch (count)
            {

                case 1:
                    // If the inner list has no items yet then the cultist that is
                    // to be added becomes the first cultist and thus gets leader status.
                    cultist.Rank = CultRanks.Leader;

                    break;
                case 10:
                    // If the cult has 10 members then it is needed to get a second in command.
                    // The second person to join the cult becomes general.
                    this.InnerList[1].Rank = CultRanks.General;
                    // The to be added cultist becomes a soldier.
                    cultist.Rank = CultRanks.Soldier;

                    break;
                default:
                    // Nothing special.
                    // The to be added cultist becomes a soldier.
                    cultist.Rank = CultRanks.Soldier;

                    break;
            }

            // If there are 20 cult members or any number dividable by 10 after that
            // then it is necessary to get a new person that can keep the peace.
            AssignNextCaptainIfNecessary();
        }
    }

    public void Clear()
    {
        // Clears the list of cultists (perhaps the police raided the place?).
        this.InnerList.Clear();
    }

    public bool Contains(Person item)
    {
        return (this.InnerList.Where(cultist => cultist.FullName == 
                     item.FullName).FirstOrDefault() != null);
    }

    public void CopyTo(Person[] array, int arrayIndex)
    {
        List<Person> tempList = new List<Person>();
        tempList.AddRange(this.InnerList);
        tempList.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return this.InnerList.Count; }
    }

    bool ICollection<Person>.IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(Person item)
    {
        // First retrieve the cultist based on the persons name.
        Cultist cultist = this.InnerList.Where(c => 
                c.FullName == item.FullName).FirstOrDefault();
        // Check if a cultist with the given name exists.

        if (cultist != null)
        {
            if (this.InnerList.Remove(cultist))
            {
                // If the cultist was removed the ranks have to be recalculated.
                // We do not know who was removed from the cult. Maybe it was the leader
                // or general, so we need some promotions.
                RecalculateRanksOnRemove(cultist);
                cultist.Rank = CultRanks.None;
                return true;
            }

        }
        return false;
    }

    public System.Collections.Generic.IEnumerator<Person> GetEnumerator()
    {
        return new GenericEnumerator.GenericEnumerator<Cultist>(this.InnerList);
    }
        
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    bool IListSource.ContainsListCollection
    {
        get { return true; }
    }

    System.Collections.IList IListSource.GetList()
    {
        return this.InnerList;
    }

}
VB:
VB
Public Class Cult
    Implements ICollection(Of Person)
    Implements IListSource

    Private _innerList As List(Of Cultist)

    Public Sub New()
        MyBase.New()
        _innerList = New List(Of Cultist)
    End Sub

    Public Sub New(ByVal people As IEnumerable(Of Person))
        Me.New()
        Me.AddRange(people)
    End Sub

    Protected ReadOnly Property InnerList As List(Of Cultist)
        Get
            Return _innerList
        End Get
    End Property

    Public Sub Add(ByVal item As Person) Implements _
           System.Collections.Generic.ICollection(Of Person).Add
        If item Is Nothing Then
            Throw New ArgumentNullException("Item cannot be nothing.")

        ' Check if the person is allowed to enter the cult.
        If CanJoin(item) Then

            ' Create a new cultist with the current person.
            Dim cultist As Cultist = cultist.CreateCultist(item, GetMark)
            ' Add the new recruit to the cult!
            Me.InnerList.Add(cultist)

            ' Check how many members the cult currently has and
            ' set the new recruits rank accordingly.
            Dim count = Me.InnerList.Count
            Select Case count

                Case 1
                    ' If the inner list has no items yet then the cultist that is
                    ' to be added becomes the first cultist and thus gets leader status.
                    cultist.Rank = CultRanks.Leader

                Case 10
                    ' If the cult has 10 members then it
                    ' is needed to get a second in command.
                    ' The second person to join the cult becomes general.
                    Me.InnerList(1).Rank = CultRanks.General
                    ' The to be added cultist becomes a soldier.
                    cultist.Rank = CultRanks.Soldier

                Case Else
                    ' Nothing special.
                    ' The to be added cultist becomes a soldier.
                    cultist.Rank = CultRanks.Soldier

            End Select

            ' If there are 20 cult members or any number dividable by 10 after that
            ' then it is necessary to get a new person that can keep the peace.
            AssignNextCaptainIfNecessary()
        End If
    End Sub

    Public Sub Clear() Implements System.Collections.Generic.ICollection(Of Person).Clear
        ' Clears the list of cultists (perhaps the police raided the place?).
        Me.InnerList.Clear()
    End Sub

    Public Function Contains(ByVal item As Person) As Boolean _
           Implements System.Collections.Generic.ICollection(Of Person).Contains
        Return Not Me.InnerList.Where(Function(cultist) cultist.FullName = _
                   item.FullName).FirstOrDefault Is Nothing
    End Function

    Public Sub CopyTo(ByVal array() As Person, ByVal arrayIndex As Integer) _
           Implements System.Collections.Generic.ICollection(Of Person).CopyTo
        Dim tempList As New List(Of Person)
        tempList.AddRange(Me.InnerList)
        tempList.CopyTo(array, arrayIndex)
    End Sub

    Public ReadOnly Property Count As Integer Implements _
           System.Collections.Generic.ICollection(Of Person).Count
        Get
            Return Me.InnerList.Count
        End Get
    End Property

    Private ReadOnly Property IsReadOnly As Boolean Implements _
            System.Collections.Generic.ICollection(Of Person).IsReadOnly
        Get
            Return False
        End Get
    End Property

    Public Function Remove(ByVal item As Person) As Boolean _
           Implements System.Collections.Generic.ICollection(Of Person).Remove
        ' First retrieve the cultist based on the persons name.
        Dim cultist = Me.InnerList.Where(Function(c) c.FullName = item.FullName).FirstOrDefault
        ' Check if a cultist with the given name exists.
        If Not cultist Is Nothing Then

            If Me.InnerList.Remove(cultist) Then
                ' If the cultist was removed the ranks have to be recalculated.
                ' We do not know who was removed from the cult. Maybe it was the leader
                ' or general, so we need some promotions.
                RecalculateRanksOnRemove(cultist)
                cultist.Rank = CultRanks.None
                Return True
            End If

        End If
        Return False
    End Function

    Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of Person) _
           Implements System.Collections.Generic.IEnumerable(Of Person).GetEnumerator
        Return New GenericEnumerator.GenericEnumerator(Of Cultist)(Me.InnerList)
    End Function

    Private Function GetEnumerator1() As System.Collections.IEnumerator _
            Implements System.Collections.IEnumerable.GetEnumerator
        Return Me.GetEnumerator
    End Function

    Private ReadOnly Property ContainsListCollection As Boolean _
            Implements System.ComponentModel.IListSource.ContainsListCollection
        Get
            Return True
        End Get
    End Property

    Private Function GetList() As System.Collections.IList _
            Implements System.ComponentModel.IListSource.GetList
        Return Me.InnerList
    End Function

End Class

First, forget the IListSource for now. You can see that while I implement ICollection<Person>, the InnerList is actually a List<Cultist>. Perhaps you also noticed that Cultist has a private constructor and an internal static (Friend Shared in VB) CreateCultist function that takes a Person as parameter and gives back a Cultist. This is exactly the function that our Cult class uses when a new Person is added to the Cult. It is also impossible for people using our library to create their own Cultist. Everything goes through our Cult class. The Cult class takes care of assigning Marks and Ranks. What is also very important is that all this class does is basically put a wrapper around the normal List<T> class! When a Person is added, I add to the InnerList; when a Person is removed, I remove from the InnerList, etc.

So we already know the GetEnumerator function. Let us take a look at the other methods ICollection<T> has to offer. In the Add method, I first check if a Person is allowed to join (no people with the same name may enter). After that, I use the Person to create a Cultist and give him a Mark. I then add the new Cultist to the InnerList. After that, I can check how many people have already joined the Cult and I check if new CultRanks have to be set. The first member is always the Leader. When a tenth member joins, the second Person to have joined the Cult becomes a General. After every tenth new Cultist after that, the next Cultist becomes a Captain. Every new recruit automatically becomes a Soldier.

Removing a Cultist in the Remove function requires some extra work. The Remove function takes a Person as argument (we implement ICollection<Person> after all), but my InnerList contains only Cultists. I use a LINQ query to get the Cultist whose name is the same as the Person who wants to leave (only unique names in our Cult, remember?). Probably not the best method to do this, but it is good enough for this example. At this point, I should say that the default Contains behaviour for collections in .NET for reference types looks if the parameter is pointing towards a place in memory that an element in the collection is also pointing to. This can be overridden as we will see in a moment. I then remove the Cultist from the InnerList, set his Rank to None, and recalculate Ranks. If the Leader was just removed from the Cult, the General gets a promotion, if the General is removed or becomes Leader, the first Captain becomes General and, if necessary, a new Captain is assigned. The part of keeping track of who gets which rank when was actually the hardest part. It is also the part you can make as hard or easy as you want. The actual adding and removing to and from the InnerList is quite simple, right? After all, Microsoft already did the hard part!

The Contains function is also worth looking at. Once again, I use LINQ to get a Cultist with the same name as the passed Person. I return True if a Cultist is found. I could also have made a new List<Person> and pass my InnerList as parameter to the constructor. I could have checked if the Person that was passed to the Contains function was an actual pointer reference to a Person that is also in the new temporary List. I could have used an InnerList<Person> too and simply added Cultists to it? That way I could also always check if the InnerList actually contains the Persons that are passed to the methods instead of checking for names. However, I wanted to make it really clear that I have an ICollection<Person> that creates Cultists.

The Clear method simply clears the InnerList. ReadOnly should return True if a collection is read-only. Ours is not, so we simply return False. Then there is the CopyTo Method. You probably will not implement this yourself, so you can simply call the InnerList.CopyTo method. This method is used to copy collections of one type to another (for example, an ICollection<T> to an IList<T>). There are some LINQ Extension Methods that use this method.

Count is an interesting property. We already have the Extension Method Count, right? So why do we have to implement it again? The Extension Method needs to loop through every item in a collection and keep a counter. It then returns how often it looped. Now that we have a collection, we can actually keep our own counter and simply increment it when a Person is added and decrement it when a Person is removed. Now when I would call the Count property, I would already have the answer right away without first possibly looping through 1000s of items. The List<T> already has a smart mechanism like that, so I do not really have to re-invent the wheel there. Simply return the InnerList.Count.

A function that was not listed in the code above, but which contains another important trick every programmer should know about, is CanJoin.

C#:
C#
protected virtual bool CanJoin(Person person)
{
    // A person is only able to join if no person
    // with the same name is already in the cult.
    return !this.Contains(person, new PersonNameEqualityComparer());
}
VB:
VB
Protected Overridable Function CanJoin(ByVal person As Person) As Boolean
    ' A person is only able to join if no person
    ' with the same name is already in the cult.
    Return Not Me.Contains(person, New PersonNameEqualityComparer)
End Function

What is this overloaded Contains function? We certainly did not implement it! It is another Extension Method on the IEnumerable<T> interface. It takes an IEqualityComparer<T> as argument, where T is the same type as your IEnumerable<T>. You can see how this is implemented in the PersonNameEqualityComparer class. This class does not actually implement the IEqualityComparer<T> Interface, but Inherits from EqualityComparer<T>, which does implement the Interface. Microsoft recommends that we inherit the base class rather than implement the Interface (read why on MSDN). For our class, it matters not.

C#:
C#
class PersonNameEqualityComparer : EqualityComparer<Person>
{
    public override bool Equals(Person x, Person y)
    {
        if (x == null | y == null)
        {
            return false;
        }
        return x.FullName == y.FullName;
    }

    public override int GetHashCode(Person obj)
    {
        return obj.ToString().GetHashCode();
    }
}
VB:
VB
Public Class PersonNameEqualityComparer_
    Inherits EqualityComparer(Of Person)

    Public Overloads Overrides Function Equals(ByVal x As Person, _
           ByVal y As Person) As Boolean
        If x Is Nothing Or y Is Nothing Then
            Return False
        End If
        Return x.FullName = y.FullName
    End Function

    Public Overloads Overrides Function _
           GetHashCode(ByVal obj As Person) As Integer
        Return obj.ToString.GetHashCode
    End Function

End Class

The most important here is the Equals function, which gets two Persons as arguments and returns a Boolean representing wither the two Persons are the same or not. As you can see in the code, two people are the same if their FullName property matches. Why do we need this? Well, usually if you check if a list contains a reference type, the list simply checks if the two items point to the same location in memory. I do not want that though, I want to make sure two people are equal if they have the same name even though they are entirely different Objects! So this can be done with the Equals function. You can make it as crazy as you want and, for example, also check for the Age property. In the CanJoin function, I called this.Contains (Me.Contains in VB) so it calls the extension method on the Cult object and not on the InnerList. This way you are able to put a breakpoint on the CanJoin function and see how it loops through every object in the Cult using the IEnumerator returned by the GetEnumerator function (similar to the Count Extension Method) and check every item for equality.

Now what was that IListSource Interface all about? In the frmCult, I have bound the Cult to a DataGridView. Binding requires an IList or IListSource though. ICollection<T> does not inherit from IList, so we are a bit in a tie. Do we implement IList and get Generic and non-Generic versions of nearly every method we have already implemented? Do we implement IList and lose our guaranteed type-safety? Microsoft really made this a hard choice on us... Yes, we should implement IList if we really want to keep all of our functionality. I am not going to do that though. I have shown you how to implement ICollection<T> and I am going to show you how to implement IList<T>. After that, you should be fully able to implement IList all on your own. For this example, I am going to use a faster solution, the IListSource. IListSource only requires you to implement a property, which should return True, and a function. The function actually returns an IList. Luckily, we do have that IList, since IList<T> does implement IList. Stay alert that you do return the InnerList directly though. People could Add or Remove Cultists (well, if they could actually instantiate them) and make a mess! Remember that our InnerList knows nothing about Ranks! So it might seem like a good, fast solution. In this case, it is just fast however. The only proper way to make your collection bindable is by implementing IList. Note that I do not add a Person to the DataGridView directly. This actually makes the IListSource do just what it needs to do. Show our list of Cultists in the grid.

You can look at the code I have omitted from this article. You will see that I have made some functions and properties Protected. This is because I want to inherit from this class for the next example of IList<T>. Generally it is not a good idea to do this though. Whoever is going to inherit your collection is not said to do this as should. Actually, any class that inherits from Cult is doomed to fail, because who knows when and why to call, for example, AssignNextCaptainIfNecessary? If people really want to extend the Cult class, they can create their own ICollection<Person> and have our Cult as their InnerList.

Also, do not forget to check out how this actually all works by starting up the frmCult. You can add members by filling in a firstname, lastname, and age, and pressing the Add button. To delete members, select one or multiple rows in the DataGridView and press the Remove button. The Form starts with 49 members. The first member you add should assign a new Captain. Try removing a Captain and see how the next member becomes a Captain. Try removing over ten Soldiers and then remove a Captain. No new Captain is assigned because it is not necessary with the current amount of members. The code in frmCult is pretty straightforward. It does not use all the functionality in our Cult class, but it shows nicely how the Marks and Ranks work (which is why we made our custom collection in the first place).

Going from Collection to List

As our Cult grows and grows, it becomes more than a group of happy together individuals. Politics start to play a role and with politics comes corruption. So a captain who has a nephew who also wants to join the Cult has an advantage because his uncle is actually a high ranking member. So what if this new member did not just wait in line, but sneaked in right between all the captains!? He would be a captain in no time! Since the Rank of our CultMembers is decided by their position in our collection, we have to have a way to get that nephew in at, let us say, position four. This is where IList<T> comes in. The IList<T> interface inherits from ICollection<T>. This is pretty easy because that means we have already discussed more than half of the IList<T> interface. So what does IList<T> add to ICollection<T>? Easy, indexing. Now this is a bit confusing with all the Extension Methods we already have. Even in an IEnumerable<T>, we can get items at specific indexes, for example, the Person at the nth position of our Cult. However, the IList<T> Interface also allows for adding at specific indexes. IList<T> only adds three methods and one property to the ICollection<T> Interface: Insert, IndexOf, RemoveAt, and Item (which is a default property, more on this later).

IList_Of_T_.png

I think the names of the methods are actually self-describing and need no further explanation. So let us look at how I have implemented them in the OrganisedCult. In this example, I have omitted the CanJoin method. In the Cult, members could join if no member with the same name has already joined. In the OrganisedCult, a new member should be at least 18 years of age. To focus on the IList<T> methods, I have not included this in the code below.

C#:
C#
public class OrganisedCult : Cult, IList<Person>
{
    public OrganisedCult()
        : base()
    {
    }

    public OrganisedCult(IEnumerable<Person> people)
        : base(people)
    {
    }

    public int IndexOf(Person item)
    {
        Cultist cultist = base.InnerList.Where(c => 
                c.FullName == item.FullName).FirstOrDefault();
        return base.InnerList.IndexOf(cultist);
    }

    public void Insert(int index, Person item)
    {
        if (item == null)
            throw new ArgumentNullException("Item cannot be nothing.");
            // First check if the person can join.

        if (base.CanJoin(item))
        {
            // Create a cultist and insert it to the inner list.
            Cultist cultist = Cultist.CreateCultist(item, base.GetMark());
            base.InnerList.Insert(index, cultist);

            if (index < 2)
            {
                // If the just inserted person has an index lower than
                // two (someone must like him or this is a hostile takeover!)
                // he is either leader or general.
                // Reset the first three ranks in the inner list.
                base.InnerList[0].Rank = CultRanks.Leader;
                base.InnerList[1].Rank = CultRanks.General;
                base.InnerList[2].Rank = CultRanks.Captain;
            }
            else if (base.InnerList[index + 1] != null && 
                     base.InnerList[index + 1].Rank == CultRanks.Captain)
            {
                // If the person above the just inserted person
                // is a captain then the just inserted cultist
                // automatically becomes a captain too.
                cultist.Rank = CultRanks.Captain;
            }
            else
            {
                // If none of the above the just
                // inserted person simply becomes a soldier.
                cultist.Rank = CultRanks.Soldier;
            }

            base.AssignNextCaptainIfNecessary();

        }
    }

    public Person this[int index]
    {
        get { return base.InnerList[index]; }
        set
        {
            // Cannot use the MyBase.InnerList.Contains,
            // because it contains cultists, not persons.
            if (!this.Contains(value) && this.CanJoin(value))
            {
                // Create a new cultist and replace the old cultist with the new one.
                Cultist cultist = Cultist.CreateCultist(value, base.GetMark());
                cultist.Rank = base.InnerList[index].Rank;
                base.InnerList[index].Rank = CultRanks.None;
                base.InnerList[index] = cultist;
            }
        }
    }

    public void RemoveAt(int index)
    {
        // Call the MyBase.Remove with the person
        // at the specified index of the inner list.
        // The MyBase.Remove handles recalculations for ranks etc.
        base.Remove(base.InnerList[index]);
    }
}
VB:
VB
Public Class OrganisedCult _
    Inherits Cult _
    Implements IList(Of Person)

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal people As IEnumerable(Of Person))
        MyBase.New(people)
    End Sub

    Public Function IndexOf(ByVal item As Person) As Integer _
           Implements System.Collections.Generic.IList(Of Person).IndexOf
        Dim cultist = MyBase.InnerList.Where(Function(c) _
            c.FullName = item.FullName).FirstOrDefault
        Return MyBase.InnerList.IndexOf(cultist)
    End Function

    Public Sub Insert(ByVal index As Integer, ByVal item As Person) _
           Implements System.Collections.Generic.IList(Of Person).Insert
        If item Is Nothing Then
            Throw New ArgumentNullException("Item cannot be nothing.")

        ' First check if the person can join.
        If MyBase.CanJoin(item) Then

            ' Create a cultist and insert it to the inner list.
            Dim cultist As Cultist = cultist.CreateCultist(item, MyBase.GetMark)
            MyBase.InnerList.Insert(index, cultist)

            Select Case True

                Case index < 2
                    ' If the just inserted person has an index lower
                    ' than two (someone must like him or this is a hostile takeover!)
                    ' he is either leader or general.
                    ' Reset the first three ranks in the inner list.
                    MyBase.InnerList(0).Rank = CultRanks.Leader
                    MyBase.InnerList(1).Rank = CultRanks.General
                    MyBase.InnerList(2).Rank = CultRanks.Captain

                Case Not MyBase.InnerList(index + 1) Is Nothing AndAlso _
                        MyBase.InnerList(index + 1).Rank = CultRanks.Captain
                    ' If the person above the just inserted person
                    ' is a captain then the just inserted cultist
                    ' automatically becomes a captain too.
                    cultist.Rank = CultRanks.Captain

                Case Else
                    ' If none of the above the just
                    ' inserted person simply becomes a soldier.
                    cultist.Rank = CultRanks.Soldier

            End Select

            MyBase.AssignNextCaptainIfNecessary()

        End If
    End Sub

    Default Public Property Item(ByVal index As Integer) As Person _
            Implements System.Collections.Generic.IList(Of Person).Item
        Get
            Return MyBase.InnerList(index)
        End Get
        Set(ByVal value As Person)
            ' Cannot use the MyBase.InnerList.Contains,
            ' because it contains cultists, not persons.
            If Not Me.Contains(value) AndAlso Me.CanJoin(value) Then
                ' Create a new cultist and replace the old cultist with the new one.
                Dim cultist As Cultist = cultist.CreateCultist(value, MyBase.GetMark)
                cultist.Rank = MyBase.InnerList(index).Rank
                MyBase.InnerList(index).Rank = CultRanks.None
                MyBase.InnerList(index) = cultist
            End If
        End Set
    End Property

    Public Sub RemoveAt(ByVal index As Integer) Implements _
               System.Collections.Generic.IList(Of Person).RemoveAt
        ' Call the MyBase.Remove with the person at the specified index of the inner list.
        ' The MyBase.Remove handles recalculations for ranks etc.
        MyBase.Remove(MyBase.InnerList(index))
    End Sub

End Class

As you can see, the IndexOf function simply returns the index of an item in a list. So what I am doing here is find the Person in my InnerList (remember that every name is unique, so we simply have to find the Cultist with the same name as the Person). When the Cultist is found, I return its 0-based index.

More complicated is the InsertAt method. Basically, what you see is that the Person is inserted at a certain point in the Cult. What happens next is that I check for his index and set his Rank accordingly. After that, all new Captain Ranks should be recalculated. (If the Cult has 49 members and we insert the new member right after the last captain, then the Count of the Cult will reach 50 and a new Captain is assigned. This will be the new member.) All pretty complicated. Probably even unnecessarily complicated, but who said Cults work easy?

Now, let us first look at RemoveAt. This is actually a lot easier than InsertAt. Simply look up the Person at the specified index and call the base.Remove (MyBase.Remove in VB) with the found Person as parameter. The Remove of the Cult collection will recalculate the Ranks.

Now the Item property is a bit of a strange one. This is a default property. In C#, this is indicated by the 'this' keyword. You can now call this Property on an instance of the OrganisedCult class without having to explicitly call it. We have already seen this in VB since it is a little Microsoft gift we get for using VB. In C#, we had to call the ElementAt Extension Method though. But with this default property, we can now do the following in C# too.

C#:
C#
OrganisedCult cult = New OrganisedCult();
// Returns the Person at the 5th index.
Person p1 = cult[5];
// Instead of:
Person p2 = cult.ElementAt(5);

Another thing that is quite odd about this property is that the getter requires a parameter. If you want to assign a new value to this property, you have to provide this parameter.

C#:
C#
// 5 is the parameter of the Getter.
cult[5] = new Person("John", "Doe", 18);
VB:
VB
' 5 is the parameter of the Getter.
cult(5) = New Person("John", "Doe", 18)

And that already concludes the implementation of the IList<T> interface! Not very hard, was it? The frmOrganisedCult calls all of the new methods in OrganisedCult. It works in much the same way as frmCult. Take a look at the code at your own leisure to see how it works. I admit that the code in this Form is not always of the same quality and beauty as my other code (hopefully) is. But it is only a little Form to test the OrganisedCult vlass. For this, it is sufficient. When entering an index, either for inserting, replacing, or deleting, remember that indexes are 0-based. This means that index 0 is the first item in the Cult and that the amount of Cultists in the Cult - 1 is the last index.

The magic stove

Once upon a time there was a beautiful girl named Elsa. Elsa was so pretty that every man in the kingdom had asked for her hand in marriage. All but one. Elsa was not interested in all those men wanting her hand in marriage. Elsa had already lost her heart to someone... The one person who did not ask her! This person was the handsome prince Eatalot. Prince Eatalot has one hobby, eating. The prince loved eating so much that he could not stop. He ate pie and sweets and fruit and vegetables and probably anything you could think of. And exactly this hobby of his was a problem for Elsa. Elsa could not cook... One day Elsa was dreaming about the prince when she decided to take some cooking lessons. She bought a stove and a cooking book. She went to cooking classes and tried to cook every meal that was in her cooking book. But no matter how hard she tried, she always failed. After practicing several weeks and still not being able to bake an egg, Elsa got so frustrated that she threw her cooking book in the stove! "Stupid food, I never want to cook a meal again!", she shouted. And just as she was about to burst out in tears, something amazing happened... The stove started to huff and puff and all of a sudden, the door opened and all kinds of food came flying out! When the stove stopped moving, it had produced a meal that could have been served to kings and emperors. Elsa stood amazed, staring at the stove, which now looked like an ordinary stove again. After her amazement, Elsa hurried to the market to buy some more ingredients. When she got home, she threw them in the stove and once again, the stove started huffing and puffing and made a delicious pie out of the ingredients. Then Elsa grabbed the stove and went to the prince. The prince was just about to have his dinner when Elsa arrived. When the prince saw Elsa, he was annoyed. "Why are you interrupting my dinner?", he demanded. "Oh, handsome prince Eatalot" Elsa said, "I have come to ask you to marry me!". The prince was impressed with Elsa's looks, but knew that he could not marry just anyone. "Serve me a meal that is better than any meal I have ever had and I will have your hand in marriage", the prince said. Elsa soon hurried to the kitchen and got all the ingredients she could find. She threw them in the stove and the stove, once again, baked the tastiest meals she had ever seen. Elsa presented the meal to the prince who took a bite and almost immediately jumped into the air shouting "My! This is by far the tastiest meal I have ever eaten!" And so the prince married Elsa and they lived happily ever after. The end.

Building the stove using IDictionary<TKey, TValue>

Now wouldn't we all want a stove like that? Well, it is possible! Using IDictionary<TKey, TValue>, we can make ourselves a stove that does just that. As the name implies, a Dictionary<TKey, TValue> is a collection type that can hold values that can be looked up by a unique key value. IDictionary<TKey, TValue> inherits from ICollection<KeyValuePair<TKey, TValue>>. That is a Generic struct (Structure in VB) in a Generic Interface! So the <T> in ICollection<T> was replaced with a KeyValuePair<TKey, TValue>. The KeyValuePair has two important properties, Key and Value. Knowing this, it should not be a surprise that you can foreach through the items in a Dictionary<int, String> as follows:

C#
C#
foreach (KeyValuePair<int, String> pair in myDictionary)
{
    Console.WriteLine(pair.Key.ToString() + " - " + pair.Value);
}
VB:
VB
For Each pair As KeyValuePair(Of Integer, String) in myDictionary
    Console.WriteLine(pair.Key.ToString() & " - " & pair.Value)
Next

The outcome could be something like:

1 - John the 1st Of Nottingham
2 - John Doe
3 - Naerling
4 - How is this for a String?

So if a Dictionary has a Key and a Value, then that must be pretty different from an ICollection and IList where we only had a Value, right? Partly true. Let us first take a look at the definition of an IDictionary<TKey, TValue>.

IDictionary_Of_TKey__TValue_.png

The methods Add, Contains, and Remove have a 'Dictionary-version'. It is possible to add a key and value separately, so the user of the Dictionary does not have to create a KeyValuePair for each entry that he wants to add. Other than that, you might notice that Values are gotten or removed through their keys. There is a ContainsKey function and a Remove function that takes only a Key as parameter. You also might have noticed that the default property Item is also back and takes a TKey as argument for the getter (much the same as in IList, except that took an index). This also means that a Dictionary Value is not gotten by index, but by Key. New here are the TryGetValue function and the properties Keys and Values.

So how does this fit into the magic stove story? Well, if you think about it, the stove needed a certain amount of ingredients. So we will need a Dictionary that has an ingredient as a Key and an amount as Value. Now I will not lie to you. The following piece of code is quite advanced and I may have made this a bit too complicated, but I will guide you through it step by step. For starters, I have created three Interfaces. One defining an ingredient, one defining a meal, and one defining a recipe. The recipe is the key link between the ingredient and the meal, and I have also created a base class called BaseRecipe. The Interfaces look as follows:

C#:
C#
public interface IIngredient
{
    String Name { get; }
}

public interface IMeal
{
    String Name { get; }
    int Calories { get; }
}

public interface IRecipe
{
    String Name { get; }
    ReadOnlyIngredients NeededIngredients { get; }
    IMeal Cook(Ingredients ingredients);
}

public abstract class BaseRecipe : IRecipe
{

    public BaseRecipe()
        : base() { }

    public abstract string Name { get; }
    public abstract ReadOnlyIngredients NeededIngredients { get; }

    protected abstract IMeal CookMeal();

    public IMeal Cook(Ingredients ingredients)
    {
        foreach (KeyValuePair<IIngredient, int> pair in NeededIngredients)
        {
            if (!ingredients.Contains(pair))
            {
                // If the ingredients are not sufficient throw an Exception.
                throw new NotEnoughIngredientsException(this);
            }
            else
            {
                // Remove the used ingredients from the ingredients.
                ingredients.Add(pair.Key, -pair.Value);
            }
        }
        return CookMeal();
    }
}
VB:
VB
Public Interface IIngredient
    ReadOnly Property Name As String
End Interface

Public Interface IMeal
    ReadOnly Property Name As String
    ReadOnly Property Calories As Integer
End Interface

Public Interface IRecipe
    ReadOnly Property Name As String
    ReadOnly Property NeededIngredients As ReadOnlyIngredients
    Function Cook(ByVal ingredients As Ingredients) As IMeal
End Interface

Public MustInherit Class BaseRecipe
    Implements IRecipe

    Public Sub New()
        MyBase.New()
    End Sub

    Public MustOverride ReadOnly Property Name As String Implements IRecipe.Name
    Public MustOverride ReadOnly Property NeededIngredients _
           As ReadOnlyIngredients Implements IRecipe.NeededIngredients

    Protected MustOverride Function CookMeal() As IMeal

    Public Function Cook(ByVal ingredients As Ingredients) As IMeal Implements IRecipe.Cook
        For Each pair As KeyValuePair(Of IIngredient, Integer) In NeededIngredients
            If Not ingredients.Contains(pair) Then
                ' If the ingredients are not sufficient throw an Exception.
                Throw New NotEnoughIngredientsException(Me)
            Else
                ' Remove the used ingredients from the ingredients.
                ingredients.Add(pair.Key, -pair.Value)
            End If
        Next
        Return CookMeal()
    End Function

End Class

So how do we get to these Interfaces? We get there through our Stove class, which Inherits from the Ingredients class. The Ingredients class is our real IDictionary<IIngredient, int>. Since this is a pretty big class, I am going to break it up to you one step at a time. Let us first look at the Add and Remove methods that the IDictionary interface gives us.

C#:
C#
public virtual void Add(IIngredient key, int value)
{
    // Cast to call the explicitly implemented Add Method.
    (this as ICollection<KeyValuePair<IIngredient, int>>).Add(
             new KeyValuePair<IIngredient, int>(key, value));
}

void ICollection<KeyValuePair<IIngredient, int>>.Add(
     System.Collections.Generic.KeyValuePair<IIngredient, int> item)
{
    // If the key already exists then do not add a new KeyValuePair.
    // Add the specified amount to the already existing KeyValuePair instead.
    if (this.ContainsKey(item.Key))
    {
        if (this[item.Key] + item.Value < 0)
        {
            throw new InvalidOperationException(ErrorMessage);
        }
        else
        {
            this[item.Key] += item.Value;
        }
    }
    else
    {
        if (item.Value < 0)
        {
            throw new InvalidOperationException(ErrorMessage);
        }
        else
        {
            _innerDictionary.Add(item.Key, 
               new ChangeableDictionaryValue<int>(item.Value));
        }
    }
}

public virtual bool Remove(IIngredient key)
{
    if (this.ContainsKey(key))
    {
        return _innerDictionary.Remove(GetPairByKeyType(key).Key);
    }
    else
    {
        return false;
    }
}

bool ICollection<KeyValuePair<IIngredient, int>>.Remove(
     System.Collections.Generic.KeyValuePair<IIngredient, int> item)
{
    this.Add(item.Key, -item.Value);
    return true;
}
VB:
VB
Public Overridable Sub Add(ByVal key As IIngredient, ByVal value As Integer) _
       Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Add
    Add(New KeyValuePair(Of IIngredient, Integer)(key, value))
End Sub

Private Sub Add(ByVal item As System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)) _
        Implements System.Collections.Generic.ICollection(-
        Of System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)).Add
    ' If the key already exists then do not add a new KeyValuePair.
    ' Add the specified amount to the already existing KeyValuePair instead.
    If Me.ContainsKey(item.Key) Then
        If Me(item.Key) + item.Value < 0 Then
            Throw New InvalidOperationException(ErrorMessage)
        Else
            Me(item.Key) += item.Value
        End If
    Else
        If item.Value < 0 Then
            Throw New InvalidOperationException(ErrorMessage)
        Else
            _innerDictionary.Add(item.Key, _
                New ChangeableDictionaryValue(Of Integer)(item.Value))
        End If
    End If
End Sub

Public Overridable Function Remove(ByVal key As IIngredient) As Boolean _
       Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Remove
    If Me.ContainsKey(key) Then
        Return _innerDictionary.Remove(GetPairByKeyType(key).Key)
    Else
        Return False
    End If
End Function

Private Function Remove(ByVal item As System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)) _
        As Boolean Implements System.Collections.Generic.ICollection(_
        Of System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)).Remove
    Me.Add(item.Key, -item.Value)
    Return True
End Function

The Add methods are pretty straightforward. The IDictionary adds an Add method that gets a key and value as parameters and it simply creates a KeyValuePair and passes it to the Add method we already know from ICollection. Now there is a little weird thing about this Dictionary. If the Dictionary already contains the key, it does not add a new KeyValuePair, instead it looks up the key and adds the value of the new KeyValuePair to that of the existing one. Normally this would be a problem. The KeyValuePair is a struct (Structure in VB) and is immutable (key and value cannot be changed after they have been set). However, I am not changing the value of the KeyValuePair, I am changing the value of the value! Notice how I add a ChangeableDictionaryValue<int> to the _innerDictionary instead of an int. I have done this specifically so I could change the value! Another thing to be watchful of in this Dictionary is that the Remove function that takes a KeyValuePair as argument actually adds the negative value to the specified key!

And here comes the real surprise. This Dictionary (pretty much like our Cult) does not check if an item is already in the Dictionary by looking at the pointer of an item (location in memory), but by looking at the Type of the key! So if two completely different KeyValuePairs with the same key would be added to the Dictionary, that would mean that a single item is added with a value that has the sum of both KeyValuePairs.

How this works specifically becomes clear when looking at the Contains and ContainsKey functions.

C#:
C#
public bool Contains(IIngredient ingredient, int amount)
{
    return this.Contains(new KeyValuePair<IIngredient, int>(ingredient, amount));
}

bool ICollection<KeyValuePair<IIngredient, int>>.Contains(
     System.Collections.Generic.KeyValuePair<IIngredient, int> item)
{
    if (this.ContainsKey(item.Key))
    {
        return GetPairByKeyType(item.Key).Value.Value >= item.Value;
    }
    else
    {
        return false;
    }
}

public bool ContainsKey(IIngredient key)
{
    KeyValuePair<IIngredient, ChangeableDictionaryValue<int>> pair = 
       _innerDictionary.Where(p => p.Key.GetType() == key.GetType()).FirstOrDefault();
    if (pair.Key == null)
    {
        return false;
    }
    else
    {
        return true;
    }
}
VB:
VB
Public Function Contains(ByVal ingredient As IIngredient, ByVal amount As Integer) As Boolean
    Return Me.Contains(New KeyValuePair(Of IIngredient, Integer)(ingredient, amount))
End Function

Private Function Contains(ByVal item As System.Collections.Generic.KeyValuePair(_
        Of IIngredient, Integer)) As Boolean Implements System.Collections.Generic.ICollection(_
        Of System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)).Contains
    If Me.ContainsKey(item.Key) Then
        Return GetPairByKeyType(item.Key).Value.Value >= item.Value
    Else
        Return False
    End If
End Function

Public Function ContainsKey(ByVal key As IIngredient) As Boolean Implements _
       System.Collections.Generic.IDictionary(Of IIngredient, Integer).ContainsKey
    Dim pair As KeyValuePair(Of IIngredient, ChangeableDictionaryValue(Of Integer)) = _
       _innerDictionary.Where(Function(p) p.Key.GetType = key.GetType).FirstOrDefault
    If pair.Key Is Nothing Then
        Return False
    Else
        Return True
    End If
End Function

As you can see, the ContainsKey checks if the Type of the provided key is the same as the key of any KeyValuePair in the Dictionary. The GetType function requires two types to be exactly the same (so it will not see base or derived types). More on that here. And as you might have guessed, the Contains that takes a KeyValuePair as argument does not check if the provided KeyValuePair is a pointer to a location in memory that some KeyValuePair in the Dictionary is also pointing to, but checks if the Type of the key is existent in the Dictionary and if its value is greater than or equal to the value of the specified KeyValuePair. So what it basically does is check if there is x amount of ingredient y in the Dictionary.

The next Function of the IDictionary is a pretty straightforward one, TryGetValue. What it does is it tries to get a value with a given key. The value is passed as an out parameter (ByRef in VB) and returns as Nothing or a default value (for value types) if the key was not found (instead of raising an Exception!). TryGetValue returns a Boolean specifying whether the value was found or not.

C#;
C#
public bool TryGetValue(IIngredient key, out int value)
{
    if (this.ContainsKey(key))
    {
        value = GetPairByKeyType(key).Value.Value;
        return true;
    }
    else
    {
        value = default(int);
        return false;
    }
}
VB:
VB
Public Function TryGetValue(ByVal key As IIngredient, ByRef value As Integer) _
       As Boolean Implements System.Collections.Generic.IDictionary(_
       Of IIngredient, Integer).TryGetValue
    If Me.ContainsKey(key) Then
        value = GetPairByKeyType(key).Value.Value
        Return True
    Else
        value = Nothing
        Return False
    End If
End Function

As you can see, if the key is found, we assign the found value to the value parameter and return True (value was found). If the value is not found, we assign the default value to the value parameter using the default keyword in C# (we could have just said the default value is 0, but I did not know the default keyword and it looked fun). For VB, it is sufficient to make it Nothing (which will assign the default value for value types). We have already seen the Item property in the IList<T> Interface, but let us just quickly review it for the IDictionary.

C#:
C#
public virtual int this[IIngredient key]
{
    get { return GetPairByKeyType(key).Value.Value; }
    set
    {
        if (value < 0)
        {
            throw new InvalidOperationException(ErrorMessage);
        }
        else
        {
            GetPairByKeyType(key).Value.Value = value;
        }
    }
}
VB:
VB
Default Public Overridable Property Item(ByVal key As IIngredient) _
        As Integer Implements _
        System.Collections.Generic.IDictionary(Of IIngredient, Integer).Item
    Get
        Return GetPairByKeyType(key).Value.Value
    End Get
    Set(ByVal value As Integer)
        If value < 0 Then
            Throw New InvalidOperationException(ErrorMessage)
        Else
            GetPairByKeyType(key).Value.Value = value
        End If
    End Set
End Property

No surprises there really, the only difference with IList<T> is that the getter does not require an index (Integer) as parameter, but a key. In this case, an IIngredient. Then there are really only two properties that we have not discussed yet. The Keys and the Values properties. As you might have guessed, these simply return a collection (ICollection<T> actually) containing every key or every value in the Dictionary. Most of the time, you can just return _innerDictionary.Keys and _innerDictionary.Values. In this case, _innerDictionary does not have the same type as the TValue of our Interface implementation. So we first need to create a new list and put our Integer values in there. I have used this trick again for the GetEnumerator and CopyTo methods that we have already seen in ICollection<T>.

C#:
C#
public System.Collections.Generic.ICollection<IIngredient> Keys
{
    get { return _innerDictionary.Keys; }
}

public System.Collections.Generic.ICollection<int> Values
{
    get
    {
        return InnerDictToIntValue().Values;
    }
}

private Dictionary<IIngredient, int> InnerDictToIntValue()
{
    Dictionary<IIngredient, int> dict = new Dictionary<IIngredient, int>();
    // Put all KeyValuePairs of the _innerDictionary
    // in the new dictionary using a one-line LINQ query.
    _innerDictionary.ToList().ForEach(pair => dict.Add(pair.Key, pair.Value.Value));
    return dict;
}
VB:
VB
Public ReadOnly Property Keys As System.Collections.Generic.ICollection(Of IIngredient) _
       Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Keys
    Get
        Return _innerDictionary.Keys
    End Get
End Property

Public ReadOnly Property Values As System.Collections.Generic.ICollection(Of Integer) _
       Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Values
    Get
        Return InnerDictToIntValue.Values
    End Get
End Property

Private Function InnerDictToIntValue() As Dictionary(Of IIngredient, Integer)
    Dim dict As New Dictionary(Of IIngredient, Integer)
    ' Put all KeyValuePairs of the _innerDictionary
    ' in the new dictionary using a one-line LINQ query.
    _innerDictionary.ToList.ForEach(Sub(pair) dict.Add(pair.Key, pair.Value.Value))
    Return dict
End Function

The only really exciting piece of code there is in the InnerDictToIntValue function. In this function, I call the ToList Extension Method. For a Dictionary, it returns an IList<KeyValuePair<TKey, TValue>>). I then call ForEach on the return value. ForEach is an Extension Method on IList<T> and takes a Delegate as argument. This is out of the scope of the article though and I have used an anonymous method to put each key and value of our _innerDictionary into a new Dictionary containing Integers as values. I then return the new Dictionary that is consistent with our Interface implementation.

That was it! It was not easy, but probably a lot easier than you would have thought! So now we have a Dictionary that can store a specific amount of ingredients, but... what about the Stove? Noticed how I made some methods in the Dictionary virtual (Overridable in VB)? Well, take a look at the Stove class. It inherits our Dictionary and overrides these methods and instead of just adding the ingredients to its _innerDictionary, it also checks if it has assembled enough ingredients to cook any meal that it knows from its learned recipes.

C#:
C#
public class Stove : Ingredients
{

    public event MealCookedEventHandler MealCooked;
    public delegate void MealCookedEventHandler(
           object sender, MealCookedEventArgs e);
        
    private List<IRecipe> _recipes;
        
    public Stove()
        : base()
    {
        _recipes = new List<IRecipe>();
    }

    public void AddRecipes(IEnumerable<IRecipe> recipes)
    {
        _recipes.AddRange(recipes);
        CookMeals();
    }

    Public void AddRecipes(IRecipe recipe)
    {
        _recipes.Add(recipe);
        CookMeals();
    }

    private void CookMeals()
    {
        foreach (IRecipe recipe in _recipes)
        {
            bool canCook = true;
            foreach (KeyValuePair<IIngredient, int> ingredient 
                     in recipe.NeededIngredients)
            {
                if (!this.Contains(ingredient.Key, ingredient.Value))
                {
                    canCook = false;
                }
            }

            if (canCook)
            {
                if (MealCooked != null)
                {
                    MealCooked(this, new MealCookedEventArgs(recipe.Cook(this)));
                }
                CookMeals();
            }
        }
    }
        
    public override void Add(IIngredient key, int value)
    {
        base.Add(key, value);
        // Only cook when ingredients were added!
        if (value > 0)
        {
            CookMeals();
        }
    }

    public override int this[IIngredient key]
    {
        get { return base[key]; }
        set
        {
            // Check if the current value is smaller than the new value.
            // If it is not smaller then ingredients are removed.
            bool mustCook = (base[key] < value);
            base[key] = value;
            // Only cook if ingredients were added!
            if (mustCook)
            {
                CookMeals();
            }
        }
    }
}
VB:
VB
Public Class Stove
    Inherits Ingredients

    Public Event MealCooked(ByVal sender As Object, ByVal e As MealCookedEventArgs)

    Private _recipes As List(Of IRecipe)

    Public Sub New()
        MyBase.New()
        _recipes = New List(Of IRecipe)
    End Sub

    Public Sub AddRecipes(ByVal recipes As IEnumerable(Of IRecipe))
        _recipes.AddRange(recipes)
        CookMeals()
    End Sub

    Public Sub AddRecipes(ByVal recipe As IRecipe)
        _recipes.Add(recipe)
        CookMeals()
    End Sub

    Private Sub CookMeals()
        For Each recipe As IRecipe In _recipes
            Dim canCook As Boolean = True
            For Each ingredient As KeyValuePair(Of IIngredient, _
                     Integer) In recipe.NeededIngredients
                If Not Me.Contains(ingredient.Key, ingredient.Value) Then
                    canCook = False
                End If
            Next

            If canCook Then
                RaiseEvent MealCooked(Me, New MealCookedEventArgs(recipe.Cook(Me)))
                CookMeals()
            End If
        Next
    End Sub

    Public Overrides Sub Add(ByVal key As IIngredient, ByVal value As Integer)
        MyBase.Add(key, value)
        ' Only cook when ingredients were added!
        If value > 0 Then
            CookMeals()
        End If
    End Sub

    Default Public Overrides Property Item(ByVal key As IIngredient) As Integer
        Get
            Return MyBase.Item(key)
        End Get
        Set(ByVal value As Integer)
            ' Check if the current value is smaller than the new value.
            ' If it is not smaller then ingredients are removed.
            Dim mustCook As Boolean = (MyBase.Item(key) < value)
            MyBase.Item(key) = value
            ' Only cook if ingredients were added!
            If mustCook Then
                CookMeals()
            End If
        End Set
    End Property

End Class

So just take a look. It is not a lot of code and it is not very hard to understand either. You can add IRecipes to the Stove and as soon as ingredients are added to the Stove, CookMeals is called. In this method I loop through the IRecipes and check if the Stove has enough ingredients by comparing it to the IRecipe.NeededIngredients. If there are enough ingredients, I call IRecipe.Cook and pass the Stove as Ingredients. The Cook method will then consume the necessary ingredients (subtracting them from the Stove's ingredients count) and return the cooked IMeal. The BaseRecipe makes sure this pattern is implemented correctly. You might have noticed that the NeededIngredients is a ReadOnlyIngredients class. This is a class to which you cannot add or remove any Ingredients. This class inherits from System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>. The class is very simple.

C#
C#
public class ReadOnlyIngredients : 
  System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>>
{
    public ReadOnlyIngredients(Ingredients ingredients)
        : base(ingredients.ToList()) { }
}
VB:
VB
Public Class ReadOnlyIngredients _
    Inherits ObjectModel.ReadOnlyCollection(Of KeyValuePair(Of IIngredient, Integer))

    Public Sub New(ByVal ingredients As Ingredients)
        MyBase.New(ingredients.ToList)
    End Sub

End Class

I have not discussed ReadOnlyCollections, but with a name like that, I do not think they need further explanation. It is simply an IList<T> that does not allow adding or removing of items.

I have made numerous ingredients by implementing IIngredient. I have also implemented three IRecipes and three corresponding IMeals. You can check them out yourself to see usages of the Ingredients Dictionary and the ReadOnlyIngredients collection. For examples and uses of the Stove class, check out frmMagicStove. You can pick any ingredient from the ComboBox on top and specify an amount to add to the Stove. You will see the ingredients getting added to the Stove and whenever the Stove has enough ingredients, it will cook the first recipe it finds until it has not enough ingredients anymore and will either cook another meal or stop cooking. Every time a meal is cooked by the Stove, an Event is raised. This Event has the IMeal that was cooked (and this is your only chance to do something with it, use it or lose it!). I add it to a List of IMeals and bind it to the lower grid, which shows all meals that have been cooked.

Points of Interest

Well, I for one found all this very interesting. Actually, to tell you the truth, I had never before made a custom collection before starting this article. So I have learned a lot from it. And I can only say that I became very enthusiastic about collections in general! While writing this article, I started using LINQ more often and I found that the System.Linq namespace alone holds eight(!) Interfaces that derive from IEnumerable! And many of those are new to Framework 4.0. Collections are a powerful tool to work with any kind of data, and you will find that it is in fact at the base of every application. This article discussed only the basics, and not even everything of those basics. For example, I have not discussed IComparable<T>, IComparer<T>, Comparer<T> (all used for sorting) and IEquatable<T> (which stands to IEqualityComparer<T> as IComparable<T> stands to IComparer<T>). I have also not discussed some more common Generic Lists such as Queue<T> and Stack<T>. These classes are all well documented on MSDN though. So if you want to know more about collections in .NET, I suggest you start there. Or here on CodeProject, where there are some very good articles on collections of all sorts. Also, if you are a C# programmer, you should check out the yield keyword, which can be used in Iterators. Yield and Iterator functions for VB are available in the new Visual Studio ASync CTP (SP1 Refresh) version, but I have not tried it yet. What has helped me a lot to learn about collections is just browsing through the System.Collections namespace and sub namespaces and checking out the collections found there on the internet and by looking at their source code using tools such as Reflector and the free JustDecompile. I also challenge you to look at my sample projects and think of where I could have done better (I can think of some points). I hope this article gave you some insights into the .NET world of collections. How they work, how they can be used, and how they can be created. We have created some very crazy and silly collections in this article, and I hope that it catches on because it is fun to do. Here are just a few links for you to further explore the possibilities with collections.

Good luck with it and Happy Coding! :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO JUUN Software
Netherlands Netherlands
Sander Rossel is a Microsoft certified professional developer with experience and expertise in .NET and .NET Core (C#, ASP.NET, and Entity Framework), SQL Server, Azure, Azure DevOps, JavaScript, MongoDB, and other technologies.

He is the owner of JUUN Software, a company specializing in custom software. JUUN Software uses modern, but proven technologies, such as .NET Core, Azure and Azure DevOps.

You can't miss his books on Amazon and his free e-books on Syncfusion!

He wrote a JavaScript LINQ library, arrgh.js (works in IE8+, Edge, Firefox, Chrome, and probably everything else).

Check out his prize-winning articles on CodeProject as well!

Comments and Discussions

 
GeneralRe: My vote of 5 Pin
Sander Rossel17-Oct-11 10:50
professionalSander Rossel17-Oct-11 10:50 
GeneralMy vote of 5 Pin
NetDefender15-Oct-11 9:47
NetDefender15-Oct-11 9:47 
GeneralRe: My vote of 5 Pin
Sander Rossel15-Oct-11 11:27
professionalSander Rossel15-Oct-11 11:27 
GeneralMy vote of 5 Pin
Espen Harlinn14-Oct-11 22:43
professionalEspen Harlinn14-Oct-11 22:43 
GeneralRe: My vote of 5 Pin
Sander Rossel15-Oct-11 0:40
professionalSander Rossel15-Oct-11 0:40 
GeneralI followed the example but it doesn't work. Pin
Olivier_Giulieri12-Oct-11 15:41
Olivier_Giulieri12-Oct-11 15:41 
GeneralRe: I followed the example but it doesn't work. Pin
Sander Rossel12-Oct-11 20:21
professionalSander Rossel12-Oct-11 20:21 
QuestionMissing code Pin
ARBebopKid12-Oct-11 5:49
ARBebopKid12-Oct-11 5:49 
AnswerRe: Missing code Pin
Sander Rossel12-Oct-11 6:48
professionalSander Rossel12-Oct-11 6:48 
GeneralRe: Missing code Pin
ARBebopKid12-Oct-11 7:07
ARBebopKid12-Oct-11 7:07 
GeneralRe: Missing code Pin
Sander Rossel12-Oct-11 7:14
professionalSander Rossel12-Oct-11 7:14 
AnswerRe: Missing code Pin
Sander Rossel14-Oct-11 21:49
professionalSander Rossel14-Oct-11 21:49 
GeneralRe: Missing code Pin
ARBebopKid18-Oct-11 4:06
ARBebopKid18-Oct-11 4:06 
QuestionCool Article Pin
Dave Kerr12-Oct-11 0:07
mentorDave Kerr12-Oct-11 0:07 
AnswerRe: Cool Article Pin
Sander Rossel12-Oct-11 0:53
professionalSander Rossel12-Oct-11 0:53 
QuestionNice Article Pin
Simon_Whale11-Oct-11 22:08
Simon_Whale11-Oct-11 22:08 
AnswerRe: Nice Article Pin
Sander Rossel11-Oct-11 22:24
professionalSander Rossel11-Oct-11 22:24 
GeneralRe: Nice Article Pin
Simon_Whale11-Oct-11 22:29
Simon_Whale11-Oct-11 22:29 
Questionyield operator Pin
JLuis Estrada11-Oct-11 9:28
JLuis Estrada11-Oct-11 9:28 
AnswerRe: yield operator Pin
Sander Rossel11-Oct-11 22:54
professionalSander Rossel11-Oct-11 22:54 
AnswerRe: yield operator Pin
Sander Rossel12-Oct-11 7:12
professionalSander Rossel12-Oct-11 7:12 
GeneralRe: yield operator Pin
JLuis Estrada12-Oct-11 7:16
JLuis Estrada12-Oct-11 7:16 
GeneralRe: yield operator Pin
Sander Rossel12-Oct-11 7:25
professionalSander Rossel12-Oct-11 7:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.