For classes, this literally doesn't make any difference; you're always just dealing with a reference and an offset from that reference. Passing a reference is plenty cheap.
When it does start to matter is with structs. Note that this doesn't impact calling methods on the type - that is a ref-based static call, typically; but when the struct is a parameter to a method, it matters.
(edit: actually, it also matters when calling methods on structs if you're calling them via a boxing operation, as a box is also a copy; that's a great reason to avoid boxed calls!)
Disclaimer: you probably shouldn't be routinely using structs.
For structs, the value takes that much space wherever it is used as a value, which could be as a field, a local on the stack, a parameter to a method, etc. This also means that copying the struct (for example, to pass as a parameter) can be expensive. But if we take an example:
struct MyBigStruct {
// lots of fields here
}
void Foo() {
MyBigStruct x = ...
Bar(x);
}
void Bar(MyBigStruct s) {...}
then at the point when we call Bar(x)
, we copy the struct on the stack. Likewise whenever a local is used for storage (assuming it isn't boiled away by the compiler):
MyBigStruct x = ...
MyBigStruct asCopy = x;
But! we can fix these things... by passing a reference around instead. In current versions of C#, this is done most appropriately using in
, ref readonly
, and readonly struct
:
readonly struct MyBigStruct {
// lots of readonly fields here
}
void Foo() {
MyBigStruct x = ...
Bar(x); // note that "in" is implicit when needed, unlike "ref" or "out"
ref readonly MyBigStruct asRef = ref x;
}
void Bar(in MyBigStruct s) {...}
Now there are zero actual copies. Everything here is dealing with references to the original x
. The fact that it is readonly
means that the runtime knows that it can trust the in
declaration on the parameter, without needing a defensive copy of the value.
Ironically, perhaps: adding the in
modifier on a parameter can introduce copying if the input type is a struct
that is not marked readonly
, as the compiler and runtime need to guarantee that changes made inside Bar
won't be visible to the caller. Those changes need not be obvious - any method call (which includes property getters and some operators) can mutate the value, if the type is evil. As an evil example:
struct Evil
{
private int _count;
public int Count => _count++;
}
The job of the compiler and runtime is to work predictably even if you are evil, hence it adds a defensive copy of the struct. The same code with the readonly
modifier on the struct will not compile.
You can also do something similar to in
with ref
if the type isn't readonly
, but then you need to be aware that if Bar
mutates the value (deliberately or as a side-effect), those changes will be visible to Foo
.