This week I was writing some code that had to respond differently depending on whether a generic argument was a reference type or a value type. This was bit complex, since “reference type” and “value type” do not have very specific meanings - which was actually a good thing because it forced me to consider exactly what my code requirements were, and it turned out that I was using just slightly different meanings of “reference type” in different places.
During this exploration, I developed a few tests to evaluate the type system (code at the end of this post). The results are summarized below, along with some of my thoughts on the weirdos (types which are sort-of reference and sort-of value, depending on your definition of “reference” and “value”).
One way of defining a “reference type” is whether Type.IsClass is true; another way of defining a “reference type” is whether it satisfies a generic class constraint (e.g., void Test<T>() where T : class). Likewise, value types have Type.IsValueType and generic struct constraints.
The table below includes tests on a variety of types, grouped into “mostly reference types” and “mostly value types”. The types that are more clearly reference/value types are at the top of each group, with the weirdos at the bottom.
Category
Example Type
IsClass
Satisfies class Constraint
IsValueType
Satisfies struct Constraint
Satisfies Without Constraints
Classes
class Class {}
Arrays
int[]
Delegates
delegate void DelegateT();
Interfaces
interface Interface {}
Pointers
int*
Value types
int
Enumerations
enum EnumT {}
Nullable value types
int?
Void
void
Interfaces are a Bit Weird
Interfaces return false for both IsClass and IsValueType. This makes sense, since either reference types or value types may inherit from an interface. However, interface variables may be declared and act like reference types (boxing value types as necessary), so interfaces do satisfy generic class constraints.
Take-home point: If IsClass is false but IsInterface is true, the type will still satisfy a generic class constraint.
Nullable Value Types are a Bit Weird
Nullable types return true for IsValueType, but do not satisfy generic struct constraints (nor class constraints). They can only be used as generic parameters without class or struct constraints.
Take-home point: Nullable types (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) will not satisfy a generic struct constraint, even though IsValueType is true.
Pointers are Definitely Weird
To be honest, I don’t know why IsClass is true for pointers (the spec says they are, but without any reason given). They act exactly like value types, and they can’t satisfy a generic class constraint. In fact, they can’t be used as any kind of generic argument. This makes pointer types a corner case: they only have to be dealt with if the user is passing a Type instance rather than a generic type argument.
Take-home point: If IsPointer is true, then the type cannot be used as a generic type parameter at all (and therefore cannot satisfy a class constraint, even though IsClass is true).
Void is Definitely Weird
Void claims to be a value type (IsValueType is true) - which sort of makes sense, if we think of it as a value type that cannot have a value - but it cannot satisfy a struct constraint. In fact, like pointers, void cannot be used as a generic type argument at all. This makes void another corner case: they only have to be dealt with if the user is passing a Type instance rather than a generic type argument.
Take-home point: The void type (type == typeof(void)) cannot be used as a generic type parameter at all (and therefore cannot satisfy a struct constraint, even though IsValueType is true).
Test Code
About Stephen Cleary
Stephen Cleary is a Christian, husband, father, and programmer living in Northern Michigan.