Разница в допустимости пустых значений ошибки ссылочных типов при сборке с помощью .NET 5 SDK

Я реализовал следующие методы расширения для IDictionary, которые попытаются получить значение из словаря, но вернут значение по умолчанию (либо 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 я получаю следующее сообщение об ошибке:

ошибка CS8620: аргумент типа 'IDictionary ‹TKey, T›' не может использоваться для параметра 'source' типа 'IDictionary ‹TKey, T?›' в 'T? DictionaryExtensions.GetValueOrDefault ‹TKey, T?› (IDictionary ‹TKey, T?› Source, TKey key, T? DefaultValue) 'из-за различий в допустимости пустых значений ссылочных типов.

Он жалуется на использование default в GetValueOrDefault(source, key, defaultValue: default). Использование default!, конечно, подавляет сообщение об ошибке, но предполагается, что значение допускает обнуление (отсюда AllowNullAttribute на defaultValue). Или, может быть, он выводит, что T допускает значение NULL из-за атрибута и использования и не разрешает вызов с T, не допускающим значения NULL?

Ошибка возникает только в том случае, если T является общим и не ограничивается class. Например, следующий код не вызывает ошибки:

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

Я делаю что-то неправильно? Были ли ужесточены ограничения на ссылочные типы, допускающие значение NULL, в новой версии .NET? Это просто ошибка .NET SDK 5.0.101?


person Thorkil Holm-Jacobsen    schedule 22.12.2020    source источник


Ответы (1)


В этом случае я думаю, что анализ допустимости пустых значений просто улучшился.

[AllowNull] и его друзья не влияют на вывод компилятором параметров универсального типа. Кажется, здесь происходит то, что компилятор смотрит на вызов GetValueOrDefault(source, key, defaultValue: default) и пытается вывести, что такое TKey и T. Поскольку вы передаете default как значение T (игнорируя [AllowNull]), он понимает, что T, присвоенный GetValueOrDefault, должен иметь значение NULL, т.е. он вызывает GetValueOrDefault<TKey, T?>(source, key, defaultValue: default).

Однако он также понял, что source может быть IDictionary<TKey, T> (так что T не допускает значения NULL), и понял, что здесь есть конфликт.

Все это академично, поскольку C # 9 представил T? синтаксис. Это значительно проще, чем добавление атрибутов, поддерживает такие вещи, как 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);
person canton7    schedule 22.12.2020
comment
Это все чисто академично, поскольку в C # 9 появилась буква T? синтаксис. Это значительно проще, чем добавление атрибутов, поддерживает такие вещи, как Task ‹T?› И лучше интегрируется с компилятором. - Это круто, и я вообще не видел ничего особенного. Собственно говоря, я ничего не могу найти об этом, даже когда искал. У вас есть ресурс с подробным описанием этого изменения? - person Thorkil Holm-Jacobsen; 22.12.2020
comment
Я искал ссылку, на которую можно было бы ссылаться, и потерпел неудачу: я не могу найти по ней никакой официальной документации. Там упоминание на собрании LDM: я видел другие источники в этом репо, включая проблемы / обсуждения, но я не могу их найти прямо сейчас - person canton7; 22.12.2020
comment
Однако я нашел this, похоже, это может быть связано с вашей проблемой (хотя это не совсем подходит) - person canton7; 22.12.2020
comment
вы можете избавиться от предупреждения только в C # 9, поэтому, как минимум, о нем следует сообщать только в C # 9 - что интересно, я использую C # 8 в этом проекте, поэтому, если я правильно это читаю, я не должен получать это предупреждение . - person Thorkil Holm-Jacobsen; 22.12.2020
comment
Да, я думаю, что это, вероятно, связанная проблема, но не совсем то, что вы видите: я подозреваю, что ваш случай - это просто улучшение анализа нулевой допустимости - person canton7; 22.12.2020
comment
Спасибо за понимание. Другой вопрос: возможно ли это вообще с .NET 5 SDK и C # 8 (без использования обходного пути !)? - person Thorkil Holm-Jacobsen; 22.12.2020
comment
Я нашел способ: отредактировал внизу своего ответа - person canton7; 22.12.2020
comment
Ага, это хороший ресурс - person canton7; 22.12.2020