Search code examples
c#propertiesabi

Why is changing a property from "init" to "set" a binary breaking change?


I am coming back to C# after a long time and was trying to catch up using the book C# 10 in a Nutshell.

The author there mentions that changing a property's accessor from init to set or vice versa is a breaking change. I can understand how changing it from set to init can be a breaking change, but I just can’t understand why changing it the other way around would be a breaking change.

For example:

// Assembly 1
Test obj = new(){A = 20};

// Assembly 2
class Test
{
   public int A {get; init;} = 10;
}

This code in Assembly 1 should not be affected even if I change the init property accessor to set. Why then is this a breaking change?


Solution

  • This is because init accessors are compiled into a setter with a modreq declaration. The IL code for an int property P might look something like this (See on SharpLab):

    .method public hidebysig specialname    
        instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_P (    
            int32 'value'   
        ) cil managed   
    {   
        ...
    }
    

    Notice the token modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit).

    A normal setter does not generate this modreq.

    On the caller's side, the call instruction must supply the modreq declaration as part of the signature of the thing to call, if and only if a modreq exists on that method. Therefore, the call to an init accessor would look like this:

    callvirt instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) SomeClass::set_P(int32)
    

    and not just

    callvirt instance void SomeClass::set_P(int32)
    

    If you changed to a setter, then all the calls to the init accessor must be changed to remove the modreq, or else it would not resolve the method correctly. Hence, this is a breaking change.

    As for why modreq is used instead of a regular attribute to mark the property, see this section in the draft spec. To summarise, this is a trade-off between binary compatibility and "what would a compiler not aware of init accessors do". In the end they decided to sacrifice binary compatibility, so that a compiler that doesn't know about init doesn't allow code that sets the property.