A while ago I had a “duh” moment while calling a method that had many overloads, and one of the overloads was using int, not the char I’d expect.
The result was that a default value for that char was used, and my parameter was interpreted as a (very small) buffer size. I only found out something went wrong when writing unit tests around my code.
The culprit is this C# char feature (other implicit type conversions nicely summarized by Muhammad Javed):
A char can be implicitly converted to ushort, int, uint, long, ulong, float, double, or decimal. However, there are no implicit conversions from other types to the char type.
Switching between various development environments, I totally forgot this is the case in languages based on C and Java ancestry. But not in VB and Delphi ancestry (C/C++ do numeric promotions of char to int and Java widens 2-byte char to 4-byte int; Delphi and VB.net don’t).
I’m not the only one who was confused, so Eric Lippert wrote a nice blog post on it in 2009: Why does char convert implicitly to ushort but not vice versa? – Fabulous Adventures In Coding – Site Home – MSDN Blogs.
Basically, it is the C ancestry: a char is an integral type always known to contain an integer value representing a Unicode character. The opposite is not true: an integer type is not always representing a Unicode character.
Lesson learned: if you have a large number of overloads (either writing them or using them) watch for mixing char and int parameters.
Note that overload resolution can be diffucult enough (C# 3 had breaking changes and C# 4 had breaking changes too, and those are only for C#), so don’t make it more difficult than it should be (:
Below a few examples in C# and VB and their IL disassemblies to illustrate their differnces based on asterisk (*) and space ( ) that also show that not all implicits are created equal: Decimal is done at run-time, the rest at compile time.
Note that the order of the methods is alphabetic, but the calls are in order of the type and size of the numeric types (integral types, then floating point types, then decimal).
A few interesting observations:
- The C# compiler implicitly converts char with all calls except for decimal, where an implicit conversion at run time is used:
L_004c: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(char)
L_0051: call void CharIntCompatibilityCSharp.Program::writeLineDecimal(valuetype [mscorlib]System.Decimal) - Same for implicit conversion of byte to the other types, though here the C# and VB.NET compilers generate slightly different code for run-time conversion.
C# uses an implicit conversion:
L_00af: ldloc.1
L_00b0: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(uint8)
L_00b5: call void CharIntCompatibilityCSharp.Program::writeLineDecimal(valuetype [mscorlib]System.Decimal)
VB.NET calls a constructor:
L_006e: ldloc.1
L_006f: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
L_0075: call void CharIntCompatibilityVB.Program::writeLineDecimal(valuetype [mscorlib]System.Decimal)
Here is the example code:
C# code:
using System; namespace CharIntCompatibilityCSharp { class Program { static void Main() { Console.WriteLine("asterisk"); char asterisk = '*'; //writeLineSbyte(asterisk); // no implicit conversion //writeLineByte(asterisk); // no implicit conversion //writeLineShort(asterisk); // no implicit conversion writeLineChar(asterisk); writeLineUshort(asterisk); // implicit conversion writeLineInt(asterisk); writeLineUint(asterisk); writeLineLong(asterisk); writeLineUlong(asterisk); writeLineFloat(asterisk); writeLineDouble(asterisk); writeLineDecimal(asterisk); Console.WriteLine("space"); byte space = 32; writeLineSbyte(space); writeLineByte(space); writeLineShort(space); //writeLineChar(space); // no implicit conversion writeLineUshort(space); writeLineInt(space); writeLineUint(space); writeLineLong(space); writeLineUlong(space); writeLineFloat(space); writeLineDouble(space); writeLineDecimal(space); Console.Write("Press <Enter>"); Console.ReadLine(); } private static void writeLineByte(byte character) { Console.WriteLine((int)character); } private static void writeLineChar(char character) { Console.WriteLine(character); } private static void writeLineDecimal(decimal character) { Console.WriteLine(character); } private static void writeLineDouble(double character) { Console.WriteLine(character); } private static void writeLineFloat(float character) { Console.WriteLine(character); } private static void writeLineInt(int character) { Console.WriteLine(character); } private static void writeLineLong(long character) { Console.WriteLine(character); } private static void writeLineSbyte(byte character) { Console.WriteLine((int)character); } private static void writeLineShort(short character) { Console.WriteLine((int)character); } private static void writeLineUint(uint character) { Console.WriteLine(character); } private static void writeLineUlong(ulong character) { Console.WriteLine(character); } private static void writeLineUshort(ushort character) { Console.WriteLine((int)character); } } }
VB.NET code:
Friend Class Program ' Methods Friend Shared Sub Main() Console.WriteLine("asterisk") Dim asterisk As Char = "*"c 'writeLineSbyte(asterisk) 'no implicit conversion 'writeLineByte(asterisk) 'no implicit conversion 'writeLineShort(asterisk) 'no implicit conversion writeLineChar(asterisk) 'writeLineUshort(asterisk) 'no implicit conversion 'writeLineInt(asterisk) 'no implicit conversion 'writeLineUint(asterisk) 'no implicit conversion 'writeLineLong(asterisk) 'no implicit conversion 'writeLineUlong(asterisk) 'no implicit conversion 'writeLineFloat(asterisk) 'no implicit conversion 'writeLineDouble(asterisk) 'no implicit conversion 'writeLineDecimal(asterisk) 'no implicit conversion Console.WriteLine("space") Dim space As Byte = 32 writeLineSbyte(space) writeLineByte(space) writeLineShort(space) 'writeLineChar(space) 'no implicit conversion writeLineUshort(space) writeLineInt(space) writeLineUint(space) writeLineLong(space) writeLineUlong(space) writeLineFloat(space) writeLineDouble(space) writeLineDecimal(space) Console.Write("Press <Enter>") Console.ReadLine() End Sub Private Shared Sub writeLineByte(ByVal character As Byte) Console.WriteLine(character) End Sub Private Shared Sub writeLineChar(ByVal character As Char) Console.WriteLine(character) End Sub Private Shared Sub writeLineDecimal(ByVal character As Decimal) Console.WriteLine(character) End Sub Private Shared Sub writeLineDouble(ByVal character As Double) Console.WriteLine(character) End Sub Private Shared Sub writeLineFloat(ByVal character As Single) Console.WriteLine(character) End Sub Private Shared Sub writeLineInt(ByVal character As Integer) Console.WriteLine(character) End Sub Private Shared Sub writeLineLong(ByVal character As Long) Console.WriteLine(character) End Sub Private Shared Sub writeLineSbyte(ByVal character As Byte) Console.WriteLine(character) End Sub Private Shared Sub writeLineShort(ByVal character As Short) Console.WriteLine(character) End Sub Private Shared Sub writeLineUint(ByVal character As UInt32) Console.WriteLine(character) End Sub Private Shared Sub writeLineUlong(ByVal character As UInt64) Console.WriteLine(character) End Sub Private Shared Sub writeLineUshort(ByVal character As UInt16) Console.WriteLine(character) End Sub End Class
C# IL of Main()
.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 1 .locals init ( [0] char asterisk, [1] uint8 space) L_0000: nop L_0001: ldstr "asterisk" L_0006: call void [mscorlib]System.Console::WriteLine(string) L_000b: nop L_000c: ldc.i4.s 0x2a L_000e: stloc.0 L_000f: ldloc.0 L_0010: call void CharIntCompatibilityCSharp.Program::writeLineChar(char) L_0015: nop L_0016: ldloc.0 L_0017: call void CharIntCompatibilityCSharp.Program::writeLineUshort(uint16) L_001c: nop L_001d: ldloc.0 L_001e: call void CharIntCompatibilityCSharp.Program::writeLineInt(int32) L_0023: nop L_0024: ldloc.0 L_0025: call void CharIntCompatibilityCSharp.Program::writeLineUint(uint32) L_002a: nop L_002b: ldloc.0 L_002c: conv.u8 L_002d: call void CharIntCompatibilityCSharp.Program::writeLineLong(int64) L_0032: nop L_0033: ldloc.0 L_0034: conv.u8 L_0035: call void CharIntCompatibilityCSharp.Program::writeLineUlong(uint64) L_003a: nop L_003b: ldloc.0 L_003c: conv.r4 L_003d: call void CharIntCompatibilityCSharp.Program::writeLineFloat(float32) L_0042: nop L_0043: ldloc.0 L_0044: conv.r8 L_0045: call void CharIntCompatibilityCSharp.Program::writeLineDouble(float64) L_004a: nop L_004b: ldloc.0 L_004c: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(char) L_0051: call void CharIntCompatibilityCSharp.Program::writeLineDecimal(valuetype [mscorlib]System.Decimal) L_0056: nop L_0057: ldstr "space" L_005c: call void [mscorlib]System.Console::WriteLine(string) L_0061: nop L_0062: ldc.i4.s 0x20 L_0064: stloc.1 L_0065: ldloc.1 L_0066: call void CharIntCompatibilityCSharp.Program::writeLineSbyte(uint8) L_006b: nop L_006c: ldloc.1 L_006d: call void CharIntCompatibilityCSharp.Program::writeLineByte(uint8) L_0072: nop L_0073: ldloc.1 L_0074: call void CharIntCompatibilityCSharp.Program::writeLineShort(int16) L_0079: nop L_007a: ldloc.1 L_007b: call void CharIntCompatibilityCSharp.Program::writeLineUshort(uint16) L_0080: nop L_0081: ldloc.1 L_0082: call void CharIntCompatibilityCSharp.Program::writeLineInt(int32) L_0087: nop L_0088: ldloc.1 L_0089: call void CharIntCompatibilityCSharp.Program::writeLineUint(uint32) L_008e: nop L_008f: ldloc.1 L_0090: conv.u8 L_0091: call void CharIntCompatibilityCSharp.Program::writeLineLong(int64) L_0096: nop L_0097: ldloc.1 L_0098: conv.u8 L_0099: call void CharIntCompatibilityCSharp.Program::writeLineUlong(uint64) L_009e: nop L_009f: ldloc.1 L_00a0: conv.r4 L_00a1: call void CharIntCompatibilityCSharp.Program::writeLineFloat(float32) L_00a6: nop L_00a7: ldloc.1 L_00a8: conv.r8 L_00a9: call void CharIntCompatibilityCSharp.Program::writeLineDouble(float64) L_00ae: nop L_00af: ldloc.1 L_00b0: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(uint8) L_00b5: call void CharIntCompatibilityCSharp.Program::writeLineDecimal(valuetype [mscorlib]System.Decimal) L_00ba: nop L_00bb: ldstr "Press <Enter>" L_00c0: call void [mscorlib]System.Console::Write(string) L_00c5: nop L_00c6: call string [mscorlib]System.Console::ReadLine() L_00cb: pop L_00cc: ret }
VB.NET IL of Main():
.method assembly static void Main() cil managed { .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() .entrypoint .maxstack 2 .locals init ( [0] char asterisk, [1] uint8 space) L_0000: nop L_0001: ldstr "asterisk" L_0006: call void [mscorlib]System.Console::WriteLine(string) L_000b: nop L_000c: ldc.i4.s 0x2a L_000e: stloc.0 L_000f: ldloc.0 L_0010: call void CharIntCompatibilityVB.Program::writeLineChar(char) L_0015: nop L_0016: ldstr "space" L_001b: call void [mscorlib]System.Console::WriteLine(string) L_0020: nop L_0021: ldc.i4.s 0x20 L_0023: stloc.1 L_0024: ldloc.1 L_0025: call void CharIntCompatibilityVB.Program::writeLineSbyte(uint8) L_002a: nop L_002b: ldloc.1 L_002c: call void CharIntCompatibilityVB.Program::writeLineByte(uint8) L_0031: nop L_0032: ldloc.1 L_0033: call void CharIntCompatibilityVB.Program::writeLineShort(int16) L_0038: nop L_0039: ldloc.1 L_003a: call void CharIntCompatibilityVB.Program::writeLineUshort(uint16) L_003f: nop L_0040: ldloc.1 L_0041: call void CharIntCompatibilityVB.Program::writeLineInt(int32) L_0046: nop L_0047: ldloc.1 L_0048: call void CharIntCompatibilityVB.Program::writeLineUint(uint32) L_004d: nop L_004e: ldloc.1 L_004f: conv.u8 L_0050: call void CharIntCompatibilityVB.Program::writeLineLong(int64) L_0055: nop L_0056: ldloc.1 L_0057: conv.u8 L_0058: call void CharIntCompatibilityVB.Program::writeLineUlong(uint64) L_005d: nop L_005e: ldloc.1 L_005f: conv.r4 L_0060: call void CharIntCompatibilityVB.Program::writeLineFloat(float32) L_0065: nop L_0066: ldloc.1 L_0067: conv.r8 L_0068: call void CharIntCompatibilityVB.Program::writeLineDouble(float64) L_006d: nop L_006e: ldloc.1 L_006f: newobj instance void [mscorlib]System.Decimal::.ctor(int32) L_0074: nop L_0075: call void CharIntCompatibilityVB.Program::writeLineDecimal(valuetype [mscorlib]System.Decimal) L_007a: nop L_007b: ldstr "Press <Enter>" L_0080: call void [mscorlib]System.Console::Write(string) L_0085: nop L_0086: call string [mscorlib]System.Console::ReadLine() L_008b: pop L_008c: nop L_008d: ret }
–jeroen
via:
Filed under: .NET, Agile, C#, C# 1.0, C# 2.0, C# 3.0, C# 4.0, C# 5.0, C++, Delphi, Development, Encoding, Java, Software Development, Unicode, Unit Testing, VB.NET
