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
2 Answers
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.
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.
Have you checked the IL for init vs set? I would assume they are different.
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."
11 hours ago