Excerpt from Accelerated C# 2008

Nov 28, 12:49 pm
tags: , ,

Generics

Support for generics is one of the nicest features of C# and .NET. Generics allow you to create open-ended types that are converted into closed types at run time. Each unique closed type is itself a unique type. Only closed types may be instantiated. When you declare a generic type, you specify a list of type parameters in the declaration for which type arguments are given to create closed types, as in the following example:


public class MyCollection
{ public MyCollection() { }

private T[] storage; }

In this case, I’ve declared a generic type, MyCollection<T>, which treats the type within the collection as an unspecified type. In this example, the type parameter list consists of only one type, and it is described with syntax in which the generic types are listed, separated by commas, between angle brackets. The identifier T is really just a placeholder for any type. At some point, a consumer of MyCollection<T> will declare what’s called a closed type, by specifying the concrete type that T is supposed to represent. For example, suppose some other assembly wants to create a MyCollection<T> constructed type that contains members of type int. Then it would do so as shown in the following code:


public void SomeMethod() {
        MyCollection<int> collectionOfNumbers = new MyCollection<int>();
}

MyCollection<int> in this example is the closed type. MyCollection<int> can be used just like any other declared type, and it also follows all of the same rules that other nongeneric types follow. The only difference is that it was born from a generic type. At the point of instantiation, the IL code behind the implementation of MyCollection<T> is JIT-compiled in a way that all of the usages of type T in the implementation of MyCollection<T> are replaced with type int.

Note that all unique constructed types created from the same generic type are, in fact, completely different types that share no implicit conversion capabilities. For example, MyCollection<long> is a completely different type than MyCollection<int>, and you cannot do something like the following:


// THIS WILL NOT WORK!!!
public void SomeMethod( MyCollection<int> intNumbers ) {
        MyCollection<long> longNumbers = intNumbers; // ERROR!
}

If you’re familiar with the array covariance rules that allow you to do the following, then you might be surprised that you cannot accomplish the same thing using constructed generic types:


public void ProcessStrings( string[] myStrings ) { object[] objs = myStrings;

foreach( object o in objs ) { Console.WriteLine( o ); } }

The difference is that with array covariance, the source and the destination of the assignment are of the same type, System.Array. The array covariance rules simply allow you to assign one array from another, as long as the declared type of the elements in the array is implicitly convertible at compile time. However, in the case of two constructed generic types, they are completely separate types.

Difference Between Generics and C++ Templates

It’s no accident that the syntax of generics is similar to that of C++ templates, when the syntax for every other element in C# is based on the corresponding C++ syntax. This approach allows you to leverage your existing knowledge. As is typical throughout C#, the designers have streamlined the syntax and removed some of the verbosity. However, the similarities end there, because C# generics behave very differently than C++ templates, and, if you come from the C++ world, you must make sure that you understand the differences. Otherwise, you may find yourself attempting to apply your C++ template knowledge in ways that simply won’t work with generics.

The main difference between the two is that expansion of generics is dynamic, whereas expansion of C++ templates is static. In other words, C++ templates are always expanded at compile time. Therefore, the C++ compiler must have access to all template types—generally through header files—and any types used to create the closed types from the template types at compile time. For this reason alone, it is impossible to package C++ templates into libraries. I know that many developers become confused by this fact when learning C++ templates for the first time. I remember plenty of times when it would have been nice to be able to package a C++ template into a static library or a DLL. Unfortunately, that is not possible. That’s why all of the code for C++ template types usually lives in headers. This makes it difficult to package proprietary library code within C++ templates, since you must essentially give your code away to anyone who needs to consume it. The STL is a perfect example: Notice how almost every bit of your favorite STL implementation exists in header files.

Generics, on the other hand, can be packaged in assemblies and consumed later. Instead of being formed at compile time, constructed types are formed at run time, or more specifically, at JITcompile time. In many ways, this makes generics more flexible. However, as with just about anything in the engineering world, advantages come with disadvantages. You must treat generics significantly differently at design time than C++ templates, as you’ll see at the end of this chapter.

Note: Each time the JIT compiler forms a closed type, a new type is initialized for the application domain that uses it. Naturally, this places a demand on the memory consumption of the application, also known as the working set. Once a type is initialized and loaded into an application domain, you cannot uninitialize and unload it without destroying the application domain as well. Under some rare circumstances, you may need to consider these ramifications when designing systems that use generics. In general, though, such concerns are typically minimal. If your generic type declares a lot of static fields, creating many closed types from it could place pressure on memory, since each closed type gets its own copy of those static fields. Additionally, if those closed types are used in multiple application domains, there will be a copy of that static data for each application domain the type is loaded into.


This excerpt was from Accelerated C# 2008 by Trey Nash.

Founders at Work



Add your comments

Please keep your comments relevant to this blog entry: inappropriate or purely promotional comments may be removed. To add hyperlink, please follow this example: "your link text":http://your.link.url