This behavior is defined in the language specification in the M. Conversions section. Item M.2.8 User-defined explicit conversions
A standardized explicit conversion was followed by a standardized explicit conversion. The exact rules for evaluating user-defined explicit converters are described in §User-defined explicit conversion.
And in part of paragraph M.4.3 Evaluation of user-defined conversions about this, too, it says:
... Once there is a need, it’s not user-defined or lifted conversion operator. Next, invoking the user-defined or lifted conversion operator to perform the conversion. Finally, if required, it can be used for the target type. ...
Indicates that a user-defined explicit conversion may include three consecutive conversions:
- Optional standard explicit conversion. (The initial type to the type of the operand, in our case it is
double
to int
) - Custom conversion implicit or explicit operator.
- Optional standard explicit conversion. (The type of the result is from 2 points to the final type. In our case this item is missing).
Those. You can say what happens like this:
// Пользовательское явное преобразование. Foo foo = (Foo)doub; // Будет вот так. Foo foo = (Foo)(int)doub;
If you look at IL, you will see the conv.i4 opcode there before calling the user cast.
IL_0000: ldc.r8 15.7 IL_0009: dup IL_000a: conv.i4 IL_000b: call valuetype Foo Foo::op_Explicit(int32)
Those. everything works according to the specification.