csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Champion "Allow `System.Enum` as a constraint" (15.7)

Open gafter opened this issue 8 years ago • 136 comments

Allows where T : System.Enum

  • [ ] Proposal added
  • [x] Discussed in LDM
  • [ ] Decision in LDM
  • [ ] Finalized (done, rejected, inactive)
  • [ ] Spec'ed

See also https://github.com/dotnet/roslyn/issues/262

gafter avatar Feb 14 '17 23:02 gafter

I'm of the opinion that Enum was not allowed as a generic constraint initially in C# due to generic code-explosion as the current .NET runtimes don't share generic code for value types like they do for reference types. With the implementation of corefx#15453 this would be less of a problem as it should cover most use cases for an Enum generic type constraint and internally it would try to reduce this generic code-explosion by limiting usage of Enum as a generic type parameter as much as possible. I'm wondering if something could be done in the runtime to allow types and methods with generic enum type arguments to share code when the enum type shares the same underlying type.

Perhaps another reason Enum was not allowed as a generic constraint initially in C# was due to the potential expectation of users that the bitwise operators would be available on those generic types for flag operations. Implementing these bitwise operators were discussed in roslyn#262 and even before that on Roslyn's CodePlex site and the consensus was that there were issues when trying to implement them in IL due to differing underlying types. This expectation would be mitigated by the implementation of the flag enum operations provided in corefx#15453.

I may be mistaken but I believe removing the explicit filters in the compiler preventing Enum as a generic constraint would allow it to just work as expected as any other class type constraint.

I would love to see this implemented as soon as possible to enable the benefits corefx#15453 would provide. I'd gladly contribute in anyway needed.

TylerBrinkley avatar Feb 16 '17 15:02 TylerBrinkley

Generic constraints make sense with enum inheritance. Example from the future:

public enum WindowMessage
{
    WM_NULL = 0,
}

public enum ListViewMessage : WindowMessage
{
    LVM_FIRST = 0x1000,
    LVM_EDITLABELW = (LVM_FIRST + 118),
}

public static IntPtr SendMessage<TMessageType>(
    HWND hwnd,
    TMessageType Msg = WindowMessage.WM_NULL,
    UIntPtr wParam = default(UIntPtr),
    IntPtr lParam = default(IntPtr))
    where TMessageType : WindowMessage
{
    // [....]
}

temporaryfile avatar Mar 24 '17 21:03 temporaryfile

@gafter & @AnthonyDGreen is there anything myself or the community can do to make this a reality? I'd really like to move forward with corefx#15453 but this seems to be the blocker.

I believe the only code changes needed would be a few line removals in roslyn/Binder_Constraints.cs.

TylerBrinkley avatar Aug 16 '17 18:08 TylerBrinkley

@TylerBrinkley I don't see what aspect of the proposed API would benefit from this.

gafter avatar Aug 16 '17 20:08 gafter

@gafter There are two issues with implementing corefx#15453 now without the generic Enum constraint.

  1. Adding the Enum constraint later to enable extension methods would be a breaking change and may not be acceptable. In the proposal I've added the constraint to the existing generic versions of Enum.Parse and Enum.TryParse but that would be a breaking change as noted in this comment. If we can't add the Enum constraint to those two methods it wouldn't be a big deal as all it may mean is an additional type check but for the other methods proposed it would be detrimental as they couldn't be made into extension methods.
  2. Even if adding the Enum constraint later is deemed an acceptable breaking change the FlagEnum methods cannot later be promoted to extension methods due to #665.

TylerBrinkley avatar Aug 17 '17 12:08 TylerBrinkley

@gafter or @AnthonyDGreen again I ask, is there anything myself or the community can do to make this a reality?

TylerBrinkley avatar Sep 08 '17 15:09 TylerBrinkley

Forgive me but I'm going to continue to spam this request until I get a response.

@gafter or @AnthonyDGreen is there anything myself or the community can do to make this a reality?

TylerBrinkley avatar Sep 22 '17 00:09 TylerBrinkley

Ditto.

jnm2 avatar Sep 22 '17 00:09 jnm2

@MadsTorgersen I'm sure you get endless GitHub mentions so I apologize upfront but I was hoping you could review this. We've been trying to get a response from @gafter or @AnthonyDGreen for awhile now on how to proceed with this request but without success.

I'm the author of the OSS library Enums.NET and have created a feature request in the corefx repo to include many of the enhancements my library provides directly to .NET Core. The one thing holding that request back is this missing C# language feature. Is there anything myself or the community can do to push this along? Thank you for your time.

TylerBrinkley avatar Oct 04 '17 20:10 TylerBrinkley

I snuck this into the 7.x milestone to force the LDM to triage this next week.

gafter avatar Oct 04 '17 22:10 gafter

Thank you so much @gafter!

TylerBrinkley avatar Oct 04 '17 23:10 TylerBrinkley

@TylerBrinkley How about if you limit the enum scope by specifying the underlying type also ? for example

 where T : enum, int
or
 where T : enum(int)

and T will accept any Enum that has int as underlying type

That will enable to use addition or bit operations and also the code can be shared

That accompanied with ref extensions methods will enable us to write any Enum flag operation possible with few generic methods

public static void SetFlag(ref this T baseVal, T flag, bool value) where T : enum(int)
public static void SetByteFlag(ref this T baseVal, T flag, bool value) where T : enum(byte)

Edit: DOH!! It appears that my suggestion was first proposed by HaloFour two and a half years ago and not only that, it appears that F# already has enum generic constraints with underlying type. I am sorry about that..

panost avatar Nov 09 '17 08:11 panost

Glad to see that after about a decade of complaints and hacked workarounds, there's finally some semblance of movement on this. I've lost count of how many times I've needed this, it's an extreme limitation when trying to do any kind of generic GUI programming. Thanks, @TylerBrinkley, for being the squeaky wheel here.

tarrowood-imangi avatar Dec 06 '17 16:12 tarrowood-imangi

When using generics, we have to use DynamicInvoke, boxing, and EnumParse when converting with enums. This is especially true of framework code that aims to be 'user friend;y' and let consumers define their own types. (See MVVM / RX type programming)

This activity causes performance issue, especially on mobile devices.

If I could implicitly cast Enums to/from int without the boxing, dynamic invocation, and conversion I would be happy.

// Replace This
void Convert<TValue>(TValue val)
{
   int b = Convert.ToInt32(val);
}

// With This
void Convert<TValue>(TValue val) where TValue : enum, int
{
     int a = val;
}

// Replace This
void Convert(int val)
{
     var a = (TValue)Enum.Parse(typeof(TValue), val);
}

// With This
void Convert<TValue>(int val) where TValue : enum, int
{
     var a = (TValue)val;
}

Updated examples, my bad.

NVentimiglia avatar Dec 06 '17 20:12 NVentimiglia

@NVentimiglia

I don't understand what you're asking. Explicitly casting to/from an enum and it's underlying integral type (which is not necessarily int or compatible with int) is a non-op. Adding helper methods into the mix would only add overhead.

HaloFour avatar Dec 06 '17 20:12 HaloFour

@HaloFour What helper method am I suggesting ?

NVentimiglia avatar Dec 07 '17 00:12 NVentimiglia

@NVentimiglia

That Convert<T> method you described. Why would that be better than just explicitly casting MyEnum to int? And you don't need Enum.Parse to convert from int to MyEnum. Just cast.

HaloFour avatar Dec 07 '17 01:12 HaloFour

@HaloFour

  • I would like an implicit cast (enum as int) so I do not need to box cast. The box casting is a heap allocation and a performance hit.
  • without generic constraints I can only handle enums as objects (heap) (thus the use of the current helper methods, which I loath).
  • A generic constraint on the method is a prerequisite for my efficiency goals.

NVentimiglia avatar Dec 07 '17 03:12 NVentimiglia

@NVentimiglia

Expliciting casting an enum to an int (or back) does not involve boxing.

Yes, an enum constraint will help when writing generic helper methods. Note that there are no generic constraints to allow you to constrain the underlying type of the enum (e.g. int) so any generic method is still going to have to deal with the fact that T could be any size and you can't do direct arithmetic operations on it. I don't see why these generic methods required doing allocations today, though.

HaloFour avatar Dec 07 '17 11:12 HaloFour

@HaloFour

are these box casts ?

void Convert(int value)
{
    MyEnum a = (MyEnum) Enum.ToObject(typeof(MyEnum), value)
}

void Convert<TValue>(TValue value)
{
   // I believe this boxes internally https://msdn.microsoft.com/en-us/library/sf1aw27b(v=vs.110).aspx
    int a = Convert.ToInt32(value);
}

Im a little lost, if this is not an issue, why do libraries such as ENUMS.NET exist ?

NVentimiglia avatar Dec 07 '17 18:12 NVentimiglia

@NVentimiglia

You could make them boxing casts by simply casting them to object first. The question is why would you? Generic constraints aren't going to help you there.

HaloFour avatar Dec 07 '17 18:12 HaloFour

@NVentimiglia Why again aren't you doing MyEnum a = (MyEnum)value and int a = (int)value?

jnm2 avatar Dec 07 '17 18:12 jnm2

@jnm2

Can not convert TValue to int; in generic code

NVentimiglia avatar Dec 07 '17 18:12 NVentimiglia

My understand that would be a box cast and a heap allocation.

Well, HaloFour told you that that isn't the case, so you can stop believing this now.

Joe4evr avatar Dec 07 '17 18:12 Joe4evr

@NVentimiglia

are these box casts ?

No, converting between an integral type to an enum does not require boxing. If the integral type is the same as the underlying integral type of the enum then it's a simple assignment, otherwise the correct IL would need to be emitted to convert between integral types.

Can not convert TValue to int; in generic code

Indeed you can't, and an enum generic constraint can't help you there because an enum could be any integral type and different IL might be required to perform the conversion. You'd likely need some other flavor of generics added to the CLR to be able to further constrain the underlying generic type, as suggested by @panost.

HaloFour avatar Dec 07 '17 18:12 HaloFour

@HaloFour

I think I may have expressed my problem incorrectly.

The problem I am facing is specifically in the context of generics, so I am forced to use the static enum methods which use object and box casting internally. If this is incorrect, then I am deeply confused as I am getting mixed messages.

Yes, I think I will need another flavor of generics as suggested by panost

https://github.com/dotnet/corefx/issues/15453#issuecomment-350055728

NVentimiglia avatar Dec 07 '17 18:12 NVentimiglia

I wonder if actually another flavor of generics is needed at CLR level. The generic method

public static void SetFlag<T>(ref this T baseVal, T flag, bool value) where T : enum(int)

can be emitted to the non-generic method

[Some Attribute to identify it as Enum generic method]
public static void SetFlag(ref int baseVal, int flag, bool value) 

since at CLR level an Enum and it's underlying type are interchangeable. Also that makes the definition of the byte overload with different name unnecessary, since the method

public static void SetFlag<T>(ref this T baseVal, T flag, bool value) where T : enum(byte)

will be emitted as

public static void SetFlag(ref byte baseVal, byte flag, bool value) 

which has already different signature

panost avatar Dec 28 '17 13:12 panost

@TylerBrinkley: Why do you belive this:

I'm of the opinion that Enum was not allowed as a generic constraint initially in C# due to generic code-explosion as the current .NET runtimes don't share generic code for value types like they do for reference types.

Not disagreeing as I don't know enough to say, but even if enums aren't optimized by underlying type (say System_CannonEnumInt), the amount of "code bloat" would be limited to the number of enums used -- efffectively trading what seems to me to be a small amount of memory for type safety, that would seem worthwhile to me. Why would it be more worrisome for enums than for the other types, given that it would only happen if they were used?

jrmoreno1 avatar Jan 01 '18 09:01 jrmoreno1

(Canon and cannon have different meanings.)

jnm2 avatar Jan 01 '18 17:01 jnm2

@jnm2 : sorry, typing on a iPad, I really meant System.__CanonEnumInt

jrmoreno1 avatar Jan 02 '18 08:01 jrmoreno1