In the past, if we wanted to create a custom List of, say, Book instances, you had to create a dedicated BookList class:
publicclassBookList{publicvoidAdd(Bookbook){}}
One way around this was to create a more reusable ObjectList, which would box value types or cast classes that you use.
publicclassObjectList{publicvoidAdd(objectobj){}}
However, this has performance costs due to boxing and casting!
So, generics were created to solve these problems! With generics, you create a class once and defer the typing until runtime.
// T is short for typepublicclassGenericList<T>{publicvoidAdd(Tvalue){}}varbooks=newGenericList<Book>();
For multiple types, a good practice is to name the types:
Pro tip: In most cases, you will find yourself using generics that are part of .NET. It's very, very rare that you'll ever need to create your own generics.
In .NET, all the generics can be found in System.Collection.Generic.XXXXX.
Applying Constraints to Accepted Types
It's valuable to be able to constrain the accepted types that can be passed in as T for two reasons:
By default, a user could pass in anyT type they want, which could be too wild.
If you don't constrain the accepted types, C# will assume T is an object, so C# will error out when you try to perform invalid operations or invoke invalid methods that objects don't have.
Suppose you're creating a generic class with a Max method that finds the max between 2 inputs:
The operation a > b won't compile because C# will consider that an invalid operation. Additionally, what happens if the user passes in a string? We wouldn't want that.
So, one solution to this problem is to constrainT to an interface, setting a requirement that the type matches a specific contract.
In our case, we can constrain T to an IComparable, giving us access to the CompareTo method:
Note: You're not limited to interfaces as constraints though. Here's some more.
public class GenericDictionary<TKey, TValue>
{
public void Add(Tkey key, TValue, value)
{
}
}
public class Utilities<T>
{
public T Max(T a, T b)
{
return a > b ? a : b;
}
}
public class Utilities<T> where T : IComparable
{
public T Max(T a, T b)
{
return a.CompareTo(b) > 0 ? a : b;
}
}
// where T : Product
// = T is the Product class or one of its subclasses
// where T : struct
// = T is a value type
// where T : class
// = T is a reference type
// where T : new()
// = T is any object with a default parameter-less constructor
public class DiscountCalculator<TProduct> where TProduct : Product
{
public float CalculateDiscount(TProduct product)
{
return product.Price * product.Discount; // these properties come from Product
}
}
// This class allows a value type to be nullable (they aren't by default)
public class Nullable<T> where T : struct
{
private object _value;
public Nullable {}
public Nullable(T value)
{
_value = value;
}
public bool HasValue
{
get { return _value != null; }
}
public T GetValueOrDefault()
{
if (HasValue)
return _value;
return default(T);
}
}