Аннотация для обнуляемой ссылки с помощью дженериков

#c# #generics #nullable

Вопрос:

Учитывая следующую общую Foo1 функцию:

 struct Key<T> {}
static readonly Key<double> MyKey = new Key<double>();
T? Foo1<T>(Key<T> key)
{
    return defau<
}
 

Наивный читатель предположил бы, что:

 var foo1 = Foo1(MyKey);
 

foo1 имеет тип double? , получается, что компилятор выбирает double тип возвращаемого значения. Мне нужно явно добавить ограничение, чтобы получить возвращаемое значение с возможностью обнуления:

 T? Foo2<T>(Key<T> key) where T : struct // really return a nullable
{
    return defau<
}
 

Может ли кто-нибудь объяснить, почему аннотация для ссылки с возможностью обнуления ? не отображается в моей первой Foo1 функции ?

Комментарии:

1. Вероятно, активируются типы ссылок, допускающие значения null, иначе подпись T? Foo2<T>(Key<T> key) — без ограничения структуры — не компилировалась бы. Для вывода аргументов типа проверяются только входные значения и их типы во время компиляции- типы возвращаемых значений игнорируются.

Ответ №1:

Давайте начнем с некоторой предыстории:

До того, как C# 9.0 Foo1 был недействителен. Даже в C# 8.0 с включенными ссылками на обнуление:

 CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type
 

Foo2 был действителен даже до C# 8.0, потому T? что имел смысл только в том случае, если T был структурой, и в этом случае T? имел другой тип от T ( Nullable<T> ). Пока что все довольно просто.

Начиная с C# 8.0 были введены ссылки, допускающие обнуление, что вызвало некоторую путаницу. Отныне T? может либо означать Nullable<T> , либо просто T . Эта версия не разрешала T? без ограничений, но она разрешала также, когда вы указали where T : class .

Без использования ограничений вам пришлось использовать атрибуты, чтобы указать, что T может быть null возвращаемым значением:

 // C# 8.0: Poor man's T?
[return: MaybeNull] T Foo1<T>(Key<T> key) => defau<
 

И что, если T теперь это тип значения? Он явно не изменит свой тип на Nullable<T> в возвращаемом значении. Чтобы вернуть double? аргумент вашего типа , он также должен быть double? , то MyKey есть также должен быть a Key<double?> .

В C# 9.0 ограничение для T? было ослаблено, теперь оно не нуждается в ограничении:

 // C# 9.0: this is valid now
T? Foo1<T>(Key<T> key) => defau<
 

Но теперь это по сути означает то же самое, что и версия C# 8.0. Без where T : struct ограничения T? это тот же тип , T что и так, это не что иное, как указание на то, что результат может быть null , что может появиться в предупреждениях компилятора. Чтобы возвращать типы значений, допускающие значение null, вы должны использовать double? в качестве общего аргумента, что также означает, что у вас Key также должен быть определен тип, допускающий значение null:

 static readonly Key<double?> MyKey = new Key<double?>();
 

Если в вашем случае ключ, допускающий обнуление, не имеет смысла, то вы не можете ничего сделать, кроме как указать where T : struct ограничение, как в Foo2 старом правиле: T? и T иметь разные типы, где T? это означает Nullable<T> .


Обновление: Основное различие между Foo1 и Foo2 , возможно, более очевидно, если вы увидите их декомпилированный исходный код:

 [System.Runtime.CompilerServices.NullableContext(2)]
private static T Foo1<T>([System.Runtime.CompilerServices.Nullable(new byte[] {
    0,
    1
})] Key<T> key)
{
    return default(T);
}

private static Nullable<T> Foo2<T>(Key<T> key) where T : struct
{
    return null;
}
 

Обратите внимание, что возвращаемый тип Foo1 просто T с некоторой аннотацией, поэтому компилятор может выдавать соответствующие предупреждения.