Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support System.Enum as generic type constraint. #306

Open
ericmutta opened this issue May 30, 2018 · 34 comments
Open

Support System.Enum as generic type constraint. #306

ericmutta opened this issue May 30, 2018 · 34 comments
Labels
Approved-in-Principle LDM Considering LDM has reviewed and we think this has merit

Comments

@ericmutta
Copy link

This concept has been discussed for years on the web and I just discovered that C# 7.3 now supports it:

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    var values = Enum.GetValues(typeof(T));

    foreach (int item in values)
        result.Add(item, Enum.GetName(typeof(T), item));
    return result;
}

@AnthonyDGreen would you happen to know what the discussion was for supporting this in VB as well when it was planned for C#?

@AnthonyDGreen
Copy link
Contributor

Hey @ericmutta ,

@KathleenDollard would be the person to say what the current priority is but as for what discussions we had on it it was uncontroversial. I'm assuming VB can consume APIs which have this constraint from C# but may still need the restriction lifted on defining them to support overriding. I imagine the C# implementation could be ported easily by a community member. I think @KlausLoeffelmann expressed an interest here.

@ericmutta
Copy link
Author

Thanks @AnthonyDGreen for the prompt reply! We are half way through the year, so fingers crossed at least one of the ideas we've all been discussing in this repo will make it into the language this year! 👍

@KathleenDollard
Copy link
Contributor

@ericmutta

Do you have a stronger case than creating a dictionary from an enum for this? (which can be done in other ways). This feature is so limited in C# that I'm not sure where the value its. I'd love to see a case made.

@reduckted
Copy link
Contributor

Just tagging in some related issues in other repos that I've just discovered:

csharplang: Champion "Allow System.Enum as a constraint"
roslyn: Proposal: support an enum constraint on generic type parameters
roslyn: Add Enum and Delegate constraints to VB

@ericmutta
Copy link
Author

ericmutta commented Jun 15, 2018

@KathleenDollard Do you have a stronger case than creating a dictionary from an enum for this?

Hi Kathleen, good to see you back online! 😃

If I am being honest, I copied the C# dictionary code more as a cop-out in the vein "look the C# guys did it so, clearly this is both possible and awesome so let's do it" kinda thing, because it was shorter that way.

Here is the longer version and an attempt to make a case with an example from a real project that I am working on:

  1. Client-server apps using the request-response communication pattern are very common if not downright ubiquitous (this github issue system is such an app).

  2. In such apps the requests and responses are typically encoded using numeric values that also have an associated name/description. For example HTTP has the world-famous 404 response code which every internet user knows to mean Not Found.

  3. This concept of "numeric values with an associated name" is beautifully handled by enum types in VB, with the added bonus that the numeric values are automatically generated and given any numeric value that belongs to the enum, you can call ToString() to get the associated name.

  4. In large client-server apps, it is common these days to break them into smaller services with each service having a client and server component.

  5. If you do decompose your client-server apps into smaller services, some things are generic and must be shared by all of them, examples include: logging, profiling/monitoring, billing, request rate controls, etc.

The lack of an enum type constraint makes number 5 above (i.e the generic features such as logging) rather awkward to implement. Concrete example:

Let's say your app has two services: ServiceA and ServiceB. The request and response codes for ServiceA are defined using an enum called EnumA and those for ServiceB use an enum called EnumB. Note that EnumA and EnumB are considered two different types, so if you wanted to write any function that can take values from both enums, that function would need to use generics, and the problem is there no way to enforce the constraint that the type of values that the function expects must support the enum property of "numeric values with an associated name".

That property is required if the function for example, counts the number of times each request/response occurs then prints a summary table in a developer dashboard showing the results using more friendly request/response names instead of the opaque numeric codes.

Ultimately, the uses for this feature are limited only by a developer's imagination and it just seems incomplete to have the language support other type constraints but not this one (especially since there's been a lot of developer interest in this for a long time as @reduckted has handily shown by tagging in other issues that touch on it).

Here's hoping the long version actually helped matters rather than making them worse 🤞

@KathleenDollard
Copy link
Contributor

It's actually the code case I'd like to see. I think people overestimate what this feature does.

In your case, you have to cast to the specific enum type. What is the code that is better with the enum constraint than without it? And this isn't an service on the edge as the infrastructure (ASP.NET) needs the specific type.

I'm happy to just have a link to a real world scenario where the C# code is fundamentally better with the constraint-beyond the dictionary example which I agree is better, but limited usage.

For the case you mention, enum alternatives would work great.

@ericmutta
Copy link
Author

@KathleenDollard It's actually the code case I'd like to see.

OK here is an example of code (using the request-response protocol scenario I mentioned earlier) that wont even compile because we can't constrain generic types to an enum (comments are inline since its quite lengthy):

Public Enum EFooUploadServiceCodes
  UploadRequest

  UploadResponse_OK
  UploadResponse_Denied
End Enum

Public Enum EFooDownloadServiceCodes
  DownloadRequest

  DownloadResponse_OK
  DownloadResponse_Denied
End Enum

Public Class CRequestCounter(Of TEnum As Structure)
  'this is an array because we want O(1) lookup performance and compact memory representation
  'to help ensure better cache behaviour. Counting requests should be really fast to prevent
  'delays in response times.
  Private mRequestCounters As Integer()

  Public Sub New()
    'initialise the request counters array to have as many elements as there are members in 
    'the enum. NOTE: this line WILL FAIL AT RUN-TIME if you pass in an actual structure type
    'such as System.DateTime.
    Me.mRequestCounters = New Integer([Enum].GetValues(GetType(TEnum)).Length - 1) {}
  End Sub

  Public Sub CountRequestResponse(ArgCode As TEnum)
    'increment the count for request/response with code given in parameter ArgCode.
    'NOTE: this line DOESN'T EVEN COMPILE because compiler doesn't know TEnum will be
    'an enum type (the 'Structure' type constraint doesn't allow us to express this statically). 
    Me.mRequestCounters(ArgCode) += 1

    'the above code WOULD compile if we could constrain types to System.Enum 
    'because you CAN index into arrays using enum members since the compiler
    'DOES KNOW that they have an underlying numeric value as shown
    'in the lines below which use two different enum types.
    Me.mRequestCounters(EFooUploadServiceCodes.UploadRequest) += 1
    Me.mRequestCounters(EFooDownloadServiceCodes.DownloadRequest) += 1
  End Sub
End Class

While I haven't looked at exactly what the C# implementation allows, what I HOPE will be possible in VB is to write this:

Public Class CRequestCounter(Of TEnum As Enum) '<--- constrain TEnum to be any enum type
End Class

So that within CRequestCounter(Of TEnum) you can write methods that make assumptions like "the type TEnum will use integer storage behind the covers" which makes it possible to write lines like:

Me.mRequestCounters(ArgCode) += 1

...which wouldn't even compile in the code given earlier because not all types that conform to the Structure type constraint use "integer storage behind the covers" as enums do. The compiler is justified in rejecting that code but it does this because it doesn't know that when we specialise CRequestCounter(Of TEnum) with an enum type, that line SHOULD compile.

@KathleenDollard ...the constraint-beyond the dictionary example which I agree is better, but limited usage.

I would like to suggest that the "limited usage" argument should NOT be the main reason for rejecting this frequently requested/discussed feature. The VB language has many things with limited/infrequent usage that are still very handy to have (example: operator overloading is an advanced feature that you can go YEARS without ever using, but it is there, because when you need it, you really really need it to avoid awkward alternatives).

@reduckted
Copy link
Contributor

@ericmutta Good example, but it looks like that won't even work in C# 7.3. 😢

Here's what I tried:

enum EFooUploadServiceCodes
{
    UploadRequest,
    UploadResponse_OK,
    UploadResponse_Denied
}

class RequestCounter<TEnum> where TEnum : Enum
{
    private int[] mRequestCounters;

    RequestCounter()
    {
        mRequestCounters = new int[Enum.GetValues(typeof(TEnum)).Length];
    }

    void CountRequestResponse(TEnum argCode)
    {
        // CS0029: Cannot implicitly convert type 'TEnum' to 'int'
        mRequestCounters[argCode] += 1;
        ~~~~~~~~~~~~~~~~~~~~~~~~~ 

        // CS0030: Cannot convert type 'TEnum' to 'int'
        mRequestCounters[(int)argCode] += 1;
                         ~~~~~~~~~~~~ 

        // CS0266: Cannot implicitly convert type 'EFooDownloadServiceCodes' to 'int'. An explicit conversion exists (are you missing a cast?)
        mRequestCounters[EFooUploadServiceCodes.UploadRequest] += 1;
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

        // Casting to an object, then to an int works, but it ends up boxing the value.
        mRequestCounters[(int)(object)argCode] += 1;

        // Explicitly casting an actual enum value to an int works.
        mRequestCounters[(int)EFooUploadServiceCodes.UploadRequest] += 1;
    }
}

@ericmutta
Copy link
Author

ericmutta commented Jun 16, 2018

@reduckted Good example, but it looks like that won't even work in C# 7.3

Thanks for trying it out to confirm the C# behaviour! 👏 👏

I am beginning to regret making the C# reference 😞 and I hope we will (re)consider this feature afresh for VB, following VB's semantics for enums (e.g. the ability to index into an array using an enum value directly which, for reasons that escape me, C# doesn't allow unless you use an explicit cast, as @reduckted's code above shows).

@reduckted
Copy link
Contributor

I haven't looked at how it's implemented in C#, but I suspect it doesn't work as we'd like because the type constraint is System.Enum (i.e. its just like saying the constraint should be SomeBaseClass), so the compiler doesn't know that the TEnum type is special and can be treated as a number.

@hartmair
Copy link

hartmair commented Jun 16, 2018

@KathleenDollard

It's actually the code case I'd like to see. I think people overestimate what this feature does.

This is what I've been using so far:

' String Enums, i.e. DescriptionAttribute
<Extension>
Public Function GetDescription(obj As [Enum]) As String

' IEnumerable-style for Enums
Public Shared Function AsEnumerable(Of T As TEnum)() As T()
  Return DirectCast([Enum].GetValues(GetType(T)), T())
End Function
Public Shared Function Count(Of T As TEnum)() As Integer
  Return [Enum].GetValues(GetType(T)).Length
End Function
Public Shared Function Min(Of T As TEnum)() As T
  Return AsEnumerable(Of T).First()
End Function
Public Shared Function Max(Of T As TEnum)() As T
  Return AsEnumerable(Of T).Last()
End Function

' helpers regarding FlagsAttribute
<Extension>
Public Function Flags(Of T As TEnum)([enum] As T) As IEnumerable(Of T)
<Extension>
Public Function HasFlag(Of T As TEnum)([enum] As T, flag As Byte) As Boolean
  Return (System.Convert.ToByte([enum]) And flag) = flag
End Function
<Extension>
Public Function HasFlag(Of T As TEnum)([enum] As T, flag As Integer) As Boolean
  Return (System.Convert.ToInt32([enum]) And flag) = flag
End Function

Note that you actually "can" put Enum as generic constraint somehow, see http://stackoverflow.com/a/1416660

@reduckted
Copy link
Contributor

If anyone's interested, here's the pull request for C#: dotnet/roslyn#24199

@ericmutta
Copy link
Author

@reduckted: If anyone's interested, here's the pull request for C#

Thanks for digging that up! It's interesting to note that in that pull request, on two seperate occassions (first by @VSadov and then by @AlekseyTs) there was mention of doing the same thing for VB.

Ignoring the C# implementation for a moment (I don't want what they did there to limit us here), it would be great to get a VB implementation where:

  1. given a value/variable V of a generic type TEnum constrained to be an enum...

  2. ...you should be able to use V in all contexts where using an enum member directly would be legal.

  3. ...you should be able to call GetType(TEnum) and get a System.Type instance suitable for use with shared methods in System.Enum such as System.Enum.GetNames()

  4. ...you should be able to call V.ToString() and get the enum member name as you would if working directly with a known enum type's value.

Below are some example operations that should be valid for V since they are legal when used directly with known enum member values:

Public Module Module1
  Public Enum EFoo
    Foo1
    Foo2
  End Enum

  Public Enum EBar
    Bar1
    Bar2
  End Enum

  Public Sub Main()
    Dim SomeArray = {1, 2, 3}

    'indexing into array using enum member.
    SomeArray(EFoo.Foo1) = 12
    SomeArray(EBar.Bar1) = 15

    'comparing against integers
    If EFoo.Foo1 > 12 Then Stop

    'comparing against other members in same enum.
    If EFoo.Foo1 < EFoo.Foo2 Then Stop

    'comparing against other enums
    If EBar.Bar1 = EFoo.Foo1 Then Stop

    'doing arithmetic with enum members.
    Dim sum = EBar.Bar1 + EFoo.Foo2

    'assigning to numerically typed variables.
    Dim number As Integer = EFoo.Foo1
  End Sub
End Module

I think the above rough spec covers most scenarios, but hope others can add to it in case I forgot something. Some pending considerations:

  1. enums with different underlying types: for example an enum declared as Public Enum EFoo As ULong will not work with array indexing since VB requires integer values for that. Maybe we could use a type constraint syntax like TEnum As Enum(Of Integer) or if that gets messy, just simplify things and just say that the enum type constraint works for enums with integer storage only (which is the vast majority of enum types out there).

  2. enums that have the FlagsAttribute applied: it looks these get special behaviour when used with CType and .ToString() and we'd have to decide how that works in a generic context (using the bitwise operators is already covered by the condition that you can use V anywhere an enum value would be legal).

@paul1956
Copy link

With Option Strict On does "If EFoo.Foo1 > 12 Then Stop" require a Cast on 12? What about comparing against other enums. The specific case I care about is working with SyntaxKind where there are 3 different Enums, VB(VB only), CSharp(C# Only) and Raw (both Lists combined with overlap for a few values). I find myself using Raw a lot but then I lose the better debugging experience that I get with the language specific Enum like debugger displaying a friendly name.

@ericmutta
Copy link
Author

@paul1956 With Option Strict On does "If EFoo.Foo1 > 12 Then Stop" require a Cast on 12? What about comparing against other enums.

Neither of those cases require explicit casting and they never should (in all cases you are comparing integers which is an operation that cannot fail unless cosmic rays don't like you). VB does the sane thing here, it just works! 👍

@KathleenDollard
Copy link
Contributor

I agree that the Enum constraint would allow protection of sending a type that wasn't an Enum into the methods described by @ericmutta and @hartmair.

The Enum type does not know the underlying type is an int (and it may not be), so there really isn't very much you can with this constraint, other than the protection. But this protection will be a little scattered as the current implementation of the Enum methods themselves do not allow compile time checks.

I don't think this is a bad idea, I'm just not sure it's more important than other things. It only provides the protection, and nothing else. And I'm not convinced the underlying fundamentals of Enum will allow the any of the requests in @ericmutta 's list.

@ericmutta
Copy link
Author

@KathleenDollard ...It only provides the protection, and nothing else.

It does provide the protection (so you CAN'T pass in types like System.DateTime which would satisfy the Structure constraint that we are forced to use right now), however it WOULD provide a lot more than just protection because the compiler would be able to assume enum semantics and allow certain behaviours, for example, this snippet from code I showed earlier:

  Public Sub CountRequestResponse(ArgCode As TEnum)
    'increment the count for request/response with code given in parameter ArgCode.
    'NOTE: this line DOESN'T EVEN COMPILE because compiler doesn't know TEnum will be
    'an enum type (the 'Structure' type constraint doesn't allow us to express this statically). 
    Me.mRequestCounters(ArgCode) += 1

    'the above code WOULD compile if we could constrain types to System.Enum 
    'because you CAN index into arrays using enum members since the compiler
    'DOES KNOW that they have an underlying numeric value as shown
    'in the lines below which use two different enum types.
    Me.mRequestCounters(EFooUploadServiceCodes.UploadRequest) += 1
    Me.mRequestCounters(EFooDownloadServiceCodes.DownloadRequest) += 1
  End Sub

In the code above if TEnum is constrained as Structure then Me.mRequestCounters(ArgCode) += 1 doesn't even compile. If we could constrain TEnum to be System.Enum, the implementation could be made to allow such code because it knows that an enum has some underlying integer type (an idea that could be expressed explicitly using syntax like TEnum As Enum(Of Integer) as I mentioned in another comment).

@KathleenDollard I don't think this is a bad idea, I'm just not sure it's more important than other things.

For any given feature request X, there is always going to be a feature request Y that is more important (for example I would throw this request out in a heartbeat if I heard your team was working on #238 instead).

Having given both real-world scenarios and code examples, I don't know what else to do! Could we at least agree to either have it rejected so the issue can be put to rest or accepted for future implementation when more pressing issues have been handled?

It would certainly help everyone to know that this is either never gonna happen or will happen "soon" even if soon refers to some undefined future period (in the Rosyln repo they have a milestone called Unknown which serves this purpose, and some VB bugs I've filed like #25414 have that milestone which gives one peace of mind that even if they are not fixing it now, they know about it and will handle it eventually).

@Nukepayload2
Copy link

The Enum constraint is useful in IL embedding scenario.
We can use the IL instruction conv.u8 to convert Enum to ULong, because the longest Enum has 64 bits.

I have implemented a faster and safer replacement of Enum.HasFlag on .NET Framework 4.6.1 (With the workaround that @hartmair has mentioned): https://github.com/Nukepayload2/FastEnumHasFlags

@gafter
Copy link
Member

gafter commented Jun 19, 2018

That would give incorrect results for negative-valued enum constants.

@Nukepayload2
Copy link

Yes. But luckily those incorrect results are still useful in some bitwise algorithms, such as Enum.HasFlag.

@LodewijkSioen
Copy link

Just ran into this on our project. We have a c# library that is consumed by a vb.net application. The C# library has a type with the following signature:

public class Thingy<T> where T :  Enum

The vb.net application has a function that returns this open generic type which cannot be defined:

Public Function GetThingy(Of T as ???)() As Thingy(Of T as ???)

So not having this feature kind of breaks using C# libraries from vb.net.

@KathleenDollard
Copy link
Contributor

It would be great to have the same level of support in VB for enum constraints that we have in C#.

Marked as Approved.

@KathleenDollard KathleenDollard added Approved-in-Principle LDM Considering LDM has reviewed and we think this has merit labels Oct 17, 2018
@ericmutta
Copy link
Author

@KathleenDollard many thanks to you and the team for giving this consideration!

@rrvenki
Copy link

rrvenki commented Dec 28, 2018

The original @ericmutta requirement is already available in B# I thought.
Ref: https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/constants-enums/how-to-iterate-through-an-enumeration
Let me know if I'm wrong.

@ericmutta
Copy link
Author

@rrvenki Let me know if I'm wrong.

The code you linked to uses a specific enum type (i.e FirstDayOfWeek). It is not possible today to write a generic version of that code that doesn't crash at run-time because someone passed in a structure type like DateTime (see earlier discussion...I know it's long!).

PS: what on earth is B#? 😕

@rrvenki
Copy link

rrvenki commented Dec 30, 2018

Got your point. If full support of enum comes in, it will be par with python where we can use the enum.range in a FOR loop.
I wish VB.NET called B# and it will automatically move to first in the alphabetical order where currently VB.NET stays the last. Lets "Be Sharp" to rechristen VB.NET to B#.

@Padanian
Copy link

Padanian commented Dec 30, 2018

May I bring your attention on this example of Java enum:

public enum DlmsUnit {
YEAR(1, "a"),
MONTH(2, "mo"),
WEEK(3, "wk"),
}

This is basically a Dictionary(of Enum, String) and it can be translated to B# as:

Public Enum DlmsUnit As Integer
YEAR = 1
MONTH = 2
WEEK = 3
End Enum
Public Function GetDlmsDictionary() As Dictionary(Of DlmsUnit, String)
Dim retValue As New Dictionary(Of DlmsUnit, String)
Dim value As String = String.Empty
For i As Integer = 1 To 12
Select Case i
Case 1
value = "a"
Case 2
value = "mo"
Case 3
value = "wk"
End Select
retValue.Add(i, value)
Next
Return retValue
End Function

which is definitely an overkill compared to Java syntax.
Thanks for your attention and please be gentle, this is my first post here.

@ericmutta
Copy link
Author

@rrvenki I wish VB.NET called B# and it will automatically move to first in the alphabetical order where currently VB.NET stays the last.

I don't know if (sane) people out there choose a language based on it's alphabetical sort position, but since this name change is never going to happen, I think it is best we avoid the B# reference - it only confuses things!

@ericmutta
Copy link
Author

@Padanian Thanks for your attention and please be gentle, this is my first post here.

Welcome aboard! We are a friendly bunch here and it is always nice to see new faces 😃

@Padanian May I bring your attention on this example of Java enum:

This looks interesting, could you post it in a seperate issue so it can get its own dedicated discussion? For example, given my limited knowledge of Java, I would like to know what the data type of DlmsUnit.YEAR would be if an enum can be used to create dictionaries too 👍

@Padanian
Copy link

@ericmutta DlmsUnit.YEAR is more or less a Tuple(Of Integer, String).

@jrmoreno1
Copy link

Well, my scenario is that I want to do a no value check, which means I need to be able to cast the enum to an long without boxing. You can't currently do that with a System.Enum, but you can for a specific type of enum. Apparently this can be done with a single IL opcode, Conv_I8, but for some reason that isn't supported for System.Enum. Basically I would want to be able to cast to a integer/long from within a constrained method.

@mdell-seradex
Copy link

I was just looking for this kind of thing in VB.Net and was sad to find that C# has this (mostly as they cannot convert to the base data type without boxing - int, uint, etc.), but VB.Net does not.
I hope this will be implemented in the near future. 🙂

@nick4814
Copy link

It would be awesome if VB.NET had this. I have Enum extensions methods that I want to constrain to only allow Enums.
Too bad that this is the only fallback as of writing.

Hopefully Microsoft knows there are many codebases out there that still rely heavily on VB.NET, and want these updates.

Of course I get that it is not a priority as the majority of devs use C#, but it would be nice to get more C# features.

@paul1956
Copy link

paul1956 commented Mar 25, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Approved-in-Principle LDM Considering LDM has reviewed and we think this has merit
Projects
None yet
Development

No branches or pull requests