Double storage in class with primary constructor

October 6th 2023 C#

.NET 8 is going to be released soon and with it C# 12 as well. Its largest new feature are most likely primary constructors. With this new feature, you can mostly get rid of trivial constructors used only for assigning parameter values to private fields. However, if you're not careful, you might end up with the same piece of data being stored in two fields which can easily diverge and cause bugs in your code.

You can reference primary constructor parameters in different parts of your class. Depending on where you do it, the parameter will be handled differently by the compiler:

  • As soon as you reference a parameter in a body of any class member (method or property), the compiler will create a hidden field that will hold the value for you.
  • If you only use a parameter in field or property initializers, the compiler won't create the before-mentioned hidden field, as the parameter is only directly needed during class initialization.

If you do both of the above, you're very likely to introduce a bug in your code, as the parameter value will end up being stored in two different fields. As soon as you modify the value of one of them, you will get a different value depending on where you read it from.

The following small class demonstrates this problem:

public class User(string username)
{
    public string Username { get; set; } = username;

    public override string ToString()
    {
        return username;
    }
}

As you can see, the username parameter is used in two places:

  • as the initial value for the Username auto-property, and
  • as the return value of the ToString() method.

This means that the value will be stored in two places:

  • in the auto-generated backing field for the Username property, and
  • in the auto-generated hidden field for the username parameter, used in the ToString() method.

You can check that for yourself in Sharplab. Just enter the above code and see the two username fields in the generated C# code:

[System.Runtime.CompilerServices.NullableContext(1)]
[System.Runtime.CompilerServices.Nullable(0)]
public class User
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <username>P;

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <Username>k__BackingField;

    public string Username
    {
        [CompilerGenerated]
        get
        {
            return <Username>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Username>k__BackingField = value;
        }
    }

    public User(string username)
    {
        <username>P = username;
        <Username>k__BackingField = <username>P;
        base..ctor();
    }

    public override string ToString()
    {
        return <username>P;
    }
}

If you change the value of the Username property after constructing the class, the values in the two fields will not be the same anymore, which most likely is not the desired behavior:

[Test]
public void PropertyAndParameterValueCanDiverge()
{
    var user = new User("damir");
    user.Username = "damira";

    user.Username.Should().NotBe(user.ToString());
}

Fortunately, the compiler will emit a warning in such a case:

Parameter 'string username' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.

As long as you make sure to regularly fix the compiler warnings in your code, you should be safe.

So, what is the recommended fix for the above code? If you're using the parameter value to initialize another member in your class, you should then always use that member to read the parameter value instead of the parameter directly. In this particular case, this means that you should read the username value in the ToString() method from the Username property instead of from the username parameter:

public class User(string username)
{
    public string Username { get; set; } = username;

    public override string ToString()
    {
        return Username;
    }
}

Now, even if you change the value of the Username property, the ToString() method will correctly return its new value instead of the original value of the username field:

[Test]
public void ParameterValueIsOnlyStoredInTheProperty()
{
    var user = new User("damir");
    user.Username = "damira";

    user.Username.Should().Be(user.ToString());
}

If you want to run the code yourself, you can check my GitHub repository. The last commit contains the code with the suggested fix. The one before that, the broken code from the start of this post.

Primary constructors can be a useful addition to C# but you should understand how it works under the hood to avoid potential pitfalls. And even more importantly, you should never ignore compiler warnings, as more often than not they are an indicator of a potential bug in your code.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License