vblang
vblang copied to clipboard
Support System.Enum as generic type constraint.
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#?
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.
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
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.
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
@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:
-
Client-server apps using the request-response communication pattern are very common if not downright ubiquitous (this github issue system is such an app).
-
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.
-
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. -
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.
-
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:
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 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 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 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).
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.
@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
If anyone's interested, here's the pull request for C#: https://github.com/dotnet/roslyn/pull/24199
@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:
-
given a value/variable
V
of a generic typeTEnum
constrained to be an enum... -
...you should be able to use
V
in all contexts where using an enum member directly would be legal. -
...you should be able to call
GetType(TEnum)
and get aSystem.Type
instance suitable for use with shared methods inSystem.Enum
such asSystem.Enum.GetNames()
-
...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:
-
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 likeTEnum 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). -
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).
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 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:
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 ...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).
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
That would give incorrect results for negative-valued enum constants.
Yes. But luckily those incorrect results are still useful in some bitwise algorithms, such as Enum.HasFlag
.
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.
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 many thanks to you and the team for giving this consideration!
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 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:
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#.
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.
@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!
@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 DlmsUnit.YEAR
is more or less a Tuple(Of Integer, String).