﻿# NullReferenceException в C\#\. Что это такое и как исправить?

NullReferenceException \(NRE\) — тип исключения платформы \.NET, возникающий при попытке обращения по нулевой ссылке\. В заметке рассмотрим причины, из\-за которых возникают исключения этого типа, а также способы их предотвращения и исправления\.  

![1049_NullReferenceException_ru/image1.png](https://import.viva64.com/docx/blog/1049_NullReferenceException_ru/image1.png)

**Примечание**\. Эта заметка рассчитана на начинающих программистов\. Разработчикам с опытом предлагаю 2 активности:

* посмотреть перечисленные здесь способы столкнуться с _NullReferenceException_ и проверить, все ли из них вы знаете;
* сыграть в [игру на поиск ошибок](https://quiz.pvs-studio.com/ru/csharp/)\. 

## Из\-за чего возникает исключение NullReferenceException?

### Теория

Переменные ссылочных типов в C\# хранят ссылки на объекты\. Чтобы обозначить, что ссылка не указывает на объект, используют значение _null_\. Стоит также отметить, что _null_ — значение выражений ссылочных типов по умолчанию\. 

Исключение типа _NullReferenceException_ возникает при попытке обращения по нулевой ссылке\. Операции, при которых может возникнуть исключение, мы перечислим ниже\.  

Рассмотрим пример: 

```cpp
Object notNullRef = new Object();
Object nullRef = default;

int hash;
hash = notNullRef.GetHashCode();
hash = nullRef.GetHashCode(); // NullReferenceException (NRE)
```

В коде объявляются две переменные ссылочного типа _Object_ — _notNullRef_ и _nullRef_:

* _notNullRef_ хранит ссылку на объект, созданный в результате вызова конструктора типа _Object_;
* _nullRef_ содержит default\-значение типа _Object_ — _null_\. 

![1049_NullReferenceException_ru/image2.png](https://import.viva64.com/docx/blog/1049_NullReferenceException_ru/image2.png)

Вызов метода _GetHashCode_ через ссылку в _notNullRef_ отработает нормально, так как ссылка указывает на объект\. При попытке вызова того же метода для _nullRef_ средой CLR будет выброшено исключение типа _NullReferenceException\._ 

Ниже мы рассмотрим, откуда могут прийти _null_\-значения и какие операции могут привести к исключению _NullReferenceException_\.   

### Как в переменную может попасть null\-значение

Рассмотрим примеры того, как в переменную может попасть значение _null_\. 

1\. Явная запись значения _null_ или _default_\.

```cpp
String name = null;
var len = name.Length; // NRE
```

Результатом выражения _default_ и _default\(T\)_ для ссылочных типов также будет _null_\.

```cpp
Object obj = default; // or default(Object)
var hash = obj.GetHashCode(); // NRE
```

2\. Инициализация поля ссылочного типа по умолчанию\. 

```cpp
class A 
{
  private String _name;
  public void Foo()
  {
    var len = _name.Length; // NRE
  }
}

var obj = new A();
obj.Foo();
```

В этом примере поле _\_name_ инициализируется значением по умолчанию\. На момент вызова _Foo_ поле _\_name_ равно _null_, поэтому при обращении к свойству _Length_ будет выброшено исключение\.

3\. Результат работы null\-conditional оператора \(?\.\)\.

```cpp
String name = user?.Name;
var len = name.Length; // Potential NRE
```

Если значение _user_ или _user\.Name_ будет равно _null_, в переменную _name_ также будет записано значение _null_\. В таком случае при обращении к свойству _Length_ без проверки на _null_ возникнет исключение\. 

4\. Результат приведения с использованием оператора _as_\. 

```cpp
Object obj = new Object();
String name = obj as String; // unsuccessful cast, name is null
var len = name.Length; // NRE
```

Результатом преобразования с помощью оператора _as_ будет значение _null_, если преобразование выполнить не удалось\. 

В примере выше переменная _obj_ хранит ссылку на экземпляр типа _Object_\. Попытка приведения _obj_ к типу _String_ закончится неудачей, в результате чего в _name_ будет записано значение _null_\. 

5\. Результат работы _\*OrDefault_ метода\. 

Методы вида _\*OrDefault_ \(_FirstOrDefault_, _LastOrDefault_ и т\. п\.\) из стандартной библиотеки возвращают значение по умолчанию, если значение предиката не подходит ни для одного элемента или коллекция пустая\.

```cpp
String[] strArr = ....;
String firstStr = strArr.FirstOrDefault();
var len = firstStr.Length; // Potential NRE
```

Если в массиве _strArr_ нет элементов, метод _FirstOrDefault_ вернёт значение _default\(String\)_ — _null_\. При разыменовании нулевой ссылки возникнет исключение\.

6\. Упаковка _default_ значения типа _Nullable<T\>_\. 

Результатом упаковки экземпляров _Nullable<T\>_ с _default_\-значением будет _null_\. 

```cpp
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
```

При записи в переменную _nullableToBox_ любого из значений _nullableLong1_ – _nullableLong5_ и последующей упаковки результатом будет _null_\. При использовании такого значения без проверки на _null_ будет выброшено исключение\. 

Подробности упаковки значений типа _Nullable<T\>_ описаны в статье "[Хорошо ли вы помните nullable value types?](https://pvs-studio.ru/ru/blog/posts/csharp/0772/)"\.  

### Операции с null\-значением, приводящие к исключению

В этом разделе перечислены операции, выполнение которых с _null_\-значением приведёт к исключению _NullReferenceException_\. 

1\. Явное обращение к члену объекта\. 

```cpp
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
```

То же самое — при разыменовании внутри метода:

```cpp
void Foo(A obj)
{
  _ = obj.Name; 
}

A aObj = null;
Foo(aObj); // NRE inside method
```

2\. Обращение по индексу\. 

```cpp
int[] arr = null;
int val = arr[0]; // NRE
```

3\. Вызов делегата\. 

```cpp
Action fooAct = null;
fooAct(); // NRE
```

4\. Итерирование в _foreach_\.

```cpp
List<long> list = null;
foreach (var item in list) // NRE
{ .... }
```

Обратите внимание, что оператор '?\.' здесь не поможет:

```cpp
foreach (var item in wrapper?.List) // Potential NRE
{ .... }
```

Если _wrapper_ или _wrapper\.List_ равны _null_, всё так же будет выброшено исключение\. Подробнее ситуацию разобрали в статье "[Использование оператора ?\. в foreach: защита от NullReferenceException, которая не работает](https://pvs-studio.ru/ru/blog/posts/csharp/0832/)"\.  

5\. Использование _null_\-значения в качестве операнда для _await_\. 

```cpp
Task GetPotentialNull()
{
  return _condition ? .... : null;
}
await GetPotentialNull(); // Potential NRE
```

6\. Распаковка _null_\-значения\. 

```cpp
object obj = null;
int intVal = (int)obj; // NRE
```

7\. Выброс исключения с _null_\-значением\.

```cpp
InvalidOperationException invalidOpException 
  = flag ? new InvalidOperationException() 
         : null;

throw invalidOpException; // Potential NRE
```

В переменную _invalidOpException_ может быть записано значение _null_\. В этом случае оператор _throw_ выбросит исключение типа _NullReferenceException_\. 

8\. Разыменование значения свойства _Target_ у экземпляра типа _WeakReference_\. 

```cpp
void ProcessIfNecessary(WeakReference weakRef)
{
  if (weakRef.IsAlive)
    (weakRef.Target as DataProcessor).Process(); // Potential NRE
}
```

Ссылка в _WeakReference_ указывает на объект, при этом не защищая его от сборки мусора\. Если объект попадёт под сборку мусора после проверки _weakRef\.IsAlive_, но до вызова метода _Process_, то:

* значением _weakRef\.Target_ будет _null_;
* результатом оператора _as_ также будет _null_;
* при попытке вызова метода _Process_ будет выброшено исключение _NullReferenceException_\.  

9\. Использование значения поля ссылочного типа до явной инициализации\. 

```cpp
class A
{
  private String _name;
  public A()
  {
    var len = _name.Length; // NRE
  }
}
```

На момент обращения к свойству _Length_ поле _\_name_ проинициализировано значением по умолчанию \(_null_\)\. Результат обращения — исключение\. 

10\. Небезопасный вызов обработчиков события в многопоточном коде\.

```cpp
public event EventHandler MyEvent;

void OnMyEvent(EventArgs e)
{
  if (MyEvent != null)
    MyEvent(this, e); // Potential NRE
}
```

Если между проверкой _MyEvent \!\= null_ и вызовом обработчиков события _MyEvent_ у него не останется подписчиков, при вызове будет выброшено исключение типа _NullRefernceException_\.

## Как исправить исключение NullReferenceException

Чтобы избежать исключений типа _NullReferenceException_, исключите ситуацию разыменования нулевых ссылок\. Для этого:

* определите, откуда в выражение попадает нулевая ссылка;
* измените логику работы приложения, чтобы доступа по нулевой ссылке не происходило\. 

Рассмотрим пример:

```cpp
foreach (var item in potentialNullCollection?.Where(....))
{ .... }
```

Если значением _potentialNullCollection_ будет _null_, оператор '?\.' также вернёт значение _null_\. При попытке обхода коллекции в цикле _foreach_ возникнет исключение\.

Если _potentialNullCollection_ в данном фрагменте кода никогда не равен _null_, стоит убрать оператор '?\.', чтобы не запутать разработчиков и инструменты анализа кода:

```cpp
foreach (var item in potentialNullCollection.Where(....))
{ .... }
```

Если _potentialNullCollection_ может принимать значение _null_, стоит добавить явную проверку или использовать оператор '??'\.  

```cpp
// 1
if (potentialNullCollection != null)
{
  foreach (var item in potentialNullCollection.Where(....))
  { .... }
}

// 2
foreach (var item in    potentialNullCollection?.Where(....) 
                     ?? Enumerable.Empty<T>)
{ .... }
```

**Примечание**\. Добавить проверку на неравенство _null_ — самый простой способ защититься от _NullReferenceException_\. Однако иногда такая правка будет не решать исходную проблему, а только маскировать её\. Поэтому при исправлении кода полезно думать о том, достаточно ли будет добавить проверку или нужно исправить в коде что\-то ещё\. 

## Как предотвратить исключения NullReferenceException

Кроме достаточно очевидного совета "не разыменовывать нулевые ссылки" есть несколько практик, которые помогут избежать возникновения исключений NRE\.

### Используйте nullable\-контекст

Без использования nullable\-контекста значение _null_ считается допустимым для ссылочных типов:

```cpp
String str = null; // No warnings
```

Начиная с C\# 8, в языке появилась возможность использовать nullable\-контекст\. Он вводит понятие nullable reference types\. В nullable\-контексте ссылочные типы считаются не допускающими значения _null_\. Например, при использовании nullable\-контекста на код, который мы только что рассмотрели, компилятор выдаст предупреждение: 

```cpp
String str = null; // CS8600
```

Предупреждение: _CS8600 Converting null literal or possible null value to non\-nullable type_\.

Аналогичная ситуация при вызове методов:

```cpp
void ProcessUserName(String userName)
{
  var len = userName.Length;
  ....
}
....
ProcessUserName(null); // CS8625
```

Предупреждение компилятора: _CS8625 Cannot convert null literal to non\-nullable reference type_\.

Чтобы указать компилятору, что переменная ссылочного типа может принимать значение _null_, используется символ '?':

```cpp
String firstName = null; // CS8600
String? lastName = null; // No warning
```

При попытке разыменовать nullable\-переменную без проверки на _null_ компилятор также выдаст предупреждение:

```cpp
void ProcessUserName(String? userName)
{
  var len = userName.Length; // CS8602
}
```

Предупреждение компилятора:_ CS8602 \- Dereference of a possibly null reference_\.

Если нужно указать компилятору, что в конкретном месте кода выражение точно не имеет значения _null_, можно использовать null\-forgiving оператор — '\!'\. Пример:

```cpp
void ProcessUserName(String? userName)
{
  int len = default;
  if (_flag)
    len = userName.Length; // CS8602
  else
    len = userName!.Length; // No warnings
}
```

Таким образом, nullable\-контекст помогает писать код так, чтобы минимизировать возможность разыменования нулевых ссылок\.

Включить nullable\-контекст можно несколькими способами:

* изменить соответствующую опцию в настройках проекта \("Nullable" в Visual Studio или "Nullable reference types" в JetBrains Rider\);
* самостоятельно прописать настройку в проектном файле \(\.csproj\): _<Nullable\>enable</Nullable\>_;
* с помощью директив _\#nullable enable_ / _\#nullable disable_ в коде\. 

У nullable\-контекста куда больше возможностей для настройки\. Подробнее о них мы писали в отдельной [статье](https://pvs-studio.ru/ru/blog/posts/csharp/1017/)\.

**Примечание**\. Обратите внимание, что nullable\-context влияет на выдачу предупреждений компилятором, но не на логику исполнения приложения\.

```cpp
String? str = null;
var len = str!.Length;
```

Компилятор не выдаст предупреждения на этот код, так как в нём используется null\-forgiving оператор\. Однако на этапе исполнения в коде возникнет исключение типа _NullReferenceException_\. 

### Используйте статический анализ

Статические анализаторы помогают находить дефекты безопасности и ошибки в коде\. В том числе анализаторы помогают находить места возникновения исключений типа _NullReferenceException_\.

Пример такого статического анализатора — [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/)\.

Рассмотрим пример C\# кода, в котором может возникнуть _NullReferenceException_\.

```cpp
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();
}
```

Во втором цикле _foreach_ разработчики выполняют обход коллекции _FilterCharacterRules_, для получения которой используют выражение _roslynItem\.Rules?\.FilterCharacterRules\. _Оператор '?\.' предполагает, что свойство _Rules_ может иметь значение _null_\. Однако если результатом выражения будет _null_, при попытке перебора _null_\-значения в _foreach_ всё равно возникнет _NullReferenceException_\. 

PVS\-Studio находит эту проблему и выдаёт предупреждение [V3153](https://pvs-studio.ru/ru/docs/warnings/v3153/)\. 

![1049_NullReferenceException_ru/image3.png](https://import.viva64.com/docx/blog/1049_NullReferenceException_ru/image3.png)

Если _items\.Rules_ действительно может иметь значение _null_, защититься от _NullReferenceException_ можно дополнительной проверкой:

```cpp
foreach (var item in items)
{
  if (item.Rules == null)
    continue;

  foreach (var rule in item.Rules.FilterCharacterRules)
  {
    ....
  }
}
```

Анализатор не будет выдавать предупреждение на такой код\.

PVS\-Studio ищет и другие ситуации в коде, при которых может возникнуть исключение _NullReferenceException_:

* [V3080](https://pvs-studio.ru/ru/docs/warnings/v3080/)\. Possible null dereference\.
* [V3083](https://pvs-studio.ru/ru/docs/warnings/v3083/)\. Unsafe invocation of event, NullReferenceException is possible\.
* [V3095](https://pvs-studio.ru/ru/docs/warnings/v3095/)\. The object was used before it was verified against null\.
* и т\. д\.



<details>
   <summary>Как установить и запустить PVS\\\-Studio?</summary>

Чтобы проверить код с помощью PVS\-Studio, нужно:

1. [Получить лицензионный ключ](https://pvs-studio.ru/ru/pvs-studio/try-free/) \(придёт на электронную почту\)\. 
1. [Загрузить](https://pvs-studio.ru/ru/pvs-studio/download/) и установить анализатор\.
1. [Ввести ключ](https://pvs-studio.ru/ru/docs/manual/0046/)\. 
1. Проверить код\. 



Документация по работе с PVS\-Studio в разном окружении:

* [Visual Studio](https://pvs-studio.ru/ru/docs/manual/6522/);
* [JetBrains Rider](https://pvs-studio.ru/ru/docs/manual/0052/);
* [консоль](https://pvs-studio.ru/ru/docs/manual/0035/)\. 




</details>