Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Generics Infrastructure

Generics were added to version 2.0 of the CLR, and it was a major task that required many people

working for quite some time. Specifically, to make generics work, Microsoft had to do the following:

 

■ Create new Intermediate Language (IL) instructions that are aware of type arguments.

 

■ Modify the format of existing metadata tables so that type names and methods with generic parameters could be expressed.

■ Modify the various programming languages (C#, Microsoft Visual Basic .NET, etc.) to support

the new syntax, allowing developers to define and reference generic types and methods.

 

■ Modify the compilers to emit the new IL instructions and the modified metadata format.

 

■ Modify the just-in-time (JIT) compiler to process the new type-argument–aware IL instructions that produce the correct native code.

■ Create new reflection members so that developers can query types and members to deter- mine if they have generic parameters. Also, new reflection emit members had to be defined so that developers could create generic type and method definitions at run time.

■ Modify the debugger to show and manipulate generic types, members, fields, and local vari- ables.

■ Modify the Microsoft Visual Studio IntelliSense feature to show specific member prototypes when using a generic type or a method with a specific data type.

Now let’s spend some time discussing how the CLR handles generics internally. This information could impact how you architect and design a generic algorithm. It could also impact your decision to use an existing generic algorithm or not.


Open and Closed Types

In various chapters throughout this book, I have discussed how the CLR creates an internal data struc- ture for each and every type in use by an application. These data structures are called type objects.

Well, a type with generic type parameters is still considered a type, and the CLR will create an internal type object for each of these. This applies to reference types (classes), value types (structs), interface types, and delegate types. However, a type with generic type parameters is called an open type, and the CLR does not allow any instance of an open type to be constructed (similar to how the CLR pre- vents an instance of an interface type from being constructed).

When code references a generic type, it can specify a set of generic type arguments. If actual data types are passed in for all of the type arguments, the type is called a closed type, and the CLR does allow instances of a closed type to be constructed. However, it is possible for code referencing a ge- neric type to leave some generic type arguments unspecified. This creates a new open type object in the CLR, and instances of this type cannot be created. The following code should make this clear.

 

using System;

using System.Collections.Generic;

 

// A partially specified open type

internal sealed class DictionaryStringKey<TValue> : Dictionary<String, TValue> {



}

 

public static class Program { public static void Main() {

Object o = null;

 

// Dictionary<,> is an open type having 2 type parameters Type t = typeof(Dictionary<,>);

 

// Try to create an instance of this type (fails) o = CreateInstance(t);

Console.WriteLine();

 

// DictionaryStringKey<> is an open type having 1 type parameter t = typeof(DictionaryStringKey<>);

 

// Try to create an instance of this type (fails) o = CreateInstance(t);

Console.WriteLine();

 

// DictionaryStringKey<Guid> is a closed type t = typeof(DictionaryStringKey<Guid>);

 

// Try to create an instance of this type (succeeds) o = CreateInstance(t);

 

// Prove it actually worked Console.WriteLine("Object type=" + o.GetType());

}

 

private static Object CreateInstance(Type t) {


Object o = null; try {

o = Activator.CreateInstance(t);

Console.Write("Created instance of {0}", t.ToString());

}

catch (ArgumentException e) { Console.WriteLine(e.Message);

}

return o;

}

}

 

When I compile the preceding code and run it, I get the following output.

 

Cannot create an instance of System.Collections.Generic. Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.

 

Cannot create an instance of DictionaryStringKey`1[TValue] because Type.ContainsGenericParameters is true.

 

Created instance of DictionaryStringKey`1[System.Guid] Object type=DictionaryStringKey`1[System.Guid]

 

As you can see, Activator’s CreateInstance method throws an ArgumentException when you ask it to construct an instance of an open type. In fact, the exception’s string message indicates that the type still contains some generic parameters.

In the output, you’ll notice that the type names end with a backtick (`) followed by a number. The number indicates the type’s arity, which indicates the number of type parameters required by the type. For example, the Dictionary class has an arity of 2 because it requires that types be specified for TKey and TValue. The DictionaryStringKey class has an arity of 1 because it requires just one type to be specified for TValue.

I should also point out that the CLR allocates a type’s static fields inside the type object (as discussed in Chapter 4, “Type Fundamentals”). So each closed type has its own static fields. In other words, if List<T> defined any static fields, these fields are not shared between a List<DateTime> and a List<String>; each closed type object has its own static fields. Also, if a generic type defines a static constructor (discussed in Chapter 8, “Methods”), this constructor will execute once per closed type. Sometimes people define a static constructor on a generic type to ensure that the type argu- ments will meet certain criteria. For example, if you wanted to define a generic type that can be used only with enumerated types, you could do the following.

 

internal sealed class GenericTypeThatRequiresAnEnum<T> { static GenericTypeThatRequiresAnEnum() {

if (!typeof(T).IsEnum) {

throw new ArgumentException("T must be an enumerated type");

}

}

}


The CLR has a feature called constraints that offers a better way for you to define a generic type indicating what type arguments are valid for it. I’ll discuss constraints later in this chapter. Unfor- tunately, constraints do not support the ability to limit a type argument to enumerated types only, which is why the previous example requires a static constructor to ensure that the type is an enumer- ated type.

 

Generic Types and Inheritance

A generic type is a type, and as such, it can be derived from any other type. When you use a generic type and specify type arguments, you are defining a new type object in the CLR, and the new type object is derived from whatever type the generic type was derived from. In other words, because List<T> is derived from Object, List<String> and List<Guid> are also derived from Object. Similarly, because DictionaryStringKey<TValue> is derived from Dictionary<String, TValue>, DictionaryStringKey<Guid> is also derived from Dictionary<String, Guid>. Understanding that specifying type arguments doesn’t have anything to do with inheritance hierarchies will help you to recognize what kind of casting you can and can’t do.

For example, if a linked-list node class is defined like this:

 

internal sealed class Node<T> { public T m_data;

public Node<T> m_next;

 

public Node(T data) : this(data, null) {

}

 

public Node(T data, Node<T> next) { m_data = data; m_next = next;

}

 

public override String ToString() { return m_data.ToString() +

((m_next != null) ? m_next.ToString() : String.Empty);

}

}

 

then I can write some code to build up a linked list that would look something like the following.

 

private static void SameDataLinkedList() { Node<Char> head = new Node<Char>('C'); head = new Node<Char>('B', head);

head = new Node<Char>('A', head); Console.WriteLine(head.ToString()); // Displays "ABC"

}

 

In the Node class just shown, the m_next field must refer to another node that has the same kind of data type in its m_data field. This means that the linked list must contain nodes in which all data items are of the same type (or derived type). For example, I can’t use the Node class to create a

linked list in which one element contains a Char, another element contains a DateTime, and another


element contains a String. Well, I could if I use Node<Object> everywhere, but then I would lose compile-time type safety, and value types would get boxed.

So a better way to go would be to define a non-generic Node base class and then define a generic TypedNode class (using the Node class as a base class). Now, I can have a linked list in which each node can be of a specific data type (not Object), get compile-time type safety, and avoid the boxing of value types. Here are the new class definitions.

 

internal class Node { protected Node m_next;

 

public Node(Node next) { m_next = next;

}

}

 

internal sealed class TypedNode<T> : Node { public T m_data;

 

public TypedNode(T data) : this(data, null) {

}

 

public TypedNode(T data, Node next) : base(next) { m_data = data;

}

 

public override String ToString() { return m_data.ToString() +

((m_next != null) ? m_next.ToString() : String.Empty);

}

}

 

I can now write code to create a linked list in which each node is a different data type. The code could look something like the following.

 

private static void DifferentDataLinkedList() { Node head = new TypedNode<Char>('.');

head = new TypedNode<DateTime>(DateTime.Now, head); head = new TypedNode<String>("Today is ", head); Console.WriteLine(head.ToString());

}

 

Generic Type Identity

Sometimes generic syntax confuses developers. After all, there can be a lot of less-than (<) and greater-than (>) signs sprinkled throughout your source code, and this hurts readability. To improve syntax, some developers define a new non-generic class type that is derived from a generic type and that specifies all of the type arguments. For example, to simplify code like this:

 

List<DateTime> dtl = new List<DateTime>();


some developers might first define a class like the following.

 

internal sealed class DateTimeList : List<DateTime> {

// No need to put any code in here!

}

 

Now, the code that creates a list can be rewritten more simply (without less-than and greater-than signs) like the following.

 

DateTimeList dtl = new DateTimeList();

 

Although this seems like a convenience, especially if you use the new type for parameters, local variables, and fields, you should never define a new class explicitly for the purpose of making your source code easier to read. The reason is because you lose type identity and equivalence, as you can see in the following code.

 

Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));

 

When the preceding code runs, sameType will be initialized to false because you are comparing two different type objects. This also means that a method prototyped as accepting a DateTimeList will not be able to have a List<DateTime> passed to it. However, a method prototyped as accepting a List<DateTime> can have a DateTimeList passed to it because DateTimeList is derived from List<DateTime>. Programmers may become easily confused by all of this.

Fortunately, C# does offer a way to use simplified syntax to refer to a generic closed type while not affecting type equivalence at all; you can use the good-old using directive at the top of your source code file. Here is an example.

 

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

 

Here, the using directive is really just defining a symbol called DateTimeList. As the code compiles, the compiler substitutes all occurrences of DateTimeList with System.Collections. Generic.List<System.DateTime>. This just allows developers to use a simplified syntax without affecting the actual meaning of the code, and therefore, type identity and equivalence are main- tained. So now, when the following line executes, sameType will be initialized to true.

 

Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));

 

As another convenience, you can use C#’s implicitly typed local variable feature, where the compiler infers the type of a method’s local variable from the type of the expression you are assigning to it.

 

using System;

using System.Collections.Generic;

...

internal sealed class SomeType { private static void SomeMethod () {


// Compiler infers that dtl is of type

// System.Collections.Generic.List<System.DateTime> var dtl = new List<DateTime>();

...

}

 

}

 

Code Explosion

When a method that uses generic type parameters is JIT-compiled, the CLR takes the method’s IL, substitutes the specified type arguments, and then creates native code that is specific to that method operating on the specified data types. This is exactly what you want and is one of the main features of generics. However, there is a downside to this: the CLR keeps generating native code for every method/type combination. This is referred to as code explosion. This can end up increasing the ap- plication’s working set substantially, thereby hurting performance.

Fortunately, the CLR has some optimizations built into it to reduce code explosion. First, if a method is called for a particular type argument, and later, the method is called again using the same type argument, the CLR will compile the code for this method/type combination just once. So if one assembly uses List<DateTime>, and a completely different assembly (loaded in the same App- Domain) also uses List<DateTime>, the CLR will compile the methods for List<DateTime> just once. This reduces code explosion substantially.

The CLR has another optimization: the CLR considers all reference type arguments to be identical, and so again, the code can be shared. For example, the code compiled by the CLR for List<String>’s methods can be used for List<Stream>’s methods, because String and Stream are both reference types. In fact, for any reference type, the same code will be used. The CLR can perform this optimiza- tion because all reference type arguments or variables are really just pointers (all 32 bits on a 32-bit Windows system and 64 bits on a 64-bit Windows system) to objects on the heap, and object pointers are all manipulated in the same way.

But if any type argument is a value type, the CLR must produce native code specifically for that value type. The reason is because value types can vary in size. And even if two value types are the same size (such as Int32 and UInt32, which are both 32 bits), the CLR still can’t share the code be- cause different native CPU instructions can be used to manipulate these values.

 

 


Date: 2016-03-03; view: 605


<== previous page | next page ==>
Delegate and Interface Contra-variant and | Nbsp;   Delegate and Interface Contra-variant and Covariant Generic Type Arguments
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.01 sec.)