Primary constructors and inheritance pitfalls

October 13th 2023 C#

In my previous blog post, I described a case in which you can have the same data stored in two fields when using primary constructors. You can run into a similar problem when using primary constructors in combination with inheritance incorrectly.

When using primary constructors, you define the base class and invoke the base constructor at the same time. Typically, you will pass some primary constructor parameters to the base constructor.

public class PrintedArticle(string author, string title, int noPages)
    : Article(author, title)

You shouldn't access those parameters directly in the body of any member of the derived class. If you do that, the compiler will create a hidden field to hold the parameter value. Since the base class will also very likely store the value of that parameter, the value will now be stored in two places, which will mean that the two values can become different when one of them is changed.

The following two classes are an example of this scenario:

public class Article(string author, string title)
{
    public string Author => author;

    public string Title
    {
        get { return title; }
        set { title = value; }
    }

    public override string ToString()
    {
        return $"{author}: {title}";
    }
}

public class PrintedArticle(string author, string title, int noPages)
    : Article(author, title)
{
    public int NoPages => noPages;

    public override string ToString()
    {
        return $"{title} by {author} ({noPages} pages)";
    }
}

The author and title parameters are both being used in both classes, but only the title is the real risk here because the value can also be modified in the base class. In this case, the value will be different depending on where it is read from:

[Test]
public void TitleValueCanDivergeBetweenBaseAndDerivedClass()
{
    var article = new PrintedArticle("Damir Arh", "What's new in C# 12", 10);
    article.Title = "New features in C# 12";

    article.ToString().Should().NotStartWith(article.Title);
}

Fortunately, the compiler will emit a warning if you do this, so you know you have to fix it:

Parameter 'string title' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.

The fix will typically be rather simple: instead of using the parameter in the derived class directly, you need to use the corresponding member in the base class:

public class PrintedArticle(string author, string title, int noPages)
    : Article(author, title)
{
    public int NoPages => noPages;

    public override string ToString()
    {
        return $"{Title} by {Author} ({noPages} pages)";
    }
}

By doing that, even the derived class will read the value from the base class as it should, ensuring that the value is always up-to-date:

[Test]
public void TitleValueIsTheSameInBaseAndDerivedClass()
{
    var article = new PrintedArticle("Damir Arh", "What's new in C# 12", 10);
    article.Title = "New features in C# 12";

    article.ToString().Should().StartWith(article.Title);
}

You can find the code in my GitHub repository and try it out yourself. The last commit contains the fixed code. The previous one contains the broken code from before the fix.

You can make the mistake of storing a value in the derived class and passing it to the base class even without primary constructors. But you can only do that by explicitly declaring a field and assigning the value to it. With primary constructors, it's easier not to notice the mistake, as you only need to access the parameter directly for that to happen. So you better not ignore the compiler warning.

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

Copyright
Creative Commons License