Wat is een NullReferenceException, en hoe los ik het op?

Ik heb wat code en wanneer het wordt uitgevoerd, gooit het een NullReferenceException, die zegt:

Object reference is niet ingesteld op een instantie van een object.

Wat betekent dit, en wat kan ik doen om deze fout te herstellen?

Oplossing

Wat is de oorzaak?

Bottom Line

Je probeert iets te gebruiken dat null is (of Nothing in VB.NET). Dit betekent dat je het of op null hebt gezet, of dat je het nooit op iets hebt gezet. Net als iets anders, wordt null doorgegeven. Als het null is in methode "A", kan het zijn dat methode "B" een null aan methode "A" heeft doorgegeven. null kan verschillende betekenissen hebben:

  1. Object variabelen die ongeïnitialiseerd zijn en dus naar niets verwijzen. In dit geval, als je eigenschappen of methodes van zulke objecten benadert, veroorzaakt dit een NullReferenceException.
  2. De ontwikkelaar gebruikt null met opzet om aan te geven dat er geen betekenisvolle waarde beschikbaar is. Merk op dat C# het concept van nullable datatypes voor variabelen heeft (zoals databasetabellen nullable velden kunnen hebben) - je kunt null aan ze toewijzen om aan te geven dat er geen waarde in is opgeslagen, bijvoorbeeld int? a = null; waarbij het vraagteken aangeeft dat het is toegestaan om null op te slaan in variabele a. Je kunt dat controleren met if (a.HasValue) {...} of met if (a==null) {...}. Nullable variabelen, zoals a dit voorbeeld, laten toe om de waarde te benaderen via a.Value expliciet, of gewoon zoals normaal via a.
    Noteer** dat het benaderen via a.Value een InvalidOperationException werpt in plaats van een NullReferenceException als a null is - je moet de controle vooraf doen, d.w.z. als je een andere on-nullable variabele int b; hebt, dan moet je opdrachten doen als if (a.HasValue) { b = a.Value; } of korter if (a != null) { b = a; }. De rest van dit artikel gaat meer in detail en laat fouten zien die veel programmeurs vaak maken die kunnen leiden tot een NullReferenceException.

    Meer specifiek

    Het gooien van een NullReferenceException door de runtime betekent altijd hetzelfde: je probeert een referentie te gebruiken, en de referentie is niet geinitialiseerd (of hij was eenmaal geinitialiseerd, maar is niet langer geinitialiseerd). Dit betekent dat de referentie null is, en dat je geen toegang kunt krijgen tot leden (zoals methodes) via een null referentie. Het eenvoudigste geval:

string foo = null;
foo.ToUpper();

Dit zal een NullReferenceException gooien op de tweede regel omdat je de instantie methode ToUpper() niet kan aanroepen op een string referentie die naar null wijst.

Debugging

Hoe vind je de bron van een NullReferenceException? Afgezien van het kijken naar de exception zelf, die precies wordt gegooid op de plaats waar hij optreedt, gelden de algemene regels van debugging in Visual Studio: plaats strategische breakpoints en inspecteer je variabelen, hetzij door met de muis over hun namen te gaan, een (Quick)Watch venster te openen of gebruik te maken van de diverse debugging panels zoals Locals en Autos. Als u wilt weten waar de referentie al dan niet is ingesteld, klik dan met de rechtermuisknop op zijn naam en selecteer "Find All References". U kunt dan een breekpunt plaatsen op elke gevonden locatie en uw programma uitvoeren met de debugger aangesloten. Elke keer dat de debugger breekt op zo'n breekpunt, moet je bepalen of je verwacht dat de verwijzing niet-null is, de variabele inspecteren en verifiëren dat deze wijst naar een instantie wanneer je dat verwacht. Door de programmastroom op deze manier te volgen, kun je de plaats vinden waar de instantie niet null zou moeten zijn, en waarom hij'niet juist is ingesteld.

Voorbeelden

Enkele veel voorkomende scenario's waar de exception gegooid kan worden:

Generic

ref1.ref2.ref3.member

Als ref1 of ref2 of ref3 null is, dan krijg je'een NullReferenceException. Als je het probleem wilt oplossen, zoek dan uit welke null is door de expressie te herschrijven naar zijn eenvoudiger equivalent:

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

Specifiek, in HttpContext.Current.User.Identity.Name, zou de HttpContext.Current null kunnen zijn, of de User eigenschap zou null kunnen zijn, of de Identity eigenschap zou null kunnen zijn.

Indirect

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.
    }
}

Als u de null reference van het kind (Person) wilt vermijden, kunt u het initialiseren in de constructor van het bovenliggende (Book) object's.

Nested Object Initializers

Hetzelfde geldt voor geneste object-initializers:

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

Dit vertaalt zich naar

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

Hoewel het new sleutelwoord wordt gebruikt, creëert het alleen een nieuwe instantie van Book, maar niet een nieuwe instantie van Person, dus de Author de eigenschap is nog steeds null.

Nested Collection Initializers

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

De geneste collectie initializers gedragen zich hetzelfde:

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

Dit vertaalt zich naar

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

De new Person creëert alleen een instantie van Person, maar de Books collectie is nog steeds null. De collectie initializer syntax creëert geen collectie voor p1.Books, het vertaalt zich alleen naar de p1.Books.Add(...) statements.

Array

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

Array Elements

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.

Jagged Arrays

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.

Collection/List/Dictionary

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

Range Variabele (Indirect/Deferred)

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(); // 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.

Events

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
    }
}

Bad Naming Conventions:

Als je velden een andere naam hebt gegeven dan locals, heb je misschien gemerkt dat je het veld nooit geïnitialiseerd hebt.

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);
    }
}

Dit kan worden opgelost door de conventie te volgen om velden voor te schrijven met een underscore:

private Customer _customer;

ASP.NET Page Life cycle:

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 Session Values

// 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 lege view models

Als de uitzondering optreedt bij het verwijzen naar een eigenschap van @Model in een ASP.NET MVC view, moet je begrijpen dat de Model wordt ingesteld in je action method, wanneer je een view return. Wanneer je een leeg model (of model property) retourneert vanuit je controller, treedt de uitzondering op wanneer de views het model benaderen:

// 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> 

WPF Besturingselement creatie volgorde en gebeurtenissen

WPF controls worden aangemaakt tijdens de aanroep van InitializeComponent in de volgorde waarin ze verschijnen in de visuele boom. Een NullReferenceException zal worden opgeworpen in het geval van te vroeg aangemaakte controls met event handlers, enz. , die afgaan tijdens InitializeComponent en die verwijzen naar laat gecreëerde besturingselementen. Bijvoorbeeld :











Hier is comboBox1 eerder aangemaakt dan label1. Als comboBox1_SelectionChanged probeert te refereren aan `label1, zal deze nog niet zijn aangemaakt.

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

Het veranderen van de volgorde van de declaraties in de XAML (d.w.z. label1 voor comboBox1, het negeren van ontwerpfilosofische kwesties, zou op zijn minst de NullReferenceException hier oplossen.

Giet met as

var myThing = someObject as Thing;

Dit gooit geen InvalidCastException maar geeft een null terug als de cast mislukt (en als someObject zelf null is). Wees je daar dus van bewust.

LINQ FirstOrDefault() en SingleOrDefault()

De gewone versies First() en Single() gooien excepties wanneer er niets is. De "OrDefault" versies geven in dat geval null terug. Wees je daar dus van bewust.

foreach

foreach gooit als je een null collection probeert te itereren. Meestal veroorzaakt door onverwachte null resultaten van methodes die collecties retourneren.

 List list = null;    
 foreach(var v in list) { } // exception

Meer realistisch voorbeeld - selecteer nodes uit een XML document. Zal gooien als nodes niet gevonden worden, maar initiële debugging toont aan dat alle eigenschappen geldig zijn:

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

manieren om te vermijden

Controleer expliciet op null en negeer null waarden.

Als je verwacht dat de referentie soms null is, kun je controleren of deze null is voordat je instance members benadert:

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

Controleer expliciet op null en geef een standaard waarde.

Methoden waarvan je verwacht dat ze een instantie teruggeven kunnen null teruggeven, bijvoorbeeld wanneer het gezochte object niet gevonden kan worden. Je kunt ervoor kiezen om een standaard waarde terug te geven wanneer dit het geval is:

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

Expliciet controleren op null bij methode-aanroepen en een aangepaste uitzondering gooien.

Je kunt ook een aangepaste exception gooien, alleen om deze op te vangen in de aanroepende code:

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;
}

Gebruik Debug.Assert als een waarde nooit null mag zijn, om het probleem eerder op te vangen dan dat de exceptie optreedt.

Wanneer je tijdens de ontwikkeling weet dat een methode misschien wel null kan, maar nooit mag retourneren, kun je Debug.Assert() gebruiken om zo snel mogelijk te breken wanneer dit toch gebeurt:

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.
}

Hoewel deze controle niet in je release build terecht zal komen, waardoor het opnieuw de NullReferenceException gooit wanneer book == null bij runtime in release mode.

Gebruik GetValueOrDefault() voor nullable value types om een standaard waarde te geven als ze null zijn.

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

Gebruik de null coalescing operator: ?? [C#] of If() [VB].

De afkorting voor het geven van een standaard waarde als er een null wordt aangetroffen:

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;
}

Gebruik de null condition operator: ?. of ?[x] voor arrays (beschikbaar in C# 6 en VB.NET 14):

Dit wordt ook wel de veilige navigatie of Elvis (naar zijn vorm) operator genoemd. Als de expressie aan de linkerkant van de operator null is, dan wordt de rechterkant niet geëvalueerd, en wordt in plaats daarvan null teruggegeven. Dat betekent dat gevallen als deze:

var title = person.Title.ToUpper();

Als de persoon geen titel heeft, zal dit een exception opleveren omdat het ToUpper probeert aan te roepen op een eigenschap met een null waarde. In C# 5 en lager, kan dit bewaakt worden met:

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

Nu zal de titel variabele null zijn in plaats van een exception te gooien. C# 6 introduceert hiervoor een kortere syntaxis:

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

Dit zal ertoe leiden dat de titelvariabele null is, en dat de aanroep aan ToUpper niet wordt gedaan als person.Title null is. Natuurlijk moet je title nog controleren op null of de null condition operator samen met de null coalescing operator (??) gebruiken om een standaard waarde te geven:

// 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;

Evenzo kun je voor arrays ?[i] als volgt gebruiken:

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

Dit zal het volgende doen: Als myIntArray null is, retourneert de expressie null en kun je het veilig controleren. Als het een array bevat, zal het hetzelfde doen als: elem = myIntArray[i]; en retourneert het ith element.

Gebruik null context (beschikbaar in C# 8):

Geïntroduceerd in C# 8 er null context's en nullable reference types voeren statische analyse uit op variabelen en geven een compiler waarschuwing als een waarde potentieel null kan zijn of op null gezet is. De nullable reference types maken het mogelijk om types expliciet toe te staan om null te zijn. De nullable annotatie context en nullable waarschuwing context kunnen worden ingesteld voor een project met behulp van het Nullable element in je csproj bestand. Dit element configureert hoe de compiler de nullabiliteit van types interpreteert en welke waarschuwingen gegenereerd worden. Geldige instellingen zijn:

  • enable: De nullable annotatie context is ingeschakeld. De nullable waarschuwingscontext is ingeschakeld. Variabelen van een referentietype, bijvoorbeeld string, zijn niet-nullable. Alle nullabiliteitswaarschuwingen zijn ingeschakeld.
  • Uitschakelen: De nullable annotatiecontext is uitgeschakeld. De nullable waarschuwingscontext is uitgeschakeld. Variabelen van een referentietype zijn nihil, net als vroegere versies van C#. Alle nullabiliteitswaarschuwingen zijn uitgeschakeld.
  • safeonly: De nullable annotatie context is ingeschakeld. De nullable waarschuwingscontext is safeonly. Variabelen van een referentietype zijn niet-nullable. Alle veiligheidswaarschuwingen voor nullabiliteit zijn ingeschakeld.
  • waarschuwingen: De nullable annotatiecontext is uitgeschakeld. De nullable waarschuwingscontext is ingeschakeld. Variabelen van een referentietype zijn nonnuleerbaar. Alle nullabiliteitswaarschuwingen zijn ingeschakeld.
  • safeonlywarnings: De nullable annotatie context is uitgeschakeld. De nullable waarschuwingscontext is safeonly. Variabelen van een referentietype zijn vergeetachtig. Alle veiligheidswaarschuwingen voor nullabiliteit zijn ingeschakeld. Een nullable reference type wordt genoteerd met dezelfde syntaxis als nullable value types: een ? wordt toegevoegd aan het type van de variabele.

    Speciale technieken voor het debuggen en repareren van null derefs in iterators

    C# ondersteunt "iterator blocks" (in sommige andere populaire talen "generators" genoemd). Null dereference uitzonderingen kunnen bijzonder lastig te debuggen zijn in iterator blokken vanwege de uitgestelde executie:

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

Als whatever resulteert in null dan zal MakeFrob gooien. Nu zou je kunnen denken dat het juiste is om het volgende te doen:

// DON'T DO THIS
public IEnumerable 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();
}

Waarom is dit fout? Omdat het iteratorblok eigenlijk pas loopt bij de foreach! De aanroep van GetFrobs retourneert gewoon een object dat wanneer iterated het iterator blok zal uitvoeren. Door op deze manier een null check te schrijven voorkom je de null dereference, maar je verplaatst de null argument uitzondering naar het punt van de iteratie, niet naar het punt van de aanroep, en dat is zeer verwarrend om te debuggen. De juiste oplossing is:

// DO THIS
public IEnumerable 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 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();
}

Dat wil zeggen, maak een private helper methode die de iterator block logica heeft, en een publieke surface methode die de null check doet en de iterator teruggeeft. Als nu GetFrobs wordt aangeroepen, vindt de null check onmiddellijk plaats, en dan wordt GetFrobsForReal uitgevoerd als de reeks wordt geiterateerd. Als je de referentie bron voor LINQ to Objects bekijkt, zul je zien dat deze techniek overal wordt gebruikt. Het is iets onhandiger om te schrijven, maar het maakt het debuggen van nullity fouten veel eenvoudiger. Optimaliseer je code voor het gemak van de aanroeper, niet het gemak van de auteur.

Een opmerking over null dereferences in onveilige code

C# heeft een "unsafe" mode die, zoals de naam al aangeeft, extreem gevaarlijk is omdat de normale veiligheidsmechanismen die zorgen voor geheugenveiligheid en typeveiligheid niet worden afgedwongen. U zou geen onveilige code moeten schrijven tenzij u een grondig en diep begrip heeft van hoe geheugen werkt. In onveilige modus, moet u zich bewust zijn van twee belangrijke feiten:

  • het dereferen van een null pointer levert dezelfde exceptie op als het dereferen van een null reference
  • het verwijzen naar een ongeldige niet-nule pointer kan deze exceptie veroorzaken in sommige omstandigheden Om te begrijpen waarom dat zo is, helpt het om te begrijpen hoe .NET in de eerste plaats null dereference excepties produceert. (Deze details zijn van toepassing op .NET dat onder Windows draait; andere besturingssystemen gebruiken vergelijkbare mechanismen). Geheugen wordt gevirtualiseerd in Windows; elk proces krijgt een virtuele geheugenruimte van vele "pages" van geheugen die worden bijgehouden door het besturingssysteem. Elke geheugenpagina heeft vlaggen die bepalen hoe hij mag worden gebruikt: gelezen, naar geschreven, uitgevoerd, enzovoort. De laagste pagina is gemarkeerd als "produce an error if ever used in any way".
    Zowel een null pointer als een null reference worden in C# intern voorgesteld als het getal nul, en dus zal elke poging om deze te dereferen naar de overeenkomstige geheugenopslag het besturingssysteem een fout doen produceren. De .NET runtime detecteert dan deze fout en maakt er de null dereference exception van. Dat is waarom het dereferen van zowel een null pointer als een null reference dezelfde uitzondering oplevert. Hoe zit het met het tweede punt? Dereferencing van elke ongeldige pointer die in de laagste pagina van het virtuele geheugen valt veroorzaakt dezelfde besturingssysteemfout, en daardoor dezelfde exceptie. Waarom is dit zinvol? Stel dat we een struct hebben met twee ints, en een onbeheerde pointer die gelijk is aan null. Als we proberen om de tweede int in de struct te verwijderen, zal de CLR niet proberen om de opslag op locatie nul te benaderen; het zal de opslag op locatie vier benaderen. Maar logisch gezien is dit een null dereference omdat we op dat adres komen via de null. Als je werkt met onveilige code en je krijgt een null dereference exceptie, wees je er dan van bewust dat de pointer in kwestie niet null hoeft te zijn. Het kan elke plaats in de onderste pagina zijn, en deze exceptie zal worden geproduceerd.
Commentaren (20)

Het betekent dat de variabele in kwestie op niets is gericht. Ik zou dit zo kunnen genereren:

SqlConnection connection = null;
connection.Open();

Dat zal de foutmelding opleveren omdat ik weliswaar de variabele "connection" heb gedeclareerd, maar hij is nergens naar gericht. Wanneer ik het lid "Open" probeer aan te roepen, is er'geen referentie voor het om op te lossen, en het zal de fout werpen.

Om deze fout te vermijden:

  1. Initialiseer altijd je objecten voordat je er iets mee probeert te doen.
  2. Als je's niet zeker weet of het object null is, controleer het dan met object == null.

JetBrains' Resharper tool zal elke plaats in uw code identificeren die de mogelijkheid van een null reference fout heeft, zodat u een null check kunt plaatsen. Deze fout is de nummer een bron van bugs, IMHO.

Commentaren (2)

Het betekent dat je code een object referentie variabele gebruikte die op null was gezet (d.w.z. het verwees niet naar een werkelijk object instantie).

Om deze fout te voorkomen, moeten objecten die null kunnen zijn, getest worden op null voordat ze gebruikt worden.

if (myvar != null)
{
    // Go ahead and use myvar
    myvar.property = ...
}
else
{
    // Whoops! myvar is null and cannot be used without first
    // assigning it to an instance reference
    // Attempting to use myvar here will result in NullReferenceException
}
Commentaren (0)