89
votes

I've recently seen the following code:

public class Person
{
    //line 1
    public string FirstName { get; }

    //line 2
    public string LastName { get; } = null!;

    //assign null is possible
    public string? MiddleName { get; } = null;

    public Person(string firstName, string lastName, string middleName)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName;
    }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = null;
    }
}

Basically, I try to dig into new c# 8 features. One of them is NullableReferenceTypes.
Actually, there're a lot of articles and information about it already. E.g. this article is quite good.
But I didn't find any information about this new statement null!
Can someone provide me an explanation for it?
Why do I need to use this?
And what is the difference between line1 and line2?

4
! is the null-forgiving operator, telling the compiler that, even though it normally wouldn't allow it, it should look the other way and allow it anyway, because we know better. null! itself has little practical use, as it all but negates the usefulness of nullable reference types. It's more useful when you know an expression can't be null, but the compiler doesn't. - Jeroen Mostert
@JeroenMostert so smth like force? I mean even if it is unusual lets do that forced. - isxaker
Yes, except it's more than unusual -- because string, under the new rules, is not a nullable reference type, and so should never be null. Assigning null! effectively says "I know this should never be null, but guess what, I'm doing it anyway". There's almost no program where that would make sense -- the only reason to do it would be because you know you're going to assign a non-null value before anyone could get a NullReferenceException, and want to signal that you haven't forgotten to assign it. Possible, but unlikely, so not very good as an example. - Jeroen Mostert
@JeroenMostert I have found the null! to be useful in unit tests. Just because a reference shouldn't be null doesn't necessarily mean it won't be, so sometimes testing for the right behavior in that situation is appropriate. - Jesse
@JeroenMostert It's also useful for game engines like Godot where you commonly defer reference field initialization for nodes and resources until the _Ready() callback method, instead of the constructor. It effectively shuts up the compiler. - knightofiam

4 Answers

128
votes

The key to understanding what null! means is understanding the ! operator. You may have used it before as the "not" operator. However, since C# 8.0 and its new "nullable-reference-types" feature, the operator got a second meaning. It can be used on a type to control Nullability, it is then called the "Null Forgiving Operator"


Typical usage

Assuming this definition:

class Person
{
    // Not every person has a middle name. We express "no middle name" as "null"
    public string? MiddleName;
}

The usage would be:

void LogPerson(Person person)
{
    Console.WriteLine(person.MiddleName.Length);  // WARNING: may be null
    Console.WriteLine(person.MiddleName!.Length); // No warning
}

This operator basically turns off the compiler null checks for this usage.

Technical Explanation

Null Safety

C# 8.0 tries to help you manage your null-values. Instead of allowing you to assign null to everything by default, they have flipped things around and now require you to explicitly mark everything you want to be able to hold a null value.

This is a super useful feature, it allows you to avoid NullReferenceExceptions by forcing you to make a decision and enforcing it.

How it works

There are 2 states a variable can be in - when talking about null-safety.

  • Nullable - Can be null.
  • Non-Nullable - Can not be null.

Since C# 8.0 all reference types are non-nullable by default. Value types have been non-nullable since C# 2.0!

The "nullability" can be modified by 2 new (type-level) operators:

  • ! = from Nullable to Non-Nullable
  • ? = from Non-Nullable to Nullable

These operators are counterparts to one another. The Compiler uses the information, you define with those operators, to ensure null-safety.

Examples

? Operator usage.

This operator tells the compiler that a variable can hold a null value.

  • Nullable string? x;

    • x is a reference type - So by default non-nullable.
    • We apply the ? operator - which makes it nullable.
    • x = null Works fine.
  • Non-Nullable string y;

    • y is a reference type - So by default non-nullable.
    • y = null Generates a warning since you assign a null value to something that is not supposed to be null.

Nice to know: Using string? is syntactic sugar for System.Nullable<string>

! Operator usage.

This operator tells the compiler that something that could be null, is safe to be accessed. You express the intent to "not care" about null safety in this instance.

string x;
string? y;
  • x = y
    • Illegal! Warning: "y" may be null
    • The left side of the assignment is non-nullable but the right side is nullable.
    • So it does not work, since it is semantically incorrect
  • x = y!
    • Legal!
    • y is a reference type with the ? type modifier applied so it is nullable if not proven otherwise.
    • We apply ! to y which overrides its nullability settings to make it non-nullable
    • The right and left side of the assignment are non-nullable. Which is semantically correct.

WARNING The ! operator only turns off the compiler-checks at a type-system level - At runtime, the value may still be null.

Use carefully!

You should try to avoid using the Null-Forgiving-Operator, usage may be the symptom of a design flaw in your system since it negates the effects of null-safety you get guaranteed by the compiler.

Reasoning

Using the ! operator will create very hard to find bugs. If you have a property that is marked non-nullable, you will assume you can use it safely. But at runtime, you suddenly run into a NullReferenceException and scratch your head. Since a value actually became null after bypassing the compiler-checks with !.

Why does this operator exist then?

There are valid use-cases (outlined in detail below) where usage is appropriate. However, in 99% of the cases, you are better off with an alternative solution. Please do not slap dozens of !'s in your code, just to silence the warnings.

  • In some (edge) cases, the compiler is not able to detect that a nullable value is actually non-nullable.
  • Easier legacy code-base migration.
  • In some cases, you just don't care if something becomes null.
  • When working with Unit-tests you may want to check the behavior of code when a null comes through.

Ok!? But what does null! mean?

It tells the compiler that null is not a nullable value. Sounds weird, doesn't it?

It is the same as y! from the example above. It only looks weird since you apply the operator to the null literal. But the concept is the same. In this case, the null literal is the same as any other expression/type/value/variable.

The null literal type is the only type that is nullable by default! But as we learned, the nullability of any type can be overridden with ! to non-nullable.

The type system does not care about the actual/runtime value of a variable. Only its compile-time type and in your example the variable you want to assign to LastName (null!) is non-nullable, which is valid as far as the type-system is concerned.

Consider this (invalid) piece of code.

object? null;
LastName = null!;
16
votes

When the "nullable reference types" feature is turned on, the compiler tracks which values in your code it thinks may be null or not. There are times where the compiler could have insufficient knowledge.

For example, you may be using a delayed initialization pattern, where the constructor doesn't initialize all the fields with actual (non-null) values, but you always call an initialization method which guarantees the fields are non-null. In such case, you face a trade-off:

  • if you mark the field as nullable, the compiler is happy, but you have to un-necessarily check for null when you use the field,
  • if you leave the field as non-nullable, the compiler will complain that it is not initialized by the constructors (you can suppress that with null!), then the field can be used without null check.

Note that by using the ! suppression operator, you are taking on some risk. Imagine that you are not actually initializing all the fields as consistently as you thought. Then the use of null! to initialize a field covers up the fact that a null is slipping in. Some unsuspecting code can receive a null and therefore fail.

More generally, you may have some domain knowledge: "if I checked a certain method, then I know that some value isn't null":

if (CheckEverythingIsReady())
{
   // you know that `field` is non-null, but the compiler doesn't. The suppression can help
   UseNonNullValueFromField(this.field!);
}

Again, you must be confident of your code's invariant to do this ("I know better").

1
votes

null! is used to assign null to non-nullable variables, which is a way of promising that the variable won't be null when it is actually used.

I use null! with Entity Framework Core to mark variables initialized via reflection:

public class MyDbContext : DbContext
{
    public DbSet<Entity> Entities { get; set; } = null!; // Assigned by EF Core
    public DbSet<Person> People { get; set; } = null!; // Assigned by EF Core
}

I also use it in unit tests to mark variables initialized by a setup method:

public class MyUnitTests
{
    IDatabaseRepository _repo = null!;

    [OneTimeSetUp]
    public void PrepareTestDatabase()
    {
        ...
        _repo = ...
        ...
    }
}

If you don't use null! in such cases, you'll have to use an exclamation mark every single time you read the variable, which would be a hassle without benefit.

-1
votes

In short, we use it wherever the compiler suggests the existence of a null variable, but we are sure or should assume it is not.

For example:

if (".js".Equals(Path.GetExtension(@"D:\file.js")))
{
    var directory = Path.GetDirectoryName(includeFile);
    var map = Path.Combine(
        directory, // Compiler: Possible 'null' assignment to nun-nullable entity
        "file.map");
}

This is how it is corrected:

if (".js".Equals(Path.GetExtension(@"D:\file.js")))
{
    var directory = Path.GetDirectoryName(includeFile);
    var map = Path.Combine(
        directory!, // The warning is ignored because we are sure the directory variable is not null
        "file.map");
}

There are other cases, such as when in a unit test we intentionally send a null value to a function that does not accept a null argument
For example:

public class Person
{
    public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));

    public string Name { get; }
}

and we test:

[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void NullNameShouldThrowTest()
{
    var person = new Person(null!);
}