Learning C# LINQ and C# 2.0

What's new in C# 2.0? Generics, Nullable types, Anonymous Methods, Partial types, Static Classes, and Iterators.

Before we get to the magic of Generics, let's review collections.

  1. Collections
    1. IEnumerator

      The IEnumerator interface consists of only three methods to traverse a collection:

      public interface IEnumerator
      {
             bool MoveNext();
             object Current { get; }
             void Reset();
      }
      

      Collections return an IEnumerable object instead of implementing it directly.

      public interface IEnumerable
      {
            IEnumerator GetEnumerator();
      }
      
      using System;
      using System.Collections;
      
      namespace Practice
      {
          class Enumerators
          {
              static void Main()
              {
                  string myString = "SeaSharp";
                  // string implements IEnumerable,  so let's get us an enumerator:
                  IEnumerator enumerator = myString.GetEnumerator();
                  while (enumerator.MoveNext())
                  {
                      char character = (char)enumerator.Current;
                      Console.Write(character + "-");
                  }
                  // Output: S-e-a-S-h-a-r-p
                  Console.Write("Press 'Enter' to exit.");Console.In.ReadLine();
              }
          }
      }
      

      Doing the above is a little tedious, so Anders gave us the "foreach" construct which does the above behind the scenes:

      string myString = "SeaSharp";
      foreach(char character in myString)
       {
           Console.Write(character + "-");
       }
      

      With the advent of generics, most collections now use the type-safe version of IEnumerator:

      public interface IEnumerator<T> : IEnumerator, IDisposable
      {
        T Current { get; }
      }
      public interface IEnumerable<T> : IEnumerable
      {
        IEnumerator<T> GetEnumerator();
      }
      

      We can create our own IEnumerable<T> class by implementing IEnumerable and using the "yield return" syntax. The compiler will do the rest. The yield statement returns control to the calling routine and then picks up exactly where it left off. This can be advantageous if creating the elements is expensive since you don't have to create them until called.

      using System;
      using System.Collections;
      using System.Collections.Generic;
      
      namespace Practice
      {
          class Enumerators
          {
              static void Main()
              {
                  MyZoo myZoo = new MyZoo();
                  foreach (string animal in myZoo)
                  {
                      Console.Out.Write("{0},", animal);//lions,tigers,bears,elephants
                  }
                  Console.Write("Press 'Enter' to exit.");Console.In.ReadLine();
              }
      
              public class MyZoo : IEnumerable<string>
              {
                  readonly string[] zoo = { "lions","tigers","bears","elephants" };
                  public IEnumerator<string> GetEnumerator()
                  {
                      foreach (string animal in zoo)
                          yield return animal;
                  }
                  IEnumerator IEnumerable.GetEnumerator() //must implement the non-generic method 
                  {
                      return GetEnumerator();
                  }
              }
      
          }
      }
      

      The ICollection<T> interface adds to IEnumerable<T> the ability to get a count of items and to add and remove items:

      public interface ICollection<T> : IEnumerable<T>, IEnumerable
      {
            int Count { get; }
            bool Contains (T item);
            void CopyTo (T[] array, int arrayIndex);
            bool IsReadOnly { get; }
            void Add(T item);
            bool Remove (T item);
            void Clear();
      }
      

      Finally we are to my favorite collection, IList<T>. IList<T> adds the ability to operate on items at a particular index.

      public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
      {
             T this [int index] { get; set; }
             int IndexOf (T item);
             void Insert (int index, T item);
             void RemoveAt (int index);
      }
      
    2. Case-Insensitive Hashtable

      These are very handy when case doesn't matter.

       Hashtable hashTable = System.Collections.Specialized.CollectionsUtil.CreateCaseInsensitiveHashtable();
      
    3. With C#2.0 you should really use a type-safe Dictionary collection instead of hashtable most of the time.

      Here is an example of using a Dictionary with a Guid key and a Survey object as a value.

      using System.Collections.Generic;
      ...
      private static Dictionary<Guid,Survey> surveyByIdDictionary = null;
      private static readonly object surveyByIdDictionaryLock = new object();
      /// <summary>
      /// return Survey object based on id.
      /// If the id requested does not exist, a null is returned.
      /// </summary>
      
      /// <param name="id">guid of the survey of interest.</param>
      /// <returns>survey or null</returns>
      public static Survey GetById(Guid id) {
         if (surveyByIdDictionary == null) {
             lock (surveyByIdDictionaryLock) {
                  if (surveyByIdDictionary == null) {
                      surveyByIdDictionary = new Dictionary<Guid, Survey>();
                      foreach (string surveyName in Survey.Default.GetNames()) {
                          Survey survey = Survey.Get(surveyName);
                          Survey.surveyByIdDictionary.Add(survey.id, survey);
                      }
                  }
              }
          }
          return surveyByIdDictionary.ContainsKey(id) ? surveyByIdDictionary[id] : null;
      }
              
      
    4. Using indexers

      You can override the "[]" operator to return whatever you wish

      /// <summary>
      /// indexer method to get SurveyStatus object by name
      /// </summary>
      
      public SurveyStatus this[ string surveyName] {
      	get {
      		foreach(SurveyStatus ss in surveyStatuses) {
      			if(CaseInsensitiveComparer.Default.Compare(surveyName, ss.GetName()) == 0) {
      				return ss;
      			}
      		}
      		return null;
      	}
      }
      
  2. Generics

    Generics are a way of using a "type" as a variable.

    1. Using generics in a List.

      A popular use of Generics is in the Collections. You can create a List of any type, since it is generic, "List<T>".

      using System.Collections.Generic;
      ...
      List<string> list = new List<string>();
      
    2. Create your own generic class

      By adding "<T>" after the class you can create a Templated, or generic, version of your object, in this case a wrapper for cached items that have their creation date associated with them so they can be flushed later. We don't have to create a CachedItemString and a CachedItemInt.

      using System;
      
      namespace PlayingAround {
          class Cacher<T> {
                      public T CachedItem;
                      public DateTime ExpirationDate;
          }
          class Runner {
              private static void Main() {
                  Cacher<string> myString = new Cacher<string>();
                  myString.CachedItem = "old string";
                  Console.Out.WriteLine("myString.CachedItem = {0}", myString.CachedItem);
                  Cacher<int> myInt = new Cacher<int>();
                  myInt.CachedItem = 6;
                  Console.Out.WriteLine("myInt = {0}", myInt.CachedItem);
                  Console.Out.Write("press return to exit."); Console.In.ReadLine();
              }
          }
      }
      

      Type parameters can only be used in methods and types - not in constructors or properties, etc...

      It is only by convention we use "T" as the name of the type, there is no magic in the name. When using multiple generic types, the convention is to make them more descriptive and start with a "T", e.g., "Node<TKey,TItem>".

      "Open Type" generics have their type still unknown like "Stack<T>", once bound to a type like "Stack<int>" it is called a "Closed Type". During compilation all open types are converted to closed types.

      The "default" keyword can be used to get the default value of a type, e.g., "default(T)".

    3. Create Generic Methods

      Methods can define their own generic types in their argument list. Note that when calling "Swap(...)" that the type is not needed since the types are infered from the arguments themselves.

      public static void Swap<T>(ref T first, ref T second)
      {
          T temp = first;
          first = second;
          second = temp;
      }
      private static void Main()
      {
          int x = 1, y = 2;
          Console.Out.WriteLine("x = {0}, y={1}",x ,y);
          Swap(ref x,ref y);
          Console.Out.WriteLine("x = {0}, y={1}", x, y);
      }
      
    4. Generic Constraints

      You can restrict the types used in Generics using "where"

      private class Animal{}
      private interface IPlant{}
      
      class ZooSpecimen<T> where T : Animal{}
      class BotanicalGardenSpecimen<T> where T : IPlant{}
      

      Constraints may be based on types, interfaces, classes, struct, a previously defined type ("Naked type constraint"), or if the type has a parameterless constructor ( "where T : new()").

    5. Subclassing Generics

      A Generic class can be subclassed. The subclass may leave the type open, or as shown below, it may close the type.

      private class Animal{}
      private class Reptile : Animal {} 
      
      class ZooSpecimen<T> where T : Animal{}
      class HerpatariumSpecimen<T> : ZooSpecimen<Reptile> {}
      
  3. Using Nullable Types

    In C# 2.0 we can use nullable types which are like regular objects but have a "HasValue()"

    using System;
    class Nullable {
        static void Main() {
            int? i = null;
            Console.Out.WriteLine("i = " + i);
            Console.Out.WriteLine("i.HasValue=" + i.HasValue); //False
            i = 6;
            Console.Out.WriteLine("i = " + i);
            Console.Out.WriteLine("i.HasValue=" + i.HasValue); //True
            Console.In.Read();
        }
    }
    
  4. Anonymous Methods

    Instead of having to name a method and then associate it with a delegate, you can use an anonymous method.

    using System;
    
    namespace Practice
    {
        class Delegates
        {
            delegate void Hello(string s);
            public static void OldWayOfTediouslyDefiningNamedMethod(string name)
            {
                System.Console.WriteLine("Hello " + name);
            }
            static void Main()
            {
                Hello oldWay = new Hello(OldWayOfTediouslyDefiningNamedMethod);
                oldWay("Old Dolly");
    
                Hello newWay = delegate (string name)
                    {
                        System.Console.WriteLine("Hello " + name);
                    };
    
                newWay("New Dolly");
                Console.Write("Press 'Enter' to exit."); Console.In.ReadLine();
            }
        }
    }
    
  5. Iterators

    Iterators allow us to use the "foreach" construct without having to implement "IEnumerable". By using the "yield return" construct, the compiler will generate all the magic for "foreach" to work.

    using System;
    
    namespace Practice
    {
        class Iterators
        {
            static void Main()
            {
                foreach (string animal in MyZoo())
                {
                    Console.Write(animal + ",");//aardvark,bear,cat,dingo
                }
                Console.Write("Press 'Enter' to exit."); Console.In.ReadLine();
            }
            public static System.Collections.IEnumerable MyZoo()
            {
                yield return "aardvark";
                yield return "bear";
                yield return "cat";
                yield return "dingo";
            }
        }
    }
    
  6. Partial types

    Partial types lets us define classes, interfaces and structs in more than one file. The name of the type must be preceeded by the modifier, "partial" and the files must be in the same namespace.

    In one file we have the following:
    using System;
    
    namespace Practice
    {
        partial class Car
        {
            string Color;
            public void Drive()
            {
                Console.WriteLine("I'm driving...");
            }
    
            static void Main()
            {
                Car car = new Car();
                car.Drive();
                car.Park();
                Console.Write("Press 'Enter' to exit.");Console.In.ReadLine();
            }
        }
    }
    

    And in another file we complete the definition of the class.

    using System;
    
    namespace Practice
    {
        partial class Car
        {
            int YearModel;
            public void Park()
            {
                Console.WriteLine("I'm parking...");
            }
        }
    }
    
  7. Static Classes

    Static classes are declared by prefacing the class name with "static". The compiler will insure that only static members are in the class. A static class cannot be instantiated.

    Static classes offer some performance benefits, but they are harder to mock.

    "Math" is an example of a static class.

    using System;
    
    namespace Practice
    {
        public static class MyStuff
        {
            public static string Version = "1.0.0.1";
            public int counter; //generates compiler error since it is not static
            static void Main()
            {
                Console.Write("Press 'Enter' to exit.");Console.In.ReadLine();
            }
        }
    }