Programming involves manipulating objects. However, often objects are not isolated. Rather, objects often only make sense in context with other, similar objects. Because of this, it becomes necessary to organize objects. There are two methods of organization: arrays and collections. This article will focus on collections.
Arrays are fine if you know exactly what will be in them at compile-time or if you can determine how many elements they will contain at run-time. If you are unable to make either determination, though, then using arrays becomes problematic. Instead, when it's necessary to organize arbitrary amounts of data at run-time, collections are more suited to the task. The .NET framework contains a good many collections designed for different scenarios. This article will explore these collections.
Lists (And a Discussion of Generic Collections)
First, let's take a look at an array, examining how it functions. There are two ways we can define an array. The first is by providing initial values, and the second is by providing a size. Here, we define two arrays using both methods:
string[] array1 = {"one", "two", "three"};
int[] array2 = new int[3];
We can access individual elements of either array by providing an index. Here, we write the first element of the first array to the console:
Console.WriteLine(array1[0]);
Assignment is done in a similar manner. Here, we re-assign the value of the second element of the first array, and we re-assign the values of each element of the second array:
array1[1] = "one and a half";
array2[0] = 1;
array2[1] = 2;
array2[2] = 3;
Looping through arrays is very easy; so is using the foreach loop. Here, we write each element of the second array to the console:
foreach (int element in array2)
{
Console.WriteLine(element);
}
From all of this, we can safely conclude that arrays are easy enough to use and manipulate. Everything about arrays is fairly straightforward. However, the problem is that arrays have fixed sizes. Both arrays in our example contain three elements – no more and no less because there's no way to change this. For example, suppose I wanted to gather all of the prime numbers less than one hundred thousand. If I made each prime number an element in an array, I would have to know beforehand how many prime numbers are under one hundred thousand. That would be quite inconvenient.
This is where collections come in. Collections contain data just as arrays do, but they offer more functionality (and different functionality) and flexibility. Suppose there was a way to create something similar to an array in behavior except that it would feature a dynamic size. This is exactly what a list does! It works similarly to an array, but you can add and remove elements as needed. A list called ArrayList is located in the System.Collections.List namespace. It will store any number of objects. Here, we define an ArrayList:
ArrayList arrayList1 = new ArrayList();
Notice how we don't specify the size as we do with a regular array. This isn't necessary. In order to add elements into the ArrayList, the Add method is used, which accepts an object. Let's add three elements to our ArrayList:
arrayList1.Add("five");
arrayList1.Add("six");
arrayList1.Add("seven");
Additionally, with .NET 3.0, the initial elements of an ArrayList can be added on the same line as the initialization of the ArrayList itself, as with arrays:
ArrayList arrayList1 = new ArrayList() { "five", "six", "seven" };
Elements in an ArrayList can be accessed by their zero-based indexes, just as in arrays. However, recall that an ArrayList's elements are all of type object. This means that in order to use a given element, it must be cast into its proper type. Here, we extract the first element from arrayList1:
string someString = (string)arrayList1[0];
This, however, looks messy. Moreover, it's also error-prone. For example, an int could be added to our ArrayList (it will be implicitly cast to object):
All sorts of problems could be caused by mixing types. What happens when one type is expected but another is retrieved? Running this code will raise a System.InvalidCastException since we've added an int to the arrayList:
foreach (string element in arrayList1)
{
Console.WriteLine(element);
}
Yes, we could fix this by checking each individual element to see if it is indeed a string, but this is quite messy:
foreach (object element in arrayList1)
{
string elementString = element as string;
if (elementString != null)
{
Console.WriteLine(elementString);
}
}
It's also important to remember that checking for the proper type is not free, especially in large collections. Surely, there must be a better, type-safe way! There is, in the form of generic collections. Generic collections are located in the namespace System.Collections.Generic. A generic list called List is available, and it works just like ArrayList except that only objects of a specific type can be put into it. Here, we create a List with string elements:
List<string> genericList1 = new List<string>() { "nine", "ten" };
Now, we know for sure that each element is a string. Attempting to add an object of any other type will raise an exception. Let's enumerate all of the elements:
foreach (string element in genericList1)
{
Console.WriteLine(element);
}
To determine how many elements are in a List, the Count property can be read. Here, we write the number of elements in genericList1 to the console:
Console.WriteLine(genericList1.Count);
Sometimes, it may be necessary to add an element into the middle of a List. Add only puts a given object at the end of the list. To create a new element anywhere within the list, use the Insert method. Here, we insert a new element into the second position:
genericList1.Insert(1, "nine and a half");
All elements to the right (that is, those with a greater index) are bumped up.
Removing an element from a List is quite simple. In order to remove an element at a given index, the RemoveAt method is used:
genericList1.RemoveAt(1);
It's also possible to remove a given object from the List by calling the Remove method. Note, however, that this only removes the first occurrence of the object:
genericList1.Remove("nine");
We can also remove all elements from a List using the Clear method:
genericList1.Clear();
Sorting in a List is very simple as well with the Sort method. Let's create a List called names that contains several names. Then, we'll sort them alphabetically:
We can also reverse the order of the elements in a List by calling the Reverse method:
names.Reverse();
Searching through a list can also be done through instance methods. To determine if a List contains a given object, the Contains method is used, which returns a boolean value indicating whether or not the object is in the list:
bool danielFound = names.Contains("Daniel");
If we need the index of an element, the IndexOf method can be used. It returns the index of the first occurrence of a given object. If the object is not found, it returns -1:
int jasonsLocation = names.IndexOf("Jason");
int chucksLocation = names.IndexOf("Chuck");
List also defines a BinarySearch method, which searches for a given object using a binary search algorithm. This, however, requires that the List to be searched is sorted. The method returns the index of the object, if found, or a negative number:
names.Sort();
int adamsLocation = names.BinarySearch("Adam");
int nicksLocation = names.BinarySearch("Nick");
No, a List isn't an array, of course, but you are able to copy the elements of a List to an array using the ToArray method:
Lists are great when you need functionality similar to arrays. However, collections aren't just substitutes for simple arrays. Another collection is called a queue. As its name suggests, it's similar to a line of people. The first person to get in line is the first person to get out of line, the second person to get in line is the second to get out of line, and so on. A queue is a first-in-first-out (FIFO) collection. We can create a (generic) queue like this:
Queue<int> genericQueue1 = new Queue<int>();
Accessing the elements of a queue is unlike accessing the elements of an array or list. Instead of adding elements and accessing them with their zero-based indexes, we enqueue and dequeue them. Let's enqueue two integers with the Enqueue method:
genericQueue1.Enqueue(11);
genericQueue1.Enqueue(12);
The Enqueue method adds the given object to the end of the queue. So, above, 11 is first in line, and 12 is second in line. We can access the first element in line with the Dequeue method. It removes and returns the first element in line. Here, we remove the first element:
int someInteger = genericQueue1.Dequeue();
Note that attempting to dequeue an element from an empty queue will raise an exception.
As was mentioned, Dequeue removes the first element. Sometimes, it may be necessary to examine the first element without removing it from the queue. This is made possible by the Peek method, which returns the first element in the queue without removing it:
int anotherInteger = genericQueue1.Peek();
We can, however, loop through the contents of a Queue without disturbing them:
foreach (int element in genericQueue1)
{
Console.WriteLine(element);
}
If we need to remove all of the elements from a Stack, the Clear method can be called:
genericQueue1.Clear();
Like List, Queue features a Contains method, which determines if the Queue contains a given element. Here, we search genericQueue1 for the number twelve:
bool twelveFound = genericQueue1.Contains(12);
It's also possible to convert a Queue into an array, just as with List:
Stacks are similar to queues in that you can't directly access most of the elements. However, a stack is a last-in-first-out (LIFO) collection. Imagine, for example, a stack of papers. Let's be more specific and say that it's a short stack of two papers. While the bottom paper was the first paper to be put into the stack, it cannot be accessed without first taking off the top piece of paper. This is how a stack, which is represented by the appropriately-named Stack class, operates. The last element to be put into, or pushed onto, a Stack is the first element that may be taken out, or popped from the Stack. Let's create a (generic) Stack of string objects:
Stack<string> genericStack1 = new Stack<string>();
Elements can be pushed onto the stack using the Push method. Here, we push three strings onto the stack:
genericStack1.Push("first");
genericStack1.Push("second");
genericStack1.Push("third");
The last element can be popped off (which will remove the item) using the Pop method:
string lastOn = genericStack1.Pop();
As with a Queue, we can also peek at the last element without removing it by using the Peek method:
string notRemoved = genericStack1.Peek();
We can also loop through the elements of a Stack without disturbing them:
foreach (string element in genericStack1)
{
Console.WriteLine(element);
}
Stack also features a Contains method, a Clear method and a ToArray method which all work just as they do in a Queue:
A list provides a zero-based integer index. For some applications, this is all that's necessary. However, it's often necessary to have other types be used as indexes or to have a non-zero-based integer system. A dictionary provides this functionality, allowing the user to define the types for both the indexes, which are known as keys, and the values.
For example, suppose we have a list of names. Each name represents a person with a room in some sort of complex (an office complex, an apartment complex – use your imagination), and each room has a number. If we want to store the room number of each person, we can use a Dictionary. The keys can be the names of the people, and the values can be their room numbers. The keys would be strings, and the values would be integers. Here, we define a Dictionary modeled after our example:
Dictionary<string, int> rooms = new Dictionary<string, int>();
A non-generic dictionary is known as a Hashtable.
Notice how we pass in two types. The first is the key type, and the second is the value type.
The Add method can be used to add entries into a Dictionary. Add takes two arguments: the key for the given entry and the value for the given entry. Here, we add two entries to our Dictionary:
rooms.Add("John Smith", 101);
rooms.Add("Jane Doe", 106);
It's also possible to skip the Add method and assign values directly to new keys. This is also how the values of existing keys can be re-assigned, and it gives away how individual entries may be accessed:
rooms["Daniel Noname"] = 115;
We can remove an entry with the Remove method, which accepts as its argument the key of the entry to be removed:
rooms.Remove("John Smith");
Dictionary, like List, Queue and Stack, also supports a Clear method to remove all entries.
Enumerating over the entries in a Dictionary is slightly different from enumerating over the values in a List, Queue, Stack or array. This is because every element in a Dictionary has two values associated with it. To iterate over both the key and the value of every element in a Dictionary, the KeyValuePair generic type is used. Here, we iterate over our Dictionary:
The above code will loop through each element in our Dictionary and write the keys and values of the entries to the console. Notice how KeyValuePair mirrors our Dictionary in the types with which it's associated.
Of course, sometimes it's not necessary to know both the key and the value of each entry. Dictionary supports this by containing two properties: Keys and Values. Keys returns a special collection called a KeyCollection, and Values returns a special collection called a ValueCollection. We can iterate over either of these as we would an array:
foreach (string occupant in rooms.Keys)
{
Console.WriteLine(occupant);
}
foreach (int roomNumber in rooms.Values)
{
Console.WriteLine("Room {0}", roomNumber);
}
To see whether a Dictionary contains a given key or a given value, the ContainsKey and ContainsValue methods can be used, both of which, as with the similar Contains method in the collections covered previously, return a boolean value:
As was said earlier, objects often come in groups of related objects. Sometimes, these groups can be organized with simple arrays. Other times, however, more dynamic, complex or creative organization is required. Collections provide this functionality, and generic collections add the benefit of type safety. In this article, we only covered lists, queues, stacks and dictionaries. However, there are many more kinds of collections, and it is even possible to create custom collections should they be needed. Here is a final summary of the collections we covered:
Lists
Each element features a zero-based integer index
Works similarly to an array
Queues
First-in-first-out (FIFO)
Elements do not features indexes
New elements are enqueued
The first element may be dequeued or peeked at
Stacks
Last-in-first-out (LIFO)
Elements do not feature indexes
New elements are pushed
The last element may be popped or peeked at
Dictionaries
Each element is made up of a key and an associated value