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

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

#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, я бы не стал этого делать, если в этом нет необходимости. Есть ли другой способ заставить мой код скомпилироваться без отключения проверки допустимости значения NULL или предупреждения CS8653?

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

person Andent    schedule 03.05.2019    source источник
comment
Я не рассматривал эту проблему более внимательно, но предполагаю, что различия между T? для типа значения и ссылочного типа просто не обрабатываются компилятором. Почему? Я мог предположить, что это просто добавило бы много сложности, но я также предполагаю, что вам понадобится ввод от настоящих разработчиков компилятора, чтобы знать наверняка. T? для типа значения обрабатывается Nullable<T>, тогда как T? для ссылочного типа в C # 8 обрабатывается T с атрибутом. В принципе, я думаю, что это просто не поддерживается.   -  person Lasse V. Karlsen    schedule 03.05.2019
comment
Когда я пытаюсь запустить этот код в VS2019, я получаю следующее: CS8652 C # Эта функция в настоящее время находится в режиме предварительного просмотра и не поддерживается. Чтобы использовать функции предварительного просмотра, используйте языковую версию. Может быть, попробовать запустить предварительную версию и посмотреть, сойдет ли вам это с рук?   -  person nixkuroi    schedule 03.05.2019
comment
@ LasseVågsætherKarlsen Я тоже этого боюсь   -  person Andent    schedule 03.05.2019
comment
@nixkuroi Я использую предварительную версию VS и предварительную версию .NET Core SDK   -  person Andent    schedule 03.05.2019
comment
Привет @Andent, добро пожаловать в SO. По сути, вы включаете параметр, который сообщает компилятору не разрешать присвоение null переменной типа T (когда T является ссылочным типом), затем вы используете default(T), который может вернуть null для ссылочных типов. Здесь нет никакой загадки. Ты должен выбрать путь, как Нео ... :-)   -  person JuanR    schedule 03.05.2019
comment
T? действительно разрешает null, проблема здесь в том, что компилятор явно требует, чтобы код был явным относительно того, является ли T ссылочным типом или типом значения, потому что скомпилированный код будет отличаться для обоих.   -  person Lasse V. Karlsen    schedule 03.05.2019
comment
Вероятно, вы тем временем собираетесь сделать Box<T> where T : class вместе с ValueBox<T> where T : struct. Я не думаю, что на данный момент существует способ унифицировать общие типы / методы над T? и Nullable<T>.   -  person Jonathon Chase    schedule 03.05.2019
comment
По сути, типы, допускающие значение NULL, и обобщенные типы не очень хорошо сочетаются. По моему опыту, это самая липкая часть дизайна.   -  person Jon Skeet    schedule 03.05.2019
comment
Я думаю, у вас здесь несколько противоречивых целей. Вы хотите иметь понятие блока default, но что еще является подходящим default для ссылочных типов? По умолчанию используется null для ссылочных типов, которые напрямую конфликтуют с использованием ссылочных типов, допускающих значение NULL. Возможно, вам нужно будет ограничить T типами, которые могут быть сконструированы по умолчанию (new()).   -  person Jeff Mercado    schedule 04.05.2019
comment
@ LasseV.Karlsen Проблема в том, что пока T? фундаментально отличается для типов значений (Nullable ‹T›), среда CLR не поддерживает специализацию общих методов. Поэтому, если универсальный метод имеет код типа T? x = y ?? z, компилятор должен испустить разные инструкции CIL, когда T является типом значения, чем когда это ссылочный тип, но это невозможно, поскольку CLR не t поддерживают существование двух версий метода. Если бы они 15 лет назад решили, что значения NULL, такие как int?, были просто обычными ссылками (null или значение в рамке), у нас не было бы этой проблемы ... у нас была бы другая   -  person Qwertie    schedule 18.01.2020
comment
@Qwertie Я согласен с этим мнением, это не значит, что я должен приветствовать статус-кво. Как автору библиотеки чрезвычайно сложно создавать общие методы, которые в некоторых случаях сигнализируют о своем намерении в отношении допустимости значений NULL. Вы вынуждены использовать атрибуты, предназначенные для использования компилятором, в надежде, что он все сделает правильно.   -  person Lasse V. Karlsen    schedule 19.01.2020


Ответы (3)


Что делать, если вы используете C # 9

В C # 9 вы можете использовать T? для параметра неограниченного типа, чтобы указать, что тип всегда допускает значение NULL, если T является ссылочным типом. Фактически, пример в исходном вопросе работает только после добавления ? к параметру свойства и конструктора. См. Следующий пример, чтобы понять, какого поведения вы можете ожидать от различных типов аргументов типа для Box<T>.

var box1 = Box<string>.CreateDefault();
// warning: box1.Value may be null
box1.Value.ToString();

var box2 = Box<string?>.CreateDefault();
// warning: box2.Value may be null
box2.Value.ToString();

var box3 = Box<int>.CreateDefault();
// no warning
box3.Value.ToString();

var box4 = Box<int?>.CreateDefault();
// warning: 'box4.Value' may be null
box4.Value.Value.ToString();

Что делать, если вы используете C # 8

В C # 8 невозможно поместить аннотацию, допускающую значение NULL, к параметру неограниченного типа (т. Е. Неизвестно, относится ли он к ссылочному типу или типу значения).

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

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

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

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

class Box<T>
{
    // We use MaybeNull to indicate null could be returned from the property,
    // and AllowNull to indicate that null is allowed to be assigned to the property.
    [MaybeNull, AllowNull]
    public T Value { get; }

    // We use only AllowNull here, because the parameter only represents
    // an input, unlike the property which has both input and output
    public Box([AllowNull] T value)
    {
        Value = value;
    }

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

    public static void UseStringDefault()
    {
        var box = Box<string>.CreateDefault();
        // Since 'box.Value' is a reference type here, [MaybeNull]
        // makes us warn on dereference of it.
        _ = box.Value.Length;
    }

    public static void UseIntDefault()
    {
        // Since 'box.Value' is a value type here, we don't warn on
        // dereference even though the original property has [MaybeNull]
        var box = Box<int>.CreateDefault();
        _ = box.Value.ToString();
    }
}

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

person Rikki Gibson    schedule 05.09.2019
comment
Очаровательный. Это была моя первая попытка, но она не сработала, и я не знал почему. Оказывается, если я изменю ваш пример, чтобы использовать значение по умолчанию (T) вместо значения по умолчанию, я получу тот же CS8653, которого я совсем не ожидал. Я должен думать, что они семантически идентичны - на самом деле я думал, что обычное значение по умолчанию было законным только тогда, когда они были! - person solublefish; 24.09.2019
comment
И если я изменю ваш пример, чтобы опустить [MaybeNull] в Value, я вообще не получу предупреждения, что для меня еще более странно. И вызов Box ‹string› .CreateDefault (). Value.Length выбрасывает NRE во время выполнения! - person solublefish; 24.09.2019
comment
Отсутствие предупреждения с default в этом сценарии является ошибкой. Отслеживается на github.com/dotnet/roslyn/issues/38339. Я обновил свой ответ, чтобы уточнить детали и, надеюсь, пролить больше света на поведение, которое вы наблюдаете. - person Rikki Gibson; 25.09.2019
comment
Предлагаемое решение C # 9 все еще неудовлетворительно, потому что оно приводит int? к int вместо _3 _...: '( - person Good Night Nerd Pride; 25.12.2020
comment
Когда @JonSkeet называет это самой липкой частью дизайна, вы понимаете, что это действительно серьезная проблема! Лично я думаю, что мы пошли по неправильному пути с языком, я считаю, что ссылочные типы, не допускающие значения NULL, должны были быть полными типами. В C # 9, когда T не ограничен (или даже не равен нулю), синтаксис T? означает тип, допускающий значение по умолчанию, соответствующий T, а не тип, допускающий значение NULL, соответствующий T, как это происходит везде в языке. Однако, если вы ограничиваете T классом или структурой, тогда T? снова начинает означать обнуляемый тип, соответствующий T. - person sjb-sjb; 03.03.2021

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

Я думаю, у вас здесь несколько противоречивых целей. Вы хотите иметь понятие поля по умолчанию, но что еще является подходящим значением по умолчанию для ссылочных типов? По умолчанию используется значение NULL для ссылочных типов, которые напрямую конфликтуют с использованием ссылочных типов, допускающих значение NULL. Возможно, вам нужно будет ограничить 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. Ну что ж.

person Andent    schedule 04.05.2019

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

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

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

Что значит null! заявление означает?

person evilGenius    schedule 03.05.2019
comment
Это не компилируется. Вы не можете просто поместить ! где угодно - он стоит в конце выражения, а не типа. - person Jon Skeet; 03.05.2019
comment
вау, это действительно Джон Скит! Я думаю, ты прав, но надеюсь, что это поможет найти правильный путь - person evilGenius; 03.05.2019
comment
Не думаю, что здесь это поможет. OP хочет, чтобы Value имел значение NULL, и, насколько мне известно, это невозможно выразить в C # 8. - person Jon Skeet; 03.05.2019