Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / C#

Generics

Rate me:
Please Sign up or sign in to vote.
4.22/5 (6 votes)
8 May 2009CPOL5 min read 25.4K   29   3
More on Generics

Introduction - Generic Collections

Introduction

There were several new C# constructs introduced in .NET 2.0 in order to deal with some type safety and Windows Forms isssues. Apart from partial clases and anonymous delgates, a C# construct called generics was introduced. Generics are conceptually similar to C++ templates in the fact that both deal with parameterized types, but generics are a runtime instantiation as opposed to a C++ compilation instantiation. Stated crudely, rather than passing a parameter to method, you are pasing a data type to an object. Should you have a generalized algorithm, you can use generics to sort through lists or perform binary searches by using this reusable code and passing the parmeterized type correspondant to the type involved in the algorithmic funcationlity-oriented operation. Generics, again, were not introduced until .NET 2.0.

Arrays have some obvious limitations. They are a sequence of homogenous items (homogenous meaning of the same type), but they also stop where collections begin. The collections in the System.Collections.Generic enable you to use generics to constrain what type of item may be stored inside an instance. The primary strength of generics lie in the fact in that they eliminate the need for type conversion by using boxing and unboxing. These types of casts cause “unsafe types” and also reduce the performance. All generic collection classes implement a core-set of base-interfaces that enable common operations independent of the algorithm used by the implementation. This allows generalized algorithms to operate on collections of things. An interface, also known as a contract, differs from a class in that an interface defines a common set of members that all classes which implement the interface must provide. For example, the IComparable interface defines the CompareTo method, which enables two instances of a class to be compared for equality.

Simple Collections (ICollections<t>)

The ICollection<t> interface is used to represent a simple collection of items, each of type T. For example, an ICollection<string> contains a collection of string references. The interface exposes the capability to access and modify the contents of a collection and to determine its length. It also derives from the IEnumerable<t>, meaning that any implementation of ICollection<t> will also supply a GetEnumerator method that returns an enumerator, using which you may walk its contents. This interface and its specific classes can be used for the manipulation of arrays where the size can vary as we add or remove elements. Such arrays are called sequences.

C#
namespace System.Collections.Generic
 {
   public interface ICollection<t> : IEnumerable<t>
  {
   // Properties
      int Count {  get; }
      bool IsReadOnly  { get; }

//Methods

void Add( T item);
void  Clear();
bool Contains(T item);
void CopyTo(T[]  array, int arrayIndex);
IEnumerator<t> GetEnumerator();
IEnumerator   GetEnumerator();
bool Remove(T item);
  }
}

A code example of System Collections (ICollection<t>):

C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
public sealed class Program {
        public static void Main()
        {
            ICollection<int> myCollection = new Collection<int>();
// Note that we start by creating a Collection<int> object, 
// in whose parameters are stored by reference to myCollection
            // First, add a few elements to the collection:
            myCollection.Add(105);
            myCollection.Add(232);
            myCollection.Add(350);

            // .And then delete one:
            myCollection.Remove(232);

            // Search for some specific elements:
            Console.WriteLine("Contains {0}? {1}", 105, myCollection.Contains(105));
            Console.WriteLine("Contains {0}? {1}", 232, myCollection.Contains(232));

            // Enumerate the collection's contents:
            foreach (int i in myCollection)
                Console.WriteLine(i);

            // Lastly, copy the contents to an array so that we may iterate that:
            int[] myArray = new int[myCollection.Count];
            myCollection.CopyTo(myArray, 0);
            for (int i = 0; i < myArray.Length; i++)
                Console.WriteLine(myArray[i]);
        }
}

So we declared the interface to expose the functionality from the classes and then use the type to create an object. We used the delimiter in the Console.WriteLine() method to actually search the items of the collection, as the Contains() method returns a Boolean true if an item exists and a false if not. The Add() method adds an item in an unspecified location of the collection.

The IList<t> interface derives from ICollection<t> and adds a few members to support adding, removing, and accessing contents using a 0-based numerical index.

C#
namespace  System.Collections.Generic 
 {
  public  interface IList<t> : ICollection<t>
   {
       // members  inherited from ICollection<t>  have been omitted.
       // Properties
      T this[int index]  {  get;  set: }
     // Methods
    int IndexOf(T item);
    void Insert(int index, T item);
    void RemoveAt(int index);
  }
}

Here is another piece of sample code:

C#
using System;
       using System.Collections.Generic; 
       public sealed class Program {
        public static void Main()
        {
            IList<double /> myList = new List<double />();

            // First, we add, insert, and remove some items:
            myList.Add(10.54);
            myList.Add(209.2234);
            myList.Insert(1, 39.999);
            myList.Add(438.2);
            myList.Remove(10.54);

            // Then, we print some specific element indexes:
            Console.WriteLine("IndexOf {0} = {1}", 209.2234, myList.IndexOf(209.2234));
            Console.WriteLine("IndexOf {0} = {1}", 10.54, myList.IndexOf(10.54));

            // Lastly, we enumerate the list using Count and IList<t>'s indexer:
            for (int i = 0; i < myList.Count; i++)
                Console.WriteLine(myList[i]);
        }
}

Output

IndexOf 209.2234 = 1 

IndexOf 10.54 = -1 
39.999
209.2234
438.2

Reiterate the code, and notice that the object made by calling the new operator has its constructor parameters stored in myList:

C#
IList<double> myList = new List<double>();

// First, we add, insert, and remove some items:
            myList.Add(10.54);
            myList.Add(209.2234);
            myList.Insert(1, 39.999);
            myList.Add(438.2);
            myList.Remove(10.54);

// Then, we print some specific element indexes:
            Console.WriteLine("IndexOf {0} = {1}", 209.2234, myList.IndexOf(209.2234));
            Console.WriteLine("IndexOf {0} = {1}", 10.54, myList.IndexOf(10.54));

// Lastly, we enumerate the list using Count and IList<t>'s indexer:
            for (int i = 0; i < myList.Count; i++)
                Console.WriteLine(myList[i]);
        }
}

We set up our list by adding two numbers, 10.54 and 209.2234, inserting 39.999 between them, and adding 438.2 at the end. Finally, we removed 10.54. We then locate the index of 209.2234, which ends up being, since we removed the previously placed element (10.54). Recall that we are using a 0-based numerical index, which means that 10.54 would have indexed to 0. We also look for 10.54, which results in -1 because we removed it from the collection. Lastly, we loop through the contents of the list, accessing its contents with the indexer.

Dictionaries IDictionary(<tkey,>)

A dictionary is a container with a collection of key and value associations. Aside from dictionary, this data structure is often called an associative array, map, or hash table, the latter of which is actually the name of a common implementation technique for creating dictionaries. With IDictionary<tkey>, the type parameters TKey and TValue represent the types of the keys and values it can store. So, for example, an IDictionary<string> is a dictionary that contains string keys that map to integer values.

C#
namespace System.Collection.Generic
   {
       // note members inherited from ICollection<t> have been omitted
            public  interface IDictionary<tkey> :
 ICollection<keyvaluepair<tkey>>
  {
   // Properties
TValue this[TKey key] {  get;  set; }
ICollection<tkey> Keys { get; }
ICollection<tvalue> Values { get; }
// Methods
void  Add(TKey key, TValue value);
bool ContainsKey(TKey key);
bool Remove(TKey key);
bool TryGetValue(TKey key, out TValue value);
  }
[Serializable, StructLayout(LayoutKind.Sequential)]
public  struct KeyValuePair<tkey>
  {
      public  TKey Key;
      public TValue Value;
      public KeyValuePair(TKey key, TValue value);
      public override string ToString();
   }
}

Each key-value association is represented by a KeyValuePair object. Thus, this type is really just a collection of KeyValuePairs. KeyValuePair is a value type that provides two public fields: Key of type TKey and Value of type TValue. Here is the sample code:

C#
       using System;
       using System.Collections.Generic;
       public sealed class Program {
       public static void Main()
           {
            IDictionary<string> salaryMap = new Dictionary<string,>();

            // Add some entries into the dictionary:
            salaryMap.Add("Sean", 62250.5M);
            salaryMap.Add("Wolf", 16000.0M);
            salaryMap.Add("Jamie", 32900.99M);

            // Now, remove one:
            salaryMap.Remove("Wolf");

            // Check whether certain keys exist in the map:
            Console.WriteLine(salaryMap.ContainsKey("Sean")); // Prints `True'
            Console.WriteLine(salaryMap.ContainsKey("Steve")); // Prints `False'

            // Retrieve some values from the map:
            Console.WriteLine("{0:C}", salaryMap["Sean"]); // Prints `$62,250.50'
            Console.WriteLine("{0:C}", salaryMap["Jamie"]); // Prints `$32,900.99'

            // Now just iterate over the map and add up the values:
            decimal total = 0.0M;
            foreach (decimal d in salaryMap.Values)
                total += d;
            Console.WriteLine("{0:C}", total); // Prints `$95,151.49'

            // Iterating over map/value pairs:
            // (The wrong way)
            foreach (string key in salaryMap.Keys)
                Console.WriteLine("{0} == {1}", key, salaryMap[key]);

            // (The right way)
            foreach (KeyValuePair<string> kvp in salaryMap)
                Console.WriteLine("{0} == {1}", kvp.Key, kvp.Value);
        }
}

Output

True
False
$62,250.50
$32,900.99
$95,151.49
Sean == 62250.5
Jamie == 32900.99
Sean == 62250.5
Jamie == 32900.99

Some more basic code to exemplify the concept:

C#
using System;
using System.Collections.Generic;
class Program 
 {
  static void Main(string[]  args)
    {
      Dictionary<int,> cl = new Dictionary<int,>();
       cl[44] = "UnitedKingdom";
       cl[33] = "France";
       cl[31] = "Netherlands";
       cl[55] = "Brazil";    
       Console.WriteLine("The 33 code is for: {0}", cl[33]);    

       foreach (KeyValuePair<int,> item in cl)
        {
            int code = item.Key;
            string country = item.Value;        
            Console.WriteLine("Code {0} = {1}", code, country);
         }   
      Console.Read();
     }
}

Output

The 33 code is for: France
Code 44 = UnitedKingdom
Code 33 = France
Code 31 = Netherlands
Code 55 = Brazil

Enumerators IEnumerable<t> and IEnumerator<t>

The general concept of an enumerator is that of a type whose sole purpose is to advance through and read anther collection’s contents. Enumerators do not provide write capabilities. IEnumerable<t> represents a type whose contents can be enumerated, while IEnumerator<t> is the type for performing the actual enumeration:

C#
namespace System.Collections.Generic
 {
  public interface IEnumerable<t> : IEnumerable
   {
     // Methods
           IEnumerator<t> GetEnumerator();
           IEnumerator GetEnumerator(); // inherited from IEnumerable
      }
    public interface IEnumerator<t> : IDisposable, IEnumerator
    {
       // Properties
        
        T Current { get; }
        object Current {  get; }   // inherited from IEnumerator
        // Methods
       void Dispose();
        bool MoveNext();
        void Reset();
    }
 } 

Upon instantiation, an enumeration becomes dependent on a collection. Walking an enumeration’s contents involve the foreach construct, which rely on enumerators to access the contents of any enumerable collection. So when you write the following code:

C#
IEnumerable<string /> enumerable = new string[] { "A", "B", "C" };
foreach (string s in enumerable)
Console.WriteLine(s);
. . . .

The C# compiler will actually emit a call to enumerable’s GetEnumerator() method for you and will use the resulting enumerator to walk through its contents. Here is a sample code:

C#
         using System;
         using System.Collections.Generic;
         public sealed class Program {
         public static void Main()
        {
            IEnumerable<string> enumerable = new string[] { "A", "B", "C" };

            // Short-hand form:
            foreach (string s in enumerable)
                Console.WriteLine(s);

            // Long-hand form:
            IEnumerator<string> enumerator = enumerable.GetEnumerator();
            while (enumerator.MoveNext())
            {
                string s = enumerator.Current;
                Console.WriteLine(s);
            }
        }
}

Output

A
B
C
A
B
C

C# Iterators

To create an iterator, you must create a (static) method that returns either IEnumerable<t> or IEnumerator<t>, and generates a sequence of values using the yield statement. The C# compiler will create the underlying IEnumerator<t> type for you:

C#
     class DoublerContainer : IEnumerable<int>
      {
          private List<int> myList = new List<int>();

          public DoublerContainer(List<int> myList)
          {
              this.myList = myList;
          }

          public IEnumerator<int> GetEnumerator()
          {
              foreach (int i in myList)
                  yield return i * 2;
          }

          IEnumerator IEnumerable.GetEnumerator()
          {
              return ((IEnumerable<int>)this).GetEnumerator();
          }
}

Now we create a static method that returns either IEnumerable<t> or IEnumerator<t>, and generates a sequence of values using the yield statement.

C#
using System;
using System.Collections.Generic;
static IEnumerable<int> Fibonacci(int max);
        {
            int i1 = 1;
            int i2 = 1;

            while (i1 <= max)
            {
                yield return i1;
                int t = i1;
                i1 = i2;
                i2 = t + i2;
            }
        }
        public class Program {
        public static void Main()
        {
            // First, walk a doubler-container:
            Console.WriteLine("Doubler:");
            DoublerContainer dc = new DoublerContainer(
                new List<int>(new int[] { 10, 20, 30, 40, 50 }));
            foreach (int x in dc)
                Console.WriteLine(x);

            // Next, walk the fibonacci generator:
            Console.WriteLine("Fibonacci:");
            foreach (int i in Fibonacci(21))
                Console.WriteLine(i);
        }
     }
   }

Output

Doubler:
20
40
60
80
100
Fibonacci:
1
1
2
3
5
8
13
21

Collections Implementations

Standard List (List<t>)

The List<t> class is the most commonly used implementation of IList<t>, providing an order, indexable collection of objects. When you construct a new list, you can optionally pass in its initial capacity as an argument using the List<t>(int capacity) constructor. List<t> also has methods like Reverse and sort that actually modify the order in which a list's items are stored.

C#
using System;
using System.Collections.Generic;
public sealed class Program {
public static void Main() {
// create an initialize byte array
Byte[] byteArray = new Byte[] { 5, 1, 2, 4, 3 };
// call byte sort algorithm
Array.Sort<byte />(byteArray);
// call binary search algorithm
Int32 i = Array.BinarySearch<byte />(byteArray, 3);
 Console.WriteLine(i);
  }
}

The output is 2.

Lastly, an example of welding .NET language constructs together to develop a practical application.

C#
        using System;
        using System.IO;
        using System.Text;
        using System.Collections.Generic;      

        struct Employee
        {
            public string FirstName;
            public string LastName;
            public int Extension;
            public string SocialSecurityNumber;
            public bool Salaried;

            public Employee(string firstName, string lastName, 
			int extension, string ssn, bool salaried)
            {
                this.FirstName = firstName;
                this.LastName = lastName;
                this.Extension = extension;
                this.SocialSecurityNumber = ssn;
                this.Salaried = salaried;
            }

            public override string ToString()
            {
                return string.Format("{0}, {1}; {2}; {3}; {4}", 
		LastName, FirstName, Extension, SocialSecurityNumber, Salaried);
            }        

        private static List<employee> CreateEmployees()
        {
            List<employee> emps = new List<employee>();
            emps.Add(new Employee("Joe", "Duffy", 32500, "000-11-2222", true));
            emps.Add(new Employee("George", "Bush", 50123, "001-01-0001", true));
            emps.Add(new Employee("Jess", "Robinson", 99332, "321-21-4321", false));
            emps.Add(new Employee("Billy", "Bob", 32332, "333-22-1111", true));
            emps.Add(new Employee("Homer", "Simpson", 93812, "999-88-7777", false));
            return emps;
        }

        private static List<employee> DeserializeEmployees(Stream s)
        {
            List<employee> employees = new List<employee>();
            BinaryReader reader = new BinaryReader(s);

            try
            {
                while (true)
                {
                    Employee e = new Employee();
                    e.FirstName = reader.ReadString();
                    e.LastName = reader.ReadString();
                    e.Extension = reader.ReadInt32();
                    e.SocialSecurityNumber = reader.ReadString();
                    e.Salaried = reader.ReadBoolean();
                    employees.Add(e);
                    Console.WriteLine("Read: {0}", e.ToString());
                }
            }
            catch (EndOfStreamException)
            {
                // ok, expected end of stream
            }

            return employees;
        }

        private static void SerializeEmployees(Stream s, IEnumerable<employee> employees)
        {
            BinaryWriter writer = new BinaryWriter(s);
            foreach (Employee e in employees)
            {
                writer.Write(e.FirstName);
                writer.Write(e.LastName);
                writer.Write(e.Extension);
                writer.Write(e.SocialSecurityNumber);
                writer.Write(e.Salaried);
                Console.WriteLine("Wrote: {0}", e.ToString());
            }
        }
        public sealed class Program {
        public static void Main()
        {
            Stream s = new MemoryStream();
            IEnumerable<employee> employees = CreateEmployees();

            SerializeEmployees(s, employees);

            // Deserialize:
            s.Seek(0, SeekOrigin.Begin);
            DeserializeEmployees(s);

            // Print out raw bytes:
            s.Seek(0, SeekOrigin.Begin);
            int read;
            while ((read = s.ReadByte()) != -1)
                Console.Write("{0:X} ", read);
        }
     }
}

Output

Wrote: Duffy, Joe; 32500; 000-11-2222; True
Wrote: Bush, George; 50123; 001-01-0001; True
Wrote: Robinson, Jess; 99332; 321-21-4321; False
Wrote: Bob, Billy; 32332; 333-22-1111; True
Wrote: Simpson, Homer; 93812; 999-88-7777; False
Read: Duffy, Joe; 32500; 000-11-2222; True
Read: Bush, George; 50123; 001-01-0001; True
Read: Robinson, Jess; 99332; 321-21-4321; False
Read: Bob, Billy; 32332; 333-22-1111; True
Read: Simpson, Homer; 93812; 999-88-7777; False
3 4A 6F 65 5 44 75 66 66 79 F4 7E 0 0 B 30 30 30 2D 31 31 2D 32 32 32 32 1 6 47
65 6F 72 67 65 4 42 75 73 68 CB C3 0 0 B 30 30 31 2D 30 31 2D 30 30 30 31 1 4 4A
 65 73 73 8 52 6F 62 69 6E 73 6F 6E 4 84 1 0 B 33 32 31 2D 32 31 2D 34 33 32 31
0 5 42 69 6C 6C 79 3 42 6F 62 4C 7E 0 0 B 33 33 33 2D 32 32 2D 31 31 31 31 1 5 4
8 6F 6D 65 72 7 53 69 6D 70 73 6F 6E 74 6E 1 0 B 39 39 39 2D 38 38 2D 37 37 37 3
7 0

History

  • 8th May, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Monroe Community
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralGenerics not an issue for C# 1.0 or 1.1 Pin
LotharLanger8-May-09 8:35
LotharLanger8-May-09 8:35 
GeneralRe: Generics not an issue for C# 1.0 or 1.1 Pin
logicchild8-May-09 9:01
professionallogicchild8-May-09 9:01 
GeneralRe: Generics not an issue for C# 1.0 or 1.1 Pin
Darchangel12-May-09 4:05
Darchangel12-May-09 4:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.