The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My work

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 1,843 other followers

.NET/C# duh moment of the day: “A char can be implicitly converted to ushort, int, uint, long, ulong, float, double, or decimal (not the other way around; implicit != implicit)”

Posted by jpluimers on 2012/11/20

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:

One Response to “.NET/C# duh moment of the day: “A char can be implicitly converted to ushort, int, uint, long, ulong, float, double, or decimal (not the other way around; implicit != implicit)””

  1. […] A while ago, I wrote about .NET/C# duh moment of the day: “A char can be implicitly converted to ushort, int, uint, long, ulo…. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: