GOOGLE ADS

понедельник, 25 апреля 2022 г.

Что такое NullReferenceException и как его исправить?

Решение проблемы

В чем причина?

Нижняя линия

Вы пытаетесь использовать то, что есть null(или Nothingв VB.NET). Это означает, что вы либо устанавливаете его на null, либо никогда не устанавливаете его вообще ни на что.

Как и все остальное,null gets passed around. If it is null in method "A", it could be that method "B" passed a null to method "A".

nullможет иметь разное значение:

  • Объектные переменные, которые не инициализированы и, следовательно, ни на что не указывают. В этом случае, если вы обращаетесь к членам таких объектов, это вызывает ошибку NullReferenceException.

  • Разработчик использует nullнамеренно, чтобы указать, что значимое значение недоступно. Обратите внимание, что C# имеет концепцию типов данных, допускающих значение NULL, для переменных (например, таблицы базы данных могут иметь поля, допускающие значение NULL) — вы можете присваивать nullим значения, например, чтобы указать, что в них не хранится значение int? a = null;(что является сокращением для Nullable<int> a = null;), где вопросительный знак указывает допускается хранить nullв переменной a. Вы можете проверить это либо с помощью, либо с if (a.HasValue) {...}помощью if (a==null) {...}. Переменные, допускающие значение NULL, как в aэтом примере, позволяют получить доступ к значению через a.Valueявно или как обычно через a. Обратите внимание, что доступ к нему через a.Valueвыдает InvalidOperationExceptionвместо NullReferenceExceptionif aisnull- вы должны сделать проверку заранее, т.е. если у вас есть другая ненулевая переменная int b;, вы должны делать присваивания типа if (a.HasValue) { b = a.Value; }или короче if (a!= null) { b = a; }.

  • Остальная часть этой статьи более подробно и показывает ошибки, которые часто допускают многие программисты, которые могут привести к ошибке NullReferenceException.

    Подробнее

    Выброс runtimea NullReferenceException всегда означает одно и то же: вы пытаетесь использовать ссылку, а ссылка не инициализирована (или когда-то была инициализирована, но больше не инициализирована).

    Это означает, что ссылка имеет значение null, и вы не можете получить доступ к членам (например, к методам) через nullссылку. Самый простой случай:

    string foo = null;
    foo.ToUpper();

    This will throw a NullReferenceException at the second line because you can't call the instance method ToUpper() on a string reference pointing to null.

    Отладка

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

    Если вы хотите узнать, где установлена ​​или не установлена ​​ссылка, щелкните ее имя правой кнопкой мыши и выберите «Найти все ссылки». Затем вы можете установить точку останова в каждом найденном месте и запустить свою программу с подключенным отладчиком. Каждый раз, когда отладчик останавливается на такой точке останова, вам нужно определить, ожидаете ли вы, что ссылка будет отличной от 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; // You never initialized the Author property.
    // there is no Person to get an Age from.
    }
    }

    Если вы хотите избежать нулевой ссылки дочернего (Person), вы можете инициализировать ее в конструкторе родительского (Book) объекта.

    Инициализаторы вложенных объектов

    То же самое относится к инициализаторам вложенных объектов:

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

    Это означает:

    Book b1 = new Book();
    b1.Author.Age = 45;

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

    Инициализаторы вложенных коллекций

    public class Person 
    {
    public ICollection<Book> Books { get; set; }
    }
    public class Book
    {
    public string Title { get; set; }
    }

    Вложенная коллекция Initializersведет себя так же:

    Person p1 = new Person 
    {
    Books = {
    new Book { Title = "Title1" },
    new Book { Title = "Title2" },
    }
    };

    Это означает:

    Person p1 = new Person();
    p1.Books.Add(new Book { Title = "Title1" });
    p1.Books.Add(new Book { Title = "Title2" });

    Создается new Personтолько экземпляр Person, но Booksколлекция по-прежнему null. Синтаксис коллекции Initializerне создает коллекцию для p1.Books, он только преобразуется в p1.Books.Add(...)операторы.

    Множество

    int[] numbers = null;
    int n = numbers[0]; // numbers is null. There is no array to index.

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

    Person[] people = new Person[5];
    people[0].Age = 20 // people[0] is null. The array was allocated but not
    // initialized. There is no Person to set the Age for.

    Зубчатые массивы

    long[][] array = new long[1][];
    array[0][0] = 3; // is null because only the first dimension is yet initialized.
    // Use array[0] = new long[2]; first.

    Коллекция/Список/Словарь

    Dictionary<string, int> agesForNames = null;
    int age = agesForNames["Bob"]; // agesForNames is null.
    // There is no Dictionary to perform the lookup.

    Переменная диапазона (косвенная/отложенная)

    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(); // Exception is thrown here, but actually occurs
    // on the line above. "p" is null because the
    // first element we added to the list is null.

    События (С#)

    public class Demo
    {
    public event EventHandler StateChanged;

    protected virtual void OnStateChanged(EventArgs e)
    {
    StateChanged(this, e); // Exception is thrown here
    // if no event handlers have been attached
    // to StateChanged event
    }
    }

    (Примечание. Компилятор VB.NET вставляет нулевые проверки для использования событий, поэтому нет необходимости проверять события Nothingв VB.NET.)

    Плохие соглашения об именах:

    Если вы назвали поля иначе, чем локальные, вы, возможно, поняли, что никогда не инициализировали поле.

    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)
    {
    // Only called on first load, not when button clicked
    myIssue = new TestIssue();
    }
    }

    protected void SaveButton_Click(object sender, EventArgs e)
    {
    myIssue.Entry = "NullReferenceException here!";
    }
    }

    Значения сеанса ASP.NET

    // if the "FirstName" session value has not yet been set,
    // then this line will throw a NullReferenceException
    string firstName = Session["FirstName"].ToString();

    Пустые модели представлений ASP.NET MVC

    Если исключение возникает при ссылке на свойство @Modelв ASP.NET MVC View, вам нужно понимать, что Modelоно устанавливается в вашем методе действия при returnпросмотре. Когда вы возвращаете пустую модель (или свойство модели) из вашего контроллера, возникает исключение, когда представления обращаются к нему:

    // Controller
    public class Restaurant:Controller
    {
    public ActionResult Search()
    {
    return View(); // Forgot the provide a Model here.
    }
    }
    // Razor view
    @foreach (var restaurantSearch in Model.RestaurantSearch) // Throws.
    {
    }

    <p>@Model.somePropertyName</p> <!-- Also throws -->

    Порядок создания элементов управления WPF и события

    WPFэлементы управления создаются во время вызова InitializeComponentв том порядке, в котором они появляются в визуальном дереве. A NullReferenceExceptionбудет возникать в случае ранее созданных элементов управления с обработчиками событий и т. д., которые срабатывают, во время InitializeComponentкоторых ссылаются на поздно созданные элементы управления.

    For example:

    <Grid>
    <!-- Combobox declared first -->
    <ComboBox Name="comboBox1"
    Margin="10"
    SelectedIndex="0"
    SelectionChanged="comboBox1_SelectionChanged">
    <ComboBoxItem Content="Item 1" />
    <ComboBoxItem Content="Item 2" />
    <ComboBoxItem Content="Item 3" />
    </ComboBox>

    <!-- Label declared later -->
    <Label Name="label1"
    Content="Label"
    Margin="10" />
    </Grid>

    Здесь comboBox1создано раньше label1. Если comboBox1_SelectionChangedпопытается сослаться на `label1, он еще не будет создан.

    private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    label1.Content = comboBox1.SelectedIndex.ToString(); // NullReferenceException here!!
    }

    Изменение порядка объявлений в XAML(т. е. перечисление label1перед comboBox1, игнорирование вопросов философии дизайна) решит, по крайней мере, NullReferenceExceptionздесь.

    В ролях сas

    var myThing = someObject as Thing;

    Это не выдает, InvalidCastExceptionно возвращает, nullкогда приведение завершается неудачно (и когда someObjectсамо по себе равно нулю). Так что знайте это.

    LINQ FirstOrDefault()иSingleOrDefault()

    Простые версии First()и Single()выбрасывают исключения, когда ничего нет. В этом случае возвращаются версии "OrDefault" null. Так что знайте это.

    для каждого

    foreachбросает, когда вы пытаетесь перебрать nullколлекцию. Обычно вызывается неожиданным nullрезультатом методов, возвращающих коллекции.

    List<int> list = null; 
    foreach(var v in list) { } // NullReferenceException here

    Более реалистичный пример - выбор узлов из XML-документа. Вызовет, если узлы не найдены, но первоначальная отладка показывает, что все свойства действительны:

    foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))

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

    Явно проверять nullи игнорировать nullзначения.

    Если вы ожидаете, что ссылка иногда будет 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вызовы метода from и создайте пользовательское исключение.

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

    string GetCategory(string bookTitle) 
    {
    var book = library.FindBook(bookTitle); // This may return null
    if (book == null)
    throw new BookNotFoundException(bookTitle); // Your custom exception
    return book.Category;
    }

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

    Когда во время разработки вы знаете, что метод может, но никогда не должен возвращаться null, вы можете использовать Debug.Assert()его как можно скорее, когда это произойдет:

    string GetTitle(int knownBookID) 
    {
    // You know this should never return null.
    var book = library.GetBook(knownBookID);
    // Exception will occur on the next line instead of at the end of this method.
    Debug.Assert(book!= null, "Library didn't return a book for known book ID.");
    // Some other code
    return book.Title; // Will never throw NullReferenceException in Debug mode.
    }

    Хотя эта проверка не попадет в вашу сборку релиза, что приведет к ее NullReferenceExceptionповторному book == nullзапуску во время выполнения в режиме релиза.

    Используйте GetValueOrDefault()для nullableтипов значений, чтобы предоставить значение по умолчанию, когда они null.

    DateTime? appointment = null;
    Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
    // Will display the default value provided (DateTime.Now), because appointment is null.
    appointment = new DateTime(2022, 10, 20);
    Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
    // Will display the appointment date, not the default

    Use the null coalescing operator: ?? [C#] or If() [VB].

    Сокращение для предоставления значения по умолчанию, когда nullвстречается a:

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

    // Note that the above "GetValueOrDefault()" can also be rewritten to use
    // the coalesce operator:
    serviceImpl.FrobPowerLevel = frobPowerLevel?? 5;
    }

    Используйте оператор нулевого условия: ?.или ?[x]для массивов (доступно в C# 6 и VB.NET 14):

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

    var title = person.Title.ToUpper();

    Если у человека нет заголовка, это вызовет исключение, потому что он пытается вызвать ToUpperсвойство с нулевым значением.

    В C# 5и ниже это может быть защищено с помощью:

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

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

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

    Это приведет к тому, что переменная title будет иметь значение null, а вызов ToUpperне будет выполнен, если person.Titleэто так null.

    Конечно, вам все равно придется проверять titleили nullиспользовать оператор условия null вместе с оператором объединения null ( ??), чтобы предоставить значение по умолчанию:

    // regular null check
    int titleLength = 0;
    if (title!= null)
    titleLength = title.Length; // If title is null, this would throw NullReferenceException

    // combining the `?` and the `??` operator
    int titleLength = title?.Length?? 0;

    Аналогично, для массивов вы можете использовать ?[i]следующее:

    int[] myIntArray = null;
    var i = 5;
    int? elem = myIntArray?[i];
    if (!elem.HasValue) Console.WriteLine("No value");

    Это сделает следующее: Если myIntArrayis null, выражение вернется null, и вы можете безопасно его проверить. Если он содержит массив, он сделает то же самое, что и:
    elem = myIntArray[i];и вернет i -й элемент.

    Использовать нулевой контекст (доступно в C# 8):

    Introduced in C# 8, null contexts and nullable reference types perform static analysis on variables and provide a compiler warning if a value can be potentially null or have been set to null. The nullable reference types allow types to be explicitly allowed to be null.

    Контекст аннотации, допускающий значение NULL, и контекст предупреждения, допускающий значение NULL, могут быть установлены для проекта с использованием Nullableэлемента в вашем csprojфайле. Этот элемент настраивает, как компилятор интерпретирует допустимость значений NULL для типов и какие предупреждения генерируются. Допустимые настройки:


    • enable: контекст аннотации, допускающий значение NULL, включен. Контекст предупреждения, допускающий значение NULL, включен. Переменные ссылочного типа, например строка, не могут принимать значение NULL. Все предупреждения об отсутствии значений включены.

    • disable: Контекст аннотации, допускающий значение NULL, отключен. Контекст предупреждения, допускающий значение NULL, отключен. Переменные ссылочного типа игнорируются, как и в более ранних версиях C#. Все предупреждения об отсутствии значений отключены.

    • safeonly: контекст аннотации, допускающий значение NULL, включен. Контекст предупреждения, допускающий значение NULL, является безопасным. Переменные ссылочного типа не могут принимать значение NULL. Все предупреждения безопасности обнуляемости включены.

    • warnings: Контекст аннотации, допускающий значение NULL, отключен. Контекст предупреждения, допускающий значение NULL, включен. Переменные ссылочного типа не обращают внимания. Все предупреждения об отсутствии значений включены.

    • safeonlywarnings: Контекст аннотации, допускающий значение NULL, отключен. Контекст предупреждения, допускающий значение NULL, является безопасным. Переменные ссылочного типа не обращают внимания. Все предупреждения безопасности обнуляемости включены.


    Ссылочный тип, допускающий значение NULL, отмечается с использованием того же синтаксиса, что и типы значений, допускающие значение NULL: ?к типу переменной добавляется a.

    Специальные приемы отладки и исправления пустых ссылок в итераторах

    C#поддерживает «блоки итераторов» (называемые «генераторами» в некоторых других популярных языках). NullReferenceExceptionможет быть особенно сложно отлаживать блоки итераторов из-за отложенного выполнения:

    public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
    {
    for (int i = 0; i < count; ++i)
    yield return f.MakeFrob();
    }
    ...
    FrobFactory factory = whatever;
    IEnumerable<Frobs> frobs = GetFrobs();
    ...
    foreach(Frob frob in frobs) {... }

    Если whateverприведут, nullто MakeFrobкинут. Теперь вы можете подумать, что правильно поступить так:

    // DON'T DO THIS
    public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
    {
    if (f == null)
    throw new ArgumentNullException("f", "factory must not be null");
    for (int i = 0; i < count; ++i)
    yield return f.MakeFrob();
    }

    Почему это неправильно? Поскольку блок итератора на самом деле не запускается до тех пор, пока foreach! Вызов GetFrobsпросто возвращает объект, который при повторении запускает блок итератора.

    Написав такую null​​проверку, вы предотвратите NullReferenceException, но переместите ее NullArgumentExceptionв точку итерации, а не в точку вызова, и это очень запутывает при отладке.

    Правильное исправление:

    // DO THIS
    public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
    {
    // No yields in a public method that throws!
    if (f == null)
    throw new ArgumentNullException("f", "factory must not be null");
    return GetFrobsForReal(f, count);
    }
    private IEnumerable<Frob> GetFrobsForReal(FrobFactory f, int count)
    {
    // Yields in a private method
    Debug.Assert(f!= null);
    for (int i = 0; i < count; ++i)
    yield return f.MakeFrob();
    }

    То есть создайте частный вспомогательный метод с логикой блока итератора и общедоступный поверхностный метод, который выполняет nullпроверку и возвращает итератор. Теперь при GetFrobsвызове nullпроверка происходит немедленно, а затем GetFrobsForRealвыполняется при повторении последовательности.

    Если вы изучите справочный источник для LINQобъектов, вы увидите, что этот метод используется повсюду. Его немного сложнее писать, но он значительно упрощает отладку ошибок nullity. Оптимизируйте свой код для удобства вызывающей стороны, а не для удобства автора.

    Примечание о нулевых разыменованиях в небезопасном коде

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

    В небезопасном режиме вы должны знать два важных факта:


    • разыменование нулевого указателя приводит к тому же исключению, что и разыменование нулевой ссылки

    • разыменование недопустимого ненулевого указателя может привести к этому исключению в некоторых обстоятельствах.


    Чтобы понять, почему это так, полезно понять, как.NET производит NullReferenceExceptionв первую очередь. (Эти детали относятся к.NET, работающему в Windows; другие операционные системы используют аналогичные механизмы.)

    Память виртуализирована в Windows; каждый процесс получает виртуальную память из множества «страниц» памяти, которые отслеживаются операционной системой. На каждой странице памяти установлены флаги, которые определяют, как она может использоваться: чтение, запись, выполнение и т. д. Самая нижняя страница помечена как «вызывает ошибку, если когда-либо использовалась каким-либо образом».

    И нулевой указатель, и нулевая ссылка в C#внутренне представлены как число ноль, поэтому любая попытка разыменовать его в соответствующее хранилище памяти приводит к тому, что операционная система выдает ошибку. Затем среда выполнения.NET обнаруживает эту ошибку и превращает ее в файл NullReferenceException.

    Вот почему разыменование как нулевого указателя, так и нулевой ссылки приводит к одному и тому же исключению.

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

    Почему это имеет смысл? Предположим, у нас есть структура, содержащая два целых числа и неуправляемый указатель, равный нулю. Если мы попытаемся разыменовать второй int в структуре, то CLRне будет пытаться получить доступ к хранилищу в нулевом местоположении; он получит доступ к хранилищу в четвертом месте. Но логически это разыменование null, потому что мы попадаем на этот адрес через null.

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

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

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

    Laravel Datatable addColumn returns ID of one record only

    Я пытаюсь использовать Yajra Datatable для интеграции DataTable на свой веб-сайт. Я смог отобразить таблицу, но столкнулся с проблемой. В по...