C# - Generics

Created:2019-03-04  Last modified:2019-07-23


  1. Introduction

    Generics (C# 2.0 and later support) gives functions (delegates), classes, interfaces, struct type parameters. Generics in C# and .NET is an runtime features.

    Consideration

    When learning generics, some common features should be evaluated.

    1. Generics inheritance: Is G<Child> is a subclass of G<Parent>? No. Because of the implementation of generics, they are two different classes.
    2. Generics type parameter restriction, e.g. the generics type must implement a specific interface. C++ does not support
    3. Generics nested type: Outter<T> and Inner...

    Naming

    Generics type start with prefix T, e.g. public class SpecialCollection<TItem>

    Generics

  2. Rules and Examples

    1. Generic Class

      T can be treated as a special parameter called type parameter at a special position.

      using System;
      using System.Net.Http;
      public class Program
      {
      	public static void Main()
      	{
      		/*
      		t is updated
      		10
      		t is updated
      		System.Net.Http.HttpClient*/
      		G<int> g_int = new G<int>(10);
      		g_int.print();
      		G<HttpClient> c_int = new G<HttpClient>(new HttpClient());
      		c_int.print();
      	}
      }
      public class G<T>{
      	private T _t;
      	public T t {
      		get{
      			return this._t;
      		}
      		set{
      			Console.WriteLine("t is updated");
      			this._t = value;
      		}
      	}
      	public G(T t){
      		this.t = t;
      	}
      	public void print(){
      		Console.WriteLine(t.ToString());
      	}
      }
                          
    2. Generic & Nested Class

      Nested class can refer to container's class parameter type.
      Nested class can also have its own parameter type.

      Parameter type can be declared in class, delegate, interface, struct,

                                      public class Outter<T>{};
                                      public interface Outter<T>;
                                      public struct Outter<T>{};
                                      public delegate A fun<A, B>(B arg);
                              
      and can be referenced in the declarations of class, delegate, interface, struct, local variables, fields, properties, constructor and method (method including parameter, return value)

                                  public class Program
                                  {
                                      public static void Main()
                                      {
                                          Outter<string> o = new Outter<string>("qinnan");
                                          Outter<string>.Inner<int> i1 = new Outter<string>.Inner<int>("qinnan", 23);
                                          Outter<string>.Inner<int> i2 = new Outter<string>.Inner<int>(o); // Outter`1 [System.String]
                                      }
                                  }
                                  public class Outter<T>{
                                      public T t;
                                      public Outter(T t){
                                          this.t = t;
                                      }
                                      public class Inner<M>{
                                          public Inner(T t, M m){
                                              Console.WriteLine(t + ", " + m);
                                          }
                                          public Inner(Outter<T> o){
                                              Console.WriteLine(o);
                                          }
                                          /*public Inner(Outter<N> o){
                                              N is not declared, N can only be referenced here.
                                          }*/
                                      }
                                  }
                          

      delegate usage: declaration and reference of parameter type.

                                  public class Program
                                  {
                                      public static void Main()
                                      {
                                          Outter<string>.Inner<int>.fun<HttpClient> foo = delegate(HttpClient arg){return "qinnan";};	
                                          Console.WriteLine(foo(new HttpClient())); // qinnan
                                      }
                                  }
                                  public class Outter<T>{
                                      public class Inner<M>{
                                          public delegate T fun<B>(B arg);
                                      }
                                  }
                          

      Generic Interface: A generic class can implement a generic interface. A non-generic class can only implement the generic interface that has been given a type argument.

      public interface IGenerics<T>{
      	T add(T t1, T t2);
      }
      public class SpecialString: IGenerics<string>{
      	public string add(string s1, string s2){
      		return s1 + s2;
      	}
      }
      public class CGenerics<T>: IGenerics<T>{ // class declare T, interface references T
      	public T add(T t1, T t2){
      		return t1;
      	}
      }
                          
    3. Generics Inheritance

                                  public class Program
                                  {
                                      public static void Main()
                                      {
                                          P p = new C();
                                          //GenericsC<P> gp = new GenericsC<C>();
                                      }
                                  }
                                  public class P{}
                                  public class C:P{}
                                  public class GenericsC<T>{}
                          
  3. Covariance and Contravariance

    Example: Manager is a subclass of Employee, but List<Manager> is not a subclass of List<Employee>. This makes sense since List<Manager> does not inherit anything from List<Employee>.

    But for a delegate, which takes inputs and return an output, if the
          delegate A takes an instance of Manager as input and return an instance of Employee as output
          delegate B takes an instance of Employee as input and return an instance of Manager as output
    then a value of delegate A can be assigned to delegate B in theory.

    we can do Employee e = A(new Manager()); and Manager m = B(new Employee());. But for delegate B, we also can do Employee e = B(new Manager()); because we can assign Manager to Employee. So we can assign A to B in theory. And the theory is implemented in C# by the feature of covariance and contravariance.

    ** The feature is only applied to generic delegate and interface. **

    1. Contravariance (reserve the potential of input, 浪费了这个function在 in 上的能力)

      If a superclass can be an input, then its subclasses can also be the input.

                                  public class Parent{
                                      public string name;
                                  }
                                  public class Child: Parent{
                                      public string value;
                                  }
                                  // Action <in T>(T obj);
                                  Action<Parent> fun_print = (Parent obj) =>{
                                          Console.WriteLine(obj.name);
                                      };
      
                                  // input of fun_print_child is a Child but we actually only used Parent's member.
                                  Action<Child> fun_print_child = fun_print;
                          
    2. Covariance (reserve the potential of output, 浪费了这个function在 output 上的能力)

      If a superclass can be an output, then its subclasses can also be the output.

                                  Converter<string, Child> fun_convert_to_child = (string obj) =>{
                                          return new Child{
                                              name = obj,
                                              value = obj
                                          };
                                      };
                                  public delegate TOutput Converter<in TInput,out TOutput>(TInput input);
                                  Converter<string, Parent> fun_convert_to_parent = fun_convert_to_child;
                                  Parent p = fun_convert_to_parent("nothing");
                                  Console.WriteLine(p.name);
                          
    3. Interface

      Same principle applied to interface

          public interface GenericInterface<in TInput, out TResult>
          {
              TResult GetResult();
              void SetInput(TInput);
          }
          public class Program{
              public static void Main(String [] args){
                  GenericInterface<SuperClass, SubClass> temp = new Parent<SuperClass, SubClass>();
                  GenericInterface<SubClass, SuperClass> temp2 = temp;
              }
          }
                          
  4. Commonly used generics in ASP.NET

    1. A function has the generic type as input

                              // do something on the input object.
                              public delegate void Action<in T>(T obj);
                              // convert input to output
                              public delegate TOutput Converter<in TInput,out TOutput>(TInput input);
                              // according to the input, generate a result.
                              public delegate TResult Func<in T,out TResult>(T arg);
                              // according to the two inputs, generate a result
                              public delegate TResult Func<in T1,in T2,out TResult>(T1 arg1, T2 arg2);
                              // ... there is a seris of func that takes a varity of inputs. 
      
      
                              // usage: extension method of to define a middlware with IApplicationBuilder as input
                              public static IRouteBuilder MapMiddlewareGet (this IRouteBuilder builder, string template, Action<IApplicationBuilder> action);
      
                          
  5. Runtime Behavior

    Type parameter is value type

    Supplied type parameter is a value type, then it behaves like C++. Specialized generic types are created one time for each unique value type that is used as a parameter. The reason is the sizes of values of different value types are different.

    Type parameter is reference type

    Supplied type parameter is a reference type, then it behaves like Java. The first time a generic type is constructed with any reference type, the runtime creates a specialized generic type with object references substituted for the parameters in the MSIL. Then, every time that a constructed type is instantiated with a reference type as its parameter, regardless of what type it is, the runtime reuses the previously created specialized version of the generic type. This is possible because all references are the same size.

  6. References