Chapter 6 - Collections and Generics

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>{}

 

posted @ 2015-03-28 15:53  tim_bo  阅读(211)  评论(0编辑  收藏  举报