C# 7.2 added two new features:
In Parameters
Using in
for a parameter let's us pass by reference, but then prevents us from assigning a value to it. However the performance can actually become worse, because it creates a "defensive copy" of the struct, copying the whole thing
Readonly Structs
A way around this is to use readonly
for a struct
. When you pass it into an in
parameter, the compiler sees that it's readonly
and won't create the defensive copy, thereby making it the better alternative for preformance.
That's all great, but every field in the struct
has to be readonly
. This doesn't work:
public readonly struct Coord
{
public int X, Y; // Error: instance fields of readonly structs must be read only
}
Auto-properties also have to be readonly
.
Is there a way to get the benefits of in
parameters (compile-time checking to enforce that the parameter isn't changed, passing by reference) while still being able to modify the fields of the struct
, without the significant performance hit of in
caused by creating the defensive copy?
When you pass [a
readonly struct
] into an in parameter, the compiler sees that it's readonly and won't create the defensive copy.
I think you misunderstood. The compiler creates a defensive copy of a readonly variable that contains a struct
(that could be an in
parameter, but also a readonly
field) when you call a method on that struct
.
Consider the following code:
struct S
{
int x, y;
public void M() {}
}
class C
{
static void Foo()
{
S s = new S();
Bar(s);
}
static void Bar(in S s)
{
s.M();
}
}
You can inspect the IL generated for the code above to see what's actually going to happen.
For Foo
, the IL is:
ldloca.s 0 // load address of the local s to the stack
initobj S // initialize struct S at the address on the stack
ldloca.s 0 // load address of the local s to the stack again
call void C::Bar(valuetype S&) // call Bar
ret // return
Notice that there is no copying: the local s
is initialized and then the address to that local is directly passed to Bar
.
The IL for Bar
is:
ldarg.0 // load argument s (which is an address) to the stack
ldobj S // copy the value from the address on the stack to the stack
stloc.0 // store the value from the stack to an unnamed local variable
ldloca.s 0 // load the address of the unnamed local variable to the stack
call instance void S::M() // call M
ret // return
Here, the ldobj
and stloc
instructions create the defensive copy, to make sure that if M
mutates the struct
, s
won't be mutated (since it's readonly).
If you change the code to make S
a readonly struct
, then the IL for Foo
stays the same, but for Bar
it changes to:
ldarg.0 // load argument s (which is an address) to the stack
call instance void S::M() // call M
ret // return
Notice that there is no copying here anymore.
This is the defensive copy that marking your struct
as readonly
avoids. But if you don't call any instance methods on the struct, there won't be any defensive copies.
Also note that the language dictates that when the code executes, it has to behave as if the defensive copy was there. If the JIT can figure out that the copy is not actually necessary, it is permitted to avoid it.
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