views:

513

answers:

4

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.)

+6  A: 

There is a work-around to that

See here

Greco
While the workaround works, it's not "in C# 3.0".
Lasse V. 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
+7  A: 

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.

Jon Skeet
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
Okay, I would only do it if there was an "easy" way to do it `=)` And yes, judging from the discussions I've found on Google, this problem isn't all that rare, and I would definitely appreciate such a library.
Blixt
Okay, will consider it. I'd have to find competent C++/CLI developers though... my C++ is awful :) Another alternative might be to write the code in C# with special markers, and do binary rewriting on the library afterwards to change the constraints. Hmm.
Jon Skeet
Nice `=)` Yeah, once upon a time I was starting to get the hang of C++, then along came languages like Python and C# and spoiled me!
Blixt
Have a look at my answer to this question for C++/CLI code that implements the `HasFlags` functionality I was looking for.
Blixt
Blog post around the library idea... http://msmvps.com/blogs/jon_skeet/archive/2009/09/10/generic-constraints-for-enums-and-delegates.aspx
Jon Skeet
You could use ILMerge (http://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
Interesting! Thanks for the link `=)`
Blixt
I'm accepting this answer as it leads to the only viable C# solution I've seen. Greco's answer is still a very nice solution, though.
Blixt
@Blixy: Have edited the library link into the answer...
Jon Skeet
+1  A: 

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);
     }
    }
}
Blixt
+1  A: 

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.

SLaks