The content and code of this article is referenced from book Pro C#5.0 and the .NET 4.5 Framework by Apress. The intention of the writing is to review the konwledge and gain better understanding of the .net framework.
When the .net platform was released, programmers frequently used the classes of the System.Collections namespace to store and interact with bits of data used within an application. In .net 2.0, the C# programming language was enhanced to support a feature termed generics; and with this change, a brand new namespace was introduced in the base class libraries: System.Collections.Generic.
1. The motivation for Collection class
The most primitive container one could use to hold application data is undoubtly the array. C# array allow you to define a set of identically typed items of a fixed upper limit.
1 public static void Main (string[] args) 2 { 3 string[] myStrings = { "string1", "string2", "string3" }; 4 5 foreach (string s in myStrings) { 6 Console.WriteLine (s); 7 } 8 9 Array.Sort (myStrings); //sort array 10 Array.Reverse (myStrings); //reverse array 11 }
While basic arrays can be useful to manage small amount of fixed-size data, there are many other times where you require a more flexible data structure, such as dynamically growing and shinking container. .Net base class library ships with a number of namespace containing collection classes. And they are built to dynamically resize themselves on the fly as you insert or remove items.
You‘ll notice that a collection class can belong to one of two broad categories: nongeneric collections, generic collections. On the one side, nongeneric collections are typically designed to operate on System.Object type and are , therefore, very loosely typed containers. In contrast, generic collections are much more type safe, given that you must specify "type of type" they contain upon creation .
2. System.Collections Namespace
Any .net application built with .net 2.0 or higher should ignore the classes in System.Collections in favor of corresponding classes in System.Collections.Generic. However, it is still important to know the basics of the nongeneric collection class.
Class | Meaning | Key Implement interfaces |
ArrayList | Represents a dynamically sized collection of objects listed in sequenial order | IList, ICollection, IEnumerable ICloneable |
BitArray | Manages a compact array of bit values, which are represented as Boolean | ICollection, IEnumerable ana ICloneable |
Hashtable | Represent a collection of key/value pairs that are organized based on the hash code of the key | IDictionary ICollection, IEnumerable and ICloneable |
Queue | Represents a standard first-in, first-out collection of objects. | ICollection, IEnumerable, and ICloneable |
SortedList | Represents a collection of key/value pairs that are sorted by the keys and are accessible by key and by index | IDictionary, ICollection, IEnumerable, ICloneable |
Stack | A last-in, first-out stack providing push and pop functionality | ICollection, IEnumerable, and ICloneable |
Key interfaces supported by classes of System.Collections
Interface | Meaning |
ICollection | Defines general characteristics for all nongeneric collection types |
ICloneable | Allows the implementing object to return a copy of itself to the caller |
IDictionary | Allows a nongeneric collection object to represent its contents using key/value paris |
IEnumerable | returns an object implementing the IEnumerator interface |
IEnumerator | enables foreach style iteration of collection items |
IList | Provides behavior to add, remove, and index items in a sequential list of objects |
2.1 Working with ArrayList
ArrayList, you can add or remove items on the fly and container automatically resize itself accordingly:
1 public static void Main (string[] args) 2 { 3 ArrayList strArray = new ArrayList (); 4 strArray.AddRange (new string[] {"First", "Second", "Third" }); 5 6 Console.WriteLine ("This collection has {0} items", strArray.Count); 7 8 //add a new item and display current count 9 strArray.Add("Fourth!"); 10 Console.WriteLine ("This collection has {0} items", strArray.Count); 11 12 //display all items 13 foreach (string s in strArray) { 14 Console.WriteLine (s); 15 } 16 17 }
2.2 System.Collections.Specialized Namespace
System.Collection.Specialized namespace defines a number of specialied collection types.
Type | Meaning |
HybridDictionary | This class implements IDictionary by using a ListDictionary while the collection is small, and then swithing to a Hashtable when the collection gets large |
ListDictionary | This class is useful when you need to manage a small number of items that can change overtime. |
StringCollection | This class provides an optimal way to manage large collections of string data |
BitVector32 | This class provides a simple structure that stores boolean values and small integers in 32 bits of memory. |
While these specialized types might be just what your projects require in some situation, I won‘t comment on their usage here. In many cases, you will likely find the System.Collections.Generic namespace provides classes with similar functionality and additional benefits.
2.3 The problem of non-generic collection
The first issue is that using the System.Collections and System.Collection.Specialied classes can result in some poorly performing code, especially when you are manipulating numerical data. The CLR must perform a number of memory transfer operations when you store structures in any nongeneric collection class prototyped to operate on System.Object.
Second issue is that most of the nongeneric collection classes are not type safe, they were deveoloped to operate on System.Objects, and they could therefore contain anything at all.
2.4 The issue of performance
C# provides a simple mechanism, termed boxing, to store the data in a value type within a reference variable.
1 static void SimpleBoxOperation() 2 { 3 int myInt = 23; 4 object boxInt = myInt; //boxing operation 5 }
Boxing can be formally defined as the process of explicitly assigning a value type to a System.Object variable. When you box a value, the CLR allocates a new object on the heap and copies and the value type‘s value into that instance. What is returned to you is a reference to the newly allocated heap-based object.
The opposite operation is unboxing, the process of converting the value held in the project reference back into a corrsponding value type on the stack. The CLR first values if the receiving data type is equivalent to the boxed type, if so, it copies the vlaue back into a local stack-based variable.
1 static void SimpleBoxOperation() 2 { 3 int myInt = 23; 4 object boxInt = myInt; //boxing operation 5 6 int unboxInt = (int)boxInt; //unboxing 7 }
The InvalidCastException will be thrown if you‘re attempting to unbox the int into a long type.
Boxing and unboxing are convenient from a programmer‘s viewpoint, but this simplified approach to stack/heap memory transfer comes with the baggages of performance issues.
3. Generic Collection
When you use Generic collection, you rectify all of the previous issues, including boxing/unboxing and a lack of type safety.
1 public static void Main (string[] args) 2 { 3 List<int> ints = new List<int> (); 4 ints.Add (10); 5 ints.Add (9); 6 ints.Add (8); 7 8 foreach (int i in ints) { 9 Console.WriteLine (i); 10 } 11 }
Generics provide better performance because they do not result in boxing or unboxing penalities when storing value types. Generics are type safe because they can contain only the type of type you specify.
3.1 Specifying type parameters for generic classes/structures
When you create an instance of a generic class or structure, you specify the type parameter when you delcare the variable and when you invoke the constructor.
List<Person> morePeople = new List<Person>();
You can read it as a list of person objects. After you specify the type parameter of a generic item, it cannot be changed.
In previous chpater, we learned about a number of nongenerice interfaces, such as IComparable, IEnumerable and IEnumerator. And we have implmented those interface on class.
1 public class Car : IComparable 2 { 3 public int CarID { get; set;} //auto property 4 5 public Car () 6 { 7 } 8 9 public int CompareTo(object c) 10 { 11 Car newCar = c as Car; //cast object to car 12 if (newCar != null) { 13 if (this.CarID > newCar.CarID) 14 return 1; 15 if (this.CarID < newCar.CarID) 16 return -1; 17 else 18 return 0; 19 } 20 else 21 { 22 Console.WriteLine ("invalid car type"); 23 } 24 } 25 }
And because the interface is nongeneric, the object type is used to as parameter and we have to cast it to appropriate type before processing. Now assume you use the generic counterpart of this interface.
public class Car : IComparable<Car> { public int CarID { get; set;} //auto property public Car () { } public int CompareTo(Car newCar) { if (this.CarID > newCar.CarID) return 1; if (this.CarID < newCar.CarID) return -1; else return 0; } }
Here, you do not need to check incoming parameter is a Car because it can only be a Car. If Someone were to pass in an incompatible data type, you would get a compile-time error.
3.2 The System.Collections.Generic namespace
When you are building a .net application and need a way to manage in-memory data, the classes of system.collections.Generic will most likely fit the bill.
Interface | Meaning |
ICollection<T> | defines a general characteristics |
IComparer<T> | Defines a way to compare to objects |
IDictionary<T> | Allows a generic collection object to represent its contents using key/value pairs |
IEnumerable<T> | Returns the IEnumerator<T> interface for a given object |
IEnumerator<T> | Enables foreach-style iteration over a generic collection |
IList<T> | Provides behavior to add, remove, and index items in a sequential list of objects |
ISet<T> | provides the base interface for the abstract of sets |
The System.Collections.Generic namespace also defines serveral classes that implement many of these key interfaces.
Generic Class | Key interface | Meaning |
Dictionary<TKey, TValue> | ICollection<T>, IDictionary<TKey, TValue>, IEnumerable<T> | This represent a generic collection of keys and vlaues |
LinkedList<T> | ICollection<T>, IEnumerable<T> | This represents a doubly linked list |
List<T> | ICollection<T>, IEnmerable<T>, IList<T> | This is a dynamically resizable sequential list of items |
Queue<T> | ICollection, IEnumerable<T> | This is a generic implementation of first-in, first-out |
SortedDictionary<TKey, TValue> | ICollection<T>, IDictionary<TKey, TValue> | This is a generic implementation of a sorted set of key/value pairs |
SortedSet<T> | ICollection<T>, IEnumerable<T>, ISet<T> | This represents a collection of objects that is maintained in sorted order with no duplication |
Stack<T> | ICollection, IEnumerable<T> | This is generic implementation of a last-in, first-out list |
3.3 Collection initialization syntax
This language feature makes it possible to populate many container with items by using syntax similar to what you use to populate a basic array.
int[] ints = { 0, 1, 2, 3, 4 }; List<int> listInts = new List<int> { 0, 2, 3, 5 }; ArrayList mList = new ArrayList { 3, 4, 9 };
3.4 Working with the List<T> class
The List<T> class is bound to be your most frequently used type in the System.Collection.Generic because it allows you to resize the contents of the container dynamically.
1 public static void UseGenericList() 2 { 3 List<Person> people = new List<Person> () 4 { 5 new Person{FirstName = "person1", LastName="test", Age= 20}, 6 new Person{FirstName = "person2", LastName="test", Age= 26}, 7 new Person{FirstName = "person3", LastName="test", Age= 23}, 8 }; 9 10 Console.WriteLine ("There are {0} People", people.Count); 11 12 foreach (Person p in people) { 13 Console.WriteLine (p.FirstName); 14 } 15 16 //insert new person 17 people.Insert(2, new Person{FirstName="Person4", LastName="test", Age=40}); 18 Console.WriteLine ("There are {0} people", people.Count); 19 20 //copy data to array 21 Person[] arrayOfPeople = people.ToArray(); 22 for (int i = 0; i < arrayOfPeople.Length; i++) { 23 Console.WriteLine (arrayOfPeople [i].FirstName); 24 } 25 26 }
Here, you use initialization syntax to pupulate List<T> with objects, as a shorthand notation for calling Add() multiple times.
3.5 Working with Stack<T> class
The stack class represents a collection that maintains items using a last-in, first-out manner. As you might expect, Stack<T> defines members named Push() and Pop() to place items onto or remove items from the stack.
1 public static void UseGenericStack() 2 { 3 Stack<Person> stackOfPeople = new Stack<Person> (); 4 5 stackOfPeople.Push (new Person{ FirstName = "person1", LastName = "test", Age = 10 }); 6 stackOfPeople.Push (new Person{ FirstName = "person2", LastName = "test", Age = 11 }); 7 stackOfPeople.Push (new Person{ FirstName = "person3", LastName = "test", Age = 13 }); 8 9 //look at the top item, pop it and look again 10 Console.WriteLine("First person is {0}", stackOfPeople.Peek().FirstName); 11 Console.WriteLine ("Popped off {0}", stackOfPeople.Pop ()); 12 13 Console.WriteLine("First person is {0}", stackOfPeople.Peek().FirstName); 14 Console.WriteLine ("Popped off {0}", stackOfPeople.Pop ()); 15 16 Console.WriteLine("First person is {0}", stackOfPeople.Peek().FirstName); 17 Console.WriteLine ("Popped off {0}", stackOfPeople.Pop ()); 18 19 try{ 20 Console.WriteLine("First person is {0}", stackOfPeople.Peek().FirstName); 21 Console.WriteLine ("Popped off {0}", stackOfPeople.Pop ()); 22 } 23 catch(InvalidOperationException ex) { 24 Console.WriteLine (ex.Message); 25 } 26 }
3.6 Working with Queue<T> class
Queues are containers that ensure items are accessed in first-in, first-out manner.
member of Queue<T> | Meaning |
Dequeue() | Removes and returns the object at the beginning of the Queue<T> |
Enqueue() | Adds an object to the end of the Queue<T> |
Peek() | Returns the object at the beginning of the Queue<T> without removing it |
1 public static void UseGenericQueue() 2 { 3 Queue<Person> peopleQ = new Queue<Person> (); 4 peopleQ.Enqueue (new Person{ FirstName = "test1", LastName = "test", Age = 13 }); 5 peopleQ.Enqueue (new Person{ FirstName = "test2", LastName = "test", Age = 13 }); 6 peopleQ.Enqueue (new Person{ FirstName = "test3", LastName = "test", Age = 13 }); 7 8 //peek at the first person 9 Console.WriteLine("{0} is in the first position", peopleQ.Peek().FirstName); 10 //remove the first person 11 peopleQ.Dequeue (); 12 Console.WriteLine("{0} is in the first position", peopleQ.Peek().FirstName); 13 }
3.7 Working with SortedSet<T> class
The SortedSet<T> class is useful because it automatically ensures that the items in the set are sorted when you insert or remove items. However, you do need to inform it how you want to sort the objects, by passing in as a constructor argument an object that implements the generic IComparer<T> interface.
1 static void SortedSet() 2 { 3 SortedSet<Person> setOfPeople = new SortedSet<Person> (new SortByAge ()) { 4 new Person{ FirstName = "test1", LastName = "test", Age = 20 }, 5 new Person{ FirstName = "test2", LastName = "test", Age = 34 }, 6 new Person{ FirstName = "test3", LastName = "test", Age = 40 } 7 }; 8 9 foreach (Person p in setOfPeople) { 10 Console.WriteLine (p.Age); 11 } 12 13 //add other people 14 setOfPeople.Add(new Person{FirstName="test4", LastName="test", Age=5}); 15 setOfPeople.Add(new Person{FirstName="test4", LastName="test", Age=80}); 16 foreach (Person p in setOfPeople) { 17 Console.WriteLine (p.Age); 18 } 19 20 }
As the result, the list is sorted by person‘s age.
4. System.Collections.ObjectModel
Now, we can briefly examine an additional collection-centric namspace, System.Collections.ObjectModel. This is a relatively small namespace, which contains a handful of classes.
System.Collections.ObjectModel Type | Meaning |
ObservableCollection<T> | Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed |
ReadOnlyObservableCollection<T> | Represents a read-only version of ObservableCollection<T> |
The observableCollection<T> class is very userful in that it has the ability to inform external objects when its contents have changed in some way.
4.1 Working with ObservableCollection<T>
In many ways, working with the ObservableCollection<T> is identical to working with List<T>, given that both of these classes implement the same core interfaces. What makes it unique is that this class supports an event named CollectionChanged. This event will fire whenever a new item is inserted, a current item is removed.
1 public static void Main (string[] args) 2 { 3 ObservableCollection<Person> people = new ObservableCollection<Person> () { 4 new Person{FirstName="test1", LastName ="test", Age=20}, 5 new Person{FirstName="test2", LastName ="test", Age=23}, 6 }; 7 8 people.CollectionChanged += (object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) => { 9 Console.WriteLine("Action for this event {0}", e.Action.ToString()); 10 switch(e.Action) 11 { 12 case NotifyCollectionChangedAction.Remove: 13 Console.WriteLine("Old items"); 14 foreach(Person p in e.OldItems) 15 { 16 Console.WriteLine(p.FirstName); 17 } 18 break; 19 case NotifyCollectionChangedAction.Add: 20 Console.WriteLine("New items"); 21 foreach(Person p in e.NewItems) 22 { 23 Console.WriteLine(p.FirstName); 24 } 25 break; 26 default: 27 break; 28 } 29 }; 30 31 //add one person 32 people.Add (new Person{ FirstName = "test3", LastName = "test", Age = 32 }); 33 34 }
5. Create custom generic methods
While most developers typically use the existing generic types within the base class libraries, it is also possible to build your own generic members and custom generic types. When you build custom generic methods, you achieve a supercharnged version of traditional method overloading. While overloading is a useful feature in an object-oriented language, one problem is that you can easily end up with a ton of methods that essentially do the same thing.
1 static void Swap(ref int a, ref int b) 2 { 3 int temp; 4 temp = a; 5 a = b; 6 b = temp; 7 } 8 9 static void Swap(ref Person a , ref Person b) 10 { 11 Person temp; 12 temp = a; 13 a = b; 14 b = temp; 15 } 16 17 //one generic methods 18 static void Swap<T>(ref T a, ref T b) 19 { 20 T temp; 21 temp = a; 22 a = b; 23 b = temp; 24 }
As you can see, we can write one generic mehtod, instead of creating two methods basically doing the same thing.
6. Create Custom Generic Structures and Classes
It‘s time to turn your attention to the construction of a gennric structure or class.
1 public struct Point<T> 2 { 3 private T xPos; 4 private T yPos; 5 6 public Point(T xValue, T yValue) 7 { 8 xPos = xValue; 9 yPos = yValue; 10 } 11 12 public T X 13 { 14 get{ return xPos; } 15 set{ xPos = value; } 16 } 17 18 public T Y 19 { 20 get { return yPos; } 21 set { yPos = value; } 22 } 23 }
6.1 The default keyword in generic code
With the introduction of generic, the C# default keyword has been given a dual identity. In addition to its use within a swith construct, it can also be used to set a type parameter to its default value. This is helpful because generic type does not know the actual placeholders up front. Numeric values have a default value of 0, reference types have a default value of null.
public void ResetValue() { xPos = default(T); yPos = default(T); }
6.2 Constraining type parameters
In this chapter, any generic item has at least one type parameter that you need to specify at the time you interact with the generic type or member. This alone allows you to build some type-safe code; however, the .net platform allows you to use the where keyword to get extremely specific about what a given type parameter must look like.
Using this keyword, you can add a set of constraints to a given type parameter, which the C# compiler will check at the compile time.
Generic | Meaning |
Where T : struct | The type parameter <T> must have System.ValueType in its chain of inheritance (i.e <T> must be structure). |
Where T : class | The type parameter <T> must not have System.ValueType in it chain of inheritance (i.e, <T> must be a reference type) |
Where T : new() | The type parameter <T> must have a default constructor. Note that this constraint must be listed last on a multiconstrained type |
where T : NameOfBaseClass | The type parameter <T> must be derived from the class specified by NameOfBaseClass |
where T : NameOfInterface | The type parameter <T> must implement the interface specified by NameOfInterface. You can separate multiple interfaces as a comma delimited list |
public class MyGeneric<T> where T : new(){} public class MyGeneric1<T> where T : MainClass, IComparable, new(){} public class MyGeneric2<T, K> where T : new() where K : struct, IComparable<T>{}