Why is changing a property from “init” to “set” a binary breaking change

Why is changing a property from “init” to “set” a binary breaking change


8

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 viceversa is a breaking change. I can understand how changing it from set to init can be a breaking change but just cant 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?

2

  • Have you checked the IL for init vs set? I would assume they are different.

    – JonasH

    11 hours ago

  • You might want to read the Questions section of the proposal "The downside of modreq is init becomes a part of the binary signature of the set accessor. Adding or removing init will break binary compatbility of the application."

    – Damien_The_Unbeliever

    11 hours ago

2 Answers
2


9

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.


6

This is documented in the design notes, specifically in the section titled "Breaking Changes".

The emit strategy for init property accessors must choose between
using attributes or modreqs when emitting during metadata. These have
different trade offs that need to be considered.

Annotating a property set accessor with a modreq declaration means CLI
compliant compilers will ignore the accessor unless it understands the
modreq. That means only compilers aware of init will read the member.
Compilers unaware of init will ignore the set accessor and hence will
not accidentally treat the property as read / write.

The downside of modreq is init becomes a part of the binary signature
of the set accessor. Adding or removing init will break binary
compatbility of the application.

And the final decision was:

Given there was also no significant support for removing init to be a
binary compatible change it made the choice of using modreq straight
forward.

So they used modreq in the implementation, with the result that replacing an init with set will be a binary breaking change.

There is more discussion about modreq here.

If you really want to know more about modreq then you can look for some details in the CLI spec.



Leave a Reply

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