Generics and primary constraints
In the previous post, we’ve started looking at constraints. Primary constraints are one type of constraints. Currently, you can specify zero or one primary constraint when you introduce a generic type argument. Whenever you restrain a generic type argument to a non-sealed class, you’re using a type constraint. Here’s an example:
When the compiler finds a primary constraint of this type, it knows that T can only be replaced by Test or from another type derived from Test. In the previous example, this means that all public members introduced by Test can be used from within the members of TestingConstraints. Btw, you can’t use a sealed type as a primary constraint. And it’s kind of logical, if you think about it. Lets suppose S is sealed. If S is sealed, then you can’t extend it (ie, you cannot create new derived types from it). That being the case, and if primary constraints could reference sealed types, then you’d be building a generic type whose type argument could only be replaced by *one* specific type. Well, we don’t really need generics to do that, right? I mean, the final result would be the same as creating a non-generic type which would reference directly the sealed type S! In other words, the generic type would end up being non-generic and that just doesn’t make sense!
Primary constraints are mandatory when introducing a generic argument. If you don’t specify one, then Object is used by default. Notice that the following types cannot be used as a primary constraint: Object, Array, Delegate, MulticastDelegate, ValuType, Enum and Void.
Besides referencing a non-sealed type, there are also two other special primary constraints: class and struct. When the compiler finds the class constraint, it knows that the generic type argument will only be replaced by a reference type. In practice, this means that the generic type argument can be replaced by any class, interface, delegate or array type. On the other hand, when the compiler finds the struct constraint, it understands that the generic type argument will be replaced by a value type. In this case, the generic type argument can be replaced by any value or enum type.
There are some important assumptions the compiler can make from these special primary constraints. Here’s a small example:
The previous code compiles without an error. Whenever we constrain a generic type argument to a struct, then the compiler knows that it newing up T is ok because all value types have a default public constructor (and that might not happen with reference types). If you concentrate on GenericReferenceType, then you’ll notice that aux is initialized with the null value. You can only attribute null to reference type variables. Since you’ve constrained T to class, then the compiler knows that this assignment is doable and won’t complain about it (try removing the constraint and see what happens – there is a way to go around it, but I’ll leave it for a future post).
Notice that there’s a small gotcha regarding primary constraints associated with the struct keyword: you cannot replace the generic type argument with the Nullable<T> type. Nullable<T> is a value type (ie, struct), but it gets special treatment from the compiler and the CLR. It’s because of that you cannot replace T with Nullable<T> on GenericValueType<T>. Nullable<T> are an interesting topic and we’ll return to them in future posts.
And I guess this sums it up for primary constraints. In the next post, we’ll go over secondary constraints. Stay tuned for more.