Generic types, introduced in version 2.0 of the CLR, differ from “normal” (nongeneric) types in one major aspect: “normal” types, even the abstract ones, are fully defined, while generic types represent pure abstractions—templates for the generation (or instantiation) of “normal” types. Generic types are pure abstractions because they describe types constructed not from other types but from abstract type parameters, or type variables. Thus, a generic type has one or more type parameters and hence belongs to parameterized types. You are already familiar with one generic type implemented in versions 1.0 and 1.1 of the CLR—a vector (single-dimensional, zero lower-bound array). A vector doesn’t exist per se—it’s always a vector “of something,” such as a vector of 32-bit integers, a vector of strings, or a vector of objects, and so on. The vector was (and still is) an intrinsic generic type in the sense it is implemented by the CLR but has no representation as a separate class.
My reference to the templates is not an error. The C++ templates, another representative of parameterized types, are probably the most-known vehicle of generic programming, so C++ templates and generics (generic types and generic functions, discussed in Chapter 12) play similar roles. There, however, the similarity ends and the differences begin, including the two most important:
In general, the set of parameters of a parameterized type is often referred to as the type’s parameter list, and the set of actual arguments used for the parameterized type instantiation is known as the instantiation environment. The parameter list may be homogenous (all parameters are of the same kind, say, type) or heterogeneous. The parameter list may also be constrained, meaning some limitations may be imposed on the instantiation contexts (for example, this type argument must be derived from type X, or that integer argument must be a prime number). So, the .NET generics are parameterized types with homogenous constrained parameter lists, while C++ templates represent parameterized types with heterogeneous unconstrained parameter lists.
The generics in .NET were introduced by the outstanding work of Don Syme and Andrew Kennedy from Microsoft Research (Cambridge, United Kingdom). Don and Andrew started their work on .NET generics shortly before version 1.0 of the CLR was released. The way to the final implementation was long and not without turns, but since version 2.0 the CLR boasts advanced, completely functional support of generics.
Being template-like abstractions for building the concrete types, generic types don’t change the .NET type hierarchy discussed in Chapter 7 (refer to Figure 7-1). Rather, the generic types add a “genericity” dimension to the type hierarchy: there can be generic and nongeneric classes, interfaces, and value types, and for example, a generic interface is as much an interface as a nongeneric one.
All concepts of inheritance (from a base class) and implementation (of the interfaces) defined for nongeneric types are valid for generic types. Both generic and nongeneric types can extend and implement only nongeneric types or instantiations of generic types, for obvious reasons: the instantiations of generic types are true types and can be used anywhere, while the generic types are the templates of true types and cannot be used anywhere but in instantiations.
Generic Type Metadata
As I mentioned in Chapter 7, the nongeneric type metadata is grouped around the concepts of type definition (TypeDef) and type reference (TypeRef). The generic type metadata uses one more basic concept—type specification (TypeSpec), representing the instantiations of generic types.
The definition of a nongeneric type involves the following information:
To define a generic type, you should also supply the list of type parameters and define the constraints of each type parameter.
Referencing a generic type is a tricky question. Strictly speaking, you cannot reference a generic type per se; you can reference only an instantiation of a generic type, providing in addition to the type’s name and resolution scope the list of type arguments.
Saving you a trip four chapters back, I’m repeating the figure that shows the metadata tables participating in type definition and referencing (see Figure 11-1). The arrows indicate cross-table referencing by means of metadata tokens.
Figure 11-1. Metadata tables participating in type definition and referencing
Three tables in the lower part of Figure 11-1 (TypeSpec, GenericParam, and GenericParamConstraint) and their associated links are related to generic types and will be discussed in this chapter.
The rest of the tables shown in Figure 11-1 are common to generic and nongeneric types, so everything I said about these tables in Chapter 7 holds true for the case of generic types. This means, in particular, that looking at a TypeDef or TypeRef record, you cannot say whether the type represented by this record is generic (for a TypeDef you need to look in the GenericParam table and see whether it contains generic parameters associated with this type). This in turn means that the genericity of the type (list of its type parameters, if any) cannot be used for type identification, and the type identification is still based on the type’s full name and resolution scope. In other words, you cannot have types G (nongeneric) and G<T> (generic with one type parameter) defined in the same module. This is rather restrictive and can be likened to the prohibition of method overloads (this design was chosen because it allowed for the introduction of generics via incremental changes in the metadata scheme; an alternative would be the complete overhaul of the metadata structure and of the ways the types are recognized in the CLR).
The high-level languages bypass this limitation and allow you to define types G and G<T> (and G<T,U>, and so on) in the same module by mangling the names of generic types, usually adding the generic arity (the number of type parameters) to the type name. For example, VB and C# emit type G as G, type G<T> as G`1, type G<T,U> as G`2, and so on (now you probably have guessed why the backtick symbol was added as a legal identifier symbol in ILAsm 2.0).
Since the generic parameters can represent only types, mangling the type name with generic arity is enough to simulate the “type overload on genericity.” If you had to deal with C++ templates rather than generics, you would probably have to use a more sophisticated name-mangling scheme, reflecting the “generic signature” of the type.
The negative side effect (rather minor) of the name mangling is that the generic types are emitted under names different from specified in the high-level language code. The positive effect is that you can identify a generic type and its arity by looking at the type’s name (however, this doesn’t work for nested types, as I will show you later in this chapter).
The IL assembler does not do the type name mangling automatically, leaving it to the programmer or to the tool (for example, a compiler) generating ILAsm code. I will follow the C#/VB name-mangling convention in the samples, but you should remember it is in no way mandatory.
Having agreed on this, let’s proceed to the discussion of the metadata tables specific to generic types.
GenericParam Metadata Table
The GenericParam table contains the information about generic parameters. You might wonder why this table is needed; if the generic parameters can be only types, their number (arity of a generic type) should be sufficient. The main reasons for the existence of the GenericParam table are the need to be able to tell a generic type from a nongeneric one (generic types have associated generic parameters) and the need to be able to define constraints of each generic parameter.
Each record in this table has four entries:
In the optimized metadata model, the GenericParam records must be sorted by their Owner field.
GenericParamConstraint Metadata Table
The GenericParamConstraint metadata table contains inheritance and implementation constraints imposed on the generic parameters. An inheritance constraint imposed on a generic parameter means that the type substituting for the parameter in a generic instantiation must be derived from the specified type. An implementation constraint means that the type substituting for this parameter must implement the specified interface.
Each record in this table has two entries:
In the optimized metadata model, the GenericParamConstraint records must be sorted by their Owner field.
TypeSpec Metadata Table
The TypeSpec metadata table, which you already encountered in Chapter 8, represents the constructed types—in versions 1.0 and 1.1 it represented vectors and arrays, and in version 2.0 it represents also instantiations of the generic types. The TypeSpec table has only one entry in each record: Signature (offset in the #Blob stream), representing the signature of the constructed type. Chapter 8 discussed the signatures of vectors and arrays, and I will describe the signatures of generic type instantiations in the “Generic Type Instantiations” section of this chapter.
I still don’t understand the purpose of introducing the TypeSpec metadata table (and the StandAloneSig table as well) in the first place. These tables serve as simple redirectors to the #Blob stream. It would make more sense to do the same trick as with mdtString tokens—interpret the RID part of an mdtTypeSpec or mdtStandAloneSig token as the offset in the #Blob stream. Maybe the concerns about the 16MB offset limit (the RID part of the token is 24-bits wide) were the reason? But I digress.
Constraint Flags
The constraint flags describe the constraints imposed on a generic parameter that are not of an inheritance or implementation nature. Table 11-1 describes the constraint flags defined in version 2.0 of the CLR (see also enumeration CorGenericParamAttr in file CorHdr.h):
Table 11-1. Constraint Flags
Defining Generic Types in ILAsm
The ILasm syntax for defining a generic type is as follows:
.class<flags> <dotted_name> < <gen_params> >
[ extends<class_ref>]
[ implements<class_refs>]
{
...
}
As you can see, the only difference between the generic type definition and nongeneric type definition is the presence of the <gen_params> clause (enclosed in angular brackets), such as
<gen_params> ::= <gen_param> [, <gen_param>]*
where
<gen_param> ::= [<constraint_flags>] [( <constraints> )] <gen_param_name>
where
<constraint_flags> ::= +| -| class| valuetype| .ctor
<constraints> ::= <class_ref> [, <class_ref>]*
<gen_param_name> ::= <simple_name>
For example,
.class public EventHandler`1< - class([mscorlib]System.IAsyncResult) T>
extends[mscorlib]System.MulticastDelegate
{
// T must be a contravariant reference type implementing IAsyncResult
...
}
The types specified as constraints cannot be less visible than the generic type itself. The reasoning is obvious enough: if you define a public generic type and constrain its type parameter with a private type, what will happen if somebody tries to instantiate your generic type in his own assembly?
The types specified as constraints can be nongeneric types (as in the previous example), generic instantiations, and even references to other type parameters of the same type. If you don’t need to put any constraints on a generic type, just declare it as G`1<T>. Don’t declare it as G`1<([mscorlib]System.Object) T>. That’s just plain silly: since any type is derived eventually from System.Object, “constraining” a generic parameter like that just bloats the GenericParamConstraint table and increases the type load time. I’m saying this because I’ve seen people and even compilers doing exactly that.
Addressing the Type Parameters
The type parameters of a generic type are referenced within the type as !<name> or !<ordinal>, where <name> is the name of the type parameter and <ordinal> is the parameter’s number (zero-based) in the type parameter list. For example,
.class public value Pair`1<T>
{
.field public!T x
.field public!0 y // fields x and y have the same type T
}
Both notations translate into the single encoded types {E_T_VAR, <compressed_ordinal>}, so both fields x and y in the previous sample have the signatures {CALLCONV_FIELD, E_T_VAR, <compressed_ordinal>} = {0x06, 0x13, 0x00}. The <compressed_ordinal> is the ordinal (zero-based index) of the type parameter, compressed according to the formula given in Table 5-1. For values below 128, this formula produces a single byte containing the value, so for all reasonable purposes <compressed_ordinal> is simply a byte containing the type parameter’s ordinal. I am yet to see a generic type with more than 127 type parameters.
Type parameters are referenced in the same way in the method signatures of generic types:
.class public List`1<T>
{
.method public void Append(!T val) { ... }
.method public!T GetLast() { ... }
...
}
Generic Type Instantiations
An instantiation of a generic type involves two items—the generic type itself and the instantiation context, representing the list of actual type arguments substituting for the generic type’s parameters. The ILAsm syntax representing a generic instantiation is
class<type_name> <<type> [, <type>]* >
or
valuetype<type_name> <<type> [, <type>]* >
where <type_name> is a fully qualified name of the generic type and the <type> sequence in angular brackets represents the type argument list. For example,
.field private class List`1< string> nameList
.field private class List`1<[mscorlib]System.Type> typeList
The keyword class or valuetype is necessary in specifications of generic type instantiations because generic type instantiations are represented in the metadata by TypeSpecs, and these keywords signal the IL assembler to produce a TypeSpec rather than a TypeRef or a TypeDef. This is a general rule of ILAsm, not specific to the generic type instantiations. For example, the notation [mscorlib]System.Type translates into a TypeRef, while the notation class [mscorlib]System.Type translates into a TypeSpec with the signature {E_T_CLASS, <token>}, where <token> is a TypeRef token of [mscorlib]System.Type.
The signatures of TypeSpecs representing the generic instantiations have the following form: {E_T_GENERICINST, E_T_CLASS, <gen_type_token>, <arity>, <arg_token>[, <arg_token>]*}, where <gen_type_token> is a TypeRef or TypeDef token representing the generic type, <arity> is a compressed number of type arguments, and the sequence of <arg_token> is a sequence of TypeRef, TypeDef, or TypeSpec tokens (or element type codes) representing the type arguments (the instantiation context). For example, the generic instantiation class List`1<string> is represented by a TypeSpec with the signature {E_T_GENERICINST, E_T_CLASS, <token_of_List`1>, 1, E_T_STRING} = {0x15, 0x12, <token_of_List`1>, 0x01, 0x0E}.
In general, any type satisfying the constraints (if any) can be used as a type argument of a generic instantiation. There are three exceptions: a managed pointer to some type, void, and a value type that contains references to the IL evaluation stack, such as [mscorlib]System.RuntimeArgumentHandle. All three are unsuitable as the type of a field, and this is the main reason they are not allowed as type arguments. The CLR doesn’t want you to declare a field of type T in class A<T> and then instantiate A<void>. It would be embarrassing.
Within the scope of a generic type its type parameters are considered regular types, so the generic instantiations can use these type parameters as type arguments:
.class public List`1<T>// Generic type
{
...
}
.class public Stack`1<T>// Generic type
{
.field private class List`1<!T> stackList// Generic type instantiated with
// parameter of host generic type
...
}
So, the instantiation context of a generic type within the scope of another generic type can itself be generic (type parameterized).
The instantiation context can also contain instantiations of other generic types, such as
.class public StackSet`1<T>
{
.field private class List`1< class Stack`1<!T>> stackList
...
}
Having said that, let’s return to the generic class declaration, where we have unfinished business.
Defining Generic Types: Inheritance, Implementation, Constraints
When I talked about generic type definition, I purposefully avoided elaborating on such important aspects of type definition as inheritance and interface implementation. The reason for that is all these aspects of a type can be generic instantiations. With a generic type, all these aspects (and the generic parameter constraints as well) are considered to be in the scope of the generic type, so their instantiation contexts can be parameterized, like so:
.class public A`2<T,U> extends class B`1<!T>
implements class IX`1<!T>, class IY`1<!U>
{
...
}
Only the declaration of a generic class itself has a type parameter list; all references to a generic type can be only generic instantiations. For example, the following notation of the previous example is wrong because it presumes the parent type and implemented interfaces have type parameters, when in fact they are instantiations with parameterized context:
.class public A`2<T,U> extends B`1<T> implements IX`1<T>, IY`1<U>// Illegal
{
...
}
The same rule applies to the specification of the constraints of generic parameters. For example,
.class public SortedList`1<(class[mscorlib]System.IComparable`1<!T>) T>
extends class List`1<!T>
{
...
}
Here you declare a generic sorted list, the type parameter of which must implement the interface System.IComparable`1 of itself (otherwise how could you possibly sort the list?).
One important note: the type parameters of a generic type are indeed considered rightful types within the generic type’s scope, but they are not instantiations. So, you cannot use the “naked” type parameters in the extends or implements clause. The following code example is wrong:
.class public AnyonesChild`1<T> extends!T// Illegal
{
...
}
This restriction allows the runtime to check the generic type validity at declaration time rather than at instantiation time. The latter is possible in principle but might be very expensive.
At the same time you can use “naked” type parameters as constraints of other type parameters. For example, the following declaration is perfectly legal:
.class public ParentChild`2<T, (!T)U>// U must be descendant of T
{
...
}
Defining Generic Types: Cyclic Dependencies
As you know from Chapter 7, cyclic dependencies in type inheritance and interface implementation are illegal. A cyclic dependence means that, for example, class A extends class B, and B extends C, and C extends A. The cyclic dependencies of nongeneric types are easily detected by the CLR loader, which throws the Type Load exception and aborts the loading.
The question of cyclic dependencies becomes more complex in the case of generic types, which use instantiations with parameterized contexts in extends and implements clauses, given that these contexts may contain other instantiations, and so on.
When loading a generic type, the loader must suspend processing this type when it encounters a generic instantiation as the base or one of the implemented interfaces of this type, load this instantiation, and then return to loading this type. As you can see, this process is recursive and can lead to a stack overflow if mutual dependencies of the instantiations are cyclic.
For example, the following three type declarations have a cyclic inheritance dependency of the instantiations:
.class public A`1<T> extends class C`1< class B`1<!T>>
{
...
}
.class public B`1<U> extends class A`1< class C`1<!U>>
{
...
}
.class public C`1<V>
{
...
}
The following algorithm is used to identify a cyclic dependency in the inheritance and implementation of a generic type declaration. First, list all generic types that have mutual dependency (in this example, A`1, B`1, and C`1) and their type parameters. Then, list all generic instantiations used in the extends and implements clauses of these types, including the instantiations used as type arguments of other instantiations (as in C`1<B`1<!T>>). Then, build a graph that has an edge from each type parameter mentioned in an instantiation to the respective type parameter of a generic type being instantiated. Use the edges of two kinds: a nonexpanding edge means a type parameter is replaced with a “naked” parameter of another type, and an expanding edge means a type parameter is replaced with an instantiation involving parameter of another type. For example, the instantiation class C`1<!U> of class C`1<V> creates a nonexpanding edge from U to V, because “naked” !U is substituting for V; at the same time, the instantiation class C`1<class B`1<!T>>of the same class C`1<V> creates an expanding edge from T to V, because the instantiation involving !T (class B`1<!T>) is substituting for V.
If the resulting graph contains a loop having at least one expanding edge in it, you have a cyclic dependency of instantiations because each expanding edge means “suspend loading this type and load the referenced instantiation first” for the CLR loader. Table 11-2 illustrates the instantiation dependency analysis of the discussed example. A single-line arrow indicates a nonexpanding edge, and a double-line arrow indicates an expanding edge.
Table 11-2. Instantiation Dependency Analysis
Generic Type |
Instantiation |
Substitution Edge |
---|---|---|
A`1<T> |
A`1<C`1<!U>> |
U ⇒ T |
B`1<U> |
B`1<!T> |
T → U |
C`1<V> |
C`1<!U> |
U → V |
C`1<V> |
C`1<B`1<!T>> |
T ⇒ V |
Figure 11-2 shows the resulting graph; expanding edges are represented by solid arrows and nonexpanding edges are represented by dashed arrows. As you can see, the graph contains a loop with an expanding edge in it.
Figure 11-2. Instantiation dependency graph
The ECMA/ISO standard specification illustrates the circular instantiation dependencies with a sample that is probably the simplest instantiation dependency with an expanding loop:
.class public A`1<T> extends class B`1< class A`1< class A`1<!T>>>
{
...
}
.class public B`1<U>
{
...
}
This sample has the instantiation dependency graph where the node representing type parameter T is connected to itself with an expanding edge.
If the instantiation dependency graph has no loops or has loops consisting only of nonexpanding edges, such instantiation dependency is noncyclic and can be loaded.
The inheritance and implementation constraints imposed on the type parameters of the generic types don’t affect the type loading in the same way as the generic instantiations, which the type itself extends and implements, so the instantiations representing these constraints are not included in the instantiation dependency analysis.
The Members of Generic Types
Declaring the members of generic types is more or less straightforward: the types of the members can be nongeneric types, type parameters of the generic type, or generic instantiations (or constructions thereof).
.class public Container`1<T>
{
.field private int32count
.field private!T[] arr
.method public int32Count()
{
...
}
.method public!T Element(int32idx)
{
...
}
...
}
There is an interesting limitation imposed on the methods of generic types: they cannot have the vararg calling convention.
Referencing the members of a generic type is a bit trickier than declaring them: their resolution scope must always be the instantiation of the parent type even if the members are referenced inside the parent type.
.class public Container`1<T>
{
.field private int32count
.field private!T[] arr
.method public int32Count()
{
ldarg.0
ldfld int32 class Container`1<!T>::count// NOT Container`1::count
ret
}
.method public!T Element(int32idx)
{
ldarg.0
ldfld!T[] class Container`1<!T>::arr// NOT Container`1::arr
ldarg.1
ldelem!T
ret
}
...
}
You cannot use the .this, .base, or .nester keywords within a generic type’s scope the same way you do within nongeneric types because the reference to a generic type must always be an instantiation. However, you can use these keywords to form instantiations. For example,
...
ldfld int32 class .this<!T>::count
...
ldfld!T[] class .this<!T>::arr
...
When addressing the members of generic instantiations outside the defining class’s scope, you need to specify their signatures as they were defined, not as they became in the instantiation. For example,
...
call instance!0 class Container`1< string>::Element(int32)// Correct
...
call instance string class Container`1< string>::Element(int32)// Incorrect
...
The return type of method Element was defined as the “type parameter number 0 of Container`1,” and it must be the same at the method call site, even though the method is called on the instantiation of Container`1 with string substituting for the type parameter number 0. You cannot use !T instead of !0 either, because !T does indeed mean the “type parameter number 0 of Container`1” but only within the lexical scope of the Container`1 declaration.
This means you can’t inadvertently create a duplicate member declaration when instantiating the generic type. For example,
.class public Pair`2<T,U>
{
.field private!T t
.field private!U u
.method public void Set(!T newT)
{
...
}
.method public void Set(!U newU)
{
...
}
...
}
Everything seems in order: method Set is overloaded on its parameter type, which is completely legal. Now, try to instantiate Pair`2<string, string>. Do you have a problem distinguishing one Set from another? Not at all. The methods will be called as follows:
...
ldstr"ABCD"
call instance void class Pair`2< string,string>::Set(!0)// first Set called
...
ldstr"EFGH"
call instance void class Pair`2< string,string>::Set(!1)// second Set called
...
Virtual Methods in Generic Types
Declaring virtual methods in generic types is not different in principle from declaring nonvirtual methods.
.class interface public abstract System.IComparable`1<T>
{
.method public abstract virtual int32CompareTo(!T other) {}
}
Implicitly overriding a virtual method is also relatively straightforward: the name of the overriding method must match the name of the overridden method, and the signature of the overriding method must match the signature of the overridden method with instantiation type arguments substituting for the type parameters of the overridden method.
.class public serializable sealed beforefieldinit System.String
implements...
class System.IComparable`1< string>,
...
{
...
.method public virtual final int32CompareTo(string otherStr)
{
...
}
...
}
Or in the case of a generic class overriding a virtual method from another generic class,
.class public Element`1<T> implements class[mscorlib]System.IComparable`1<!T>
{
...
.method public virtual int32CompareTo(!T other)
{
...
}
...
}
And of course you cannot override a method that doesn’t have the type parameters in its signature with a method that does have them.
.class interface public abstract System.IComparable
{
.method public abstract virtual int32CompareTo(object obj) {}
}
...
.class public Element`1<T> implements[mscorlib]System.IComparable
{
...
.method public virtual int32CompareTo(!T other)// Invalid override
{
...
}
...
}
When dealing with implicit overriding, however, you should watch for possible duplicate overrides upon instantiation (yes, in the same vein as inadvertent duplicate member declarations, which I said are not a problem). For example,
.class interface public abstract IX<T,U>
{
.method public abstract virtual int32XX(!T t) {}
.method public abstract virtual int32XX(!U u) {}
}
...
.class public A implements class IX< string, string>
{
...
.method public virtual int32XX(string s)// Which XX does it override?
{
...
}
...
}
If some evil person (not you, of course) declared an interface (or a class) such as IX<T,U> in the previous sample and you need to override its methods, your only option is to give the overriding methods other names and use explicit override.
Just to save you a trip back to Chapter 10, let me remind you of the short form of an explicit override directive in a nongeneric case:
.class public Element implements[mscorlib]System.IComparable
{
...
.method public virtual int32Comp(object other)
{
.override[mscorlib]System.IComparable::CompareTo
...
}
...
}
Explicitly overriding virtual methods of generic types is more complicated: you have to supply the signature of the overridden method. So, the short form of an explicit override directive used within the overriding method’s body looks as follows:
.class public Element`1<T> implements class[mscorlib]System.IComparable`1<!T>
{
...
.method public virtual int32Comp(!T other)
{
.override method instance int32
class[mscorlib]System.IComparable`1<!T>::CompareTo(!0)
...
}
...
}
Note the keyword method followed by the overridden method’s calling convention and return type.
It is necessary to specify the overridden method’s signature because the overriding method’s signature is different: the original signature of CompareTo has a single argument of type “type parameter number 0 of IComparable`1.” That’s why, by the way, the argument type in the signature of CompareTo has the form !0 instead of !T—!T means the “type parameter T of Element`1.”
To illustrate this point, the following is an example of a method of a nongeneric class overriding a method of a generic class:
.class public EStr implements class[mscorlib]System.IComparable`1< string>
{
...
.method public virtual int32Comp(string other)
{
.override method instance int32
class[mscorlib]System.IComparable`1< string>::CompareTo(!0)
...
}
...
}
The long form of the explicit override directive follows the pattern of the short form. First, let me remind you of the long form of an explicit override directive in a nongeneric case:
.class public Element implements[mscorlib]System.IComparable
{
.override[mscorlib]System.IComparable::CompareTo with
instance int32 .this::Comp(object)
...
.method public virtual int32Comp(object other)
{
...
}
...
}
The long form of the explicit override directive in the case of a generic class overriding a method of another generic class looks as follows:
.class public Element`1<T> implements class[mscorlib]System.IComparable`1<!T>
{
.override method instance int32
class[mscorlib]System.IComparable`1<!T>::CompareTo(!0)
with method instance int32 class .this<!T>::Comp(!0)
...
.method public virtual int32Comp(!T other)
{
...
}
...
}
Note that when explicitly overriding a method of a generic class, you need to specify the overriding method the same way as the overridden method, and it does not matter whether the overriding class is generic.
.class public EStr implements class[mscorlib]System.IComparable`1< string>
{
.override method instance int32
class[mscorlib]System.IComparable`1< string>::CompareTo(!0)
with method instance int32 .this::Comp(string)
...
.method public virtual int32Comp(string other)
{
...
}
...
}
As you can see, the long form of the .override directive is even more cumbersome in the case of generic types, and strictly speaking this form is not necessary: the short form is fully sufficient for explicit overriding.
Nested Generic Types
As you know, the nested types have full access to the members, even private ones, of their immediate enclosers. But the types nested in generic types don’t have any access to the type parameters of their enclosers. This means if a nongeneric type is nested in a generic type, this nested type must not use the encloser’s environment:
.class public A`1<T>
{
.class nested public B
{
...// Cannot use !T here
}
...// Can use !T here
}
When a generic type is nested in another (generic or nongeneric) type, its encloser, naturally, has no access to the nested type’s generic environment. In short, the generic environments (if any) of the nested and the enclosing types are completely independent, like so:
.class public A`1<T>
{
.class nested public B
{
...// Cannot use !T or !U here
}
.class nested public C`1<U>
{
...// Cannot use !T here
}
...// Can use !T but not !U here
}
The reason for this independence is that the nested and enclosed types are instantiated separately. When you instantiate an enclosing type, the nested types are not instantiated automatically, and you don’t need to instantiate the encloser to instantiate the nested type. This goes for both meanings of instantiate—the creation of a generic instantiation of the type and the creation of an instance of it.
As you know, the nested types are referenced in ILAsm as <encloser_ref>/<nested_type_name>, where <encloser_ref> is a fully qualified name of the enclosing type, for example, [mscorlib]System.RuntimeTypeHandle/DispatchWrapperType. This is true for the types nested in generic types because of the generic environment independence. For example, the nested class B described earlier is referenced as A`1/B. There is no such thing as a type nested in a generic instantiation, so the notation class A`1<string>/B makes no sense. But an instantiation of a nested generic type is a very real thing, and the notation class A`1/C<string> is completely legal.
I must warn you about one helpful feature of the C# compiler. When you declare a class nested in a generic class, the compiler presumes that the nested class needs “access” to the type parameters of the encloser and makes the nested types generic. So, you can’t possibly define a nongeneric type nested in a generic type using C#. For example, C# code that looks like
public class A<T>
{
public class B
{
...
}
...
}
translates into ILAsm code that looks like
.class public A`1<T>
{
.class nested public B<T>// Note: no `1 added to B's name
{
...
}
...
}
And if you declare the nested class as generic, the C# compiler concatenates the declared type parameter lists of the encloser and the nested type and assigns the result to the nested type.
Note that the C# compiler mangles the nested type’s name according to its own declared generic arity, not according to the summary encloser’s and nested type’s arity, probably because the nested type’s name must be unique only within its encloser, such as
public class A<T>
{
public class B<U>
{
...
}
...
}
which in ILAsm looks like
.class public A`1<T>
{
.class nested public B`1<T,U>// Note: `1 instead of `2 added to B's name
{
...
}
...
}
An interesting thing happens if you declare a generic nested type with the same type parameter name as the encloser’s. Note that
public class A<T>
{
public class B<T>
{
...// Here T means B's type parameter
}
...// Here T means A's type parameter
}
produces the following ILAsm code:
.class public A`1<T>
{
.class nested public B`1<T,T>
{
...// Here !0 means A's type parameter
...// And !1 or !T means B's type parameter
}
...// Here !0 or !T means A's type parameter
}
Of course, after such a declaration, the encloser’s type parameter T cannot be accessed inside the nested type in C#, because in C# the type parameters are referenced only by their names, and T inside the nested type means the nested type’s T. The C# compiler doesn’t diagnose this declaration as an error, but, of course, it issues a warning.
ILAsm can reference the type parameters by ordinal as well as by name, so duplicate names of type parameters don’t prevent these parameters from being addressed.
Summary of the Metadata Validity Rules
The metadata tables specific to the generic types (other type-related tables were discussed in Chapter 7) include TypeSpec, GenericParam, and GenericParamConstraint. The records of these tables contain the following entries:
18.116.86.255