- Reference types(class):
- This type of constraint restricts the type argument to be a reference type. For example classes, interfaces, delegates or arrays or any other type known to be a reference type.
- Value types(struct):
- This type of constraint restricts the type argument to be a value type. For example structs, data types, enums. This excludes the use of nullable types.
- Parameterless public constructor types(new):
- This type constraint restricts the type argument to have a parameterless public constructor. Notice that this excludes sealed, static and classes without an explicit parameterless constructor. Value types are said to be okay since they all have a default public parameterless constructor. This constraint to allow the generic type to make new instances of the type.
- Conversion types(<interface, base class>):
- This type constraint restricts the type argument to be convertible to the types specified. The type specified could be an interface or a base class. There could be more than one interface but only one class naturally since not class can inherit from more than one class anyway and specifying classes in a single inheritance hierarchy is redundant.
In order to apply any of these constraints to a generic declaration, the "where" keyword should be used after the type definition:
class GenericType<T> where T : <constrain type>
{
}
A mixture of these constraints may be applied. Of course some mixtures are not valid as in "where T : class, struct".
Declaring your own generic types
Now we'll move to declaring of custom generic types. In this section we would try to create a custom list class which is capable of comparing it's elements for equality. Why we do we need to implement the equality operator you might ask. Why can't we just use == or != on the type ? Well, the reason is that the type may not have overloaded that operation. You may say what about the scenario where we have constrained the type to be of something specific which has actually overloaded those operators ? Well let's look at the answer exhaustively :
- Reference type constraint: With this type of constraints the only assumption being made is that the value of the two types at any point are references to objects on the heap. So the only possible and "correct" action is to compare the references. So == and != are basically going to compare the references. This may seem counter intuitive if you don't know how overloading is resolved in C# since you may think that why wouldn't we use == and != for reference types that already overload this operator ? Let's we pass in a string which is a reference type and already overloads the operators. Then can't the JIT just use == and != ?. To understand function overloading resolutions in .Net, I suggest reading this article by Jon Skeet. Basically the C# compiler always does function overloading resolutions at compile time. This decision is made to prevent a set of problems like the "brittle base class" problem blogged extensively by Eric Lippert(link). The brittle base class problem happens due to the use of forwarding rather than delegation when a subclass calls a base class's method.
- Value type constraint: When the type is constrained by this constraint type, the use of == and != are prohibited ? Why ? because value types include structs which may or may not overload == and !=. Again, we can't wait until runtime to see which value type is being passed so any use of these operators are prohibited.
- Conversion type constraint: This is where we can guarantee that the type argument will always either overload the operators directly or be a subclass of a class that has overloaded them. The compiler checks to see if this is true and if it is, the operators may be used.
Okay now after this rather long digression, let's get to the point. We want to create a custom list class. This class should be able to allow comparison between the elements. In order to do this we are going to constrain our type argument using a conversion type constraint as we see below:
public class CustomList<T> : IEquatable<CustomList<T>> where T : IEqualityComparer<T>
{
private T[] list;
private static IEqualityComparer<T> comparer = EqualityComparer<T>.Default;
public int ListSize {get; set;}
public T this[int i ]
{
// for brevity we'll ignore the check for a valid index.
get { return list[i]; }
}
public void Add(T newItem)
{
T[] temp;
if (ListSize == 0)
list = new T[2];
else if(ListSize == list.Length)
{
temp = list;
list = new T[ListSize * 2];
Array.Copy(temp, list, ListSize);
}
list[ListSize++] = newItem;
}
public bool Equals(CustomList<T> other)
{
int size = Math.Min(this.ListSize, other.ListSize);
for (int i = 0; i < size; i++)
if (!this[i].Equals(other[i]))
return false;
return true;
}
}
Okay. So let's see what we have done here. We have a class that wraps an array. Also, this class being a generic, can hold any type of element. There is one constraint on the elements though ! The elements have to implement the IEquatable<T> generic interface. This interface forces the class to implement a strongly typed equals(T) method. We did this because we wanted to make sure that the statement at line 34 is a strong typed comparison rather than a comparison of references.
This concludes my post on intermediate generics. From my next post I'm going to move into advanced generics and we're going to see how JIT handles generics.
No comments:
Post a Comment