vblang icon indicating copy to clipboard operation
vblang copied to clipboard

Support System.Enum as generic type constraint.

Open ericmutta opened this issue 6 years ago • 34 comments

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#?

ericmutta avatar May 30 '18 17:05 ericmutta

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.

AnthonyDGreen avatar Jun 03 '18 08:06 AnthonyDGreen

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! :+1:

ericmutta avatar Jun 03 '18 17:06 ericmutta

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

KathleenDollard avatar Jun 14 '18 23:06 KathleenDollard

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

Hi Kathleen, good to see you back online! :smiley:

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 :crossed_fingers:

ericmutta avatar Jun 15 '18 02:06 ericmutta

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.

KathleenDollard avatar Jun 15 '18 20:06 KathleenDollard

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

ericmutta avatar Jun 16 '18 02:06 ericmutta

@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;
    }
}

reduckted avatar Jun 16 '18 03:06 reduckted

@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! :clap: :clap:

I am beginning to regret making the C# reference :disappointed: 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).

ericmutta avatar Jun 16 '18 03:06 ericmutta

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.

reduckted avatar Jun 16 '18 04:06 reduckted

@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

hartmair avatar Jun 16 '18 17:06 hartmair

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

reduckted avatar Jun 17 '18 08:06 reduckted

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

ericmutta avatar Jun 17 '18 19:06 ericmutta

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.

paul1956 avatar Jun 17 '18 21:06 paul1956

@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! :+1:

ericmutta avatar Jun 17 '18 22:06 ericmutta

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.

KathleenDollard avatar Jun 18 '18 16:06 KathleenDollard

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

ericmutta avatar Jun 19 '18 00:06 ericmutta

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

Nukepayload2 avatar Jun 19 '18 08:06 Nukepayload2

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

gafter avatar Jun 19 '18 16:06 gafter

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

Nukepayload2 avatar Jun 20 '18 10:06 Nukepayload2

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.

LodewijkSioen avatar Aug 30 '18 15:08 LodewijkSioen

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 avatar Oct 17 '18 20:10 KathleenDollard

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

ericmutta avatar Oct 18 '18 15:10 ericmutta

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.

rrvenki avatar Dec 28 '18 11:12 rrvenki

@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#? :confused:

ericmutta avatar Dec 30 '18 06:12 ericmutta

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

rrvenki avatar Dec 30 '18 06:12 rrvenki

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.

Padanian avatar Dec 30 '18 08:12 Padanian

@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 avatar Dec 30 '18 16:12 ericmutta

@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 :smiley:

@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 :+1:

ericmutta avatar Dec 30 '18 16:12 ericmutta

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

Padanian avatar Dec 30 '18 18:12 Padanian