Nullable reference types: "Try" method pattern, getting warning for null when returning false

Viewed 1091

I have the following method which is using the "Try" pattern.

public bool TryGetValue(string subnet, [NotNullWhen(true)] out TValue value)
{
    if (!TryFindNode(subnet, out var node))
        throw new InvalidOperationException();
    if (TValueIsDefault(node.Value) == false)
    {
        value = node.Value;
        return true;
    }
    value = default;
    return false;
}

On the line value = default - I am getting a compiler warning CS8601: "Possible null reference assignment". The next line returns false, and value has the [NotNullWhen(true)] attribute.

Am I missing something, or is the compiler not carrying the state of value for some reason?

Full sample on sharplab.io and a Github gist

2 Answers

(The following answer is applicable as of Visual Studio 16.6/.NET Core 3.1)

You should use [MaybeNullWhen(false)] instead of [NotNullWhen(true)] in this scenario.

public bool TryGetValue(string subnet, [MaybeNullWhen(false)] out TValue value)
{
    if (!TryFindNode(subnet, out var node))
        throw new InvalidOperationException();
    if (TValueIsDefault(node.Value) == false)
    {
        value = node.Value;
        return true;
    }
    value = default;
    return false;
}

Explanation:

The key point to understand is that applying a [NotNullWhen(true)] attribute does not imply [MaybeNullWhen(false)].

In your sample, it is permitted to supply a non-nullable reference type such as string as a type argument for TValue. This would produce the following method:

public bool TryGetValue(string subnet, [NotNullWhen(true)] out string value)
{
    if (!TryFindNode(subnet, out var node))
        throw new InvalidOperationException();
    if (TValueIsDefault(node.Value) == false)
    {
        value = node.Value;
        return true;
    }
    value = default; // Wait, default(string) is null! We can't do this!
    return false;
}

What you probably want to do is instead of telling your caller the method will return "not null when true", tell them it "may return null when false". Then the type argument to your containing type TreeNode<TValue> will inform whether the caller should expect a non-null value when this method returns true. Thus the instantiation looks like:

public bool TryGetValue(string subnet, [MaybeNullWhen(false)] out string value)
{
    if (!TryFindNode(subnet, out var node))
        throw new InvalidOperationException();
    if (TValueIsDefault(node.Value) == false)
    {
        value = node.Value;
        return true;
    }
    value = default; // ok
    return false;
}

Since the target of the assignment has MaybeNullWhen, the compiler permits you to assign null to it. We simply check in your return statement that the null-state of the variable is compatible with the return value. For example, if we changed return false to return true above, you would get a nullability warning.

Related