Overview
Generics are not a completely new construct;
similar concepts exist with other languages. For example, C++
templates can be compared to generics. However, there’s a big
difference between C++ templates and .NET generics. With C++
templates the source code of the template is required when a
template is instantiated with a specific type. Contrary to C++
templates, generics are not only a construct of the C# language;
generics are defined with the CLR. This makes it possible to
instantiate generics with a specific type in Visual Basic even
though the generic class was defined with C#.
The following sections look into the advantages and
disadvantages of generics, particularly:
-
Performance
-
Type safety
-
Binary code reuse
-
Code bloat
-
Naming guidelines
Performance
One of the big advantages of generics is
performance. In Chapter 10, you will see non-generic and generic
collection classes from the namespaces System.Collections and System.Collections .Generic. Using value types with non-generic
collection classes result in boxing and unboxing when the value
type is converted to a reference type and vice versa.
|
|
Tip |
Boxing and unboxing is discussed in Chapter
6, “Operators and Casts.” Here is just a short
refresher about these terms.
Value types are stored on the stack. Reference
types are stored on the heap. C# classes are reference type;
structs are value types. .NET makes it easy to convert value types
to reference types, and so you can use a value type everywhere an
object (which is a reference type) is needed. For example, an
int can be assigned to an object. The
conversion from a value type to a reference type is known as
boxing. Boxing happens automatically if a method requires an object
as a parameter, and a value type is passed. On the other side, a
boxed value type can be converted to a value type by using
unboxing. With unboxing, the cast operator is required.
|
The following example shows the ArrayList class from the namespace System.Collections. ArrayList stores objects, the Add() method is defined to require an object as a
parameter, and so an integer type is boxed. When the values from an
ArrayList are read, unboxing occurs when
the object is converted to an integer type. This may be obvious
with the cast operator that is used to assign the first element of
the ArrayList collection to the variable
i1, but also happens inside the
foreach statement where the variable
i2 of type int is accessed:
Boxing and unboxing are easy to use, but have a big
performance impact, especially when iterating through many
items.
Instead of using objects, the List<T> class from the namespace System.Collections.Generic allows you to define the
type when it is used. In the example here, the generic type of the
List<T> class is defined as
int, so the int type is used inside the class that is generated
dynamically from the JIT compiler. Boxing and unboxing no longer
happens:
Type Safety
Another feature of generics is type safety.
As with the ArrayList class, if objects
are used, any type can be added to this collection. This example
shows adding an integer, a string, and an object of type
MyClass to the collection of type
ArrayList:
Now if this collection is iterated using the
following foreach statement, which
iterates using integer elements, the compiler accepts this code.
However, because not all elements in the collection can be cast to
an int, a runtime exception will
occur:
Errors should be detected as early as possible.
With the generic class List<T>,
the generic type T defines what types
are allowed. With a definition of List<int>, only integer types can be added to
the collection. The compiler doesn’t compile this code because the
Add() method has invalid arguments:
Binary Code Reuse
Generics allow better binary code reuse. A
generic class can be defined once and can be instantiated with many
different types. Unlike C++ templates, it is not necessary to
access the source code.
As an example, here the List<T> class from the namespace System.Collections.Generic is instantiated with an
int, a string, and a MyClass
type:
Generic types can be defined in one language
and used from any other .NET language.
Code Bloat
How much code is created with generics when
instantiating them with different specific types?
Because a generic class definition goes into
the assembly, instantiating generic classes with specific types
doesn’t duplicate these classes in the IL code. However, when the
generic classes are compiled by the JIT compiler to native code, a
new class for every specific value type is created. Reference types
share all the same implementation of the same native class. This is
because with reference types only a 4-byte memory address (with
32-bit systems) is needed within the generic instantiated class to
reference a reference type. Value types are contained within the
memory of the generic instantiated class, and because every value
type can have different memory requirements, a new class for every
value type is instantiated.
Naming Guidelines
If generics are used in the program, it helps
when generic types can be distinguished from nongeneric types. Here
are naming guidelines for generic types:
-
Generic type names should be prefixed with
the letter T.
-
If the generic type can be replaced by any
class because there’s no special requirement, and only one generic
type is used, the character T is good as a generic type name:
-
If there’s a special requirement for a
generic type (for example, it must implement an interface or derive
from a base class), or if two or more generic types are used,
descriptive names should be used for the type names: