Watch Out For This C# struct Constructor Gotcha

October 03, 2022#Software Development
Article
Author image.

Philippe Vaillancourt, Senior Consultant

In this article we’ll show how structs and classes have different behaviors when it comes to constructors and why you need to be careful when using a struct to enforce preconditions in a constructor.

The following article is an adaptation of the above video ☝️ I made about this topic. If you prefer to watch the video, you can do so righ here.

So I was watching a video on Youtube about how to avoid the primitive obsession code smell by using a custom type to enforce preconditions in the constructor. In the video, the author uses a struct instead of a class to enforce preconditions. I thought that was a great idea, so I tried it out. I was surprised to find that the struct constructor didn’t work the way I expected. Let’s take a look at what I mean.

The following is almost the exact implementation proposed by the author in the video. It’s a struct that represents a user name string. It has a constructor that throws an exception if the string is empty or null. The idea is that instead of passing around the username as a string throughout our application and always having to worry that the string might be null or empty and having to check for that, we can instead pass around this UserName type and be sure that it’s always valid.

public readonly record struct UserName
{
  private string Value { get; }

  public UserName(string value)
  {
    if (string.IsNullOrEmpty(value))
    {
      throw new ArgumentNullException(nameof(value));
    }

    Value = value;
  }

  public override string ToString() => $"The value is: {Value}";
}

Incidentally, if you’re finding yourself writing a lot of these guard clauses, you might find a library like Ardalis.GuardClauses helpful, since it turns the 4-line if statement into a single line.

At first glance, it looks like it should work just as we expect. The constructor throws an exception if the value is null or empty. If more familiar with classes than structs, we could easily think that this implementation is enough to guarantee that we’d never get a null or empty username when using this type. But things get interesting once we realize that we can actually instantiate a UserName struct without passing in any value at all to the constructor. Give it a try, remove the argument on line 3, and see what happens.

So what’s going on here? Why is it that we can instantiate a struct without passing in any value to the constructor? Well, it turns out that structs have a default constructor that takes no arguments. And that default constructor will initialize all the fields in the struct to their default values. So in this case, the Value field will be initialized to null. And that’s why we can instantiate a UserName struct without passing in any value to the constructor.

Now you’re probably thinking “Ok, nothing special there, classes pretty much do the same thing”. And you’re right. If we change the UserName struct to a class, and remove our constructor, we get the same behavior. We can instantiate a UserName class without passing in any value to the constructor. And the Value field will be initialized to null.

But notice the difference here, we had to remove our constructor. If we leave the constructor in place, the compiler won’t let us instantiate UserName without passing in a value to the constructor. And here lies the difference between how classes and structs deal with constructors.

Classes have a default empty constructor. But when we define our own constructor, that new constructor now becomes the default constructor and the default empty constructor is no longer available. So if we want to instantiate a UserName class without passing in a value to the constructor, we need to define a default empty constructor ourselves.

Structs on the other hand, don’t work the same way. You can think of structs as always having an empty constructor available. The fact that you define a constructor with parameters doesn’t remove the struct empty constructor. And that is why we were able to instantiate a UserName struct without passing in any value to the constructor.

I’m sure you can see how that is a problem. If we want to enforce preconditions in a constructor, we need to make sure that the constructor is always called. And if we use a struct, we can’t guarantee that the constructor will always be called. So if we want to enforce preconditions in a constructor, we’re probably better off using a class.

That being said, if you’re dead set on using a struct, you can still enforce preconditions, at runtime, in a constructor. You just need to make sure that you define a default empty constructor yourself. And that’s what we do in the following example.

public readonly record struct UserName
{
  private string Value { get; }

  public UserName(): this(null) {}

  public UserName(string value)
  {
    if (string.IsNullOrEmpty(value))
    {
      throw new ArgumentNullException(nameof(value));
    }

    Value = value;
  }

  public override string ToString() => $"The value is: {Value}";
}

Now, if we instantiate a UserName struct without passing in any value to the constructor, we’ll get an exception. And that’s because the default empty constructor will call the constructor with the string parameter and pass in null. And that will throw an exception.

Note that this will enforce our preconditions at runtime, but not at compile time. With a class, we can enforce the rule at runtime AND at compile time, by getting rid of the empty constructor altogether. For this reason, I would recommend using a class instead of a struct if you want to enforce preconditions in a constructor.


Copyright © 2024 NimblePros - All Rights Reserved