84

Когда я выполняю некоторый код, выбрасывается исключение NullReferenceException со следующим сообщением:

Object reference not set to an instance of an object.

или

В экземпляре объекта не задана ссылка на объект.

Что это значит, и как мне исправить код?

Pavel Mayorov
  • 58,537
Kyubey
  • 32,103

2 Answers2

119

Причина

Вкратце

Вы пытаетесь воспользоваться чем-то, что равно null (или Nothing в VB.NET). Это означает, что либо вы присвоили это значение, либо вы ничего не присваивали.

Как и любое другое значение, null может передаваться от объекта к объекту, от метода к методу. Если нечто равно null в методе "А", вполне может быть, что метод "В" передал это значение в метод "А".

Остальная часть статьи описывает происходящее в деталях и перечисляет распространённые ошибки, которые могут привести к исключению NullReferenceException.

Более подробно

Если среда выполнения выбрасывает исключение NullReferenceException, то это всегда означает одно: вы пытаетесь воспользоваться ссылкой. И эта ссылка не инициализирована (или была инициализирована, но уже не инициализирована).

Это означает, что ссылка равна null, а вы не сможете вызвать методы через ссылку, равную null. В простейшем случае:

string foo = null;
foo.ToUpper();

Этот код выбросит исключение NullReferenceException на второй строке, потому что вы не можете вызвать метод ToUpper() у ссылки на string, равной null.

Отладка

Как определить источник ошибки? Кроме изучения, собственно, исключения, которое будет выброшено именно там, где оно произошло, вы можете воспользоваться общими рекомендациями по отладке в Visual Studio: поставьте точки останова в ключевых точках, изучите значения переменных, либо расположив курсор мыши над переменной, либо открыв панели для отладки: Watch, Locals, Autos.

Если вы хотите определить место, где значение ссылки устанавливается или не устанавливается, нажмите правой кнопкой на её имени и выберите "Find All References". Затем вы можете поставить точки останова на каждой найденной строке и запустить приложение в режиме отладки. Каждый раз, когда отладчик остановится на точке останова, вы можете удостовериться, что значение верное.

Следя за ходом выполнения программы, вы придёте к месту, где значение ссылки не должно быть null, и определите, почему не присвоено верное значение.

Примеры

Несколько общих примеров, в которых возникает исключение.

Цепочка

ref1.ref2.ref3.member

Если ref1, ref2 или ref3 равно null, вы получите NullReferenceException. Для решения проблемы и определения, что именно равно null, вы можете переписать выражение более простым способом:

var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member

Например, в цепочке HttpContext.Current.User.Identity.Name, значение может отсутствовать и у HttpContext.Current, и у User, и у Identity.

Неявно

public class Person {
    public int Age { get; set; }
}
public class Book {
    public Person Author { get; set; }
}
public class Example {
    public void Foo() {
        Book b1 = new Book();
        int authorAge = b1.Author.Age; // Свойство Author не было инициализировано
                                       // нет Person, у которого можно вычислить Age.
    }
}

То же верно для вложенных инициализаторов:

Book b1 = new Book { Author = { Age = 45 } };

Несмотря на использование ключевого слова new, создаётся только экземпляр класса Book, но экземпляр Person не создаётся, поэтому свойство Author остаётся null.

Массив

int[] numbers = null;
int n = numbers[0]; // numbers = null. Нет массива, чтобы получить элемент по индексу

Элементы массива

Person[] people = new Person[5];
people[0].Age = 20; // people[0] = null. Массив создаётся, но не
                    // инициализируется. Нет Person, у которого можно задать Age.

Массив массивов

long[][] array = new long[1][];
array[0][0] = 3; // = null, потому что инициализировано только первое измерение.
                 // Сначала выполните array[0] = new long[2].

Collection/List/Dictionary

Dictionary<string, int> agesForNames = null;
int age = agesForNames["Bob"]; // agesForNames = null.
                               // Экземпляр словаря не создан.

LINQ

public class Person {
    public string Name { get; set; }
}
var people = new List<Person>();
people.Add(null);
var names = from p in people select p.Name;
string firstName = names.First(); // Исключение бросается здесь, хотя создаётся
                                  // строкой выше. p = null, потому что
                                  // первый добавленный элемент = null.

События

public class Demo
{
    public event EventHandler StateChanged;
protected virtual void OnStateChanged(EventArgs e)
{        
    StateChanged(this, e); // Здесь бросится исключение, если на
                           // событие StateChanged никто не подписался
}

}

Неудачное именование переменных

Если бы в коде ниже у локальных переменных и полей были разные имена, вы бы обнаружили, что поле не было инициализировано:

public class Form1 {
    private Customer customer;
private void Form1_Load(object sender, EventArgs e) {
    Customer customer = new Customer();
    customer.Name = &quot;John&quot;;
}

private void Button_Click(object sender, EventArgs e) {
    MessageBox.Show(customer.Name);
}

}

Можно избежать проблемы, если использовать префикс для полей:

private Customer _customer;

Цикл жизни страницы ASP.NET

public partial class Issues_Edit : System.Web.UI.Page
{
    protected TestIssue myIssue;
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        // Выполняется только на первой загрузке, но не когда нажата кнопка
        myIssue = new TestIssue(); 
    }
}

protected void SaveButton_Click(object sender, EventArgs e)
{
    myIssue.Entry = &quot;NullReferenceException здесь!&quot;;
}

}

Сессии ASP.NET

// Если сессионная переменная "FirstName" ещё не была задана,
// то эта строка бросит NullReferenceException.
string firstName = Session["FirstName"].ToString();

Пустые вью-модели ASP.NET MVC

Если вы возвращаете пустую модель (или свойство модели) в контроллере, то вью бросит исключение при попытке доступа к ней:

// Controller
public class Restaurant:Controller
{
    public ActionResult Search()
    {
         return View();  // Модель не задана.
    }
}

// Razor view @foreach (var restaurantSearch in Model.RestaurantSearch) // Исключение. { }


Способы избежать

Явно проверять на null, пропускать код

Если вы ожидаете, что ссылка в некоторых случаях будет равна null, вы можете явно проверить на это значение перед доступом к членам экземпляра:

void PrintName(Person p) {
    if (p != null) {
        Console.WriteLine(p.Name);
    }
}

Явно проверять на null, использовать значение по умолчанию

Методы могут возвращать null, например, если не найден требуемый экземпляр. В этом случае вы можете вернуть значение по умолчанию:

string GetCategory(Book b) {
    if (b == null)
        return "Unknown";
    return b.Category;
}

Явно проверять на null, выбрасывать своё исключение

Вы также можете бросать своё исключение, чтобы позже его поймать:

string GetCategory(string bookTitle) {
    var book = library.FindBook(bookTitle);  // Может вернуть null
    if (book == null)
        throw new BookNotFoundException(bookTitle);  // Ваше исключение
    return book.Category;
}

Использовать Debug.Assert для проверки на null для обнаружения ошибки до бросания исключения

Если во время разработки вы знаете, что метод может, но вообще-то не должен возвращать null, вы можете воспользоваться Debug.Assert для быстрого обнаружения ошибки:

string GetTitle(int knownBookID) {
    // Вы знаете, что метод не должен возвращать null
    var book = library.GetBook(knownBookID);
// Исключение будет выброшено сейчас, а не в конце метода.
Debug.Assert(book != null, &quot;Library didn't return a book for known book ID.&quot;);

// Остальной код...

return book.Title; // Не выбросит NullReferenceException в режиме отладки.

}

Однако эта проверка не будет работать в релизной сборке, и вы снова получите NullReferenceException, если book == null.

Использовать GetValueOrDefault() для Nullable типов

DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Отобразит значение по умолчанию, потому что appointment = null.

appointment = new DateTime(2022, 10, 20); Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now)); // Отобразит дату, а не значение по умолчанию.

Использовать оператор ?? (C#) или If() (VB)

Краткая запись для задания значения по умолчанию:

IService CreateService(ILogger log, Int32? frobPowerLevel)
{
    var serviceImpl = new MyService(log ?? NullLog.Instance);
    serviceImpl.FrobPowerLevel = frobPowerLevel ?? 5;
}

Использовать операторы ?. и ?[ (C# 6+, VB.NET 14+):

Это оператор безопасного доступа к членам, также известный как оператор Элвиса за специфическую форму. Если выражение слева от оператора равно null, то правая часть игнорируется, и результатом считается null. Например:

var title = person.Title.ToUpper();

Если свойство Title равно null, то будет брошено исключение, потому что это попытка вызвать метод ToUpper на значении, равном null. В C# 5 и ниже можно добавить проверку:

var title = person.Title == null ? null : person.Title.ToUpper();

Теперь вместо бросания исключения переменной title будет присвоено null. В C# 6 был добавлен более короткий синтаксис:

var title = person.Title?.ToUpper();

Разумеется, если переменная person может быть равна null, то надо проверять и её. Также можно использовать операторы ?. и ?? вместе, чтобы предоставить значение по умолчанию:

// обычная проверка на null
int titleLength = 0;
if (title != null)
    titleLength = title.Length;

// совмещаем операторы ?. и ?? int titleLength = title?.Length ?? 0;

Если любой член в цепочке может быть null, то можно полностью обезопасить себя (хотя, конечно, архитектуру стоит поставить под сомнение):

int firstCustomerOrderCount = customers?[0]?.Orders?.Count() ?? 0;
Cordis
  • 453
Kyubey
  • 32,103
  • 3
    Мне кажется, в рекомендациях по предотвращению не хватает вот чего: в большинстве случаев семантика полей — non-nullable. В таких случаях NRE означает ошибку в логике программы: что-то недоинициализировано, объект в неправильном состоянии. В этом случае не проверять на null и игнорировать ошибку (что прячет баг, а не способствует его исправлению), а исправить код, который должен гарантировать, что null не будет. – VladD Oct 11 '15 at 11:09
  • @VladD Есть слишком много случаев, когда null — допустимое значение: ленивая инициализация, отсутствие значения "не задано" у типа и т. п. Не везде получается избавиться от null. Так что рекомендация сводится к "старайтесь не использовать null, если это возможно". – Kyubey Oct 11 '15 at 13:06
  • Это да, есть случаи, когда null валиден. Тогда, вероятно, первым вопросом должно быть: валиден ли null в этой точке? Если да, нужна проверка на null и адекватная реакция. Если нет, то код верен, а проблема где-то раньше (скорее всего в инициализации). – VladD Oct 11 '15 at 17:03
  • c# 6.0 null propagation: ref1?.ref2?.ref3?.member – A K May 29 '17 at 21:10
41

В дополнение к ответу @Discord @Squidward @Athari @Kyubey, давайте рассмотрим вопрос с другой стороны.

Если у вас в процессе выполнения программы случился NullReferenceException при доступе по какой-то ссылке, вы должны прежде всего задать себе важный вопрос:

а имеет ли право эта ссылка иметь значение null?

Во многих случаях правильным ответом будет «нет», и значит, исправлять придётся истинную причину ошибки, которая находится в другом месте, и произошла раньше.

Пример: если у вас есть такой класс:

class Car
{
    Engine engine;
    Driver driver;
    // ... остаток класса
}

Так вот, мотор у машины быть обязан в любом случае, всегда. А вот водитель может в принципе и не сидеть в машине.

Поэтому если вы видите, что обращение к engine.HorsePower падает с NullReferenceException, реальная проблема состоит в том, что вы забыли инициализировать engine в конструкторе. Поэтому и исправлять ошибку нужно не в точке, где падает, а в том месте, которое реально должно бы обеспечить ненулевое значение engine.

А вот если вылетает обращение driver.Age, то здесь уже проблема прямо в точке обращения, вам необходимо сначала проверить, что driver != null, а потом уж обращаться.

Таким образом: если ваша ссылка в точке обращения не имеет права иметь значение null, то вы не должны дописывать проверку на null, тем самым «замазывая» ошибку. Вы должны либо ничего не проверять, а исправить в том месте, где ссылка должна быть инициализирована, либо добавить Debug.Assert, либо проверку на null и выброс исключения.

Если же ссылка имеет право быть null-ом, то в этом случае нужно корректно обработать и этот случай.


Важное замечание: Если вашу функцию вызывает «внешний мир», вы не должны рассчитывать, что вашей функции передадут хорошие, правильные аргументы. Даже если вы требуете, чтобы объект, который вам передан, не был null-ом, всё равно вам могут передать неправильный объект. Поэтому для функций, доступных внешним модулям, необходимо проверять аргументы на null сразу же в начале кода, и бросать нужное исключение:

public decimal ComputePrice(Car car)
{
    if (car == null)
        throw new ArgumentNullException("car");
    // ...

Где именно проводить границу между «внутренним» и «внешним» миром, вопрос достаточно нетривиальный. Обычно эта граница есть граница модуля (сборки), или даже той её логической части, которая находится в вашей ответственности. Слишком мелкое дробление ведёт к повторению бессмысленного кода (одна часть программы не доверяет другой и постоянно перепроверяет её). Слишком крупное дробление ведёт к необходимости держать в голове миллионы зависимостей («могу я тут передавать null или нет?»). Пользуйтесь здравым смыслом и личным опытом.


В C# 8 введено, наконец, явное различие между этими двумя случаями. Для тех ссылок, которые могут содержать null, введён специальный синтаксис (в файле должно быть включено #nullable enable (или в проекте <NullableContextOptions>enable</NullableContextOptions>):

class Car
{
    Engine engine;  // не может быть null
    Driver? driver; // может быть null
    // ... остаток класса
}

В этом случае сам компилятор сможет проконтролировать, что вы забыли проинициализировать значение поля engine, и выдать вам предупреждение.

VladD
  • 206,799
  • Очень странно приводить информацию о фиче, которая только планируется в новой версии языка, причём в устаревшем виде (согласно текущему плану, будет вводиться "nullable reference type" с модификатором ?, а не "non-nullable reference type" с модификатором !; последний символ будет использоваться для извлечения значения, а не определения переменных). – Kyubey Nov 27 '15 at 08:01
  • @Discord: Последние design notes, в которых это упоминалось, упоминают и явный T!. Хотя вы правы, поменяю-ка я ссылку. – VladD Nov 27 '15 at 12:00
  • @Discord: А в чём проблема с информацией о новой фиче? – VladD Nov 27 '15 at 12:14
  • Проблема в том, что фича на очень ранней стадии реализации, поэтому есть большой риск, что ваша информация будет не только бесполезна, как сейчас, но и откровенно неверна, когда фича будет наконец реализована. / Модификатор ! при объявлении обсуждается только в контексте параметров обобщённых типов. Насколько я понимаю, ваш пример должен иметь вид Engine engine; Driver? driver;. – Kyubey Nov 27 '15 at 18:12
  • @Discord: Возможно, актуальность потеряется. Но я буду подправлять ответ, как только новая информация станет известна. – VladD Nov 27 '15 at 18:15
  • Отличный ответ! Как там, окончательный синтаксис фичи уже известен? – Nick Volynkin Dec 03 '18 at 18:10
  • @NickVolynkin: Спасибо! Хорошие шансы на то, что выйдет именно с ?, но в последний момент теоретически возможны изменения. Релиз C# 8 будет где-то в первой половине 2019 года, ждём. – VladD Dec 05 '18 at 00:50
  • А как можно объяснить такую ситуацию? http://i.piccy.info/i9/0212a95b060462ecfcd2c88464359e9d/1582881583/26672/1364921/nre.png – Олег Feb 28 '20 at 09:19
  • @Олег: (1) Многопоточность? (2) А посмотрите, в отладчике, что из item и item.Items реально равно null. – VladD Feb 28 '20 at 10:25
  • В том то и загадка, что ни один из элементов не равен null http://i.piccy.info/i9/878b560d3459179ec8d96ccb52583b40/1582893314/32542/1364921/nre2.png – Олег Feb 28 '20 at 12:36
  • @Олег: Прикольно! А можно stack trace из исключения? – VladD Feb 28 '20 at 12:39
  • Вот stack trace: http://i.piccy.info/i9/961eb531213f6a5520f65ae2d8d10658/1582896252/69357/1364921/st.png Но я вообще перестаю понимать что либо :( http://i.piccy.info/i9/34d79dbe1215eb77ed54098371c20dc4/1582896371/30502/1364921/st3.png Выяснил, что ошибка выпадает при использовании определённой темы. Но всё равно я не понимаю почему так? :( – Олег Feb 28 '20 at 13:24
  • @Олег: Ну вот вы сами и можете увидеть, что произошло. NullReferenceException произошло глубоко внутри функции Clear, так что ни item, ни item.Items тут не при чём. – VladD Feb 28 '20 at 13:28
  • @Олег: У вас произошло изменение выбранного (selected) элемента (скорее всего, выбранный элемент исчез), изменилось какое-то свойство, к которому был привязан триггер. – VladD Feb 28 '20 at 13:30
  • Кажеться я понял, в чём дело. Спасибо! P.S. А всё равно ситуация прикольная :) – Олег Feb 28 '20 at 13:52
  • @Олег: Пожалуйста! Да, выглядит реально необычно. Казалось бы, просто Clear(), что может пойти не так? – VladD Feb 28 '20 at 14:19