Выражение намерения
Из предыдущего урока вы узнали, как компилятор 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
определен как допускающий значение NULLIStateSalesTax
. - В теле метода параметр
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 в коде. На следующем уроке вы примените полученные знания при работе с имеющимся проектом.