Обнуляемые ссылочные типы: как указать "T"? тип без ограничения класса или структуры

Я хочу создать универсальный класс, который имеет член типа T T может быть классом, обнуляемым классом, структурой или обнуляемой структурой. Так что в основном все. Это упрощенный пример, который показывает мою проблему:

#nullable enable

class Box<T> {
    public T Value { get; }

    public Box(T value) {
        Value = value;
    }

    public static Box<T> CreateDefault()
        => new Box<T>(default(T));
}

Из-за использования новой #nullable enable я получаю следующее предупреждение: Program.cs(11,23): warning CS8653: A default expression introduces a null value when 'T' is a non-nullable reference type.

Это предупреждение имеет смысл для меня. Затем я попытался это исправить, добавив ? к свойству и параметру конструктора:

#nullable enable

class Box<T> {
    public T? Value { get; }

    public Box(T? value) {
        Value = value;
    }

    public static Box<T> CreateDefault()
        => new Box<T>(default(T));
}

Но теперь я получаю две ошибки:

Program.cs(4,12): error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
Program.cs(6,16): error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.

Тем не менее, я не хочу добавлять ограничения. Мне все равно, если T класс или структура.

Очевидное решение - обернуть нарушающих членов в директиву #nullable disable. Однако, как и #pragma warning disable, я бы хотел избежать этого, если в этом нет необходимости. Есть ли другой способ заставить мой код компилироваться без отключения проверок обнуляемости или предупреждения CS8653?

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.0.100-preview4-011223
 Commit:    118dd862c8

Ответы

Ответ 1

Как обсуждалось в комментариях к этому вопросу, вам, вероятно, потребуется подумать о том, является ли Box<string> со значением по умолчанию допустимым или нет в контексте, допускающем обнуляемость, и потенциально скорректировать поверхность API соответствующим образом. Возможно, тип должен быть Box<string?>, чтобы экземпляр, содержащий значение по умолчанию, был действительным. Однако существуют сценарии, в которых вы хотите указать, что свойства, возвращаемые методы или параметры и т.д. Все еще могут иметь значение null, даже если они имеют необнуляемые ссылочные типы. Если вы находитесь в этой категории, вы, вероятно, захотите использовать атрибуты, связанные с обнуляемостью.

Атрибуты MaybeNull и AllowNull были введены в .NET Core 3 для обработки этого сценария.

Некоторые из специфических поведений этих атрибутов все еще развиваются, но основная идея заключается в следующем:

  • [MaybeNull] означает, что вывод чего-либо (чтение поля или свойства, возврат метода и т.д.) Может быть null.
  • [AllowNull] означает, что ввод чего-либо (запись поля или свойства, параметра метода и т.д.) Может быть null.
#nullable enable
using System.Diagnostics.CodeAnalysis;

class Box<T>
{
    [MaybeNull]
    public T Value { get; }

    public Box([AllowNull] T value) // 1
    {
        Value = value;
    }

    public static Box<T> CreateDefault()
    {
        return new Box<T>(default(T)!); // 2
    }

    public static void UseStringDefault()
    {
        var box = Box<string>.CreateDefault();
        _ = box.Value.Length; // 3
    }

    public static void UseIntDefault()
    {
        var box = Box<int>.CreateDefault();
        _ = box.Value.ToString(); // 4
    }
}

Примечание:

  1. Атрибуты анализа потока в настоящее время влияют только на вызовы аннотированных элементов, а не на реализацию. Это означает, что если мы удалим атрибут [MaybeNull] из свойства Value, мы не получим предупреждение о назначении ему параметра [AllowNull] T value. [AllowNull] на практике просто предотвращает выдачу предупреждений о возможных нулевых аргументах на сайте вызовов.
  2. Анализ компилятором параметров неограниченного (то есть неизвестного типа значения или типа ссылки) ограничен. На этом этапе это означает, что компилятор выдает предупреждение: "Выражение по умолчанию вводит нулевое значение, когда" T "является необнуляемым ссылочным типом.". Поскольку компилятор не всегда может определить, является ли использование значения типа T безопасным для нулевого значения или нет, он вместо этого выдает предупреждения в местах, где T может быть необнуляемым ссылочным типом, но можно ввести "нулевое". Наиболее очевидным местом, где это происходит, является default(T), но это также может произойти при вызове универсальных методов, чьи результаты аннотируются с помощью [MaybeNull]. Сейчас мы решаем проблему, подавляя предупреждение с помощью default(T)!.
  3. Это более простой случай, когда мы выдаем предупреждение об обнуляемости при разыменовании box.Value из-за аннотации [MaybeNull] в свойстве.
  4. В этом случае атрибут MaybeNull не вызывает предупреждение при использовании, поскольку box.Value является типом значения, не допускающим значения NULL.

Пожалуйста, смотрите https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types для получения дополнительной информации, особенно в разделе "проблема с T?".

Ответ 2

Джефф Меркадо поднял хороший момент в комментариях:

Я думаю, у вас есть несколько противоречивых целей. Вы хотите иметь представление о поле по умолчанию, но для справочных типов, что еще является подходящим значением по умолчанию? Значение по умолчанию равно нулю для ссылочных типов, что напрямую конфликтует с использованием нулевых ссылочных типов. Возможно, вам нужно будет ограничить T типами, которые могут быть созданы по умолчанию (new()).

Например, default(T) для T = string будет null, поскольку во время выполнения нет различия между string и string? , Это текущее ограничение языковой функции.

Я обошел эту проблему, создав отдельные методы CreateDefault для каждого случая:

#nullable enable

class Box<T> {
    public T Value { get; }

    public Box(T value) {
        Value = value;
    }
}

static class CreateDefaultBox
{
    public static Box<T> ValueTypeNotNull<T>() where T : struct
        => new Box<T>(default);

    public static Box<T?> ValueTypeNullable<T>() where T : struct
        => new Box<T?>(null);

    public static Box<T> ReferenceTypeNotNull<T>() where T : class, new()
        => new Box<T>(new T());

    public static Box<T?> ReferenceTypeNullable<T>() where T : class
        => new Box<T?>(null);
}

Мне кажется, что это безопасный тип за счет более уродливых сайтов вызовов (CreateDefaultBox.ReferenceTypeNullable<object>() вместо Box<object?>.CreateDefault()). В примере класса, который я разместил, я бы просто полностью удалил методы и напрямую использовал конструктор Box. Ну что ж.

Ответ 3

Как насчет сохранения его как объекта и возвращения приведения T?

public class Box<T>
{
    public T Value { get { return (T)oValue; } }
    private object oValue;

    public Box(T value)
    {
        oValue = value;
    }

    public static Box<T> CreateDefault()
        => new Box<T>(default(T));
}