There is an interesting feature that is worth mentioning in C♯ 7.2. As most long time C♯ programmers may know, parameter passing in C♯ is passed by value by default. That is to say, a method call Foo(T)
will:
- Copy the value of
T
once into memory and pass that copy intoFoo
ifT
is a value type (int
,enum
,struct
, etc.) - Copy the value of the reference to
T
and passes it intoFoo
ifT
is a reference type (object
,class
,string
, etc.)
The only exceptions before C♯ 7.2 are the out
and ref
keywords, which changes the parameter passing semantics to pass by reference. I will not go into details on the difference between out
and ref
here because they are quite similar. This should not be unfamiliar to C♯ programmers as one of most common operations: checking if a key exists in a Dictionary<TKey, TValue>
and simultaneously returning the value if the key exists, is implemented in the BCL’s System.Collections.Generic.Dictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value)
, and it contains an out
parameter.
Difference in memory impact between reference and value types
- The size of a reference value is 4 bytes when running in a 32-bit process and 8 bytes when running in a 64-bit process
- The size of a value type depends on the fields it holds
As an example, let us consider this:
- When
UseReferenceInt
is called, an 4 byte (on a 32-bit process)/8 byte (on a 64-bit process) reference value is copied - When
UseStructInt
is called, exactly 4 bytes are copied sinceStructInt
wraps only a singleint
, which is an alias ofSystem.Int32
, a 32-bit integer
In this specific scenario, using a struct
always gives you an equal (in a 32-bit process) or even smaller (in a 64-bit process) memory footprint.
Drawbacks on using structs
However, one important aspect to consider on using struct
is that size matters. If your struct
contains four int
fields for example, passing it to method calls incurs a 16-byte copy, in contrast to a 4/8-byte copy if this were a class
.
That is why the .NET Framework Design Guidelines mentions that
❌ AVOID defining a struct unless the type has all of the following characteristics:
It logically represents a single value, similar to primitive types (
int
,double
, etc.).It has an instance size under 16 bytes.
It is immutable.
It will not have to be boxed frequently.
In all other cases, you should define your types as classes.
Because copying a large struct
to method call(s) is potentially memory-expensive, and it has a snowballing effect when the struct
has to be passed multiple times into multiple methods.
However, this is all changed in C♯ 7.2.
Passing an (immutable) struct by reference
Consider a variation of the example above, now with four integers instead of one:
There are quite a number of differences here I would like to highlight:
- Both
ReferenceInt
andStructInt
contain four integers instead of one, as mentioned above - [C♯ 7.2 feature]
StructInt
is marked asreadonly
on line 11 - [C♯ 7.2 feature] In order to mark
StructInt
asreadonly
, it must be immutable as required by the compiler. All fields are upgraded toget
-only properties as a result - [C♯ 7.2 feature]
UseStructInt
has anin
keyword specified before theStructInt
parameter type, indicating that this parameter should be passed by reference - [C♯ 7.2 feature] As a result of 4, the call site (line 39) is also required to include the
in
keyword when calling theUseStructInt
method
The result is really best of both worlds. You get value type semantics (no null
, no GC pressure) without the memory penalty due to copying, because the large StructInt
is now passed by reference which only involves 4/8 bytes of copying, in contrast to 16 bytes if it were not passed by reference.
Why not use ref instead?
I believe you can achieve the same result by using the ref
keyword in UseStructInt
and keeping StructInt
non-readonly
. However, I wholeheartedly agree with the open-source community who suggests the inclusion of an extra keyword, in
.
The in
and readonly
keywords remind programmers that mutable structs are (usually) evil by raising a compile error if the struct in question is mutable and forces the author to think carefully before allowing the struct to be passed by reference. This is a wise move in my opinion.
If you would like to know more, I suggest reading this blog article before blindly changing all your structs to readonly
: http://faithlife.codes/blog/2017/12/in-will-make-your-code-slower/