Обнуляемые ссылочные типы с универсальным типом возврата

Я немного поиграюсь с новой функцией C# 8 для обнуляемых ссылочных типов, и во время рефакторинга моего кода я наткнулся на этот (упрощенный) метод:

public T Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default;
}

Теперь это дает предупреждение Possible null reference return что логично, так как default(T) даст ноль для всех ссылочных типов. Сначала я думал, что я изменил бы это к следующему:

public T? Get<T>(string key)

Но это не может быть сделано. Он говорит, что я либо должен добавить общее ограничение where T : class или же where T : struct, Но это не вариант, так как это может быть и другое (я могу хранить int или же int? или экземпляр FooBar или что угодно в кеше). Я также читал о предполагаемом новом родовом ограничении where class? но это не похоже на работу.

Единственное простое решение, о котором я могу подумать, - это изменение оператора return с помощью оператора null forgiving:

return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;

Но это кажется неправильным, поскольку оно определенно может быть нулевым, поэтому я вру здесь компилятору:-)

Как я могу это исправить? Я что-то упускаю здесь совершенно очевидно?

1 ответ

Решение

Я думаю default! это лучшее, что вы можете сделать на данный момент.

Причина по которой public T? Get<T>(string key) не работает, потому что обнуляемые ссылочные типы очень отличаются от обнуляемых типов значений.

Обнуляемые ссылочные типы - это просто время компиляции. Маленькие вопросительные и восклицательные знаки используются только компилятором для проверки возможных нулей. Для глаз времени выполнения, string? а также string точно так же.

Обнуляемое значение типов, с другой стороны, является синтаксическим сахаром для Nullable<T>, Когда компилятор компилирует ваш метод, он должен определить тип возврата вашего метода. Если T это ссылочный тип, ваш метод будет иметь тип возвращаемого значения T, Если T является типом значения, ваш метод будет иметь тип возвращаемого значения Nullable<T>, Но компилятор не знает, как справиться с этим, когда T может быть как. Это, конечно, не может сказать, что "тип возврата T если T является ссылочным типом, и это Nullable<T> если T является ссылочным типом. "потому что CLR не поймет этого. Предполагается, что метод должен иметь только один возвращаемый тип.

Другими словами, говоря, что вы хотите вернуться T? это как сказать, что ты хочешь вернуться T когда T является ссылочным типом, а возвращаемый Nullable<T> когда T это тип значения Это не похоже на допустимый тип возвращаемого значения для метода, не так ли?

Как действительно плохой обходной путь, вы можете объявить два метода с разными именами - один имеет T ограничены типами значений, а другой имеет T ограничен ссылочными типами.

Вы были очень близки. Просто напишите свой метод так:

[return: MaybeNull]
public T Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;
}

Вы должны использовать default!чтобы избавиться от предупреждения. Но вы можете сказать компилятору[return: MaybeNull] что он должен проверять значение null, даже если это тип, не допускающий значения NULL.

В этом случае разработчик может получить предупреждение (зависит от аналитики потока), если он использует ваш метод и не проверяет значение null.

Для получения дополнительной информации см. Документацию Microsoft: Укажите пост-условия: MaybeNull и NotNull

В C# 9 вы можете более естественно выразить возможность нулевого значения неограниченных дженериков:

public T? Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default;
}

Обратите внимание, что нет ! оператор на defaultвыражение. Единственное отличие от исходного примера - добавление ? к T тип возврата.

В дополнение к ответу Дрю о С # 9

Имея T? Get<T>(string key) нам по-прежнему нужно различать типы ссылок, допускающие значение NULL, и типы значений, допускающие значение NULL, в вызывающем коде:

SomeClass? c = Get<SomeClass?>("key"); // return type is SomeClass?
SomeClass? c2 = Get<SomeClass>("key"); // return type is SomeClass?

int? i = Get<int?>("key"); // return type is int?
int i2 = Get<int>("key"); // return type is int
Другие вопросы по тегам