10

This question already has an answer here:

Update: See the bottom of this question for a C# workaround.

Hi there,

Consider the following extension method:

public static bool HasFlags<T>(this T value, T flags)
    where T : System.Enum
{
    // ...
}

This will, as you may know, throw an error at compile-time, since a class is not normally allowed to inherit from System.Enum. The problem is that any enumeration specified using the enum keyword does in fact inherit from System.Enum, so the above code would be the ideal way to limit an extension method to enumerations only.

Now the obvious work-around here is to use Enum instead of T, but then you lose the benefits of generic types:

MyEnum e;
e.HasFlags(MyOtherEnum.DoFunkyStuff);

The above code would throw a compile-time error using generic types, while it can only throw a runtime error using the Enum type (if I implement it to do so.)

Are there any compiler options that can be used to turn off the constraint check, or is there some other nifty way to do this?

Before it is suggested, I would like to say that I will not be using where T : struct or some such, since then you'd be able to do weird stuff like 123.HasFlags(456).

I'm stumped as to why this error exists at all... It's the same problem you'd get using where T : System.Object, but for that you have where T : class... Why is there no where T : enum?

C# workaround

Jon Skeet has started work on a library that compiles classes with a constraint to an IEnumConstraint, which is then replaced with System.Enum post-build. This is, I believe, the closest one can get to working around this issue at this time.

See:

If this workaround is unfeasible, you will have to write your library as C++/CLI code, which does not limit what can be used for generic type constraints (see the code in my answer below.)


  • Maybe didn't feel such a constraint was needed in all but very rare cases. Who knows... I would agree with them if that was the case :) - Skurmedel
  • @Skurmedel: What's odd is that it's specifically prohibited... in other words the spec has been made more complicated to restrict it. I don't see why. Maybe Eric Lippert will explain :) - Jon Skeet
  • Yes I agree with Jon Skeet, although it could just be that it was overlooked (they used the same inheritance check for constraints as they do for class inheritance)... But I'm really not familiar with the inner workings of C#. - Blixt
  • Eric Lippert previously posted about Enum Type Constraints in C#: stackoverflow.com/questions/1331739/enum-type-constraints-in-c/… - Randy Levy
  • I see. Can I take it from his answer that even if there had been an option to turn off the compiler error, the code would still not work? Unless that is the case, I don't see why this limitation is forced on me (it would be nice to be able to explicitly turn off the CS0702 error.) - Blixt

5 답변


10

EDIT: A library is now available supporting this via ildasm/ilasm: UnconstrainedMelody.


Members of the C# team have previously said they'd like to be able to support where T : Enum and where T : Delegate, but that it's never been a high enough priority. (I'm not sure what the reasoning is for having the restriction in the first place, admittedly...)

The most practical workaround in C# is:

public static bool HasFlags<T>(this T value, T flags) where T : struct
{
    if (!(value is Enum))
    {
        throw new ArgumentException();
    }
    // ...
}

That loses compile-time checking for the "enum-ness" but keeps the check that you're using the same type in both places. It has the execution-time penalty of the check as well, of course. You can avoid that execution-time penalty after the first call by using a generic nested type for the implementation which throws the exception in a static constructor:

public static bool HasFlags<T>(this T value, T flags) where T : struct
{
    if (!(value is Enum))
    {
        throw new ArgumentException();
    }
    return EnumHelper<T>.HasFlags(value, flags);
}

private class EnumHelper<T> where T : struct
{
    static EnumHelper()
    {
        if (!typeof(Enum).IsAssignableFrom(typeof(T))
        {
            throw new InvalidOperationException(); // Or something similar
        }
    }

    internal static HasFlags(T value, T flags)
    {
        ...
    }
}

As Greco mentions, you could write the method in C++/CLI and then reference the class library from C# as another option.


  • I think it's important to keep extension methods limited in scope and therefore I will not be using where T : struct due to the reasons mentioned in my question (123.HasFlags(456) would compile fine.) I do like Greco's solution, but I can't justify creating a whole library for one method. Is it possible to set up a process to "inject" the C++/CLI into a C# library when compiling? - Blixt
  • @Blixt: Well, you could look into binary rewriting, but it's probably going to be ugly. I wonder whether there'd be interest in an open source project for a bunch of "useful things one can do with delegates and enums"... - Jon Skeet
  • Blog post around the library idea... msmvps.com/blogs/jon_skeet/archive/2009/09/10/… - Jon Skeet
  • You could use ILMerge (research.microsoft.com/en-us/people/mbarnett/ilmerge.aspx) to include what would be a fairly vestigial C++/CLI assemly into your main code. - Steve Gilham
  • @Yngvar: Well, it's now codeblog.jonskeet.uk, so: codeblog.jonskeet.uk/2009/09/10/… - Jon Skeet

7

There is a work-around to that

See here


  • While the workaround works, it's not "in C# 3.0". - Lasse Vågsæther Karlsen
  • Hmm... Since it's only one extension method I wouldn't want to create a new C++ project for it. I doubt it is possible to set up the environment to compile C++/CLI into a C# library? - Blixt
  • That's a nice work-around. I am wondering why this is not wiedly known after more than 4 years... - Daniel Brückner
  • I implemented the workaround and it works perfectly! Sadly I don't see myself using it unless I make the whole utility library into C++ code... Anyways, see my answer to this question for the code. - Blixt

3

Actually, it is possible, with an ugly trick. However, it cannot be used for extension methods.

public abstract class Enums<Temp> where Temp : class {
    public static TEnum Parse<TEnum>(string name) where TEnum : struct, Temp {
        return (TEnum)Enum.Parse(typeof(TEnum), name); 
    }
}
public abstract class Enums : Enums<Enum> { }

Enums.Parse<DateTimeKind>("Local")

If you want to, you can give Enums<Temp> a private constructor and a public nested abstract inherited class with Temp as Enum, to prevent inherited versions for non-enums.



2

I couldn't resist having a go at the C++ work-around, and since I got it to work I figured I'd share it with the rest of ya!

Here's the C++ code (my C++ is very rusty so please point out any errors, in particular how the arguments are defined):

#include "stdafx.h"

using namespace System;
using namespace System::Runtime::CompilerServices;

namespace Blixt
{
namespace Utilities
{
    [Extension]
    public ref class EnumUtility abstract sealed
    {
    public:
        generic <typename T> where T : value class, Enum
        [Extension]
        static bool HasFlags(T value, T flags)
        {
            __int64 mask = Convert::ToInt64(flags);
            return (Convert::ToInt64(value) & mask) == mask;
        }
    };
}
}

And the C# code for testing (console application):

using System;
using Blixt.Utilities;

namespace Blixt.Playground
{
    [Flags]
    public enum Colors : byte
    {
        Black = 0,
        Red = 1,
        Green = 2,
        Blue = 4
    }

    [Flags]
    public enum Tastes : byte
    {
        Nothing = 0,
        Sour = 1,
        Sweet = 2,
        Bitter = 4,
        Salty = 8
    }

    class Program
    {
        static void Main(string[] args)
        {
            Colors c = Colors.Blue | Colors.Red;
            Console.WriteLine("Green and blue? {0}", c.HasFlags(Colors.Green | Colors.Red));
            Console.WriteLine("Blue?           {0}", c.HasFlags(Colors.Blue));
            Console.WriteLine("Green?          {0}", c.HasFlags(Colors.Green));
            Console.WriteLine("Red and blue?   {0}", c.HasFlags(Colors.Red | Colors.Blue));

            // Compilation error:
            //Console.WriteLine("Sour?           {0}", c.HasFlags(Tastes.Sour));

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey(true);
        }
    }
}


1

You can achieve this using IL Weaving and ExtraConstraints

Allows you to write this code

public static bool HasFlags<[EnumConstraint] T>(this T value, T flags)
{
    // ...
} 

What gets compiled

public static bool HasFlags<T>(this T value, T flags)
    where T : System.Enum
{
    // ...
} 

Linked


Related

Latest