Incorrect Overloaded functions called when specifying Enum Parameters
I was sent the following code with an subject similar to this blog entry title:
Option Strict On
Option Explicit On
Option Compare Text
Public Class ExampleClass
Public Overloads Function Member(ByVal IntegerArgument As Long) As String
Return CStr(IntegerArgument)
End Function
Public Overloads Function Member(ByVal IntegerArgument As Integer) As String
Return CStr(IntegerArgument)
End Function
Public Overloads Function Member(ByVal IntegerArgument As Int16) As String
Return CStr(IntegerArgument)
End Function
Public Enum Enumeration
EnumerationValue0
EnumerationValue1
End Enum
Public Overloads Function Member(ByVal EnumerationArgument As Enumeration) As String
Return System.Enum.GetName(GetType(Enumeration), EnumerationArgument)
End Function
Public Shared Sub Main()
Dim obj As New ExampleClass
If Not obj.Member(Enumeration.EnumerationValue1) = "EnumerationValue1" Then Stop 'Test 1 works fine
If Not obj.Member(Enumeration.EnumerationValue0) = "EnumerationValue0" Then Stop 'Test 2 works fine
If Not obj.Member(1) = "1" Then Stop 'Test 3 works fine
If Not obj.Member(1 - 1) = "0" Then Stop 'Test 4 works fine
If Not obj.Member(CShort(0)) = "0" Then Stop 'Test 5 works fine
If Not obj.Member(0) = "0" Then Stop 'But test 6 returns "EnumerationValue0"
End Sub
End Class
The problem is that when he called obj.Member(0), the code will run the overloads function with enum function but not the long version. While obj.Memeber(1) will run the long version.
I spent some time investigating this, have a look at what I did:
When you specify constant values within your source code the compiler tries to do a type resolution on the constant value. The compiler checks for local type declarations (class level, then in the namespace, then in the references). In this case the compiler finds the declaration for the Enum and evaluates 0 to be EnumerationValue0. We can check this by looking at the source code and the IL:
If Not obj.Member(0) = "0" Then Stop 'But test 6 returns "EnumerationValue0"
IL for the above code:
IL_0085: ldc.i4.0
IL_0086: beq.s IL_008e
IL_0088: call void [mscorlib]System.Diagnostics.Debugger::Break()
IL_008d: nop
IL_008e: ldloc.0
IL_008f: ldc.i4.0
IL_0090: callvirt instance string ConsoleApplication1.Module1/ExampleClass::Member(valuetype ConsoleApplication1.Module1/ExampleClass/Enumeration) IL_0095: ldstr "0"
IL_009a: ldc.i4.0
IL_009b: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string,
string,
bool)
Things are about to get interesting!
We can do something really cool to prove our point. Let's break the application even more! Add another enumeration and an overloaded method that accepts this new enumeration. You'll see that when you try to compile you'll get the following error on the source line mentioned previously.
Overload resolution failed because no accessible 'Member' is most specific for these arguments:
'Public Overloads Function Member(EnumerationArgument As e2) As String': Not most specific.
'Public Overloads Function Member(EnumerationArgument As Enumeration) As String': Not most specific.
Here's what I added to the code:
Public Enum e2
a
b
End Enum
Public Overloads Function Member(ByVal EnumerationArgument As e2) As String
Return System.Enum.GetName(GetType(e2), EnumerationArgument)
End Function
You can actually make this error go away if you change the call to Member to use a value that is not declared in any Enum (like 5) except the value 0 (I’ll get to this in a little later). We can take this a step further to show things are really broken. Change the enum declarations so that each enum as different values. I modified EnumerationValue to be 5 & 7 and e2 to have values of 3 & 4.
Public Enum Enumeration As Integer
EnumerationValue0 = 5
EnumerationValue1 = 7
End Enum
Public Enum e2 As Integer
a = 3
b = 4
End Enum
Now when you modify the code so that the call to Member contains any non-zero integer value that the Integer overloaded method is called (Public Overloads Function Member(ByVal IntegerArgument As Integer) As String). When you specify 0 in the call to Member we again get the overload resolution failure error message.
So, what does all of this tell us?
Basically that there are some problems with the compiler! From the documentation we know that we are supposed to reference all enums with a variable of the enum type or by the name of the enumeration. However, this doesn’t help us as we want the integer overloaded function to be called when we pass in the value of zero. I thought that there might be a default value of zero associated with either System.Enum or System.ValueType (since System.Enum inherits from System.ValueType) so I checked these classes using the object browser. I didn’t see anything useful.
Can you tell me what’s going on?
BTW, this all works as you'd expect in C# J
This all started here: https://msdn.microsoft.com/newsgroups/default.aspx?dg=microsoft.public.dotnet.framework&mid=ca24595b-4d00-400b-9d17-b8a79b19eade
Comments
- Anonymous
March 26, 2004
Actually, this is a bug in the language, not the compiler. According to the language rules, the literal 0 widens to both enumerated types and Integer. And enumerated types by default widen to Integer because that's the default underlying type. So when resolving the overloaded call, we end up choosing the enumerated type as the "most specific" type.
Obviously, this is not the ideal behavior, and we're planning on adding a new rule in Whidbey to make this scenario work "as expected." (Thanks go to Bill McCarthy who originally pointed out this problem to us.)