使用 .NET 5 SDK 构建时引用类型错误的可空性差异

Difference in nullability of reference types error when building with .NET 5 SDK

我在 IDictionary 上实现了以下扩展方法,它们将尝试从字典中获取值,但是 return 默认值(default(T) 或用户提供的) 如果密钥不存在。没有用户提供值的第一个方法将通过 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;
}

使用 .NET SDK 3.1.100 可以很好地构建此代码。但是,使用最新的 .NET SDK 5.0.101,我收到以下错误消息:

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.

它抱怨在 GetValueOrDefault(source, key, defaultValue: default) 中使用 default。使用 default! 当然会抑制错误消息,但该值应该可以为空(因此 defaultValue 上的 AllowNullAttribute)。或者可能是由于属性和用法而推断 T 可以为空,并且不允许使用不可为空的 T?

进行调用

仅当 T 是通用的且不受限于 class 时才会产生错误。例如,以下代码不会产生错误:

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

我做错了什么吗?新的 .NET 版本是否进一步收紧了对可空引用类型的限制?这仅仅是 .NET SDK 5.0.101 的错误吗?

在这种情况下,我认为可空性分析刚刚得到改进。

[AllowNull] 和朋友不会影响编译器对泛型类型参数的推断。这里似乎发生的是编译器正在查看对 GetValueOrDefault(source, key, defaultValue: default) 的调用并试图推断 TKeyT 是什么。因为您将 default 作为 T 的值传递(忽略 [AllowNull]),所以它意识到给予 GetValueOrDefaultT 必须可以为 null,即它正在调用 GetValueOrDefault<TKey, T?>(source, key, defaultValue: default).

但是,它也意识到 source 可能是 IDictionary<TKey, T>(因此 T 不可为空),并意识到这里存在冲突。

这都是学术性的,因为 C# 9 introduced the T? syntax。这比添加属性要简洁得多,支持 Task<T?> 之类的东西,并且可以更好地与编译器集成。

这如您所料:

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;
}

这里编译器仍然注意到您正在传递 default,但它意识到 defaultValue 具有类型 T? 而不是 T(它忽略了[AllowNull] 属性),因此不会强制 T 可以为空。


如果您坚持使用 C# 8,似乎显式指定泛型类型参数会阻止编译器将 T 推断为 T?,从而消除警告:

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