Generics are a way to declare types, interfaces, delegates, and methods in a type-agnostic way. For example, IComparable<T>
defines an interface that specifies a comparison between objects of type T
. The T
is defined by you where needed.
One of the most common ways to use generics is in collection classes. Before generics, the ArrayList was commonly used to store a dynamic array of objects. ArrayList stored references to objects, so you had to always cast the items to the right type when pulling them out. Not so with generics. This topic deserves a chapter all its own (see Chapter 10, “Collections”), but is introduced here in the first section since it’s by far the most common scenario.
The rest of this chapter shows the various ways to use generics, not specifically in collections.
Solution: Use List<T>
to create a list of objects of type T, as in this example:
You should always default to using the generic collections rather than the older ArrayList-style collections from early versions of .NET.
See Chapter 10 for more information on generic collections of all types in .NET.
Solution: Generics are specified with angle brackets that include the type name (usually T). T can then be used wherever a real type name is used, as in the following example:
The following output is produced:
Solution: Generics are commonly used on interfaces where the methods are defined with respect to a specific type, as in the following example:
Solution: This is similar to the declaration for interfaces. As an example, consider this indexing class with a (very) naive implementation for brevity:
Solution: Delegates are like type-safe function pointers, and those types can also be generic.
Solution: Separate the types by commas within the brackets, as in this example:
Type parameters are sometimes labeled starting with T and using the alphabet from there on (there should not usually be more than a few type parameter arguments). However, you can also label them according to purpose. For example, Dictionary<TKey, TValue>
.
Solution: By adding certain constraints to the type, you are allowed to do more with it since the compiler can infer addition information. For example, by telling C# that T
must be a reference type, you can assign null
to an instance of type T
.
To add a constraint, you add a where
clause after the declaration, as in the examples in the following sections.
The class keyword
is used to denote a reference type, as in this example:
Sometimes, you’ll want to restrict interfaces or collections—for example, to value types only. You can do this with the struct
keyword, as in this example:
Note that even though the syntax says struct
, any value type, including the built-in ones, is allowed.
You can constrain to any type, whether it’s an interface, base class, or a class itself. Anything that is that type or derived from that type will be valid. By doing this, the compiler allows you to call methods defined on the specified type. Here’s an example:
If you need to create a new object of the unknown, generic type, you must tell the compiler that the type is restricted to those with a default (parameterless) constructor. You do this with a special usage of the new
keyword, as shown here:
Creation of generic types can be problematic in some cases. You may find yourself needing to use factory methods to create the type instead of new
:
You can separate multiple constraints with commas. Note that, if present, new()
must appear last in the list.
You can apply constraints to as many type arguments as you desire.
IEnumerable<string>
to IEnumerable<object>
(Covariance)Solution: In .NET 4, covariance and contravariance are supported on interfaces and delegates, so the preceding code will work as-is. This is known as covariance. It only works because IEnumerable
is declared as IEnumerable<out T>
, which means that you cannot give T
as an input to any method on IEnumerable
. This rule implies that any output of this collection can also be treated as a collection of the parent class of T
, which makes sense since iterating over IRectangle
is equivalent to iterating over IShape
.
On the other hand, imagine if we had another type of shape, ICircle, which is also derived from
IShape. The collection should not be treated as a collection of IShape
for the purposes of insertion, because this would imply you could also add an ICircle
, which is not the case, since it’s really a collection of IRectangle objects
. That’s why T
is declared as an out parameter: It only works when reading values out of the collection, not putting them in, and it explains why covariance can work in the preceding code example.
IComparer<Child>
to IComparer<Parent>
(Contravariance)Solution: In .NET 4, IComparer<T> has been changed to IComparer<in T>, which means that objects of type T are used only as input parameters. Therefore, an object implementing this interface can be assigned to interfaces of a more derived type. This is called contravariance. The preceding code sample will now work.
For more on contravariance and to see how it applies to delegates, see Chapter 15.
Solution: Rather than creating your own class to handle tuples on a case-by-case basis, use the Tuple class:
var name = Tuple.Create("Ben", "Michael", "Watson");
string firstName = name.Item1;
Tuple values can be different types:
There are Create
methods from 1 to 8 arguments (singleton to octuple).