How To Ensure Correct Struct Initialization In C#

Posted by : on

Category : C#

Introduction

Since structs are value types, initializing a struct defaults to an empty instance with all its fields set to their default values, which means all bits are zeroed. Subsequently, the struct’s fields are initialized either by field initializers or by the struct’s constructor.

Even if we define a constructor for our struct, including a parameterless one, in C#, structs always have an implicit parameterless constructor. Under certain conditions, our custom constructors might not execute, causing the fields to remain uninitialized by our initializers and instead be set by the default constructor.

This can lead to bugs if these rules are not well understood. Let’s explore when this can occur and how to ensure the correct initialization of our structs, even when our constructors do not run and our field initializers fail to initialize our fields.

When Only The Default Struct Constructor Is Used In Initialization

Let’s suppose that we have the following struct:

public struct ExampleStruct
{
   public readonly int ValueField = 2;
   public readonly bool BoolField = true;
   public readonly string ReferenceField = "Test field";

   public int ValueProperty { get; } = 3;
   public bool BoolProperty { get; } = true;
   public string ReferenceProperty { get; } = "Test property";

   public ExampleStruct() {}
}

If we create a new instance of a struct normally with the new keyword:

ExampleStruct newStruct = new ExampleStruct();

And run the following method with the newStruct as parameter:

void ShowStruct(ExampleStruct ex)
{
   Console.WriteLine($"ValueField = {ex.ValueField}");
   Console.WriteLine($"BoolField = {ex.BoolField}");
   Console.WriteLine($"ReferenceField = {ex.ReferenceField}");
   Console.WriteLine($"Value Property = {ex.ValueProperty}");
   Console.WriteLine($"BoolProperty = {ex.BoolProperty}");
   Console.WriteLine($"ReferenceProperty = {ex.ReferenceProperty}");
}

We will get what we expect:

ValueField = 2
BoolField = True
ReferenceField = Test field
Value Property = 3
BoolProperty = True
ReferenceProperty = Test property

But if we instantiate our struct like this:

ExampleStruct defaultStruct = default;
ShowStruct(defaultStruct);

Then, the fields never get initialized with our own values, they get initialized with their default values:

ValueField = 0
BoolField = False
ReferenceField = 
Value Property = 0
BoolProperty = False
ReferenceProperty = 

If this was the only case, it would be easy to avoid, but someone can also do this:

ExampleStruct[] structArray = new ExampleStruct[2];
ShowStruct(structArray[0]);

Or even initialize a struct with a less obvious way like this:

public struct ExternalStruct
{
   public ExampleStruct ExampleStruct;
}

ExternalStruct externalStruct = new ExternalStruct();
ShowStruct(externalStruct.ExampleStruct);

In all three of the above cases, the result would be the same, our struct would only be initialized by the default values of the implicit parameterless constructor, that as mentioned, is the zeroing of all the bits of the struct’s fields.

The same would have happened, even if we had initialized our fields in our own parameterless constructor, like this:

public struct ExampleStruct
{
   public readonly int ValueField;
   public readonly bool BoolField;
   public readonly string ReferenceField;

   public int ValueProperty { get; }
   public bool BoolProperty { get; }
   public string ReferenceProperty { get; }

   public ExampleStruct()
   {
      ValueField = 2;
      BoolField = true;
      ReferenceField = "Test field";
      ValueProperty = 3;
      BoolProperty = true;
      ReferenceProperty = "Test property";
   }
}

Or even if we had used a constructor with parameters:

public struct ExampleStruct
{
   public readonly int ValueField;
   public readonly bool BoolField;
   public readonly string ReferenceField;

   public int ValueProperty { get; }
   public bool BoolProperty { get; }
   public string ReferenceProperty { get; }

   public ExampleStruct(int valueField, bool boolField, string referenceField, int valueProperty, bool boolProperty, string referenceProperty)
   {
      ValueField = valueField;
      BoolField = boolField;
      ReferenceField = referenceField;
      ValueProperty = valueProperty;
      BoolProperty = boolProperty;
      ReferenceProperty = referenceProperty;
   }
}

How to Ensure Correct Struct Initialization

Let’s see the different ways that we can have a solution to this problem.

Choose Default Values That Correspond To C#’s Default Values

The first way to ensure correct initialization of our structs, is to create our fields in such a way that their default value is our desired default value. For example, the default value for any boolean is false, so instead of creating a boolean isOpen with an assigned default value true, we can have our struct implement a boolean isClosed that anyway default to false.

This is what Microsoft has done with the struct that is responsible for the nullable value types. This struct, instead of having an IsNull boolean variable that would need to default to true, so that the default value for a nullable value type would be null, has a HasValue boolean variable that automatically defaults to false anyway.

Use Computed Properties Instead Of Default Values

Computed properties are evaluated each read, so instead of initializing our properties we can return the desired value from the getter, for example:

public int ValueProperty => 3;
public string ReferenceProperty => "Test property";

Use The Null-Coalescing Operator For Reference Types

Combining computed properties with the null-coalescing operator can ensure that we get the desired value, as the default value for all reference types is null. This is especially useful, if our constructor has parameters that initialize our property, or our property is not read-only. For example:

public struct ExampleStruct
{
   private string _referenceProperty;
   public string ReferenceProperty
   {
      get => _referenceProperty ?? "Test property";
      set => _referenceProperty = value;
   }

   public ExampleStruct(string referenceProperty)
   {
      _referenceProperty = referenceProperty;
   }
}

Use A Nullable Value Type As A Backing Field For Value Type Properties

The trick that we used above for reference types, can be used for value types, if we have a nullable value type as a backing field to our property. As mentioned before, the nullable value type is a generic struct, that will default to null, so we can use this behavior to write our structs like this:

public struct ExampleStruct
{
   private int? _valueField;
   public int ValueField
   {
      get => _valueField ?? 2;
      set => _valueField = value;
   }

   public ExampleStruct(int valueField)
   {
      _valueField = valueField;
   }
}

The above will always give a default value for our property of 2, no matter the way the instance of our struct was created.

Conclusion

When creating structs, especially those intended for use by other programmers, it is our responsibility to ensure they always maintain a valid state—just as we do with classes. Given that structs are value types, they can be instantiated in ways where C# assigns them default values by zeroing the bits of their fields. This can bypass our initialization logic, whether from field initializers or constructors. By using the above solutions, we can ensure that no bugs occur due to structs being initialized in an invalid state.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: