15

If I have a generic interface with a struct constraint like this:

public interface IStruct<T> where T : struct { }

I can supply an enumeration as my type T like so, because an enum satisfies a struct constraint:

public class EnumIsAStruct : IStruct<DateTimeKind> { }

C# 7.3 added an Enum constraint. The following code, which was previously illegal, now compiles:

public class MCVE<T> : IStruct<T> where T : struct, Enum { }

However, to my surprise, the following fails to compile:

public class MCVE<T> : IStruct<T> where T : Enum { }

...with the error

CS0453 The type 'T' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'IStruct'

Why is this? I would expect a generic type constrained by Enum to be usable as a type argument where the type is constrained by struct but this doesn't seem to be the case - I am having to change my Enum constraint to struct, Enum. Is my expectation wrong?

Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
  • That's not really surprising since you have to repeat constraints when inheriting. Though the tidbit about the new `Enum` constraint in your answer is interesting. – juharr May 11 '18 at 14:06
  • 3
    Definitely one of the reasons it was omitted from C# before. The CLR does distinguish between System.Enum and a concrete enum type as the actual type argument (Ecma-355, chapter II.10.1.7). That matters because System.Enum is a reference type and not a value type, values have to be boxed. You are probably not interested in actually use System.Enum so nail it shut by adding the struct contraint. – Hans Passant May 11 '18 at 14:30
  • 1
    What an absolutely useless feature. You can constrain the type to `Enum`, but you can't do anything with it! You can't cast it to 'int', you can't combine flags with `|`. These all give errors: `TEnum val; var i = (int)val; //ERROR: "Cannot conver type 'TEnum' to 'int'"` or `TEnum a, b; var c = a | b; //ERROR: "Operator '|' cannot be applied to type 'TEnum' and 'TEnum'."` Useless. – Triynko May 15 '18 at 22:55

1 Answers1

17

This issue is strange (arguably), but expected, behavior.

The class System.Enum itself could be supplied as the type of T. Being a class, System.Enum is of course not a struct!

public class MCVE<T> where T : Enum { }
public class MCVE2 : MCVE<Enum> { }

As explained by contributor HaloFour:

This is an odd behavior by the CLR itself. System.Enum is a class, but every type that derives from System.Enum is a struct. So a constraint on System.Enum by itself doesn't imply struct since you could pass System.Enum as the generic type argument...

It is weird, but it was easier to simply remove the imposed limitation on the compiler than to argue over different syntax for "enum" constraints that might have different behavior.

The solution is to make it your standard practice to constrain to struct, Enum when you wish to constrain concrete types to being any specific enumeration. If additionally you wish to accept the class System.Enum as your generic type, only then would you constrain to Enum.

Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
  • 6
    I'm still wondering why the `enum` keyword wasn't used instead, to avoid precisely this situation. – Arturo Torres Sánchez May 15 '18 at 18:06
  • 2
    Can't even use it like an enum. Can't cast to (int) and can't combine values with '|'. Only way to work with the TEnum generic value is to use Convert.ToInt32 and work with integers, then cast back either using `(TEnum)(object)tenumValue` or `(TEnum)Enum.ToObject(enumType,tenumValue)`, and ToObject is a terrible name (should have been FromValue) since it's documented to create an Enum object from a plain integer value. Funny thing is, this workaround works even without the use of the Enum type constraint feature, so it really is completely useless! – Triynko May 15 '18 at 23:18