Billion Dollar Mistake
Null Handling in C#

Rainer Stropek | @rstropek

The Problem

var p = Parse("FooBar");
if (string.IsNullOrEmpty(p.LastName))
{
    Console.WriteLine("Person does not have a last name");
}

/// <summary>
/// Parses person data
/// </summary>
/// <returns>
/// Person data or <c>null</c> if given string is invalid.
/// </returns>
static Person Parse(ReadOnlySpan<char> personData)
{
    var commaIx = personData.IndexOf(',');
    if (commaIx == -1) { return null; }

    return new Person(
        personData[..commaIx].ToString(),
        personData[(commaIx + 1)..].ToString());
}

record Person(string FirstName, string LastName);

Developer did not
read documentation 🥴

static Person? Parse(ReadOnlySpan<char> personData)
{
    var commaIx = personData.IndexOf(',');
    if (commaIx == -1) { return null; }
    
    return new Person(
        personData[..commaIx].ToString(), 
        personData[(commaIx + 1)..].ToString());
}

readonly record struct Person(string FirstName, string LastName);

Situation has always been a little bit better with value types (structs)

Goal: Make C# Null-Safe

  • Other languages don't have null at all (e.g. safe Rust)
    • Let's make C# equally powerful (or lets at least try to come near)
  • How? Let's make reference types nullable, similar to value types
    • Forces us to be explicit regarding nullability

Excursus: How Other Langs Solve it

fn main() {
    let result = match get_maybe_null_string(2) {
        Some(s) => s,
        None => "Sorry, not string".to_string()
    };
    println!("{result}");

    let result = get_maybe_null_string(2);
    if let Some(s) = result {
        println!("We got a result and it is {s}");
    }
}

fn get_maybe_null_string(ix: i32) -> Option<String> {
    if ix % 2 == 0 { Some("FooBar".to_string()) } else { None }
}

Rust

Code does not compile if None handling is forgotten

Challenges

  • What about existing code?
    • Errors would break existing code bases ➡️ Warnings
    • Big bang migration not feasible ➡️ Stepwise enabling of null safeness
  • If it is a compiler feature, what about class libraries?
    • ➡️ Automatically add attributes
  • ​What about manual checks?
    • ➡️ Intelligent flow analysis
    • And in libraries? ➡️ Instrument code with attributes
  • ​What if I know better than the compiler?
    • ➡️ Null forgiving operator !

Nullable Reference Types

Person? p = TryGetPerson(...)

Nullable Contexts

  • disable
    • Work just like in the past
    • No warnings, no protection, no nullable ref types
    • Legacy projects that do not get a lot of love💔
  • warnings
    • See what warnings you would get if you enable nullable ref types,
      work on the warnings
    • No declaration of nullable ref types yet (would lead to warnings)

Nullable Contexts

  • annotations
    • Nullable reference types are possible
    • No warnings in case of potential dereferencing of null
  • enable
    • All shiny new features, including nullable ref types
    • Warnings in case of potential dereferencing of null
    • For your most 💕 projects

Nullable Contexts

How to Enable/Disable?

  • In .csproj
    • For entire project
    • <Nullable>enable</Nullable>
  • Using #nullable in source code
    • E.g.#nullable enable

⚠️ Not a Perfect System! ⚠️

// Note: This code is warning free

#nullable enable

var p = new Person("Foo");
// ⚠️ Uninitialized struct member, initialized with default = null

if (p.LastName.Length > 0) // ⚡ null dereferencing
{
    Console.WriteLine($"Person has a last name and it is {p.LastName}");
}

record struct Person(string FirstName)
{
    public string LastName { get; set; }
}

⚠️ Not a Perfect System! ⚠️

// This code is warning free

#nullable enable

var people = new Person[10];
// ⚠️ Uninitialized array members, initialized with default = null

if (string.IsNullOrEmpty(people[0].LastName)) // ⚡ null dereferencing
{
    Console.WriteLine($"Person has a last name and it is {people[0].LastName}");
}

record Person(string FirstName, string LastName);

⚠️ Not a Perfect System! ⚠️

// Set c to default. That will lead to a null reference.
c = default;
c.Value = 0815;

/// <summary>
/// Simple container using a ref field in a ref struct
/// </summary>
ref struct Container<T>
{
    ref T value;

    public Container(ref T value) => this.value = ref value;

    public T Value
    {
        get => this.value;
        set => this.value = value;
    }
}

Flow Analysis

C# is smart 🧠

Intelligent Flow Analysis

#nullable enable

static string? GetHolidayName(DateOnly date)
{
    return date switch
    {
        { Day: 1, Month: 1 } => "New Year's Day",
        { Day: 4, Month: 7 } => "Independence Day",
        { Day: 25, Month: 12 } => "Christmas Day",
        var d when d == GaussEaster(date.Year) => "Easter",
        _ => null
    };
}

static string TranslateHolidayName(string holidayName)
{
    return holidayName switch {
        "New Year's Day" => "Neujahr",
        "Independence Day" => "Unabhängigkeitstag",
        "Christmas Day" => "Weihnachten",
        "Easter" => "Ostern",
        _ => throw new ArgumentException($"Unknown holiday name: {holidayName}")
   };
}

static DateOnly GaussEaster(int Y)
{
    float A, B, C, P, Q, M, N, D, E;

    A = Y % 19;
    B = Y % 4;
    C = Y % 7;
    P = (float)(int)(Y / 100);
    Q = (float)(int)((13 + 8 * P) / 25);
    M = (int)(15 - Q + P - (int)(P / 4)) % 30;
    N = (int)(4 + P - (int)(P / 4)) % 7;
    D = (19 * A + M) % 30;
    E = (2 * B + 4 * C + 6 * D + N) % 7;
    var days = (int)(22 + D + E);

    if ((D == 29) && (E == 6))
    {
        return new DateOnly(Y, 4, 19);
    }

    else if ((D == 28) && (E == 6))
    {
        return new DateOnly(Y, 4, 18);
    }
    else
    {
        if (days > 31)
        {
            return new DateOnly(Y, 4, days - 31);
        }
        else
        {
            return new DateOnly(Y, 3, days);
        }
    }
}

Intelligent Flow Analysis

#nullable enable
using System.Diagnostics;

var holidayName = GetHolidayName(new(2023, 4, 9));

// ⚠️ The following line gives us a warning as holidayName might be null
Console.WriteLine(TranslateHolidayName(holidayName));

if (holidayName != null)
{
    // No warning as holidayName cannot be null (because of the if statement)
    Console.WriteLine(TranslateHolidayName(holidayName));
}

Debug.Assert(holidayName != null, "holidayName cannot be null");
// No warning as holidayName cannot be null (because of Debug.Assert)
Console.WriteLine(TranslateHolidayName(holidayName));

if (!string.IsNullOrEmpty(holidayName))
{
    // No warning as holidayName cannot be null
    // (because of the IsNullOrEmpty inside the if statement)
    Console.WriteLine(TranslateHolidayName(holidayName));
}

Sometimes, C# Needs Help

No flow analysis accross function calls

If that is the case, how can Debug.Assert and
string.IsNullOrEmpty do their magic 🪄?

Sometimes, C# Needs Help

#nullable enable

string? input = null;
const bool defaultValue = true;
var transformed = (input == null && defaultValue) ? "default" : input;
Console.WriteLine(transformed.Length);

var val = Transform(null, defaultValue);
Console.WriteLine(val.Length);

string? Transform(string? input, bool defaultValue)
{
    if (input == null && defaultValue) { return "default"; }
    return input;
}

Sometimes, C# Needs Help

namespace System.Diagnostics
{
    public static partial class Debug
    {
    	public static void Assert([DoesNotReturnIf(false)] bool condition) => ...
        ...
    }
}
namespace System
{
    public sealed partial class String : ...
    {
    	public static bool IsNullOrEmpty([NotNullWhen(false)] string? value) { ... }
        ...
    }
}

Sometimes, C# Needs Help

Null-related Operators

???!!!

Null Coalescing

#nullable enable

// Null coalescing operator
Console.WriteLine(IntoNotNull(null));
Console.WriteLine(MultipleIntoNotNull(null, "FooBar"));
static string IntoNotNull(string? maybeNullString) => maybeNullString ?? "default";
static string MultipleIntoNotNull(string? maybeNullString, string? second)
    => maybeNullString ?? second ?? "default";

// Null coalescing operator with throw expression
Console.WriteLine(ThrowIfNull("FooBar"));
static string ThrowIfNull(string? maybeNullString) 
    => maybeNullString ?? throw new ArgumentNullException(null, nameof(maybeNullString));

ThrowIfNullStatement("FooBar");
static void ThrowIfNullStatement(string? maybeNullString) 
    => _ = maybeNullString ?? throw new ArgumentNullException(null, nameof(maybeNullString));

Null Coalescing Assignment

#nullable enable

List<int>? numbers = null;
Push(42);
Push(43);
Console.WriteLine(JsonSerializer.Serialize(numbers));

void Push(int number)
{
    // Null coalescing assignment operator
    numbers ??= new List<int>();
    numbers.Add(number);
}

Null Conditional Operator

#nullable enable
using System.Text.Json;
using System.Text.Json.Nodes;

Console.WriteLine(GetNameOfHero(42));
Console.WriteLine(GetNameOfHero(44));
string GetNameOfHero(int id)
{
    var heroes = JsonNode.Parse("""
        {
            "theBoys": [
                { "id": 42, "name": "Starlight", "canFly": false },
                { "id": 43, "name": "Homelander", "canFly": true }
            ]
        }
        """);

    var theBoys = heroes?["theBoys"] as JsonArray;
    //                                     +---- Null conditional indexer
    //                                     |       +---- Null conditional operator
    //                                     |       |                  +---- Null coalescing operator
    //                                     v       v                  v
    return theBoys?.FirstOrDefault(b => ((b?["id"])?.GetValue<int?>() ?? -1) == id)?["name"]
        ?.GetValue<string?>() ?? "not found";
}

Null Forgiving Operator

#nullable enable
using System.Text.Json;
using System.Text.Json.Nodes;

Console.WriteLine(GetNameOfHeroForgiving(42));
Console.WriteLine(GetNameOfHeroForgiving(44));
string GetNameOfHeroForgiving(int id)
{
    var heroes = JsonNode.Parse("""
        {
            "theBoys": [
                { "id": 42, "name": "Starlight", "canFly": false },
                { "id": 43, "name": "Homelander", "canFly": true }
            ]
        }
        """);

    var theBoys = heroes!["theBoys"] as JsonArray;

    // Assumption: We KNOW that theBoys is not null and that properties id and name exist
    // -> We can use the ! operator to tell the compiler that we know what we are doing
    return theBoys!.FirstOrDefault(b => (b!["id"])!.GetValue<int>() == id)?["name"]
        !.GetValue<string?>() ?? "not found";
}

Null Pattern

#nullable enable

Console.WriteLine(NullPattern1(null));
string NullPattern1(string? input)
{
    // Note: Null pattern ignores overloads of == operator
    if (input is null)
    {
        return "default";
    }

    return input;
}

Console.WriteLine(NullPattern2(null));
string NullPattern2(string? input)
{
    if (input is not null)
    {
        return input;
    }

    return "default";
}

C# 11 🤘

Rainer Stropek | @rstropek