Dec 22, 2023

Structs in C# are fun - Part 5/9: Other scenarios in which struct constructor behavior may surprise you

Leia este post em português

Lire cet post en français.

  1. Structs in C# are fun.
  2. Brief introduction to Value Types vs Reference Types.
  3. Field initialization in structs.
  4. Constructors and struct behavior
  5. Other scenarios in which struct constructors behavior may surprise you (this post)
  6. Struct with default argument values in constructors, a.k.a, are you not confused yet?
  7. `required` feature from C# 11 will not save your a** job.
  8. Struct used as default argument values.
  9. Bonus: Struct evolution in C#.

In the previous post I've presented a scenario in which a struct ctor would not be invoked, no matter how the syntax may lead us to believe one would be. This post expands on that, adding a couple more of those scenarios shown in the code below:

Given the code above, what do you expect to be printed ? 42, 42, -1, 42?

With the knowledge from the previous post we know that the fourth invocation (marked as // 4) will invoke the parameterless constructor whence, s_4.v will have a value of 42; but what about the other 3 cases? 

If we inspect the generated IL we'll see that the compiler emitted a call to S(int) constructor for s_3 but no constructors were invoked for the two first cases meaning that s_1 and s_2 will have its v field initialized to 0, so the code above will print 0, 0, -1, 42.

Why ?

One could argue that in all 4 scenarios a value type is being instantiated and whence a constructor should have been executed, so lets dig a little more on the 2 scenarios above for which no constructor is invoked and try to reason about the motivation  behind the decision to do so.

Starting with s_1, if we look into the documentation for the default expression we can read:

A default value expression is used to obtain the default value (§9.3) of a type.

referencing section 9.3 Default Values where we can read:

For a variable of a value_type, the default value is the same as the value computed by the value_type’s default constructor (§8.3.3).

which itself references section 8.3.3 Default Constructors where we can finally read:

All value types implicitly declare a public parameterless instance constructor called the default constructor. The default constructor returns a zero-initialized instance known as the default value for the value type:
...
  • For a struct_type, the default value is the value produced by setting all value type fields to their default value and all reference type fields to null.

so, even though the documentation mentions invoking the default constructor to obtain the default value it also says that the default constructor for a value type is equivalent to zeroing out all the memory reserved for the type1 so, no constructor being invoked in this scenario is the expected behavior.

This leaves us with our last scenario: arrays of value types. To tackle that one lets resort again on what language specification says about array creation:

Array instances are created by array_creation_expressions (§12.8.16.5)....

Elements of arrays created by array_creation_expressions are always initialized to their default value (§9.3).

which means that when an array is instantiated all of its elements are set to the default value of the array type, which for value types, as we saw above, is the equivalent of zeroing out the allocated memory.

I believe that this behavior of not invoking the default constructor on value types has two main motivations: i) historically C# did not supported the concept of explicit parameterless (default) constructors on value types until C# 102, and ii) performance: imagine some code doing something like new S[1_000_000]; if default constructors were to be executed this could take an unpredictable amount of time.

As always, all feedback is welcome.

No comments: