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).
Array
s (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 Array
s over .NET collections. However, Array
s 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
Object
s. Object
s 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 String
s, Integer
s, Form
s... 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 Form
s 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 Array
s or List
s? 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#:
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)
{
}
}
}
public class TestEnumerator : System.Collections.IEnumerable
{
public System.Collections.IEnumerator GetEnumerator()
{
throw new NotImplementedException(
"No need to implement this for our example.");
}
}
}
VB:
Public Class Test
Public Sub New()
Dim te As New TestEnumerator
For Each o In te
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 Object
s 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#:
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:
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#:
foreach(var o in te)
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#:
System.Collections.IEnumerable te = new TestEnumerable();
IEnumerable<int> te = new TestEnumerable();
VB:
Dim te As IEnumerable = New TestEnumerable
Dim te As IEnumerable(Of Integer) = New TestEnumerable
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...
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.
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#:
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:
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#:
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) + ", ";
}
txtAlphabet.Text += _alphabet.ElementAt(count - 1);
}
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
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#:
private void btnGetChar_Click(object sender, EventArgs e)
{
if (txtIndex.Text != String.Empty)
{
txtChar.Text =
_alphabet.ElementAtOrDefault(Convert.ToInt32(txtIndex.Text) - 1);
}
}
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.
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 Person
s is actually a Cultist
factory! That also means that while
it is an ICollection<Person>
, the GetEnumerator
function actually returns an Enumerator that contains Cultist
s.
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#:
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.");
if (CanJoin(item))
{
Cultist cultist = Cultist.CreateCultist(item, GetMark());
this.InnerList.Add(cultist);
int count = this.InnerList.Count;
switch (count)
{
case 1:
cultist.Rank = CultRanks.Leader;
break;
case 10:
this.InnerList[1].Rank = CultRanks.General;
cultist.Rank = CultRanks.Soldier;
break;
default:
cultist.Rank = CultRanks.Soldier;
break;
}
AssignNextCaptainIfNecessary();
}
}
public void Clear()
{
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)
{
Cultist cultist = this.InnerList.Where(c =>
c.FullName == item.FullName).FirstOrDefault();
if (cultist != null)
{
if (this.InnerList.Remove(cultist))
{
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:
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.")
If CanJoin(item) Then
Dim cultist As Cultist = cultist.CreateCultist(item, GetMark)
Me.InnerList.Add(cultist)
Dim count = Me.InnerList.Count
Select Case count
Case 1
cultist.Rank = CultRanks.Leader
Case 10
Me.InnerList(1).Rank = CultRanks.General
cultist.Rank = CultRanks.Soldier
Case Else
cultist.Rank = CultRanks.Soldier
End Select
AssignNextCaptainIfNecessary()
End If
End Sub
Public Sub Clear() Implements System.Collections.Generic.ICollection(Of Person).Clear
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
Dim cultist = Me.InnerList.Where(Function(c) c.FullName = item.FullName).FirstOrDefault
If Not cultist Is Nothing Then
If Me.InnerList.Remove(cultist) Then
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 Mark
s
and Rank
s. 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 CultRank
s 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 Cultist
s. 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 Rank
s. 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 Cultist
s to it?
That way I could also always check if the InnerList
actually contains the Person
s 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 Cultist
s.
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#:
protected virtual bool CanJoin(Person person)
{
return !this.Contains(person, new PersonNameEqualityComparer());
}
VB:
Protected Overridable Function CanJoin(ByVal person As Person) As Boolean
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#:
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:
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 Person
s as arguments and returns a Boolean
representing
wither the two Person
s 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 Object
s!
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
Cultist
s (well, if they could actually instantiate them) and make a mess! Remember that our InnerList
knows nothing about Rank
s! 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 Cultist
s 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
Soldier
s 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
Mark
s and
Rank
s 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 CultMember
s
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).
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#:
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.");
if (base.CanJoin(item))
{
Cultist cultist = Cultist.CreateCultist(item, base.GetMark());
base.InnerList.Insert(index, cultist);
if (index < 2)
{
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)
{
cultist.Rank = CultRanks.Captain;
}
else
{
cultist.Rank = CultRanks.Soldier;
}
base.AssignNextCaptainIfNecessary();
}
}
public Person this[int index]
{
get { return base.InnerList[index]; }
set
{
if (!this.Contains(value) && this.CanJoin(value))
{
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)
{
base.Remove(base.InnerList[index]);
}
}
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.")
If MyBase.CanJoin(item) Then
Dim cultist As Cultist = cultist.CreateCultist(item, MyBase.GetMark)
MyBase.InnerList.Insert(index, cultist)
Select Case True
Case index < 2
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
cultist.Rank = CultRanks.Captain
Case Else
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)
If Not Me.Contains(value) AndAlso Me.CanJoin(value) Then
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
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
Cult
s 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 Rank
s.
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#:
OrganisedCult cult = New OrganisedCult();
Person p1 = cult[5];
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#:
cult[5] = new Person("John", "Doe", 18);
VB:
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 Cultist
s 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#
foreach (KeyValuePair<int, String> pair in myDictionary)
{
Console.WriteLine(pair.Key.ToString() + " - " + pair.Value);
}
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>
.
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 Value
s 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#:
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))
{
throw new NotEnoughIngredientsException(this);
}
else
{
ingredients.Add(pair.Key, -pair.Value);
}
}
return CookMeal();
}
}
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
Throw New NotEnoughIngredientsException(Me)
Else
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#:
public virtual void Add(IIngredient key, int value)
{
(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 (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:
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 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 KeyValuePair
s 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 KeyValuePair
s.
How this works specifically becomes clear when looking at the Contains
and ContainsKey
functions.
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:
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#;
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:
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#:
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:
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#:
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>();
_innerDictionary.ToList().ForEach(pair => dict.Add(pair.Key, pair.Value.Value));
return dict;
}
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)
_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 Integer
s 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#:
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);
if (value > 0)
{
CookMeals();
}
}
public override int this[IIngredient key]
{
get { return base[key]; }
set
{
bool mustCook = (base[key] < value);
base[key] = value;
if (mustCook)
{
CookMeals();
}
}
}
}
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)
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)
Dim mustCook As Boolean = (MyBase.Item(key) < value)
MyBase.Item(key) = value
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 IRecipe
s to the Stove
and as soon as ingredients are added to the Stove
, CookMeals
is called. In this method I loop through the IRecipe
s
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 Ingredient
s.
This class inherits from System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>
. The class is very simple.
C#
public class ReadOnlyIngredients :
System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>>
{
public ReadOnlyIngredients(Ingredients ingredients)
: base(ingredients.ToList()) { }
}
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 ReadOnlyCollection
s, 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 IRecipe
s and three corresponding
IMeal
s. 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 IMeal
s 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 List
s 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! :)