I have this struct:
public struct MyValue
{
public string FirstPart { get; private set; }
public string SecondPart { get; private set; }
public static implicit operator MyValue(string fromInput)
{ // first breakpoint here.
var parts = fromInput.Split(new[] {'@'});
return new MyValue(parts[0], parts[1]);
}
public static implicit operator string(MyValue fromInput)
{ // second breakpoint here.
return fromInput.ToString();
}
public override string ToString()
{
return FirstPart + "@" + SecondPart;
}
public MyValue(string firstPart, string secondPart) : this()
{
this.FirstPart = firstPart;
this.SecondPart = secondPart;
}
}
And I've set breakpoints as indicated by the comments above.
Then I do this:
var first = new MyValue("first", "second");
if (first == (MyValue) null) throw new InvalidOperationException();
I'm observing some strange behavior when it enters if (first == (MyValue) null)
: the second breakpoint is hit for some reason. Why is it trying to convert the MyValue
into a string for a simple equality comparison?
Then, if I let the code continue, it hits the first breakpoint, and now I'm wondering why is it trying to convert a string (the value is null
despite the fact that I've explicitly cast null
into a MyValue
) into a MyValue
? Strings shouldn't be involved when using a statement like if (first == (MyValue) null)
, so what is actually happening here?
Was busy commenting and it became clear what the issue is.
The C# compiler cannot compile (MyStruct) null
, but in your case it does.
This happens as you have an implicit operator from a reference type (this case string
) where null
is perfectly valid.
I think you can follow now why it executes the way you see :)
PS: This is a good example why 'lossy' implicit operators are discouraged in general.
To complete @leppies answer, this is the calling code (Release mode):
public void X()
{
var first = new MyValue("first", "second");
if (first == (MyValue) null) throw new InvalidOperationException();
}
Which actually compiles to this:
public void X()
{
if (new MyValue("first", "second") == null)
{
throw new InvalidOperationException();
}
}
And this is the emitted IL for the call:
// Methods
.method public hidebysig
instance void X () cil managed
{
// Method begins at RVA 0x20dc
// Code size 45 (0x2d)
.maxstack 8
IL_0000: ldstr "first"
IL_0005: ldstr "second"
IL_000a: newobj instance void MyValue::.ctor(string, string)
IL_000f: call string MyValue::op_Implicit(valuetype MyValue)
IL_0014: ldnull
IL_0015: call valuetype MyValue MyValue::op_Implicit(string)
IL_001a: call string MyValue::op_Implicit(valuetype MyValue) <--- This!
IL_001f: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0024: brfalse.s IL_002c
IL_0026: newobj instance void [mscorlib]System.InvalidOperationException::.ctor()
IL_002b: throw
IL_002c: ret
} // end of method C::X
As you can see, after you create the new instance of MyValue
, operation IL_001a
calls the implicit conversion to string
, as that is the only possibility for the compiler to make the value type comparison to null
actually compile.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With