NullReferenceException (NRE) — тип исключения платформы .NET, возникающий при попытке обращения по нулевой ссылке. В заметке рассмотрим причины, из-за которых возникают исключения этого типа, а также способы их предотвращения и исправления.
Исключение типа NullReferenceException возникает при попытке обращения по нулевой ссылке. Операции, при которых может возникнуть исключение, мы перечислим ниже.
Рассмотрим пример:
В коде объявляются две переменные ссылочного типа Object — notNullRef и nullRef:
Вызов метода GetHashCode через ссылку в notNullRef отработает нормально, так как ссылка указывает на объект. При попытке вызова того же метода для nullRef средой CLR будет выброшено исключение типа NullReferenceException.
Ниже мы рассмотрим, откуда могут прийти null-значения и какие операции могут привести к исключению NullReferenceException.
1. Явная запись значения null или default.
Результатом выражения default и default(T) для ссылочных типов также будет null.
2. Инициализация поля ссылочного типа по умолчанию.
В этом примере поле _name инициализируется значением по умолчанию. На момент вызова Foo поле _name равно null, поэтому при обращении к свойству Length будет выброшено исключение.
3. Результат работы null-conditional оператора (?.).
Если значение user или user.Name будет равно null, в переменную name также будет записано значение null. В таком случае при обращении к свойству Length без проверки на null возникнет исключение.
4. Результат приведения с использованием оператора as.
Результатом преобразования с помощью оператора as будет значение null, если преобразование выполнить не удалось.
В примере выше переменная obj хранит ссылку на экземпляр типа Object. Попытка приведения obj к типу String закончится неудачей, в результате чего в name будет записано значение null.
5. Результат работы *OrDefault метода.
Методы вида *OrDefault (FirstOrDefault, LastOrDefault и т. п.) из стандартной библиотеки возвращают значение по умолчанию, если значение предиката не подходит ни для одного элемента или коллекция пустая.
Если в массиве strArr нет элементов, метод FirstOrDefault вернёт значение default(String) — null. При разыменовании нулевой ссылки возникнет исключение.
6. Упаковка default значения типа Nullable<T>.
Результатом упаковки экземпляров Nullable<T> с default-значением будет null.
При записи в переменную nullableToBox любого из значений nullableLong1 – nullableLong5 и последующей упаковки результатом будет null. При использовании такого значения без проверки на null будет выброшено исключение.
1. Явное обращение к члену объекта.
То же самое — при разыменовании внутри метода:
2. Обращение по индексу.
3. Вызов делегата.
4. Итерирование в foreach.
Обратите внимание, что оператор '?.' здесь не поможет:
Если wrapper или wrapper.List равны null, всё так же будет выброшено исключение.
5. Использование null-значения в качестве операнда для await.
6. Распаковка null-значения.
7. Выброс исключения с null-значением.
В переменную invalidOpException может быть записано значение null. В этом случае оператор throw выбросит исключение типа NullReferenceException.
8. Разыменование значения свойства Target у экземпляра типа WeakReference.
Ссылка в WeakReference указывает на объект, при этом не защищая его от сборки мусора. Если объект попадёт под сборку мусора после проверки weakRef.IsAlive, но до вызова метода Process, то:
На момент обращения к свойству Length поле _name проинициализировано значением по умолчанию (null). Результат обращения — исключение.
10. Небезопасный вызов обработчиков события в многопоточном коде.
Если между проверкой MyEvent != null и вызовом обработчиков события MyEvent у него не останется подписчиков, при вызове будет выброшено исключение типа NullRefernceException.
Если значением potentialNullCollection будет null, оператор '?.' также вернёт значение null. При попытке обхода коллекции в цикле foreach возникнет исключение.
Если potentialNullCollection в данном фрагменте кода никогда не равен null, стоит убрать оператор '?.', чтобы не запутать разработчиков и инструменты анализа кода:
Если potentialNullCollection может принимать значение null, стоит добавить явную проверку или использовать оператор '??'.
Примечание. Добавить проверку на неравенство null — самый простой способ защититься от NullReferenceException. Однако иногда такая правка будет не решать исходную проблему, а только маскировать её. Поэтому при исправлении кода полезно думать о том, достаточно ли будет добавить проверку или нужно исправить в коде что-то ещё.
Начиная с C# 8, в языке появилась возможность использовать nullable-контекст. Он вводит понятие nullable reference types. В nullable-контексте ссылочные типы считаются не допускающими значения null. Например, при использовании nullable-контекста на код, который мы только что рассмотрели, компилятор выдаст предупреждение:
Предупреждение: CS8600 Converting null literal or possible null value to non-nullable type.
Аналогичная ситуация при вызове методов:
Предупреждение компилятора: CS8625 Cannot convert null literal to non-nullable reference type.
Чтобы указать компилятору, что переменная ссылочного типа может принимать значение null, используется символ '?':
При попытке разыменовать nullable-переменную без проверки на null компилятор также выдаст предупреждение:
Предупреждение компилятора: CS8602 - Dereference of a possibly null reference.
Если нужно указать компилятору, что в конкретном месте кода выражение точно не имеет значения null, можно использовать null-forgiving оператор — '!'. Пример:
Таким образом, nullable-контекст помогает писать код так, чтобы минимизировать возможность разыменования нулевых ссылок.
Включить nullable-контекст можно несколькими способами:
Примечание. Обратите внимание, что nullable-context влияет на выдачу предупреждений компилятором, но не на логику исполнения приложения.
Компилятор не выдаст предупреждения на этот код, так как в нём используется null-forgiving оператор. Однако на этапе исполнения в коде возникнет исключение типа NullReferenceException.
Пример такого статического анализатора — PVS-Studio.
Рассмотрим пример C# кода, в котором может возникнуть NullReferenceException.
Во втором цикле foreach разработчики выполняют обход коллекции FilterCharacterRules, для получения которой используют выражение roslynItem.Rules?.FilterCharacterRules. Оператор '?.' предполагает, что свойство Rules может иметь значение null. Однако если результатом выражения будет null, при попытке перебора null-значения в foreach всё равно возникнет NullReferenceException.
PVS-Studio находит эту проблему и выдаёт предупреждение V3153.
Если items.Rules действительно может иметь значение null, защититься от NullReferenceException можно дополнительной проверкой:
Анализатор не будет выдавать предупреждение на такой код.
Автор Сергей Васильев
источник pvs-studio.ru
Из-за чего возникает исключение NullReferenceException?
Теория
Переменные ссылочных типов в C# хранят ссылки на объекты. Чтобы обозначить, что ссылка не указывает на объект, используют значение null. Стоит также отметить, что null — значение выражений ссылочных типов по умолчанию.Исключение типа NullReferenceException возникает при попытке обращения по нулевой ссылке. Операции, при которых может возникнуть исключение, мы перечислим ниже.
Рассмотрим пример:
C:
Object notNullRef = new Object();
Object nullRef = default;
int hash;
hash = notNullRef.GetHashCode();
hash = nullRef.GetHashCode(); // NullReferenceException (NRE)
- notNullRef хранит ссылку на объект, созданный в результате вызова конструктора типа Object;
- nullRef содержит default-значение типа Object — null.
Вызов метода GetHashCode через ссылку в notNullRef отработает нормально, так как ссылка указывает на объект. При попытке вызова того же метода для nullRef средой CLR будет выброшено исключение типа NullReferenceException.
Ниже мы рассмотрим, откуда могут прийти null-значения и какие операции могут привести к исключению NullReferenceException.
Как в переменную может попасть null-значение
Рассмотрим примеры того, как в переменную может попасть значение null.1. Явная запись значения null или default.
C:
String name = null;
var len = name.Length; // NRE
C:
Object obj = default; // or default(Object)
var hash = obj.GetHashCode(); // NRE
C:
class A
{
private String _name;
public void Foo()
{
var len = _name.Length; // NRE
}
}
var obj = new A();
obj.Foo();
3. Результат работы null-conditional оператора (?.).
C:
String name = user?.Name;
var len = name.Length; // Potential NRE
4. Результат приведения с использованием оператора as.
C:
Object obj = new Object();
String name = obj as String; // unsuccessful cast, name is null
var len = name.Length; // NRE
В примере выше переменная obj хранит ссылку на экземпляр типа Object. Попытка приведения obj к типу String закончится неудачей, в результате чего в name будет записано значение null.
5. Результат работы *OrDefault метода.
Методы вида *OrDefault (FirstOrDefault, LastOrDefault и т. п.) из стандартной библиотеки возвращают значение по умолчанию, если значение предиката не подходит ни для одного элемента или коллекция пустая.
C:
String[] strArr = ....;
String firstStr = strArr.FirstOrDefault();
var len = firstStr.Length; // Potential NRE
6. Упаковка default значения типа Nullable<T>.
Результатом упаковки экземпляров Nullable<T> с default-значением будет null.
C:
long? nullableLong1 = default;
long? nullableLong2 = null;
Nullable<long> nullableLong3 = default;
Nullable<long> nullableLong4 = null;
Nullable<long> nullableLong5 = new Nullable<long>();
var nullableToBox = ....; // nullableLong1 — nullableLong5
object boxedValue = (Object)nullableToBox; // null
_ = boxedValue.GetHashCode(); // NRE
Операции с null-значением, приводящие к исключению
В этом разделе перечислены операции, выполнение которых с null-значением приведёт к исключению NullReferenceException.1. Явное обращение к члену объекта.
C:
class A
{
public String _name;
public String Name => _name;
public String GetName() { return _name; }
}
A aObj = null;
_ = aObj._name; // NRE
_ = aObj.Name; // NRE
_ = aObj.GetName(); // NRE
C:
void Foo(A obj)
{
_ = obj.Name;
}
A aObj = null;
Foo(aObj); // NRE inside method
C:
int[] arr = null;
int val = arr[0]; // NRE
C:
Action fooAct = null;
fooAct(); // NRE
C:
List<long> list = null;
foreach (var item in list) // NRE
{ .... }
C:
foreach (var item in wrapper?.List) // Potential NRE
{ .... }
5. Использование null-значения в качестве операнда для await.
C:
Task GetPotentialNull()
{
return _condition ? .... : null;
}
await GetPotentialNull(); // Potential NRE
C:
object obj = null;
int intVal = (int)obj; // NRE
C:
InvalidOperationException invalidOpException
= flag ? new InvalidOperationException()
: null;
throw invalidOpException; // Potential NRE
8. Разыменование значения свойства Target у экземпляра типа WeakReference.
C:
void ProcessIfNecessary(WeakReference weakRef)
{
if (weakRef.IsAlive)
(weakRef.Target as DataProcessor).Process(); // Potential NRE
}
- значением weakRef.Target будет null;
- результатом оператора as также будет null;
- при попытке вызова метода Process будет выброшено исключение NullReferenceException.
C:
class A
{
private String _name;
public A()
{
var len = _name.Length; // NRE
}
}
10. Небезопасный вызов обработчиков события в многопоточном коде.
C:
public event EventHandler MyEvent;
void OnMyEvent(EventArgs e)
{
if (MyEvent != null)
MyEvent(this, e); // Potential NRE
}
Как исправить исключение NullReferenceException
Чтобы избежать исключений типа NullReferenceException, исключите ситуацию разыменования нулевых ссылок. Для этого:- определите, откуда в выражение попадает нулевая ссылка;
- измените логику работы приложения, чтобы доступа по нулевой ссылке не происходило.
C:
foreach (var item in potentialNullCollection?.Where(....))
{ .... }
Если potentialNullCollection в данном фрагменте кода никогда не равен null, стоит убрать оператор '?.', чтобы не запутать разработчиков и инструменты анализа кода:
C:
foreach (var item in potentialNullCollection.Where(....))
{ .... }
C:
// 1
if (potentialNullCollection != null)
{
foreach (var item in potentialNullCollection.Where(....))
{ .... }
}
// 2
foreach (var item in potentialNullCollection?.Where(....)
?? Enumerable.Empty<T>)
{ .... }
Как предотвратить исключения NullReferenceException
Кроме достаточно очевидного совета "не разыменовывать нулевые ссылки" есть несколько практик, которые помогут избежать возникновения исключений NRE.Используйте nullable-контекст
Без использования nullable-контекста значение null считается допустимым для ссылочных типов:
C:
String str = null; // No warnings
C:
String str = null; // CS8600
Аналогичная ситуация при вызове методов:
C:
void ProcessUserName(String userName)
{
var len = userName.Length;
....
}
....
ProcessUserName(null); // CS8625
Чтобы указать компилятору, что переменная ссылочного типа может принимать значение null, используется символ '?':
C:
String firstName = null; // CS8600
String? lastName = null; // No warning
C:
void ProcessUserName(String? userName)
{
var len = userName.Length; // CS8602
}
Если нужно указать компилятору, что в конкретном месте кода выражение точно не имеет значения null, можно использовать null-forgiving оператор — '!'. Пример:
C:
void ProcessUserName(String? userName)
{
int len = default;
if (_flag)
len = userName.Length; // CS8602
else
len = userName!.Length; // No warnings
}
Включить nullable-контекст можно несколькими способами:
- изменить соответствующую опцию в настройках проекта ("Nullable" в Visual Studio или "Nullable reference types" в JetBrains Rider);
- самостоятельно прописать настройку в проектном файле (.csproj): <Nullable>enable</Nullable>;
- с помощью директив #nullable enable / #nullable disable в коде.
Примечание. Обратите внимание, что nullable-context влияет на выдачу предупреждений компилятором, но не на логику исполнения приложения.
C:
String? str = null;
var len = str!.Length;
Используйте статический анализ
Статические анализаторы помогают находить дефекты безопасности и ошибки в коде. В том числе анализаторы помогают находить места возникновения исключений типа NullReferenceException.Пример такого статического анализатора — PVS-Studio.
Рассмотрим пример C# кода, в котором может возникнуть NullReferenceException.
C:
private ImmutableArray<char>
GetExcludedCommitCharacters(ImmutableArray<CompletionItem> items)
{
var hashSet = new HashSet<char>();
foreach (var item in items)
{
foreach (var rule in item.Rules?.FilterCharacterRules)
{
if (rule.Kind == CharacterSetModificationKind.Add)
{
foreach (var c in rule.Characters)
{
hashSet.Add(c);
}
}
}
}
return hashSet.ToImmutableArray();
}
PVS-Studio находит эту проблему и выдаёт предупреждение V3153.
Если items.Rules действительно может иметь значение null, защититься от NullReferenceException можно дополнительной проверкой:
C:
foreach (var item in items)
{
if (item.Rules == null)
continue;
foreach (var rule in item.Rules.FilterCharacterRules)
{
....
}
}
Автор Сергей Васильев
источник pvs-studio.ru