Why in C# an explicit decimal => long conversion operator is called implicitly, losing precision?

Why in C# an explicit decimal => long conversion operator is called implicitly, losing precision?

6

The following C# program silently and implicitly calls an explicit decimal-to-long conversion operator, losing precision.

I don’t understand why this happens. As far as I understand, in C# explicit operator should not be called implicitly by the language. Especially that in this case the silent explicit conversion is losing precision (1.1M => 1L).

This odd behavior actually caused a bug in my program.

Here is the simplified code:

// Custom number class
struct Num
{
    long Raw;
    public static implicit operator Num(long v) => new Num { Raw = v };
}

class Program
{
    static void Main()
    {
        decimal d = 1.1m;
        // The following line implicitly converts d to long (silently losing precision), 
        // then calls Num.op_Implicit(long)
        Num num = (Num)d;  // <=== should not compile???
    }
}

Here is the resulting IL. You can see that System.Decimal::op_Explicit is called, even though it’s never asked for.

IL_0000: ldc.i4.s 11
IL_0002: ldc.i4.0
IL_0003: ldc.i4.0
IL_0004: ldc.i4.0
IL_0005: ldc.i4.1
IL_0006: newobj instance void [mscorlib]System.Decimal::.ctor(int32, int32, int32, bool, uint8)
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: call int64 [mscorlib]System.Decimal::op_Explicit(valuetype [mscorlib]System.Decimal)  // <=== ???
IL_0012: call valuetype Num Num::op_Implicit(int64)
IL_0017: stloc.1
IL_0018: ret

Share
Improve this question

1

  • Num num = d;, now it is implicit.

    – Hans Passant

    2 hours ago

4 Answers
4

Reset to default

Highest score (default)

Trending (recent votes count more)

Date modified (newest first)

Date created (oldest first)

3

The rules for user defined explicit conversions are given in the C# spec. 10.5.5 User-defined explicit conversions. The rules are quite complex, but specifically allow for additional explicit conversions to be performed “implicitly”:


If E does not already have the type Sₓ, then a standard explicit conversion from E to Sₓ is performed.

(Here E is the source type – decimal and Sₓ is the type your conversion operator converts from – long).

Note that you are performing an explicit conversion to Num (even though it uses the implicit operator you have defined), so the rules for explicit conversions are followed, which allows the extra “implicit” explicit conversion from decimal to long.

Share
Improve this answer

1

I’d say that Num num = (Num)d; is very, very explicit because (Num) is put right in front of d.

It looks like the compiler sees you want an explicit cast, and then it does (Num)(long)d because d is a decimal and (Num) enables it to "force" a long out of the decimal. I have to be honest that I’m guessing, but it could be an explanation.

The following would be implicit, in my (not so expert) opinion:

Num num = d;

And indeed that does not compile with the given struct.

Share
Improve this answer

3

  • Yes, what else can be more explicit than a direct explict cast (Num)

    – Dmitry Bychenko

    15 hours ago

  • 1

    You misunderstood the question. The implicit conversion to long is called, not to Num.

    – kaalus

    15 hours ago

  • I updated my answer to give a possible explanation.

    – Peter B

    14 hours ago

1

The rules for explicit conversion are incredibly complex in C# (wart).

The C# draft specification section that explains how a user-defined explicit conversion is called has a lot of sub-cases for different type possiblities. Note that the specification hasn’t been updated, and seems to have some errors / be inconsistent.

In your example, I think these are the rules that apply:

  • A user-defined explicit conversion from an expression d to a type Num is processed as follows:

  • If d has a type, let S be that type.

    • let Sᵢ and Tᵢ be Decimal and Num
    • let S₀ and T₀ be Decimal and Num
  • Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of Decimal and Num.

  • If a standard implicit conversion exists from a type long to type Decimal, then long is said to be encompassed by Decimal.

  • Find the set of applicable user-defined conversion operators, U. This set consists of the user-defined explicit conversion operators declared by the structs in { Decimal, Num } that convert from a type encompassed by Decimal to Num.

U is public static implicit operator Num(long v)

  • Find the most-specific source type, Sₓ, of the operators in U:

    • Sₓ is the most-encompassing type in the combined set of source types of the operators in U

    Sₓ is long

  • If d does not already have the type long, then a standard explicit conversion from d to long is performed.

Share
Improve this answer

0

The C# reference documents built-in numeric conversions. Conversion from decimal to long is one of these conversions and that this will be performed using an explicit numeric conversion. It is valid C# syntax, and will compile. The reference highlights the fact that data loss is possible. It also states that the conversion uses rounding.

While this behaviour is not what you desire: it is specified in the official documentation. While data loss is possible, the alternative would be to disallow it, which would be harsh restriction. The language designers had to make a call, and chose the possibility of data loss instead of disallowing it.

Share
Improve this answer

Your Answer

Draft saved
Draft discarded

Post as a guest

Required, but never shown


By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged

or ask your own question.

Leave a Reply

Your email address will not be published. Required fields are marked *