Article 4FVZZ Lying to the compiler

Lying to the compiler

by
jonskeet
from Jon Skeet's coding blog on (#4FVZZ)
Story Image

This morning I tweeted this:

Just found a C# 8 nullable reference types warning in Noda Time. Fixing it by changing Foo(x, x?.Bar) to Foo(x, x?.Bar!) which looks really dodgy" anyone want to guess why it's okay?

This attracted more interest than I expected, so I thought I'd blog about it.

First let's unpack what x?.Bar! means. x?.Bar means "if x is non-null, the value of x.Bar; otherwise, the corresponding null value". The ! operator at the end is introduced in C# 8, and it's the damn-it operator (more properly the "null-forgiving operator", but I expect to keep calling it the damn-it operator forever). It tells the compiler to treat the preceding expression as "definitely not null" even if the compiler isn't sure for whatever reason. Importantly, this does not emit a null check in the IL - it's a compile-time only change.

When talking about the damn-it operator, I've normally given two scenarios where it makes sense:

  • When testing argument validation
  • When you have invariants in your code which allow you to know more than the compiler does about nullability. This is a little bit like a cast: you're saying you know more than the compiler. Remember that it's not like a cast in terms of behaviour though; it's not checked at execution time.

My tweet this morning wasn't about either of these cases. It's in production code, and I absolutely believe that it's possible for x?.Bar to be null. I'm lying to the compiler to get it to stop it emitting a warning. The reason is that in the case where the value is null, it won't matter that it's null.

The actual code is in this Noda Time commit, but the code below provides a simplified version. We have three classes:

  • Person, with a name and home address
  • Address, with some properties I haven't bothered showing here
  • Delivery, with a recipient and an address to deliver to
using System;public sealed class Address{ // Properties here}public sealed class Person{ public string Name { get; } public Address HomeAddress { get; } public Person(string name, Address homeAddress) { Name = name ?? throw new ArgumentNullException(nameof(name)); HomeAddress = homeAddress ?? throw new ArgumentNullException(nameof(homeAddress)); }}public sealed class Delivery{ public Person Recipient { get; } public Address Address { get; } public Delivery(Person recipient) : this(recipient, recipient?.HomeAddress!) { } public Delivery(Person recipient, Address address) { Recipient = recipient ?? throw new ArgumentNullException(nameof(recipient)); Address = address ?? throw new ArgumentNullException(nameof(address)); }}

The interesting part is the Delivery(Person) constructor, that delegates to the Delivery(Person, Address) constructor.

Here's a version that would compile with no warnings:

public Delivery(Person recipient) : this(recipient, recipient.HomeAddress)

However, now if recipient is null, that will throw NullReferenceException instead of the (preferred) ArgumentNullException. Remember that nullable reference checking in C# 8 is really just advisory - the compiler does nothing to stop a non-nullable reference variable from actually having a value of null. This means we need to keep all the argument validation we've already got.

We could validate recipient before we pass it on to the other constructor:

public Delivery(Person recipient) : this(recipient ?? throw new ArgumentNullException(...), recipient.HomeAddress)

That will throw the right exception, but it's ugly and more code than we need. We know that the constructor we're delegating to already validates recipient - we just need to get that far. That's where the null-conditional operator comes in. So we can write:

public Delivery(Person recipient) : this(recipient, recipient?.HomeAddress)

That will behave as we want it to - if recipient is null, we'll pass null values as both arguments to the other constructor, and it will validate them. But now the compiler warns that the second argument could be null, and the parameter is meant to be non-null. The solution is to use the damn-it operator:

public Delivery(Person recipient) : this(recipient, recipient?.HomeAddress!)

Now we get the behaviour we want, with no redundant code, and no warnings. We're lying to the compiler and satisfied that we're doing so sensibly, because recipient?.HomeAddress is only null if recipient is null, and we know that that will be validated first anyway.

I've added a comment, as it's pretty obscure otherwise - but part of me just enjoys the oddity of it all :)

External Content
Source RSS or Atom Feed
Feed Location http://codeblog.jonskeet.uk/feed/
Feed Title Jon Skeet's coding blog
Feed Link https://codeblog.jonskeet.uk/
Reply 0 comments