
C# and the .NET platform have a plethora of collection classes for grouping a variable number of data items. With the advent of Generics in .NET 2.0, most of these collection classes are type-safe, meaning that the collections contain items of the same type or derived from the same type.
In this article:
- Arrays
- The Params Array
- Multi-Dimensional Arrays
- Collection Interfaces
- Making Your Custom Types Enumerable
- Sorting Your Collections
- Custom Indexing
- Specialised Collections
- Other Useful Collection Types
- Adding Constraints When Working With Generics
.NET classes supporting collections include the Array
, List
, Dictionary
, Sorted Dictionary
, Queue
, and Stack
.
Arrays
The simplest and most familiar collection is the Array.
The array is the only collection type for which C# provides built-in support and therefore provides native syntax for their declaration. An array is a fixed-length, indexed, collection of objects – all of the same type.
Arrays can be one dimensional, multi-dimensional, or jagged. When an array is created in memory, an object of type System.Array
is created. System.Array
is a reference type. Instances of array have access to the following properties and methods:
- [Method] BinarySearch. An overloaded static method that searches one-dimensional sorted arrays.
- [Method] Clear. Sets a range of element in the array to eiter 0 or null depending on the data type.
- [Method] Copy. An overloaded method that copies a section of one array to another array.
- [Method] CreateInstance. An overloaded statis method that instantiates a new instance of an array.
- [Method] IndexOf. An overloaded static method that returns the index of the first instance of a value in a one-dimensional array.
- [Method] LastIndexOf. An overloaded static method that returns the index of the last instance of a value in a one-dimensional array.
- [Method] Reverse. An overloaded static method that reverses the order of all the elements in a one-dimensional array.
- [Method] Sort. An overloaded static method that sorts the values in a one-dimensional array.
- [Prop] IsFixedSize. This is a required interface property as
System.Array
implements theICollection
interface. Arrays are of a fixed size so this will always return true. - [Prop] IsReadOnly. This is a required interface property as
System.Array
implements theIList
interface. Returns true if the array id read-only. - [Prop] IsSynchronized. This is a required interface property as
System.Array
implements theICollection
interface. Returns true if the array is thread-safe. - [Prop] Length. A read-only property that gets the number of elements in the array.
- [Prop] Rank. A read-only property that gets the number of dimensions in the array.
- [Prop] SyncRoot. A read-only property that gets an object that can be used to synchronise access to the array.
- [Method] GetEnumerator. A method that returns an
IEnumerator
that can be used to iterate through the array. - [Method] GetLength. A method that returns the length of the specified dimension of the array.
- [Method] GetLowerBound. A method that returns the lower boundary of the specified dimension of the array.
- [Method] GetUpperBound. A method that returns the upper boundary of the specified dimension of the array.
- [Method] Initialise. Initialises all values in a value type array by calling the default constructor for each value. With reference type arrays, all elements are set to null.
- [Method] SetValue. An overloaded method that sets the specified array elements to a value.
An array is declared using the following syntax:
Type[] arrayName = new Type[size];
// For example...
int[] numbers = new int[10];
Code language: C# (cs)
In the example immediately above, we are declaring a reference to an Array
object in memory. The square brackets ([]
) tell the compiler that we are declaring an array of type integer. The array object is instantiated using the new
keyword.
The size of the array is set to 10 in the array declaration example. This is the size of the array and represents the number of elements; not the upper bound of the array. All native C# arrays are 0 based, i.e. their first index is 0, with indices 0 through 9 in this case, and with the upper bound being 9 not 10. The size of the array cannot be changed once it is declared.
It is possible to create non-zero based arrays, but it is generally considered bad programming practice and is therefore considered beyond the scope of this tutorial.
If the array is declared with a value type, such as int
in the example, each element is initialised to 0 – which is the default value for numeric value types. It is worth spending a moment to think about how the array is allocated in memory. An array is a reference type object, so in the example array declaration above, numbers
is a reference (memory pointer) held in the Stack which points to a System.Array
object on the Heap. The Array
object on the Heap then points to the 10 integer type values on the Stack.

Unlike arrays of value types, the elements of a reference type array are not initialised to their default value, they are initialised to null
. If you attempt to access an element in an array of reference type values before you have specifically initialised it, The CLR will throw a NullReferenceException
.
Consider the following example where you have created two Account classes. The following line declares an array of Accounts with two elements:
Account[] myAccounts = new Account[2];
Code language: C# (cs)
The above line of code does not create an array with two references to initialised Account
objects. It creates a 2-element array with null set as both elements.

To use this array, we must first construct and assign Account objects to each of the references in the array.
Account myAccount = new Account();
myAccounts[0] = myAccount;
// NOTE: this creates a new memory reference for the myAccount variable so
// we can re-use the variable without any side effects.
myAccount = new Account();
myAccounts[1] = myAccount;
Code language: C# (cs)
The revised memory structure now looks a little more complicated…

It is possible to initialise the contents of an array when the array is declared by providing a list of comma delimited values in curly braces ({ }
). For example:
int[] numbers = new int[] { 3, 4, 5, 6 };
Code language: C# (cs)
numbers[0]
will have a value of 3, numbers[1]
a value of 4, etc.
Let’s go through a worked example of using arrays. We’ll stick with our Account
class and build out use of arrays from there.
// Account.cs
using System;
namespace UsingArrays
{
public class Account
{
public Account(string holderName, int accountNumber, decimal balance)
{
HolderName = holderName;
AccountNumber = accountNumber;
Balance = balance;
}
public string HolderName { get; private set; }
public int AccountNumber { get; private set; }
public decimal Balance { get; set; }
public override string ToString()
{
return string.Format(
"Account Number: {0}, Balance: £{1:F2}",
AccountNumber,
Balance);
}
}
}
Code language: C# (cs)
// Program.cs
using System;
namespace UsingArrays
{
internal class Program
{
static void Main()
{
const int numberOfAccounts = 5;
// Create an array of accounts.
Account[] myAccounts = new Account[numberOfAccounts];
// Create the account objects.
for (int i = 0; i < myAccounts.Length; i++)
{
myAccounts[i] = new Account(i, (i + 5) * 1000);
}
// Print out the account summary details.
for (int i = 0; i < myAccounts.Length; i++)
{
Console.WriteLine(myAccounts[i].ToString());
}
Console.ReadLine();
}
}
}
Code language: C# (cs)
The two for
loops use the Length
property of the Array
class to ascertain how many objects are in the array. Array
objects are indexed from 0
to Length – 1
.
Notice that the expression is i < myAccounts.Length
, and not i < myAccounts.Length – 1
. This is because less than is being used (so when i
gets to 10 the loop stops without executing its body).
Build and run the console application and confirm the program outputs the accounts data as expected.
The ‘foreach’ Statement
The foreach
statement allows us to iterate through all items in a collection, examining each item in turn to get any desired information, but it should never be used to alter the information, as this may lead to unpredictable results.
In the above example, we can re-write code so that the second for
loop implements foreach
loop syntax to print out the account summary details for each of the accounts in our array:
foreach (Account account in myAccounts)
{
Console.WriteLine(account.ToString());
}
Code language: C# (cs)
Make the above changes to the code. Build and run the console app again. Observe that the output from the program is exactly the same as before.
NOTE: the foreach
statement can only be used on types that implement the IEnumerable
interface.
The Params Array
The params
keyword allows us to pass an indeterminate number of items into a method without having to first create an array. It is a useful keyword for situations where you don’t know the number of parameters a method will require ahead of time.
For example, the native string.Format()
method (from the string
data-type (aka the System.String
class)) uses params
to allow an indeterminate number of arguments to be passed into the method to format a text value with data. For example:
string myMessage = string.Format("{0}{1}", message, Environment.NewLine);
// or...
string myMessage =
string.Format("{0}{2}{1}{2}", message1, message2, Environment.NewLine);
Code language: C# (cs)
Using params
ourselves we might do something like this:
private static void PrintAccounts(params Account[] accounts)
{
foreach (Account account in accounts)
{
Console.WriteLine(account.ToString());
}
}
Code language: C# (cs)
Multi-Dimensional Arrays
C# and the .NET framework support multi-dimensional arrays. We have been dealing with one-dimensional arrays so far, which we can think of as rows of data items (i.e. a single column). Adding more rows of equal length alongside the first would create a classic two-dimensional array of rows and columns.
It is possible to add more dimensions, all be it getting more difficult to visualise what they would look like with each new axis.
C# supports two primary array types: rectangular and jagged.
Rectangular Arrays
A rectangular array is one of two or more dimensions where each row is the same length. A rectangular array is declared using the following syntax:
Type[,] arrayName;
// For example...
int[,] table = new int[2,3];
Code language: C# (cs)
The first number in the instantiation declares the number of rows, and the second the number of columns. When accessing an array of this type, we again use the square brackets and separate the index of each dimension by using a comma.
For example, if we wanted to access the element at row 2 column 2 we would write (remembering that all arrays are 0 based):
table[1, 1] = 10; // Assign the element a value.
int cellValue = table[1, 1]; // Read back the element value.
Code language: C# (cs)
All of the elements in the above array would be accessed like this with the first indexer being the row number and the second indexer being the column number. For example:

Just like with one dimensional arrays, a rectangular array can be initialised with values at the same time that the array is instantiated. For example, to initialise our 2 by 3 array above we would write:
int[,] rectangularArray = new int[,]
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};
// Or, just...
int[,] rectangularArray =
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};
Code language: C# (cs)
In the second declaration above a shorthand initialisation supported by C# is being used.
Jagged Arrays
A jagged array is essentially an array of one dimensional arrays. Each row of the array need not be the same length and therefore the array can be thought of as jagged. A jagged array is declared using the following syntax:
Type[][] arrayName;
// For example...
int[][] jaggedTable = new int[2][];
Code language: C# (cs)
Since a jagged array is an array of arrays, the array that forms each row is not instantiated in the above line. We have not declared the size of any of the rows, just how many there will be. Trying to access one of the elements in the array now would throw a NullReferenceException
error.
The arrays that form each row must be instantiated individually. For example, to create a row of 3 and a row of 4 elements we would write:
jaggedTable[0] = new int[3];
jaggedTable[1] = new int[4];
// Or, we can instantiate the jagged arrays at the same time...
jaggedTable[0] = new int[] { 1, 2, 3 };
jaggedTable[1] = new int[] { 4, 5, 6, 7 };
Code language: C# (cs)
To access a value in the jagged array now, we use two sets of square brackets. For example, to access element two in the second row we would write:
int cellValue = jaggedTable[1][1];
Code language: C# (cs)
Collection Interfaces
The .NET framework provides two sets of interfaces for enumerating and comparing collections: the traditional, non-type safe, interfaces contained in the System.Collections
namespace (these days included primarily for backwards compatibility); and, the new type-safe interfaces contained in the System.Collections.Generic
namespace.
Generic collections refer to the fact that they are not forced to be related to any specific type, but we can still perform work on them in a type-safe manner. It is best practice that we now use the type-safe versions that use the <T>
generic notation instead of involving the boxing overheads of using non type-safe variants.
When using generic type interfaces, the generic type T
is replaced with the type we are using in our collection, for example, one of the built-in types such as int
or string
, or even a custom user defined type (class).
The main generic collection interfaces are:
- ICollection<T>. A base interface for all generic collection types and implemented by all collections to provide the
CopyTo()
method as well as theCount
,IsSynchronized
andSyncRoot
properties (used in Threading). - IEnumerator<T>. Defines methods such as
MoveNext()
andReset()
and aCurrent
property which are called by the compiler when iterating through a collection using theforeach
loop statement. - IEnumerable<T>. Defines a collection as enumerable by exposing a
GetEnumerator()
method so theforeach
loop statement can use it. - IComparer<T>. Defines a
Compare()
method which can be used to compare two objects in a collection so it can be sorted. - IComparable<T>. Defines a collection as comparable by exposing a
CompareTo()
method that can be used to compare the current collection to another one. - IList<T>. Used by array-indexable collections that are not a fixed size.
- IDictionary<TKey, TValue>. Used for key/value pair based collections such as Dictionary.
Making Your Custom Types Enumerable
If you have a type that holds a collection of data (e.g. as a field utilising standard collection type from the System.Collections.Generic
namespace), or even if you have implemented own custom collection type, then you may want to let the compiler enumerate the elements of it.
All you need to do is inhert from IEnumerable<T>
and then implement the interface members. For example:
using System;
using System.Collections.Generic;
namespace MakingCollectionsEnumerable
{
public class Portfolio : IEnumerable<Account>
{
private IList<Account> _accounts = new List<Account>();
public Portfolio()
{
// Initialise with some dummy data.
_accounts.Add(new Account("John Smith", 19057928, 34567.12M));
_accounts.Add(new Account("Sarah Blane", 47817771, 3012.06M));
_accounts.Add(new Account("Jonathan Grape", 29458881, 4.96M));
_accounts.Add(new Account("David Evans", 30188221, 27456.01M));
_accounts.Add(new Account("Judy McGovern", 10380001, 9100.01M));
_accounts.Add(new Account("Amandeep Bhagat", 50199922, 14079.41M));
}
public int NumberOfAccounts
{
get
{
return _accounts.Count;
}
}
#region IEnumerable<Account> Members
// NOTE: IEnumerable<Account> is implemented implicilty.
public IEnumerator<Account> GetEnumerator()
{
foreach (Account myAccount in _accounts)
{
yield return myAccount;
}
}
#endregion
#region IEnumerable Members
// NOTE: IEnumerable is implemented explicitly to avoid name collision.
IEnumerator IEnumerable.GetEnumerator()
{
// The array class provides a GetEnumerator method that returns
// an non type safe IEnumerator of this type
return this.accounts.GetEnumerator();
}
#endregion
}
}
Code language: C# (cs)
Notice the reference to the IEnumerable<Account>
interface in the class definition. Implementing this interface has forced us to implement the GetEnumerator()
method. The C# language provides a keyword for creating enumerators, the yield
keyword. This implementation of IEnumerable
allows us to use the foreach loop to iterate through the contents.
Sorting Your Collections
The Array
class has two very useful static methods for manipulating the order of arrays. These are Sort()
and Reverse()
. These methods are fully supported for the built-in C# primitive types such as int
, bool
, etc. and string
.
To be able to sort arrays of our custom types, our types must either implement the IComparable
interface, or we must specify an object that implements IComparer
to do the comparisons for us. Let’s modify the Account
class from the above example to implement the IComparable<T>
generic interface.
The IComparable
interface forces us to implement one method named CompareTo()
that is called to compare this class to another Account class passed in as the method argument.
Modify the Account class as follows:
public class Account : IComparable<Account>
{
// Existing code omitted for brevity.
#region IComparable<Account> Members
/// <inheritdoc />
public int CompareTo(Account other)
{
return other == null || AccountNumber < other.AccountNumber
? return -1
: AccountNumber > other.AccountNumber
? return 1
: return 0;
}
#endregion
}
Code language: C# (cs)
Above, we’ve done the CompareTo()
implementation a complex way, since we’re using a value type that implements IComparable
itself, so we could’ve just done this:
return other == null ? return -1 : AccountNumber.CompareTo(other.AccountNumber);
Code language: C# (cs)
Now, if we do the following, the accounts will be sorted into ascending account number order:
Account[] accounts = new Account[]
{
new Account("John Smith", 19057928, 34567.12M)
new Account("Sarah Blane", 47817771, 3012.06M),
new Account("Jonathan Grape", 29458881, 4.96M),
new Account("David Evans", 30188221, 27456.01M),
new Account("Judy McGovern", 10380001, 9100.01M),
new Account("Amandeep Bhagat", 50199922, 14079.41M)
};
accounts.Sort();
Code language: C# (cs)
Alternatively, the same outcome could be achieved with a custom comparer that implements IComparer<T>
. For example:
public class AccountsComparer : IComparer<Account>
{
#region IComparer<Account> Members
public int Compare(Account x, Account y)
{
return x == null && y == null
? return 0
: x == null && y != null
? return -1
: x != null && y == null
? return 1
: x.AccountNumber.CompareTo(y.AccountNumber);
}
#endregion
}
// Use the comparer class...
Array.Sort(accounts, new AccountsComparer());
Code language: C# (cs)
Although it might, on first glance, look like more work to use the IComparer<T>
for no additional benefit, what we haven’t demonstrated here is the flexibility that the latter approach provides.
Implementing IComparable<T>
defines the ‘default’ sorting for a collection, but there can only be one. The advantage of using IComparer<T> is that you can implement as many comparers as you want. For example, the comparer above works on account number so let’s declare ones that work by account holder name and account balance respectively:
public class AccountsByHolderComparer : IComparer<Account>
{
#region IComparer<Account> Members
public int Compare(Account x, Account y)
{
return x == null && y == null
? return 0
: x == null && y != null
? return -1
: x != null && y == null
? return 1
: x.HolderName.CompareTo(y.HolderName);
}
#endregion
}
public class AccountsByBalanceComparer : IComparer<Account>
{
#region IComparer<Account> Members
public int Compare(Account x, Account y)
{
return x == null && y == null
? return 0
: x == null && y != null
? return -1
: x != null && y == null
? return 1
: x.Balance.CompareTo(y.Balance);
}
#endregion
}
Code language: C# (cs)
So, now we can sort the accounts array 3 different ways:
// Sort into ascending account number order.
Array.Sort(accounts);
// Sort into ascending account holder name order.
Array.Sort(accounts, new AccountsByHolderComparer();
// Sort into ascending account balance order.
Array.Sort(accounts, new AccountsByBalanceComparer();
Code language: C# (cs)
Custom Indexing
It is often desirable when encapsulating a collection in a custom class to make that class look like a collection or an array itself. Our Account Portfolio class in the above example holds an underlying array of accounts. We have extended the functionality so that we can use a foreach loop to enumerate through and return each account object in turn, but what if we want to access each account object by the index in which it appears in the array?
We could expose the Accounts array as a property, or we could add an index operator (an ‘indexer’) to the Portfolio class. The syntax for an indexer is:
Type this[Type index] { get; set; }
Code language: C# (cs)
The return type determines the data-type of object that will be returned by the indexer, while the parameter argument specifies what type of object is used to index the underlying collection. The indexing parameters do not have to be singular – several parameters can be used to index an element in a collection if required.
The this
keyword refers to the object in which the indexer appears. As with all properties, you are required to define the get or set accessors to determine how the requested object is retrieved or assigned. Indexers can also be read-only or write-only too.
Let’s add indexing to our Portfolio
class and then demonstrate using it.
using System;
using System.Collections.Generic;
namespace MakingCollectionsEnumerable
{
public class Portfolio : IEnumerable<Account>
{
// Existing code omitted for brevity.
public Account this[int index]
{
get
{
return _accounts[index];
}
set
{
_accounts[index] = value;
}
}
}
}
Code language: C# (cs)
We can now use both the for
loop and the foreach
loop on our Portfolio
class, like so:
foreach (Account account in myPortfolio)
{
Console.WriteLine(account.ToString());
}
for (int i = 0; i < myPortfolio.NumberOfAccounts; i++)
{
Console.WriteLine(myPortfolio[i].ToString());
}
Code language: C# (cs)
As previously mentioned, indexer don’t have to just accept a single parameter or a numeric value. Both of the following are valid:
public Employee this[int index] { get; set; }
public Employee this[string firstName, string lastName] { get; set; }
Code language: C# (cs)
Any object type can be used as an indexer, from built in system types to custom user defined types.
It is also worth noting that when a numeric indexer is declared, it is good practice to expose a property returning the number of elements in the collection. This would usually be called Count
for consistently with FCL classes, but we’ve used NumberOfAccounts
in the Portfolio
class instead as it was more readable.
Specialised Collections
List<T>
The main problem with arrays is their fixed size. If you don’t know in advance how many objects your collection is going to hold, you may declare an array with too few or too many elements.
The solution is the List<T>
class, and it is a collection whose size dynamically changes as you add or remove items. A list is declared as follows:
List<Type> listName;
Code language: C# (cs)
This is a type safe, generic list, so we replace the T
in its declaration with the type of objects we want to put into the List
. We can use any .NET object such as int
, string
, object
, etc. or even user defined types as list items. For example, if we were to keep a list of accounts from our previous examples, we may declare it like this:
List<Account> accounts = new List<Account>();
Code language: C# (cs)
Notice we don’t declare the size of the list when we declare it. We call the Add()
method to add new items to the List
, and can access any element using the indexer, like so:
accounts.Add(new Account("Joe Bloggs", 23112209, 6785.98M));
Account firstAccount = accounts[0];
Code language: JavaScript (javascript)
The commonly used properties and methods of the List<T>
class are:
- Capacity. A property to get or set the maximum capacity (maximum number of items) supported by the list. This value is increased automatically if the list size exceeds capacity as new items are added. Explicitly setting this property (pre-sizing) can reduce the number of times the backing array for the list needs reallocating.
- Count. A property that returns the current number of elements in the list.
- Add. Adds a new element to the list.
- AddRange. Adds a collection of elements, that implements
ICollection
, to the list. - BinarySearch. Uses a binary search technique to search the list to locate a specific element in a list that has been sorted. This is likely to be much quicker than iterating from 0 to n-1 on large lists.
- Clear. Removes all of the elements of the list.
- Contains. Determines if an item appears in the list.
- CopyTo. Copies the contents of the list to a one-directional list.
- Exists. Determines if an element matching the specified filtering predicate is present in the list.
- Find. Returns the first element in the list that matches the specified filtering predicate.
- FindAll. Returns all elements in the list that match the specified filtering predicate.
- GetEnumerator. Returns an enumerator to iterate through the list.
- GetRange. Copies a range of elements to a new list.
- IndexOf. Returns the element index of the first occurrence of the specified item, or -1 if not found.
- Insert. Inserts an item into the list at the specified index.
- InsertRange. Inserts a collection of items into the list.
- LastIndexOf. Returns the last index of the specified value in the list.
- Remove. Removes the first occurrence of an item from the list.
- RemoveAt. Removes an element at the specified index.
- RemoveRange. Removes a range of elements from the list.
- Reverse. Reverses the order of the elements in the list.
- Sort. Sorts the list.
- ToArray. Copies the elements of the list into a new array.
- TrimExcess. Sets the list capacity to the current number of elements in the list and reallocates the backing array.
Dictionary<TKey, TValue>
A dictionary is a collection of key-value pairs. That is, it stores a specific value against a specific key in much the same way as a language dictionary stores a word definition (the value) against a word (the key). A dictionary is declared as follows:
Dictionary<Type, Type> dictionaryName;
Code language: C# (cs)
We declare a dictionary object, replacing the TKey
and TValue
generic types for types we wish to use as keys and values. The key and the value in a dictionary collection can be any .NET type such as int
, string
, object
, a user defined type, etc.
Objects used as keys must implement GetHashCode()
and Equals()
, although in most cases you can use the inherited implementation from System.Object
anyway.
Typically the key is something lightweight, and the value is often a more complex object. This form of collection can be more useful than simple collection types such as arrays. For example, say we were storing the capital cities of the countries of the world. We could store them in an array:
string[] worldCapitals = new string[194];
// Code to populate the array omitted for brevity.
// Get the capital of Albania...
string capitalCity = worldCapitals[1];
Code language: C# (cs)
Unless we know the exact index of each country we would struggle to find the capital city. We could use two arrays, one holding country names and the other holding capital cities and keep them in sync so you can find the country index and use it to lookup the capital city name.
That feels a bit messy though, and it is prone to programming error and the arrays getting out of sync so that the wrong city is returned. Instead, we can define the dataset as a dictionary like so:
Dictionary<string, string> worldCapitals = new Dictionary<string, string>();
worldCapitals.Add("Albania", "Tirane");
// Code to populate the dictionary omitted for brevity, apart from the example given.
// Now let's get the capital of Albania...
string capitalCity = worldCapitals["Albania"];
Code language: C# (cs)
The commonly used properties and methods of the dictionary class are:
- Count. A property that returns the current number of elements in the dictionary.
- Add. Adds a new key-value pair to the dictionary.
- Keys. A property that returns a list of the keys that exist in the dictionary.
- Values. A property that returns a list of the values that exist in the dictionary.
- Clear. Removes all objects from the dictionary.
- ContainsKey. Determines if the dictionary contains a specified key.
- ContainsValue. Determines if the dictionary contains a specified value.
- GetEnumerator. Returns an enumerator that can be used to loop through the key-value pairs.
- Remove. Removes an entry with a specified key.
SortedList<TKey, TValue>
This collection type is very similar to the Dictionary<TKey, TValue>
collection, but it is more flexible in that Keys
and Values
can be indexed directly, and it automatically sorts keys into ascending order.
The advantage of being able to index the keys/values can be observed when you don’t want to enumerate an entire list or when you want to be able to add or remove elements within the loop construct.
Dictionary collections can only be enumerated (e.g. foreach
), whereas SortedList
collections can be enumerated and iterated over (foreach
and for
loops).
Other Useful Collection Types
Stack<T>
This collection represents a simple last-in, first-out (LIFO) collection of objects. It provides Push()
, Pop()
, and Peek()
methods to add and remove items in the collection. An example of when you might want to use a LIFO collection is when maintaining a navigation history (in a web browser, screens of an app, etc.) That way you can have a back button that pops the most recent URL or screen ID off the stack.
Queue<T>
This collection represents a simple first in, first out (FIFO) collection of objects. This collection type provides, Enqueue()
, Dequeue()
and Peek()
methods to add and remove items from the collection. An example of when you might use it is where instructions or commands need to be executed specifically in the order they were received.
Adding Constraints When Working With Generics
There are times when we must ensure that the elements you add to a generic collection meet certain constraints, such as deriving from a certain base-type or implementing a certain interface. To specify a constraint in a class we use a where
clause in the class declaration.
For example, let’s modify the Portfolio
class from the previous examples to become a generic class. We will still implement the IEnumerable
interface so that its contents can be iterated over using a foreach
loop, but we’ll add a constraint that the items we add to our portfolio class must implement the IComparable
interface too. That way we can be sure we can effectively sort them.
The Portfolio
class won’t be limited to elements of type Account
after doing this – elements only need to be of the declared type (T
) and be comparable.
public class Portfolio<T> : IEnumerable<T> where T : IComparable<T>
Code language: C# (cs)
Here we have declared a generic class named Portfolio
that will take a generic type T
. We are implementing the IEnumerable
interface and we are constraining the generic type to be only instances of a class that implements the IComparable
interface.
Below is the complete modified Portfolio
class. Notice that in a number of cases, the type-specific object (Account
) is now replaced with the generic type T
that can be specialised when the Portfolio
class is instantiated.
using System;
using System.Collections.Generic;
namespace ConstrainingGenerics
{
public class Portfolio<T> : IEnumerable<T> where T : IComparable<T>
{
private List<T> theList = new List<T>();
public void Add(T item)
{
this.theList.Add(item);
}
public void Sort()
{
this.theList.Sort();
}
#region IEnumerable<T> Members
/// <inheritdoc />
public IEnumerator<T> GetEnumerator()
{
foreach (T item in this.theList)
{
yield return item;
}
}
#endregion
#region IEnumerable Members
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
foreach (T item in this.theList)
{
yield return item;
}
}
#endregion
}
}
Code language: C# (cs)
We can now use this Portfolio
class with any .NET type that implements the IComparable<T>
interface. Since the Account
class does so, we can still instantiate the Portfolio
class specifying that it holds a collection of Account
objects like so:
Portfolio<Account> myPortfolio = new Portfolio<Account>();
Code language: C# (cs)
The myPortfolio
variable can then continue to be used just like before.
If we try to create a Portfolio
object using a type that does not implement the IComparable<T>
interface, we get the following compiler error:
The type 'ConstrainingGenerics.Customer' cannot be used as type parameter 'T'
in the generic type or method 'ConstrainingGenerics.Portfolio<T>'. There is no
implicit reference conversion from 'ConstrainingGenerics.Customer' to
'System.IComparable<ConstrainingGenerics.Customer>'.
Code language: plaintext (plaintext)