1
votes

I have implemented the following extension methods on IDictionary, which will try to get a value from a dictionary, but return a default value (either default(T) or user-provided) if the key doesn't exist. The first method without a user-provided value will call through to the other method with default.

[return: MaybeNull]
public static T GetValueOrDefault<TKey, T>(this IDictionary<TKey, T> source, TKey key) where TKey : notnull
{
    return GetValueOrDefault(source, key, defaultValue: default);
}

[return: MaybeNull]
public static T GetValueOrDefault<TKey, T>(this IDictionary<TKey, T> source, TKey key, [AllowNull] T defaultValue) where TKey : notnull
{
    if (source is null) throw new ArgumentNullException(nameof(source));
    if (key is null) throw new ArgumentNullException(nameof(key));
    
    if (source.TryGetValue(key, out var item))
    {
        return item;
    }

    return defaultValue;
}

With .NET SDK 3.1.100 this code builds fine. However, with the newest .NET SDK 5.0.101, I get the following error message:

error CS8620: Argument of type 'IDictionary<TKey, T>' cannot be used for parameter 'source' of type 'IDictionary<TKey, T?>' in 'T? DictionaryExtensions.GetValueOrDefault<TKey, T?>(IDictionary<TKey, T?> source, TKey key, T? defaultValue)' due to differences in the nullability of reference types.

It complains about the use of default in GetValueOrDefault(source, key, defaultValue: default). Using default! suppresses the error message of course, but the value is supposed to be nullable (hence the AllowNullAttribute on defaultValue). Or maybe it is deducing that T is nullable due to the attribute and usage, and won't allow a call with the non-nullable T?

The error is only produced when T is generic and not constrained to class. For instance, the following code does not produce the error:

var dict = new Dictionary<string, string>();
dict.GetValueOrDefault("key", null);

Am I doing something wrong? Has the constraints on nullable reference types been further tightened with the new .NET version? Is this simply a bug with .NET SDK 5.0.101?

1

1 Answers

2
votes

In this case, I think the nullability analysis just improved.

[AllowNull] and friends don't affect the compiler's inference of generic type parameters. What seems to be happening here is that the compiler is looking at the call to GetValueOrDefault(source, key, defaultValue: default) and trying to infer what TKey and T are. Because you're passing default as a value of T (ignoring [AllowNull]), it realises that the T given to GetValueOrDefault has to be nullable, i.e. it's invoking GetValueOrDefault<TKey, T?>(source, key, defaultValue: default).

However, it's also realised that source could be an IDictionary<TKey, T> (so that T isn't nullable), and realised that there's a clash here.

This is all academic, as C# 9 introduced the T? syntax. This is significantly neater than adding attributes, supports things like Task<T?>, and is better integrated with the compiler.

This works as you'd expect:

public static T? GetValueOrDefault<TKey, T>(this IDictionary<TKey, T> source, TKey key) where TKey : notnull
{
    return GetValueOrDefault(source, key, defaultValue: default);
}

public static T? GetValueOrDefault<TKey, T>(this IDictionary<TKey, T> source, TKey key, T? defaultValue) where TKey : notnull
{
    if (source is null) throw new ArgumentNullException(nameof(source));
    if (key is null) throw new ArgumentNullException(nameof(key));

    if (source.TryGetValue(key, out var item))
    {
        return item;
    }

    return defaultValue;
}

Here the compiler still notices that you're passing default, but it realises that defaultValue has a type T? rather than T (where it was ignoring the [AllowNull] attribute before), and so doesn't force T to be nullable.


If you're stuck with C# 8, it seems that explicitly specifying the generic type parameters stops the compiler from inferring the T as a T?, which gets rid of the warning:

return GetValueOrDefault<TKey, T>(source, key, defaultValue: default);