Nullable and Required Types

Introduction

Since the introduction of nullable reference types in C#, and also with the required keyword, I sometimes see some confusion, which I'm going to address here. This is kind of back to basics, but, yet, here we are! ;-)

Reference Types

A reference type in C#/.NET is a type that:

  • Are instantiated on the heap
  • Are passed by reference (pointer)
  • Can be null
  • Are declared with the class keyword (can also be record, in latest versions)

Meaning, for example, that we can have this (string is a reference type):

string str = null;
str = "Hello, referece types!";

Value Types

On the other hand, value types:

  • Are instantiated on the stack
  • Are passed by value (copied on every method call)
  • Cannot be null
  • Always have a value
  • Are declared using the struct, enum, or record struct keywords
  • Implicitly inherit from System.ValueType

Value types are numbers, booleans, bytes, characters, enumerations, and some basic structures such as DateTime, Guid, or TimeSpan (amongst many others).

int i = null; //error: a value type cannot be null
int n = 10; //ok

Nullable Value Types

Because of the limitation that value types cannot be null, and because it is sometimes so convenient, nullable value types were introduced: just suffix the type with ?.

int i = null; //error: a value type cannot be null
int? n = null; //ok
n = 10; //ok

A nullable reference type exposes the inner value through the Value property (HasValue checks that it does have one), and always implicitly inherit from Nullable<T>. A nullable value type may or may not have a value (if assigned to null or if not assigned at all). If one attempts to access the Value of an unassigned nullable value type without checking HasValue (or comparing to null), then we have an exception at runtime.

It is safe to pass a value to where a nullable value is expected, but not the other way round:

bool IsNull(int? value) { return !value.HasValue; }
bool IsOdd(int value) { return (value % 2) == 0; }

bool isNull = IsNull(10); //ok
int? n = null;
int i = n; //error: a value type cannot be null
bool isSet = n == null; //or n.HasValue
int i = n.Value; //error: nullable value does not have a value
bool isOdd = IsOdd(n); //error: cannot pass a nullable value to where a value is expected
n = 10;
int i = n.Value; //or just n

Nullable Reference Types

Nullable reference types was a game-changer introduced in .NET. It means that even reference types can no longer be null, as opposes to regular reference types. The compiler enforces this at compile-time, making sure all the possible paths that lead to a variable attribution are checked for nulability. This can actually be good, as nulls sometimes bite us when we last expect.

This is configurable in the project (.csproj), the default is to allow null for reference types:

<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>

If this is set, then we can no longer set a reference type to null, except if we declare it as nullable, again, using ?:

string s = null; //error: reference type cannot be null
string? n = null; //ok

Required Members

Now, required members are a different thing. required can only be applied to members (properties, fields) declared inside some type (reference or value type):

public class Options
{
    public required string Url { get; set; }
}

What this means is, when we construct an instance of the Options class, we need to provide an explicit value for Url:

var options = new Options(); //error: required member Url is not provided
var options = new Options { Url = "..." }; //ok

So, required is independent of the member type being nullable or not (reference or value) - if it is, then at least null has to be explicitly set.

Conclusion

Hope this serves to dissipate some of the confusion. Stay tuned for more!

Comments

Popular posts from this blog

C# Magical Syntax

OpenTelemetry with ASP.NET Core

ASP.NET Core Middleware