Aug 31, 2023

Structs in C# are fun - Part 3/9: Field initialization in structs

Leia este post em português

Lisez cet post en french.

  1. Structs in C# are fun.
  2. Brief introduction to Value Types vs Reference Types.
  3. Field initialization in structs (this post).
  4. Constructors and struct behavior.
  5. Other scenarios in which struct constructors behavior may surprise you.
  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#.

Continuing with our series on Value Types, given the code below, what do you expect it to print (I recommend trying to answer without running it first):

var s2 = new S2();
System.Console.WriteLine(s2.v);
s2.v = 1;
struct S2
{
public int v = 32;
public S2(int i) => v = i;
}

If you answered 0 (zero) you nailed it and probably already have a good understanding of how .NET/C# handles value type instantiation / field initialization; by the other hand, if you answered 32 you may have been tricked.

In this post I'll briefly discuss one of the aspects that leads to this behavior: field initialization in .NET, or to be more precise, in C#.

In an oversimplified way, whenever C# compiler finds a field initialization it will simply move the initialization code to the constructors, i.e, initializing a field is equivalent to setting its value in the constructors (static fields are initialized in static constructors), so given the code below:

public class Foo
{
private int f = 42;
public Foo()
{
System.Console.WriteLine("Foo()");
}
public Foo(int i)
{
System.Console.WriteLine("Foo(int)");
}
}

the C# compiler will process it as it was written as:

public class Foo
{
private int f;
public Foo()
{
f = 42;
System.Console.WriteLine("Foo()");
}
public Foo(int i)
{
f = 42;
System.Console.WriteLine("Foo(int)");
}
}
which you can confirm in the generated IL below, but keep the following in mind if you're not familiar with IL code
:

  1. There's no need to be daunted by its apparent complexity.
  2. I've included comments to emphasize the key parts.
  3. I've omitted certain less crucial specifics.
  4. Grasping all the details isn't important to understand the main concept.

.class public auto ansi beforefieldinit Foo extends [System.Runtime]System.Object
{
.field private int32 f // `f` is a *private* field of type *int32* (i.e, System.Int32)
// Constructor taking no parameters
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
// IL_0000~IL_0003 is the generated IL for the C# code: `f = 42;`
IL_0000: ldarg.0
IL_0001: ldc.i4.s 42
IL_0003: stfld int32 Foo::f
// IL_0008~IL_0009: runs base constructor
IL_0008: ldarg.0
IL_0009: call instance void [System.Runtime]System.Object::.ctor()
// IL_000e~IL_0013: calls `Console.WriteLine("Foo()")`
IL_000e: ldstr "Foo()"
IL_0013: call void [System.Console]System.Console::WriteLine(string)
IL_0018: ret
} // end of method Foo::.ctor
// Constructor taking 1 int parameter
.method public hidebysig specialname rtspecialname instance void .ctor (int32 i) cil managed
{
// IL_0000~IL_0003 is the generated IL for the C# code: `f = 42;`
// Note that the same code used to assign 42 to `f` was emitted in this constructor as well.
IL_0000: ldarg.0
IL_0001: ldc.i4.s 42
IL_0003: stfld int32 Foo::f
// rest of constructor removed for brevity.
//...
IL_0018: ret
} // end of method Foo::.ctor
} // end of class Foo

Lines 6~22 and 25~36 defines, respectively, the constructor taking no parameters and the one taking an integer;  note that the code related to field initialization (`f = 42`) has been introduced in both (lines 9~11 and 29~31).

In the next post we'll start exploring struct constructor behavior, a.k.a, the second part of the puzzle that explains why the program at the top of the post prints 0.

As always, all feedback is welcome.

Have fun!

No comments: