Generics in C#

C# permits classes, structs, interfaces and methods to be parameterized by the types of data they store and manipulate, through a set of features known collectively as generics. C# generics will be immediately familiar to users of generics in Eiffel or Ada, or to users of C++ templates, though they do not suffer many of the complications of the latter.

Generics are useful because many common classes and structs can be parameterized by the types of data being stored and manipulated - these are called generic classes and generic structs. Similarly, many interfaces define contracts that can be parameterized by the types of data transacted - these are called generic interfaces. Classic examples of generic classes are collection classes such as Hash Tables, Maps and Lists. It is intended to extend the .NET Framework with generic collection classes. Methods may also be parameterized by type in order to implement "generic algorithms", and these are known as generic methods. Often the static methods in a generic class will be parameterized in this way. Classic examples are sort algorithms.

Notation

Throughout this chapter we use the following notation:

C, C1, C2, ... refer to classes, either generic or non-generic.

I, I1, I2, ... refer to interfaces, either generic or non-generic.

V,U, W, ... refer to type-parameters

T, T1, T2, ... refer to types, including basic C# types, as well as constructed types and type parameters used as types.

B, B1, B2, ... refer to types used as explicit-type-parameter-constraints.

<inst>, <inst2>, ... refer to type instantiations.

Generics by examples

Generic collections represent the simplest and most common use of generic classes, interfaces and structs. The code below shows a very simple generic class that wraps an array as a read-only generic vector (the indexer does not allow elements to be updated):

class=Code>class Vector<V>
{
   private V[] data;
   public virtual V this[int n]  { get {return data[n]; }  }
   public Vector(V[] init)  { data = init; }
   public int Length { get { return data.Length; } }
}

The code below provides arrays that automatically expand as values are assigned at indices:

class ExpandingArray<V>
{
   private V[] data;
   public virtual V this[int n] 
   { 
      get { return data[n]; } 
      set 
      { 
         if (data == null)  
            data = new V[n+1];
         else if (n >= data.Length) 
         {
            int oldlen = data.Length;
            int newlen =  Math.max(n+1,oldlen * 2); 
            V[] newdata = new V[newlen];
            for (int i = 0; i < oldlen; i++) 
               newdata[i] = data[i];
            data = newdata; 
         }
         data[n] = value;
      }
   }
   public ExpandingArray () { data = null; }
}

Generic methods can be used to implement generic algorithms over generic collections. For example, the following code checks if the given vector is sorted:

interface IComparer<V> 
{ 
   int Compare(V v1,V v1); 
   ... 
}

class VectorMethods
{
   public static bool IsSorted<V>(IComparer<V> comparer, Vector<V> inp) 
   {
      for (int i=1; i<inp.Length; i++)
         if (comparer.Compare(inp[i-1], inp[i])> 0)
            return false;
      return true;
   }
}

Generic classes and methods often use additional methods that help them "interpret" type parameters. For example, the class may require a way of comparing values of type V. These methods are typically provided in one of the following ways:

(a)    Explicitly, by requiring that the client of the class pass extra parameters to the class that provide the functionality. For example, an instance constructor for the class may require a value of type Icomparer<V> to be passed in. This value can then be used to compare values of type V, as shown in the example above.

(b)    Explicitly, by using explicit constraints. In this case, the signature of the generic class specifies that any type used to instantiate the class must support particular types. Constraints are written in a "where" clause that follows the type parameters, e.g. class C<V> where V: IComparable. The example below shows how explicit constraints can be specified for a type method.
Implementation note: Currently only one constraint is allowed per generic parameter. This restricition will be relaxed in future releases.

(c)    Implicitly, by casting values of type V to an appropriate type within the body of the class (e.g. casting value to type IComparable) and then calling a method. This technique is known as implicit constraints. A similar way to implicitly access functionality from a type parameter is to use reflection.

The following code illustrates the use of explicit constraints to define a method that checks if the given vector is sorted.

interface IComparable<V> { int CompareTo(V); }

class VectorMethods
{
   public static bool IsSorted<V> where V: IComparable<V>(Vector<V> inp) {
      for (int i=1; i<inp.Length; i++)
         if (inp[i-1].CompareTo(inp[i]) > 0)
            return false;
      return true;
   }
}

What generics are not

Generics should not be confused with attempts to cover a very wide scope of possible applications of "generative programming." The .NET Framework offers rich dynamic code generation facilities through its reflection libraries, and for advanced generative programming these libraries can be used in conjunction with generics.

Constructed types, type arguments, type parameters and type instantiations

Generics extend the set of types available to the C# programmer in two ways, by adding constructed types and type parameters. Constructed types can be used in both generic and non-generic code to declare values whose types involve generic classes, structs and interfaces.

Type parameters

Type parameters can be used only in generic code, i.e. type parameters are visible within the bodies of generic-class-declarations, generic-struct-declarations, generic-method-declarations, generic-interface-declarations and generic-delegate-declarations. Properties, indexers and events may not be generic themselves, although instance properties, indexers and events may utilize the type parameters of their surrounding class.

The following rules apply to all type parameters:

         A type parameter has a scope, and within that scope may be used to form a type. The scope of a type parameter depends on the kind of declaration to which it is attached.

         When used to form a type, the accessibility of a type parameter is public.

Constructed types

A constructed type C<T1,...,Tn> is formed by applying a type name C to an appropriate number of type arguments T1,...,Tn. The type arguments T1,...,Tn together form a type instantiation. C# supports four different kinds of constructed types: constructed-class-types, constructed-struct-types, constructed-interface-types, and constructed-delegate-types.

A constructed type C<T1,...,Tn> is only valid if all the types T1, ..., Tn are well formed, the class name C refers to a generic class accessible from the current context, the generic class expects exactly n type parameters, and the instantiation <T1, ..., Tn> satisfies the constraints for the generic class. The validity of constructed interface types, delegate types and struct types is defined in a similar fashion.

2.2.1 Accessibility of constructed types

A constructed type C<T1,...,Tn> is accessible when all its parts C, T1, ..., Tn are accessible. For instance, if the generic type name C is public and all of the type-arguments T1,...,Tn are accessible as public, then the constructed type is accessible as public, but if either the type-name or one of the type-arguments has accessibility private then the accessibility of the constructed type is private. If some part of the constructed-type has accessibility protected, and anothe part has accessibility internal, then the constructed-type is accessible only in this class and its subclasses in this assembly.

More precisely, the accessibility domain for a constructed type is the intersection of the accessibility domains of its constituent parts. Thus if a method has a return type or argument type that is a constructed-type where one constituent part is private, then the method must have an accessibility domain that is private;

For example:

public class B<T,U> { }

internal class C 
{
  // Because C is internal,all the following types
  // have their given accessibility domains intersected with
  // "internal"

  protected internal class CProInt { } //i.e.internal
  public class CPub { } // i.e. internal
  protected class CPro { } // i.e. intersect(protected,internal)
  internal class CInt { } // i.e. internal
  private class CPri { } // i.e. private

  // Because C is internal,all the following methods
  // really have an accessibility domain of "internal"
  public B<CPub,CPub> m11() { ... }    //Ok
  public B<CPub,CProInt> m12() { ... } // Ok
  public B<CPub,CInt> m13() { ... }    // Ok
  public B<CPub,CPro> m14() { ... }    // Error, CPro is protected
  public B<CPub,CPri> m15() { ... }    // Error, CPri is private

  // Because C is internal,all the following methods
  // really have an accessibility domain of "internal"
  protected internal B<CProInt,CPub> m21() { ... }  // Ok
  protected internal B<CProInt,CProInt> m22() { ... }  // Ok
  protected internal B<CProInt,CInt> m23() { ... }  // Ok
  protected internal B<CProInt,CPro> m24() { ... } // Error, CPro prot.
  protected internal B<CPro,CPri> m25() { ... }   // Error, CPri private 

  internal B<CInt,CPub> m31() { ... }    // Ok
  internal B<CInt,CProInt> m32() { ... } // Ok
  internal B<CInt,CInt> m33() { ... }    // Ok
  internal B<CInt,CPro> m34() { ... }    // Error, CPro protected
  internal B<CInt,CPri> m35() { ... }    // Error, CPri is private 

// Because C is internal,all the following methods // really have an accessibility domain that is the intersection of // "protected" and "internal" protected B<CPro,CPub> m41() { ... } // Ok protected B<CPro,CProInt> m42() { ... } // Ok protected B<CPro,CInt> m43() { ... } // Ok protected B<CPro,CPro> m44() { ... } // Ok protected B<CPro,CPri> m45() { ... } // Error, CPri is private private B<CPri,CPub> m51() { ... } // Ok private B<CPri,CProInt> m52() { ... } // Ok private B<CPri,CPro> m53() { ... } // Ok private B<CPri,CInt> m54() { ... } // Ok private B<CPri,CPri> m55() { ... } // Ok

}
public class D
{
  protected internal class DProInt { }
  public class DPub { }
  protected class DPro { }
  internal class DInt { }
  private class DPri { }

  public B<DPub,DPub> m11() { ... }    // Ok
  public B<DPub,DProInt> m12() { ... } // Error, DProInt not public
  public B<DPub,DInt> m13() { ... }    // Error, DInt not public
  public B<DPub,DPro> m14() { ... }    // Error, DPro not public
  public B<DPub,DPri> m15() { ... }    // Error, DPri not public 

protected internal B<DProInt,DPub> m21() { ... } // Ok protected internal B<DProInt,DProInt> m22() { ... } // Ok protected internal B<DProInt,DInt> m23() {...}// Error, DInt not prot. protected internal B<DProInt,DPro> m24() {...}// Error, DPro not int. protected internal B<DPro,DPri> m25() { ... } // Error, DPri is private internal B<DInt,DPub> m31() { ... } // Ok internal B<DInt,DProInt> m32() { ... } // Ok internal B<DInt,DInt> m33() { ... } // Ok internal B<DInt,DPro> m34() { ... } // Error, DPro not internal internal B<DInt,DPri> m35() { ... } // Error, DPri not internal

protected B<DPro,DPub> m41() { ... } // Ok protected B<DPro,DProInt> m42() { ... } // Ok protected B<DPro,DInt> m43() { ... } // Error, DInt not protected protected B<DPro,DPro> m44() { ... } // Ok protected B<DPro,DPri> m45() { ... } // Error, DPri not protected

private B<DPri,DPub> m51() { ... } // Ok private B<DPri,DProInt> m52() { ... } // Ok private B<DPri,DPro> m53() { ... } // Ok private B<DPri,DInt> m54() { ... } // Ok private B<DPri,DPri> m55() { ... } // Ok }

Type arguments

Type arguments may in turn be constructed types. In unsafe code, the type-arguments may include pointer types. Every constructed type must satisfy any constraints on the corresponding type parameters of the type-name.

Type instantiations

A type instantiation <V1 -> Tn, ..., Vn -> Tn> for a class C with type parameters V1, ..., Vn specifies a type Ti for each type parameter Vi of C. Type instantiations are a notion that are used only within this specification for the purpose of succinctness and clarity - they do not occur syntactically in a C# program.

A constructed type may simply be written C<inst> where C is a type name and inst is a type instantiation. Thus when we write a constructed type as C<inst>, it should be understood that inst can be used to refer not just to the vector of types T1, ..., Tn but also to the instantiation <V1 -> T1, ..., Vn -> Tn> when the class C has formal type parameters V1, ..., Vn. Likewise, an instantiation <V1 -> T1, ..., Vn -> Tn > may just be written <T1, ..., Tn> when it is clear which class and type parameters are being referred to e.g. the formal type parameters V1, ..., Vn for the class C when writing C<T1, ..., Tn>.

Type instantiations may also be built for generic structs, interfaces, methods and delegates. An empty class type instantiation has no entries at all, and corresponds to an instantiation for a non-generic class, struct, interface, method or delegate.

Except where otherwise indicated, C<inst> covers both the cases where the class is a non-generic class (e.g. a non-generic class such as string) and inst is the empty type instantiation, and the case where C is a generic class and inst is non-empty.

For example, in the code

class B<U1,U2> { ... }

class C<V1,V2>
{
   public B<V1,string> f1() { ... }
   public B<V1,V2> f2() { ... }
   public B<string,string> f3() { ... }
}

class D
{
   public B<string,string> f1() { ... }
   public D f2() { ... }
}

the following type instantiations occur:

<U1 -> V1, U2 -> string> (maps U1 to V1, U2 to string)

<U1 -> V1, U2 -> V2> (maps U1 to V1, U2 to V2)

<U1 -> string, U2 -> string> (maps U1 to string, U2 to string)

<> (contains no mappings, e.g. for the type D)

Note that the instantiation <U1 -> string, U2 -> string> occurs twice in the code.

Conversions

Reference conversions to and from constructed reference types

Constructed reference types support implicit reference conversions. These rules replace the corresponding rules for non-generic classes.

Constructed reference types support the following explicit reference conversions. These rules replace the corresponding rules for non-generic types.

  • From any reference-type S to any reference-type T if T is derived from S, i.e. S is one of the base types of T.

    Note that this single general rule includes the following as special cases:

  • From any class-type S to any class-type T if T is derived from S, i.e. S is one of the base class types of T.

  • From any class-type S to any interface-type T, provided T implements S, i.e. S is one of the base interface types of T.

  • From any interface-type S to any interface-type T, provided T is derived from S, i.e. S is one of the base interface types of T.

    Constructed reference types also support the conversions common to all reference types, e.g. from the "null" type, and to the type object, and from any constructed-delegate-type to System.Delegate.

    Conversions to and from constructed value types

    Constructed value types also support the conversions common to all value types, e.g. to and from the types object and System.ValueType.

    User defined conversions to and from constructed types

    User-defined conversions are not yet implemented.

    Conversions to and from type parameters

    An implicit, unchecked conversion exists from a type parameter V to type S under the following conditions:

    The method Wrap in the following code takes a value of type V and returns it as a value of type object.

    public class Main
    {
       static object Wrap<V>(V x)   {  return x; }
       static void Main()
       {
          object s = "string";
          object obj1 = Wrap<string>("string"); // Ok
          string s1 = (string) obj1; // Ok, cast succeeds
          object obj2 = Wrap<int>(3);    // Ok
          int i2 = (int) obj2; // Ok, unbox
          object obj3 = Wrap<object>((object)3);  // Ok
          string s3 = (string) obj3; // InvalidCastException
       }
    }
    

    An explicit, checked conversion exists from type S to a type parameter V under the following conditions:

    These conversions are checked at runtime.

    The following code takes an object and attempts to return it as type V.

    public class Main
    {
       static V Cast<V>(object x)   {  return (V) x; }
    
       static void Main()
       {
          object s = "string";
          string v1 = Cast<string>(s);  // Ok
          string v2 = Cast<int>((object) 3);  // Ok, 3 is boxed then unboxed
          string v3 = Cast<string>((object) 3);  // InvalidCastException
       }
    }
    

    No "co-variance" for constructed types

    No special conversions exist between constructed reference types other than those listed above. In particular, constructed reference types do not exhibit "co-variant" conversions, unlike C# array types. This means that a type List<B> has an (identity) reference conversion to List<B>, but no reference conversion (either implicit or explicit) exists to List<A> even if B is a derived from A. In particular, no conversion exists from List<B> to List<object>.

    The rationale for this is simple: if a conversion to List<A> is permitted, then apparently one can store values of type A into the list. But this would break the invariant that every object in a list of type List<B> is always a value of type B, or else unexpected failures may occur when assigning into collection classes.

    One choice is to support a runtime check every time a generic data structure is modified. For example, C# does support co-variant array types, and therefore typically performs a runtime check on every store into an array of reference type. However, one of the aims of generics is to avoid the performance costs associated with this kind of check, and hence co-variance is not supported. In addition, when co-variance is not supported, the programmer can be assured that assignments into collection classes will not raise exceptions at runtime. Also note that if the generic class List is itself derived from some class (e.g. a non-generic class Collection) then there will be reference conversions from types such as List<B> to that class.

    The behavior of conversions and runtime type checks is illustrated below:

    class A { ... }   
    
    class B : A { ... }   
    
    class List<V> { ... }   
    
    public static void MyMethod(List<A> argl) { ... }
    
    public static void Main() {
       List<A> al = new List<A>();   
       List<B> bl = new List<B>();   
       if (al is List<A>)                        // true   
          Console.WriteLine("al is List<A>");   
       if (al is List<B>)                        // false
          Console.WriteLine("al is List<B>");   
       if (bl is List<A>)                        // false    
          Console.WriteLine("bl is List<A>");   
       if (bl is List<B>)                        // true   
          Console.WriteLine("bl is List<B>");
       MyMethod(al); // Ok   
       MyMethod(bl); // Error, bl is not List<A>
    }
    

    The last method call causes a compile time error. If the last method call in the above program were omitted, the program would produce:

    al is List<A>
    bl is List<B>
    

    "Default" values for type parameters

    No implicit conversion exists from the null type to a type parameter V. This is because V may be instantiated as a value type, and "null" does not have the same, intuitive meaning that may be expected for those types.

    However, the expression V.default will be guaranteed to produce a "default" value for the type corresponding to V. This can be considered an explicit, unboxing conversion from the "null" type to a type parameter.

    Note: this is not yet implemented.

    The default value for a type is guaranteed to have the following properties:

    Default values are useful in a number of circumstances, e.g. for initializing slots in data structures. Another use is to "blank out" slots of data structures once the data previously held in the slot is no longer required. Blanking out data can help eliminate a source of bugs in garbage collected programs known as "memory leaks", which occur because the garbage collector cannot reuse space if data structures continue to contain handles to objects, even if those objects will no longer be used.

    Default values may also be generated by creating an instance variable of the type V and declaring it readonly, so that it is never written to, as its initial value will also be the default value for V.

    Generic classes

    Generic class declarations follow the same rules as normal class declarations except where noted, and particularly with regard to naming, nesting and the permitted access controls. Generic class declarations may be nested inside other class and struct declarations.

    The class-type-parameters of a generic-class-declaration can be used to form types in certain parts of the class, according to the following rules:

    • A class-type-parameter may be used to form a type in every non-static declaration in the class, as well as the instance constructors, the specification of the explicit-type-parameter-constraints and the class-base of the generic-class-declaration.

    • A class-type-parameter may not be used to form a type in the attributes, static methods, static fields, operators, destructors, the class initializer or nested types of the generic-class-declaration An attempt to do so is a compile-time error, and thus the class-type-parameter effectively hides any other visible type names. This means that class type parameters are in scope in static members, but illegal, rather than not in scope at all.

    • It is a compile time error to have a nested class with the same name as a class type parameter of the enclosing class.

    • A class-type-parameter may be used to form a type in the field initializers of all non-static fields.

    Class base specification

    The specification of the base class of any class-declaration is a class type, and in particular this might be a constructed-class-type. In a generic-class-declaration it may not be a class-type-parameter on its own, though it may involve the class-type-parameters that are in scope.

    The specification of each base interface of any class-declaration is an interface type, though note some of these may be constructed-interface-types. In a generic-class-declaration it may not be a class-type-parameter on its own, though it may involve the class-type-parameters that are in scope.

    The following code illustrates how a class can implement and extend constructed types:

    class C<U,V> { }
    
    interface I1<V> { }
    
    class D : C<string,int>, I1<string> { }
    

    Methods in a class that overrides or implements methods from a base class or interface must provide appropriate methods at specialized types.

    The following code illustrates how methods are overridden and implemented.

    class C<U,V> 
    {    
       public virtual void m1(U x,List<V> y) { ... }
    }   
    
    interface I1<V> 
    {    
       V m2(V);
    }   
    
    class D : C<string,int>, I1<string> 
    {    
       public override void m1(string x, List<int> y) { ... }   
       public string m2(string x) { ... }
    }
    

    The base class and base interfaces of a class may not be type parameters:

    class Extend<V> : V { ... } 
        // Error, thebase class may not be a 
        // type parameter

    Uniqueness of interface types

    The set of base interface types of a class must not contain two constructed-interface-types with the same generic-interface-name and different instantiations. This simplifies the implementation of dynamic dispatch mechanisms and prevents ambiguities arising amongst methods and ambiguities when generic interfaces are instantiated. Because of this restriction, the following class hierarchy is illegal:

    interface I<V> { }   
    
    class C : I<string>, I<object> { }
    

    Members in generic classes

    Generic classes can contain essentially the same kinds of members as non-generic classes, although particular rules apply in each case.

    The set of all function members of a class must be well formed. In particular, an "override" method must have the correct signature to override a virtual method in a parent class. If this set is not well formed an error is reported at compile-time.

    The static constructor of a generic class is executed once only, and not once for each different instantiation of that class. Destructors are executed once for each instance object created for each instantiation.

    4.3 Fields in generic classes

    Instance variables in generic classes

    The instance variables of a generic class may have types and variable initializers that include any type parameters from the enclosing class. For example:

    class C<V> {   
      public V f1;   
      public C<V> f2 = null;   
      public C(V x) { this.f1 = x; }   
    }   
    
    class Application {   
      static void Main() {   
        C<int> x1 = new C<int>(1);   
        Console.WriteLine(x1.f1);  // Prints 1   
        C<double> x2 = new C<double>(3.1415);   
        Console.WriteLine(x2.f1);  // Prints 3.1415   
      }   
    }   
    

    Static variables in generic classes

    The types of static variables in a generic class may not refer to any type parameters from the enclosing class.

    A static variable in a generic class is shared amongst all the types constructed from it. There is no way to specify fields where a new storage location is created for each instantiation of the class.

    For example:

    class C<V> {   
      static int count = 0;   
      public C() { count++; }   
      static public int Count { get { return count;} }   
    }   
    
    class Application {   
      static void Main() {   
        C<int> x1 = new C<int>();   
        Console.WriteLine(C.Count);  // Prints 1   
        C<double> x2 = new C<double>();   
        Console.WriteLine(C.Count);  // Prints 2   
        C<object> x3 = new C<object>();   
        Console.WriteLine(C.Count);  // Prints 3   
      }   
    }   
    

    Methods in generic classes

    This section discusses method-declarations within generic classes. In passing it is worth noting that the signatures of all methods may involve constructed types of some kind, e.g. constructed-class-types, whether generic or non-generic, or whether in generic classes or non-generic classes.

    Methods within a generic class can be overloaded. Virtual methods declared within generic classes can be overridden, whether the overriding method occurs in a generic class or in a non-generic class. Method declarations may themselves be generic.

    Instance, abstract and virtual methods in generic classes

    For non-static methods inside generic classes the signature may also involve one or more of the class-type-parameters. The body of such a method may also use these type parameters.

    The following example shows an instance method, an abstract method and two virtual methods within a generic class:

    abstract class C<V>
    {   
       private string name;   
       private V data;   
       public bool CheckName(C<V> x)    
          { return (this.name == x.name); }   
       ...
       
       public virtual V GetData()    
          { return data; }   
       public abstract C<V> CopyData();   
       public virtual void ReplaceData(C<V> x)   
          {  this.data = x.data; }   
    }   
    

    4.4.2 Static methods in generic classes

    Static methods do not automatically acquire the type parameters of a generic class in which they are used. For this reason neither the argument types nor the return types of a static method can include any type parameters from the enclosing class. Similarly the body of such a method cannot use the type parameters from the enclosing class. In this way type parameters are very similar to instance fields of the class being defined - they are only accessible in instance methods.

    The following sample shows how you can make a static method generic in order to manipulate generic data:

    class C<V>
    {   
       private string name;   
       private V data;   
       public static bool CheckName(C<V> x, C<V> y) // Error, cannot access V   
          { return (x.name == y.name); }   
       public static bool CheckName<V>(C<V> x, C<V> y) // Ok, CheckName generic   
          { return (x.name == y.name); }   
       public bool CheckName(C<V> y)         //Ok, instance method can access V   
          { return (this.name == y.name); }   
    }   
    

    Param-array methods and type parameters

    Type parameters may be used in the type of an argument array expected for a params method. For example, given the declaration

    class C<V>
    {   
       void F(int x, int y, params V[] args);   
    }   
    

    the following invocations of the expanded form of the method

    (new C<int>).F(10, 20);
    (new C<object>).F(10, 20, 30, 40);
    (new C<string>).F(10, 20, "hello", "goodbye");
    

    correspond exactly to

    (new C<int>).F(10, 20, new int[] {});
    (new C<object>).F(10, 20, new object[] {30, 40});
    (new C<string>).F(10, 20, new string[] {"hello", "goodbye"}
    );
    

    Generic methods in generic classes

    Methods in both non-generic and generic classes may themselves be generic-method-declarations. This applies to all kinds of methods, including static methods, instance methods, virtual methods, and methods declared with the abstract, new or overrides keywords.

    Non-static generic methods within a generic class are rare, but are permitted. These can use both the type parameters of the class and the type parameters of the method.

    class C<V>
    {
       public static bool f1(C<V> x, C<V> y)     // Error, cannot access V
         { ...}   
       public static bool f2<V>(C<V> x, C<V> y)  // Ok
         { ... }   
       public bool f3(C<V> x)                    // Ok
         { ...  }   
       public bool f4<U>(C<V> x, C<U> y)         // Ok
         { ...  }   
       public virtual bool f5(C<V> x)            // Ok
         { ...  }   
       public virtual bool f6<U>(C<V> x, C<U> y) // Ok
         { ...  }
    }
    

    4.4.5 Uniqueness of signatures

    The method signatures within a generic class must be unique prior to any instantiations. However, the following additional rules apply in determining when two signatures are identical:

    • The names of all type parameters are ignored, and instead their numerical position in a left-to-right ordering of the type parameters is used.
    • The number of type parameters accepted by a generic method is significant.
    • Any constraints on type parameters accepted by generic method are ignored.

    Thus in the following class the indicated methods cause conflicts with those of the same name:

    class C<V>
    {
       public V f1() { ... } 
       public string f1() { ... }  // Error, return types differ   
    
       public void f2(V x) { ... } 
       public void f2(object x) { ... }  // Ok, unique prior to instantiation   
    
    }   
    
    class D 
    {
       public static void g1<V>(V x) { ... } 
       public static void g1<U>(U x) { ... }  // Error, identical ignoring names   
    
       public static void g2() { ... } 
       public static void g2<V>() { ... }  // Ok, number of params significant   
    
       public static void g3<V>() { ... } 
       public static void g3<V,U>() { ... }  // Ok, number of params significant   
    
       public U g4<U>(object x) { ... } 
       public U g4<U>(U x) { ... } // Ok, differ by formal argument type   
    
       public static void g5<V> where V : I1() { ... } 
       public static void g5<V> where V : I2() { ...}  // Error, constraints
                                                                             // are not significant
    }
    

    Nested types in generic classes

    A generic-class-declaration can itself contain type declarations, just as with ordinary classes. However, the type parameters of the generic-class-declaration are not implicitly visible within the nested class declaration, i.e. the nested class declaration is not implicitly parameterized by the type parameters of the outer class declaration. Thus the type parameters play a similar role to the "this" value and the instance fields of the outer class declaration, which are not accessible in the nested type. The rationale for this is that it is frequently the case that the programmer wishes to define nested classes that take more or fewer type parameters than the outer class, and given this possibility it is better to be explicit about the exact degree of type parameterization desired.

    Nested types may themselves be parameterized. The class-type-parameters of the enclosing class are not visible in the nested class, but within the enclosing class constructed types involving the nested class may involve the type parameters of the enclosing class. Nested types may also be parameterized by type parameters with the same names as the type parameters of the enclosing class.

    For example:

    public class LinkedList<V> 
    {
       Node<V> first, last; 
       private class Node<V> 
       {
          public Node<V> prev, next;
          public V item; 
       }
    }
     

    Properties, Events and Indexers in generic classes

    This section discusses property-declarations, event-declarations and indexer-declarations within generic classes. In passing it is worth noting that the signatures of all properties, indexers and events may contain constructed types, regardless of whether these are in a generic or non-generic class.

    Instance properties, events and indexers in generic classes

    Within a generic-class-declaration the type of an instance property may include the class-type-parameters of the enclosing class. For example, the following shows an instance property in a generic class:

    class C<V>
    {   
       private V name;   
       private V name2;   
       public V Name 
       {
          get { return name; }   
          set { name = value; }
       }   
       public V this[string index]    
         { get { if (index == "main") return name; else return name2 }   }
    }
    

    Static properties and events in generic classes

    Because class type parameters are not accessible at static property declarations, the type of a static property cannot include any type parameters.

    delegate D<V>(T x);  // Ok, a generic delegate   
    
    class C<V>
    {   
       public static V Name // Error, static properties can't use type parameter   
          { ... }   
       static event D<V> e; // Error, static events can't use type parameters
    }
    

    4.6.3 No generic properties, events or indexers

    Properties, events and indexers may not themselves be generic. If a property-like construct is required that must itself be generic then a static or virtual generic method should be used instead.

    For example:

    class C<V>
    {
       public C<V> p1         // Ok   
          { get { return null; } }   
       public C<U> p2<U>     // Error, properties may not be generic   
          { ... }   
       public event D<V> fire1;  // Ok   
       public event D<V> fire2<V>;  // Error, events may not be generic   
       public V this[int index] { ... }  // Ok   
       public V this<U>[int index] { ... } // Error, syntax error   
    }
    

    Overriding and generic classes

    A member in a derived class with the override attribute must respect the instantiations specified in the inheritance chain. That is, the resulting signature must be identical to the signature of a method with the virtual or abstract attribute after the instantiations implied by the inheritance chain are taken into account.

    The following example shows some examples of this:

    abstract class C<V>
    {   
       private string name;   
       private V data;   
       public bool CheckName(C<V> x)  { ... }   
       public virtual V GetData()  { ...   }   
       public abstract C<V> CopyData();   
       public virtual void ReplaceData(C<V> x) { ... }   
    }   
    
    class D : C<string>
    {   
       public override string GetData()  { ... }   
       public override C<string> CopyData()  { ...  }   
       public override void ReplaceData(C<int>) // Error, incorrect override,
                                                // should be C<string>   
          { ... }
    }   
    
    class E<U,W> : C<U>
    {   
       public override U GetData()  { ... }   
       public override C<W> CopyData()  // Error, incorrect override, 
          { ... }                      // should be C<U> as we derive 
                                       // from C<U>   
       public override void ReplaceData(C<U>) { ... }
    }
    

    Note that the signatures required for the virtual methods as we override them are the same as the signatures for these methods in the class C<V> once we have substituted string for V (in the case of class D) and U for V (in the case of generic class E).

    Operators in generic classes

    Generic-class-declarations >can include operator declarations. However, because class type parameters are not in scope at static member declarations, the type of a static operator cannot use any of the type parameters of the enclosing class.

    Instance constructors in generic classes

    Instance constructors in a generic-class-declaration are "generic" in the sense that they must work for arbitrary class-type-parameters. Within the constructor this means that for a class C with class-type-parameters V1,...,Vn the "this" pointer is considered to have the constructed type C<V1,...Vn>, just as for any instance function member.

    The class-type-parameters may be used in the argument expressions of a constructor initializer and in the field initializers of all non-static fields. The following example illustrates both of these:

    class C<V> 
    {
       V[] x = new V[10];
       public C(V[] x) { this.x = x; }
    }
    
    class D<W> : C<W>
    {
       public D(int x): base(new W[x]) {}   // Ok
       public D<int>(): base(new int[0]) {} // Error, syntax error
    }
    

    Generic structs

    The rules for class-declarations and generic-class-declarations apply identically to generic-struct-declarations, modulo the usual differences between structs and classes.

    Generic methods

    A generic-method is a method member that is abstract with respect to certain types. Generic methods can be used at two places: at a call site or to construct a delegate from the generic method. Generic methods can only be used once an instantiation for the type parameters of the generic method is explicitly specified by the user.

    The method-type-parameters are in scope throughout the generic-method-declaration, and may be used to form types throughout that scope including the return-type, the method-body, and the explicit-type-parameter-constraint but excluding the attributes.

    The following example finds the first element in an array, if any, that satisfies the given test delegate.

    delegate bool Test<V>(V);   
    
    class Finder
    {   
       public V Find<V>(V[] inp, Test<V> p) 
       {   
          foreach (V x in inp)   
             if (p(x)) return x;   
          throw new InvalidArgumentException("Find");
       }
    }
    

    A generic method may not be declared extern.

    A generic method must differ in formal signature from all other methods in its class. For the purposes of signature comparisons any explicit-type-parameter-constraints are ignored, as are the names of the method-type-parameters and any class-type-parameters that are accessible from the surrounding class definition, but the number of generic type parameters is relevant, as are the relative numeric positions of type-parameters in left-to-right ordering from the outermost declaration to the innermost.

    Generic interfaces

    A generic-interface-declaration is identical to an interface-declaration except for the inclusion of the interface-type-parameters, which themselves follow an identical syntax to class-type-parameters.

    An interface-type-parameter can be used as a type-name in certain parts of the interface, according to the following rules:

    • An interface-type-parameter may be used to form a type in the signature of every member in the interface.

    • The specification of each base interface of any interface-declaration is an interface-type, and in particular this might be a constructed-interface-type. It may not be an interface-type-parameter on its own, though it may involve the interface-type-parameters that are in scope.

    Interface implementations

    Interfaces may extend interface-types, and these may include constructed-interface-types. This may happen even for non-generic interfaces if they extend interfaces that themselves derive from constructed-interface-types.

    Uniqueness of interface types

    The set of base interface types of an interface must not contain two constructed-interface-types with the same generic-interface-name and different instantiations. This simplifies the implementation of dynamic dispatch mechanisms and prevents ambiguities arising amongst methods and ambiguities when generic interfaces are instantiated.

    For example, consider the following interfaces:

    interface I1<U> { ... }   
    
    interface I2<V> : I1<V[]> { ... }   
    
    interface I3<W> : I1<Object>, I2<W> { ... }   
    

    The interface set of I3 is as follows: { I1<Object>, I1<W[]>, I2<W> }

    This contains the interface I1 at more than one instantiation, and is hence a compile-time error.

    Explicit interface member implementations

    Explicit interface member implementations may be provided for those interfaces which are constructed-interface-types. This type may be a constructed-interface-type, as in the following example:

    interface IList<V>
    {
       V[] Elements();
    }   
    
    interface IDictionary<K,D>
    {
       D this[K];   
       void Add(K x, D y);
    }   
    
    class List<V>: IList<V>,
    IDictionary<int,V>
    {
       V[] IList<V>.Elements() {...}   
    
       V IDictionary<int,V>.this[int idx] { ... } // return the element at index   
       void IDictionary<int,V>.Add(int idx, V y) { ... } // add y at the index
    }   
    

    The constructed-interface-type must be a member of the immediate base interface types of the class C.

    The algorithm that maps class methods to the set of base interfaces of the class works essentially unchanged with the additional propagation of instantiations throughout the process.

    Generic delegate declarations

    A generic-delegate-declaration is a generic-type-declaration that declares a new generic delegate type. It is identical to a delegate-declaration except for the inclusion of the delegate-create-type-parameters, which themselves follow an identical syntax to class-type-parameters.

    Delegate type parameters are specified at the point where a delegate object is created. The method provided for the delegate must have the signature that results after applying the specified instantiation throughout the delegate signature.

    The following example specifies the arguments <int,string> at the point the instance of the delegate is created in the expression new Test<int,string>(One). Note that the One method has the signature that corresponds to the signature required for delegates after the instantiation <V -> int, U -> string> is applied.

    delegate bool Test<V,U>(V,U);   
    
    struct Pair<V,U> { V e1; U e2; }   
    
    class PairSearch
    {   
       public static U Find<V,U>(Pair<V,U>[] inp, Test<V,U> test) 
       {   
          foreach (Pair<V,U> p in inp)   
             if (test(p.e1, p.e2)) return p.e2;   
          throw new InvalidArgumentException("Find");
       }   
    
       public static bool One(int n, string s) 
          { return n == 1; }   
    
       public static void Main()
       {   
          Pair<int,string>[] a1 = { 
              new Pair<int,string>(1,"one"),
              new Pair<int,string>(2,"two"),
              new Pair<int,string>(3,"three")
          };   
          string three = Find<int,string>(a1, new Test<int,string>(One));
       }
    }   
    

    Note that the same delegate object cannot be used at different types when all the type parameters are specified at the moment the delegate is created. That is, the delegate object cannot itself encapsulate a generic operation, only the generic operation specialized to a particular set of types. A new delegate object must be allocated for each different set of types.

    Constraints

    Each type-parameter (including class-type-parameters, method-type-parameters etc.) may be qualified by one or more explicit-type-parameter-constrainst. The specification of explicit constraints is optional. If given, a constraint is a reference-type that specifies a minimal "type-bound" that every instantiation of the type parameter must support. This is checked at compile-time at every use of the corresponding generic class, interface, method, delegate or struct. (Currently only one constraint is permitted per type parameter).

    Constraints are speficied using the "where" keyword. Values of the type constrained by the type parameter can be used to access the instance members, including instance methods, specified in the constraint.

    interface IPrintable { void Print() ; }   
    
    class Printer<V> where V : IPrintable> 
    { 
       void PrintOne(V x) { x.Print(); }
    }   
    

    Constraints may involve the type parameters themselves. This is used when the constraint specifies an operation that involves passing or returning values involving the type parameter. For example:

    interface Icomparable<V> { int CompareTo(V); }   
    
    class Sorter<V> where V : IComparable<V>  { ... }   
    

    Constraints may even involve the class, interface or method for which they are acting as a constraint:

    interface I<V> where I<V>  { ... }  // A strange, but legal constraint   
    

    Constraints may also be class types, e.g. abstract base classes:

    abstract class Printable { abstract void Print(); }   
    
    interface I<V> where V : Printable { ... }   
    

    "Different" constraints may be attached to the type parameters declared on generic static methods within a generic class. This is because generic static methods must declare their own type parameters and do not automatically acquire the type parameters of the enclosing (generic) class. For example:

    interface IPrintable { void Print(); }   
    
    class List<V>  
    { 
       ...    
       static void Print<V> where V : IPrintable(List<V> list) { ... }
    }   
    

    Constraints may not themselves be a type parameter:

    class Extend<V, U> where U : V { ... }  // Error, a type parameter may not
                                            // be used as a constraint    
    

    Explicit constraints are most useful in two circumstances:

    • Generic classes: to require class type parameters to support simple functionality such as an "equals" method or a "less than" method to implement an ordering, especially when the class implements an abstract data type that makes no sense without these methods (e.g. balanced binary trees require an ordering on the type used as the key). Note, however, that in many circumstances it is unreasonable to expect instantiating types to support exactly the right base classes, and it may be more appropriate and flexible to use "provider" functions (e.g. System.Collections.IHashCodeProvider or System.ICollections.IComparer) to provide the necessary functionality.
    • Generic methods: constraints can be used to help define generic methods that "plug together" functionality provided by different types, thus defining "generic algorithms". This can also be achieved by subclassing and runtime polymorphism, but static, constrained polymorphism can in many cases result in more efficient code, more flexible specifications of generic algorithms, and more errors being caught at compile-time rather than run-time. However, constraints need to be used with care and taste, as code that does not implement the constraints will not be easily usable in conjunction with the methods.

    In either case, constraints are most useful in the context of defining a framework, i.e. a collection of related classes, where it is an advantage to ensure that a number of types support some common signatures and/or base types. Constraints also allow types to be related when they may not been explicitly related via inheritance and/or implementation.

    Satisfying Constraints

    Every time a type is supplied for a type parameter, e.g. at a method call to a generic-method-declaration or when building a constructed-class-type out of a generic-class-declaration, the actual type parameters must satisfy the constraints on the type-parameters of the declaration being used.

    For example, the following constraint is satisfied by all of the types declared below it.

    interface IPrintable { void Print(); }   
    
    class C1 : IPrintable
    { 
        ...
        public virtual void Print() { ... }
    }   
    
    class C2 : IPrintable
    { 
        ...
        public abstract void Print() { ... }
    }   
    
    struct C3 : IPrintable
    { 
        ...
        public void Print() { ... }
    }   
    
    class C4 : IPrintable
    { 
        ...
        void IPrintable.Print() { ... }
    }   
    

    This means the following code fragments are accepted by the compiler:

    public class Printer<V> where V :
    IPrintable
    { 
        ...
        public void Print(V x) { ...x.Print(); ...  }
    }   
    
    ... C1 x = new C1(); Printer<C1> p1 = new Printer<C1>(); p1.Print(x); // OK   
    
    ... C2 y = new C2(); Printer<C2> p2 = new Printer<C2>(); p2.Print(y); // OK   
    
    ... C3 y = new C3(); Printer<C3> p3 = new Printer<C3>(); p3.Print(y); // OK   
    
    ... C4 y = new C4(); Printer<C4> p4 = new Printer<C4>(); p4.Print(y); // OK   
    

    However, the following type does not satisfy the constraint because the class does not explicitly implement the interface:

    public class C5
    { 
        ...
        public void Print() { ... }
    }   
    
    public static void Main()
    {   
       C5 y = new C5();    
       Printer<C5> p5 = new Printer<C5>(); // Error   
       p5.Print(y); 
    }   
    

    When and how an instantiation satisfies its constraints

    A constructed-class-type C<inst> for a generic-class-declaration C with class-type-parameters <V1,...,Vn> is only valid under certain conditions. In particular, for each Vi that is constrained, e.g. by a constructed-constraint B<T1,...,Tm>, first form a new constructed-constraint B<S1,...,Sm> by applying the instantiation inst to each type Ti to form Si. Then the individual type that corresponds to Vi in the instantiation inst must satisfy this constraint.

    A type T (whether reference or a value type, constructed or non-constructed) satisfies a constraint S if there is an implicit non-user-defined conversion from the type S to the type S. The conversion may be a boxing conversion, which means that value types may satisfy constraints given by interface types, if the value types implement those interface types.

    Similarly, constraints must be satisfied for constructed-interface-types, constructed-delegate-types, constructed-struct-types, generic-method-invocations and generic-delegate-invocations.

    Expressions and statements

    The semantics of many expressions and statements is affected by the presence of constructed types, variable types and their corresponding values. This section describes these effects.

    Expression classifications

    The existing expression classifications are extended in the following ways:

    • Whenever expressions have a type, e.g. values, variables, property accesses, event accesses and indexer accesses, the type may involve constructed-types, and, if the expression occurs within a generic-declaration the type may also involve type-parameters.
    • When an expression is itself a type, e.g. is an operand of the as operator, the type may be a constructed-type, and, if the expression occurs within a generic-declaration, the type may also involve any type-parametersof that declaration.
    • In addition, the class-name associated with a generic-class-declaration may be used to form a type in the following situations: on the left hand side of a member-access and as the operand of the typeof operator. This is in order to access the static members contained within a generic-class-declaration and to pass an (uninstantiated) generic type to reflection libraries. The names associated with generic interface, struct and delegate types can be used in a similar fashion.
    • When an expression has an associated instance expression, e.g. when the expression is an invocation-expression or a property-access-expression, the instance expression may have a type that is a constructed type.
    • When an expression is classified as a method group, e.g. in an invocation-expression or a delegate-creation-expression, then each method in the method group will be paired with a (possibly constructed) type that indicates the reference-type or struct-type related to the position of this method within an inheritance hierarchy that may contain constructed types. Namely, the method group is a subset of the set of all function members of a class, interface or struct which annotates members with type information according to the inheritance hierarchy.

    • Similarly, when an expression is a property, event or indexer access, then the member is paired with a (possibly constructed) type that indicates the reference-type or struct-type related to the position of this method within an inheritance hierarchy that may contain constructed types.

    Member lookup

    The rules for member lookup are unchanged with the following exceptions:

    • Member lookups for classes return member groups that contain pairs of members and governing types, drawn from the set of all function members of a class.
    • Similarly for member lookups for interfaces and structs.

    In addition, member lookup for an expression that has a type that is a type variable V returns the union of all the member groups from all the constraints of V. These will again be groups containing pairs of members and governing types.

    For instance, in the following example the expression "x" has type V. The method group for the expression "x.Print" will contain two members, the first being the type IPrintable twinned with the method "void Print();", the second the type IBatik<string> twinned with the method "U Print(U);". The second method will be the one selected by the process of overload resolution.

    public void IPrintable { void Print(); }   
    
    public void IBatik<U> { U Print(U); }   
    
    public void Print<V> where V: IPrintable, V: IBatik<string>(V x)
    { 
        x.Print("abc");
    }   
    

    Member access

    Constructed types are not allowed to appear as types on the left of a member-access. An uninstantiated generic type may appear on the left of a member-access. In this case the accessed member must be static.

    Invocation expressions

    An invocation-expression is used to invoke a method. At the point of invocation a generic-instantiation may be specified. As before, the primary-expression of an invocation-expression must be a method group or a value of a delegate-type.

    10.4.1 Instance method invocation

    An instance method invocation has:

    • An instance expression obj associated with the primary-expression.

    • A method group arising from the primary-expression. This method group will contain pairs of methods and governing types.

    • An optional generic-method-instantiation specified in the invocation expression.

    Resolving an instance method invocation involves the following steps:

    1. Incorporate the type of obj with the governing types of the method group, which initially do not take the detailed type of obj into account. In particular, if obj has type D<inst1> and the governing type in the method group is C<inst2>, then replace the governing type with C<inst> where inst is the composition of inst1 and inst2.

      Note that:

      • C and D may be generic classes taking type parameters U1,...,Um and V1,...,Vn respectively.

      • Under our notational conventions this also covers the case where D is non-generic and inst1 is the empty instantiation, and also where C is non-generic and inst2 is the empty instantiation.

      • Note also that C and D may be identical, or the class C will appear somewhere in the class hierarchy above D.

      • The type C<inst2> will appear in the base type set for the class D.

      • The type C<inst> will appear in the base type set for the type D<inst1>.

      • The instantiation inst1 will map type parameters V1,...,Vn of D to types that may involve the type-parameters that are accessible at the point where the overall invocation-expression occurs. These types will contain no other type parameters.

      • The instantiation inst2 will map type parameters U1,...,Um of C to types that may involve the type-parameters V1,...,Vn, but no other type parameters.

    2. Use the method-type-instantiation specified in the invocation expression for every method in the method set. Those methods for which the method-type-instantiation is not valid (because it does not satisfy the appropriate constraints or specifies the wrong number of type parameters) are excluded.

    3. Apply overload resolution to the method group that results from step 2. In the absence of an error this will yield a single method, a single governing type and a single method instantiation.

    We now explain how the method, governing type and method instantiation are combined to determine the exact instantiations of type parameters used for the expression, and how the return type of the expression can be determined.

    Let us suppose that the steps above produce a method Rm (A1,...,Aq), a governing type C<inst > and a method instantiation minst. Then the overall class type instantiation is inst, and the overall method type instantiation is minst.

    Note that:

    • C may be a generic class taking type parameters V1,...,Vn.

    • m may be a generic method accepting type parameters W1,...,Wp.

    • Under our notational conventions this also covers the case where C is non-generic and inst is the empty instantiation, and/or m is non-generic and minst is the empty instantiation.

    • The instantiations inst and minst will map type parameters V1,...,Vn and W1,...,Wp respectively to types that may involve the type-parameters that are accessible at the point where the overall invocation-expression occurs. These types will contain no other type parameters.

    Furthermore:

    • The constraints that inst must satisfy are found by substituting inst through the formal constraints associated with V1,...,Vm.

    • The constraints that minst must satisfy are found by substituting the combination of inst and minst throughout the formal constraints associated with W1,...,Wp.

    • The actual argument types and return type can be found by substituting the combination of inst and minst throughout A1,..., Aq and R.

    In combination with the instantiations and eventual method called via virtual dispatch, the overall class and method instantiations determine the exact types assigned to type parameters when the code for the invoked method is executed.

    Static method invocation

    Static method invocation proceeds along essentially the same lines as instance method invocation, except that no governing types need be considered. Overload resolution is applied.

    Delegate invocation

    Delegate invocation may not accept type parameters. No overloading resolution is applied since it is not required.

    Type parameter inference

    Currently, all type instantiations must be written explicitly. A future goal is to use type parameter inference to allow programmers to omit type instantiations in certain commonly occurring, highly predictable situations. Type parameter inference will be applied when a generic method is called but no explicit instantiation is given at the call site, or when an instance constructor on a generic class is called with no explicit instantiation.

    For example, given the class and statemements below, the type instantiations "string" and "int" are essentially obvious given the arguments to the constructor.

    class List<V> : ICollection
    { 
       List(V[] x1) { ... }   
    }   
    
    ...  ICollection c1 = new List<string>(new string[] {"abc", "def" });             
    ...  ICollection c2 = new List<int>(new int [] { 3, 4 });             
    

    Type parameter inference will allow these arguments to be omitted:

    Overload Resolution

    The overload resolution rules apply to both the members of a constructed type and the functionality available via a constraint. Instead of beginning with the members of a class (including its inherited members), the process begins with a method group that contains triples of:

    • a method m;

    • a governing type C<inst>; and

    • a method instantiation minst.

    For each entry in the method group, the combination of inst and minst is applied to the signature of the method, forming a group of methods. The standard method overloading rules are then applied.

    For example:

    public class Foo { }   
    
    public class C<U,V>
    {    
        ...   
        public void m(U x, V y, object z) { ... }   // method A   
        public void m(U x, V y, string z) { ... }   // method B   
        public void m(object x,Foo y, V z) { ... }  // method C
    }
    
    public void Main 
    {
       C<string,string> c1 = new C<string,string>() ;   
       c1.m("a", "b", new object());         // calls method A   
       c1.m("a", "b", new Foo());            // calls method A   
       c1.m("a", "b", 1);                    // calls method A, boxing the int   
       c1.m("a", "b", "c");                  // calls method B   
       c1.m(new object(), new Foo(), "abc")  // calls method C    
       c1.m(new Foo(), new Foo(), "abc")     // calls method C    
       c1.m(new Foo, new Foo(), new Foo())   // error in last parameter
    }   
    
    // Note that C<string,string> is considered to have methods:
    //    public string m(string x,string y, object z) { ... }   // method A   
    //    public string m(string x,string y, string z) { ... }   // method B   
    //    public string m(object x,Foo y, string z) { ... }   // method C   
    

    In the above example, the type being activated is C<string,string>, and because all the candidate methods come from this class, the governing type for each is C<string,string>. The full set of methods available for this type is determined by applying the instantiation <string,string>, which is the instantiation associated with the governing type C<string,string>. This makes the individual resolutions performed above apparent using the standard rules for overload resolution.

    Particular substitutions may result in overload resolution errors where others may not.

    C<Foo,Foo> c2 = new C<Foo,Foo>();  
    
    // Note that C<Foo,Foo> has methods:
    //    public object m(Foo x,   Foo y, object z) { ... }   // method A   
    //    public object m(Foo x,   Foo y, string z) { ... }   // method B   
    //    public object m(object x,Foo y, Foo z) { ... }   // method C   
    
    public void Main ()   
    {   
       c2.m(new Foo(), new Foo(), new Foo());   // Error, ambiguous   
    }   
    

    This situation can be avoided easily by restrained and sensible use of overloading, just as in normal class design.

    Element access expressions

    An array access expression may access an array whose element type is either a type-parameter or a constructed-type. The rules carry over unchanged, for example, if the element type is a type-parameter V, and the array type is V[], then the overall type of the element access expression will be V.

    An indexer access expression may access an object whose type is either a type-parameter or a constructed-type. When accessing an object whose type is a type-parameter, the type-parameter must be constrained by one or more types that have indexers. The set of indexers for an arbitrary type is the set of members determined by the rules of inheritance modified to take into account inheritance through structured types.

    This access expressions

    As before a this-access is permitted only in the block of a constructor, an instance method, or an instance accessor, and it has different meanings in these situations. The standard rules carry over directly when the this-access occurs within a generic-class-declaration or a generic-struct-declaration. In all cases, a this-access expression within the instance members of a generic-class-declaration of a type T with class-type-parameters V1,...,Vn is considered to have the type T<V1,...,Vn>, i.e. the most general type possible in the given generic class.

    Base access expressions

    A "base" access expression is used to access functionality of the base class within a subclass. The rules for base access expressions are essentially unmodified. Note that the base class may be a constructed-class-type. Thus, at compile-time, base-access expressions of the form base.I and base[E] are evaluated exactly as if they were written ((B<C,D>)this).I and ((B<C,D>)this)[E], where B<C,D> is the constructed-class-type that forms the base class of the class or struct in which the construct occurs.

    Object creation expressions

    The type T in a "new" expression new T(args) may be a constructed-class-type or a constructed-struct-type.

    The type type-parameter, and thus it is not permitted to simply write new V(). However, the expression new V[] is permitted, as is accessing the constructors of V by using a "typeof" expression and reflection. An alternative design pattern is to require that clients pass in a factory object capable of producing values of type V.

    Array creation expressions

    An array creation expression includes a type for the array which may be a constructed type. The type of the array may be a type-parameter, and thus it is permitted to simply write new V[size]. The elements of the array are assigned the "default" value for the type V.

    Delegate creation expressions

    An delegate creation expression includes a type for the delegate which may be a constructed type. The compile-time processing of a delegate-creation-expression of the form new T(E) where T is a constructed-delegate-type or is augmented by the following steps:

    If E is a method group:

    • We can assume that T has the form D<inst> for some delegate type D and instantiation inst. Note that inst must satisfy the constraints on the type parameters for D.

    • The method group for E must include exactly one entry whose signature is compatible with D<inst>.

    Otherwise, if E is a value of a delegate-type:

    • We can assume that T has the form D<inst> for some delegate type D and instantiation inst. Note that the type arguments must satisfy the constraints on the type parameters for D.

    • D<inst> and E must be compatible; otherwise, a compile-time error occurs. The notion of compatibility is extended in a similar manner to that immediately above to cope with delegate-create-type-parameters.

    • The result is a value of type D<inst>, namely a newly created delegate that refers to the same invocation list as E.

    Note that no method type arguments may ever be specified in a delegate creation expression even if the argument passed is a generic method That is, you can't use delegate creation to "partially apply" a generic method or generic delegate to a set of type parameters.

    "typeof" expressions

    A "typeof" expression takes a type as its argument. The type may be a constructed-class-type or a constructed-struct-type or a constructed-delegate-type. The actual type object will be different for different constructed types. The type may also be a type-parameter. The type object that results will depend on the instantiation under which the generic code is being used. The type may be an uninstantiated generic type.

    "sizeof" expressions

    The type may specify a constructed-struct-type. Different instantiations of a generic-struct-declaration may have different sizes. As usual, sizeof can only be used in unsafe code.

    Local variable declarations

    A type involved in a local variable declaration may be a constructed-type.