Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange conversion operator behavior

Tags:

c#

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?

like image 480
rory.ap Avatar asked Oct 01 '15 19:10

rory.ap


2 Answers

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.

like image 64
leppie Avatar answered Sep 21 '22 15:09

leppie


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.

like image 30
Yuval Itzchakov Avatar answered Sep 25 '22 15:09

Yuval Itzchakov