Выражение намерения

Завершено

Из предыдущего урока вы узнали, как компилятор C# может выполнять статический анализ для защиты от исключения NullReferenceException. Вы также узнали, как включить контекст, допускающий значение NULL. Из этого урока вы узнаете больше о явном выражении намерений в контексте, допускающем значение NULL.

Объявление переменных

При включенном контексте, допускающем значение NULL, вы можете определить, как ваш код выглядит для компилятора. Вы можете действовать с предупреждениями, созданными из контекста с поддержкой NULL, и в этом случае вы явно определяете ваши намерения. Например, давайте продолжим изучение кода FooBar и внимательно проанализируем объявление и присвоение:

// Define as nullable
FooBar? fooBar = null;

Обратите внимание на то, что к FooBar добавлен ?. Это указывает компилятору, что вы явно намерены допускать значение NULL для fooBar. Если вы не намерены допускать значения NULL для fooBar, но все равно хотите избежать предупреждений, учитывайте следующее:

// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;

В этом примере к значению null добавляется оператор обеспечения допустимости NULL (!), который указывает компилятору, что эта переменная явно инициализируется со значением NULL. Компилятор не будет выдавать предупреждения о том, что эта ссылка имеет значение NULL.

Переменным, не допускающим значение NULL, рекомендуется по возможности присваивать значения, отличные от null:

// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");

Операторы

Как объяснялось на предыдущем уроке, в C# определено несколько операторов, выражающих намерение относительно ссылочных типов, допускающих значения NULL.

Оператор обеспечения допустимости значений NULL (!)

Вы получили общее представление об операторе обеспечения допустимости значений NULL (!) на предыдущем уроке. Он сообщает компилятору игнорировать предупреждение CS8600. Это один из способов сообщить компилятору о том, что вы делаете, однако здесь есть одна оговорка: вы должны знать, что у вас происходит на самом деле!

При инициализации типов, не допускающих значения NULL, с включенным контекстом, допускающим значение NULL, может потребоваться явно указать компилятору, что необходимо обеспечить допустимость этого значения. Рассмотрим следующий пример кода:

#nullable enable

using System.Collections.Generic;

var fooList = new List<FooBar>
{
    new(Id: 1, Name: "Foo"),
    new(Id: 2, Name: "Bar")
};

FooBar fooBar = fooList.Find(f => f.Name == "Bar");

// The FooBar type definition for example.
record FooBar(int Id, string Name);

В предыдущем примере создается предупреждение CS8600, FooBar fooBar = fooList.Find(f => f.Name == "Bar"); так как Find может вернуться null. Это потенциальное значение null может быть присвоено переменной fooBar, которая не допускает значения NULL в этом контексте. Однако в этом вымышленном примере очевидно, что Find никогда не возвратит null как написано. Это намерение можно выразить компилятору с помощью оператора обеспечения допустимости значений NULL:

FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;

Обратите внимание на ! в конце fooList.Find(f => f.Name == "Bar"). Это говорит компилятору о вашей осведомленности о том, что объект, возвращаемый методом Find, может иметь значение null, и это нормально.

Оператор null-forgiving можно применить к встроенному объекту перед вызовом метода или оценкой свойств. Рассмотрим другой вымышленный пример:

List<FooBar>? fooList = FooListFactory.GetFooList();

// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning

static class FooListFactory
{
    public static List<FooBar>? GetFooList() =>
        new List<FooBar>
        {
            new(Id: 1, Name: "Foo"),
            new(Id: 2, Name: "Bar")
        };
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

В предыдущем примере:

  • GetFooList — это статический метод, возвращающий тип List<FooBar>?, допускающий значение NULL.
  • fooList присваивается значение, возвращаемое GetFooList.
  • Компилятор создает предупреждение о fooList.Find(f => f.Name == "Bar"); том, что значение, присваиваемое fooList, может быть null равно.
  • Когда значение fooList не равно null, метод Find может возвратить значение null, но мы понимаем, что этого не произойдет, поэтому применяется оператор обеспечения допустимости значений NULL.

Чтобы отключить предупреждение, можно применить оператор обеспечения допустимости значений NULL к fooList.

FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;

Примечание.

Вы должны использовать оператор null-for прощать разумно. Если использовать его просто для того, чтобы убрать предупреждения, компилятор сможет помочь обнаружить возможные неполадки из-за использования значения NULL. Используйте его смешно, и только если вы уверены.

Дополнительные сведения см. в справочнике! Оператор (null-forgiving) (справочник по C#).

Оператор объединения со значением NULL (??)

При работе с типами, допускающими значение NULL, может потребоваться оценить, имеют ли они в данный момент значение null, и предпринять определенные действия. Например, если типу, допускающему значение NULL, было присвоено значение null или же этот тип не инициализирован, возможно, потребуется присвоить ему значение, отличное от NULL. Именно в таких случаях удобно использовать оператор объединения со значением NULL (??).

Рассмотрим следующий пример:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    salesTax ??= DefaultStateSalesTax.Value;

    // Safely use salesTax object.
}

В приведенном выше коде C#:

  • Параметр salesTax определен как допускающий значение NULL IStateSalesTax.
  • В теле метода параметр salesTax присваивается условно с помощью оператора объединения со значением NULL.
    • Это гарантирует, что если параметр salesTax передан в виде значения null, он будет иметь значение.

Совет

Это функционально эквивалентно следующему коду C#:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    if (salesTax is null)
    {
        salesTax = DefaultStateSalesTax.Value;
    }

    // Safely use salesTax object.
}

Ниже приведен пример другого распространенного идиома C#, где можно использовать оператор объединения null:

public sealed class Wrapper<T> where T : new()
{
    private T _source;

    // If given a source, wrap it. Otherwise, wrap a new source:
    public Wrapper(T source = null) => _source = source ?? new T();
}

В приведенном выше коде C#:

  • Определяет универсальный класс-оболочку, в котором параметр универсального типа ограничен new().
  • Конструктор принимает параметр T source, который по умолчанию имеет значение null.
  • Упакованная в оболочку переменная _source условно инициализируется в new T().

Дополнительные сведения см. в статье ?? и ?? = операторы (справочник по C#).

Оператор условия допустимости значений NULL (?.)

При работе с типами, допускающими значение NULL, может потребоваться выполнять действия условно, в зависимости от состояния объекта null. Например, в предыдущем уроке FooBar запись использовалась для демонстрации NullReferenceException путем разыменовки null. Это исключение возникало при вызове метода ToString. Рассмотрим тот же пример, но теперь с применением оператора условия допустимости значений NULL:

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

В приведенном выше коде C#:

  • Выполнено условное разыменование fooBar с присвоением результата ToString переменной str.
    • Переменная str имеет тип string? (строка, допускающая значение NULL).
  • Значение str записывается в стандартный поток вывода, что не дает никаких результатов.
  • Вызов Console.Write(null) допустим, поэтому предупреждения отсутствуют.
  • При вызове метода Console.Write(str.Length) вы получите предупреждение, так как это может потенциально привести к разыменованию NULL.

Совет

Это функционально эквивалентно следующему коду C#:

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

Оператор можно сочетать с другими элементами, чтобы лучше выразить свое намерение. Например, можно связать операторы ?. и ??:

FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown

Дополнительные сведения см. в справочнике по операторам ?и ?[] (null-условный).

Итоги

Из этого урока вы узнали о том, как выразить намерение о допустимости значений NULL в коде. На следующем уроке вы примените полученные знания при работе с имеющимся проектом.

Проверьте свои знания

1.

Чему равно значение default ссылочного типа string?

2.

Каково ожидаемое поведение при разыменовании null?

3.

Что произойдет при выполнении этого кода C# throw null;?

4.

Какое утверждение является наиболее точным для ссылочных типов, допускающих значение NULL?