Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

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


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


  Object reference not set to an instance of an object.


или


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


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


Ответы

Ответ 1



Причина Вкратце Вы пытаетесь воспользоваться чем-то, что равно 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 agesForNames = null; int age = agesForNames["Bob"]; // agesForNames = null. // Экземпляр словаря не создан. LINQ public class Person { public string Name { get; set; } } var people = new List(); 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 = "John"; } 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 = "NullReferenceException здесь!"; } } Сессии 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, "Library didn't return a book for known book ID."); // Остальной код... 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;

Ответ 2



В дополнение к ответу @Discord @Squidward @Athari, давайте рассмотрим вопрос с другой стороны. Если у вас в процессе выполнения программы случился 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 (или в проекте enable): class Car { Engine engine; // не может быть null Driver? driver; // может быть null // ... остаток класса } В этом случае сам компилятор сможет проконтролировать, что вы забыли проинициализировать значение поля engine, и выдать вам предупреждение.

Комментариев нет:

Отправить комментарий