It’s been a big week of releases in the world of .NET with the release of both .NET Core 3.0 and C# 8. Let’s upgrade the Wordtutor projects to all the latest versions and see what we learn.

Upgrading Visual Studio 2019

Using the Visual Studio Installer, I upgraded Visual Studio 2019 to the latest available version; this brought down the latest versions of .NET Core and the C# compiler automatically. If you want to upgrade to .NET Core 3.0 separately, there’s a download to do that.

Nullable Reference Types

One of the headline features in C# 8 is the introduction of nullable reference types. This language feature addresses the long-standing issues with NullReferenceException, helping you to write code that avoids this most common of errors.

To turn it on for our projects, we need to set these two values in each of our .csproj files:

<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>

With those turned on, what do we find from a full compile? We’ve been somewhat careful with null references so far in the project, have we been careful enough?

It’s important to note at the outset that all of the feedback from the compiler is in the form of warnings, highlighting code that requires closer attention. The compiler isn’t omniscient - and the halting problem is unsolvable. The point of the feature is to highlight probable problem areas, nothing more.

Constructor parameters

Starting with AddVocabularyWordScreen, we see the warnings in the constructor, complaining that null isn’t valid to apply to a non-nullable type. Since the point of this constructor is to make a near-clone of an existing instance, we want to permit null values so that we know to copy properties from the original. So we change the parameters to string? to indicate they should be nullable.

protected AddVocabularyWordScreen(
    AddVocabularyWordScreen original,
    string? spelling = null,
    string? pronunciation = null,
    string? phrase = null)
{
    if (original is null)
    {
        throw new ArgumentNullException(nameof(original));
    }

    Spelling = spelling ?? original.Spelling;
    Pronunciation = pronunciation ?? original.Pronunciation;
    Phrase = phrase ?? original.Phrase;
}

Note that we don’t get any warnings that the existing original is null test is redundant. The goal of nullable reference types is to help you find potential issues, not to reduce the safety of your existing code.

The pattern of needing to mark constructor parameters as nullable repeats with all of the private near-clone constructors we’ve previously written for our immutable types.

The various Equals() methods on AddVocabularyWordScreen all have warnings for the same reason: it’s valid to pass null, so we mark the parameters as nullable:

public override bool Equals(object? other) => ...

public override bool Equals(Screen? other) => ...

public bool Equals(AddVocabularyWordScreen? other) { ... }

Again, we have a pattern that repeats across many of the types we’ve already written.

Properties

In VocabularyBrowserScreen, it’s quite valid for us to have no selection. You might recall that we recently talked about how no selection is something we need to handle.

Marking the property as nullable is an easy fix:

public VocabularyWord? Selection { get; }

Potential NullReferenceExceptions

Given that Selection can be null, it’s not surprising to get a warning in the Equals(VocabularyBrowserScreen?) about a potential issue:

return Selection.Equals(other.Selection)
    && Modified == other.Modified;

To fix this, let’s declare a static member that can do the comparison for us:

private static readonly EqualityComparer<VocabularyWord> _screenComparer 
    = EqualityComparer<VocabularyWord>.Default;

We can then use that to do the comparison:

return _selectionComparer.Equals(Selection, other.Selection)
    && Modified == other.Modified;

Further down, in GetHashCode(), we take a different approach by using the Elvis operator (?.) to protect the call and provide a useful default:

var selectionHash = Selection?.GetHashCode() ?? 0;
return selectionHash * 23
    + Modified.GetHashCode();

Fields not initialized at construction

The constructor for ReduxSubscription<TState, TValue> is giving us an interesting warning:

Non-nullable field ‘_lastValue’ is uninitialized. Consider declaring the field as nullable.

Making the obvious change, we modify the field declaration to this:

private TValue? _lastValue;

But then we get this error instead:

A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a ‘class’, ‘struct’, or type constraint.

We don’t want to constrain redux subscriptions to just particular types, so adding a type constraint isn’t a good solution for our context.

Elsewhere in ReduxSubscription<TState, TValue>, we compare _lastValue with the value we’ve just read, as a part of deciding whether to publish the value.

public override void Publish(TState state)
{
    var value = _reader(state);
    if (!Released && !_comparer.Equals(value, _lastValue))
    {
        _lastValue = value;
        _whenChanged(value);
    }
}

If the value we read is a null, then we won’t ever publish the value until it changes to something else.

Surely this is a bug? We should always publish the first time, and then only publish when the value changes. Adding a new unit test, we verify that this is indeed a bug!

After fixing things up, we address the original warning by modifying the declaration:

[AllowNull]
private TValue _lastValue = default;

The field is marked with the [AllowNull] attribute - this is us telling the compiler “trust us, we know what we’re doing”. We also need to ensure the field is always initialized.

Conclusion

That’s not a bad process for an upgrade - a handful of places where we needed to explicitly indicate that null was accepted, one place where we needed to tell the compiler we knew what was going on, and three actual bugs identified and fixed.

Prior post in this series:
Commands and CommandBinding
Next post in this series:
Nullable types redux

Comments

blog comments powered by Disqus