Minulla on koodia, ja kun se suoritetaan, se heittää NullReferenceException
, joka sanoo:
Object reference not set to an instance of an object.
Mitä tämä tarkoittaa ja miten voin korjata tämän virheen?
Yrität käyttää jotain, joka on null
(tai Nothing
VB.NET:ssä). Tämä tarkoittaa sitä, että joko asetat sen arvoksi null
tai et aseta sitä ollenkaan.
Kuten mikä tahansa muukin, null
kulkee ympäriinsä. Jos se on null
menetelmässä "A", se voi olla, että menetelmä "B" välitti null
menetelmälle "A".
null
voi tarkoittaa eri asioita:
NullReferenceException
.null
osoittaakseen, ettei ole mitään merkityksellistä arvoa. Huomaa, että C#:ssa on käsite muuttujien nullable datatypes (kuten tietokantataulukoissa voi olla nullable-kenttiä) - voit antaa niille null
osoittaaksesi, ettei siihen ole tallennettu mitään arvoa, esimerkiksi int? a = null;
, jossa kysymysmerkki ilmaisee, että muuttujaan a
on sallittua tallentaa null. Voit tarkistaa sen joko komennolla if (a.HasValue) {...}
tai komennolla if (a==null) {...}
. Nollattavat muuttujat, kuten a
tässä esimerkissä, mahdollistavat arvon käyttämisen a.Value
:n kautta nimenomaisesti tai aivan normaalisti a
:n kautta. a.Value
heittää InvalidOperationException
eikä NullReferenceException
, jos a
on null
- sinun pitäisi tehdä tarkistus etukäteen, eli jos sinulla on toinen on-nullable-muuttuja int b;
, sinun pitäisi tehdä tehtävät kuten if (a.HasValue) { b = a.Value; }
tai lyhyemmin if (a != null) { b = a; }
.
Loppuosa tästä artikkelista on yksityiskohtaisempi ja osoittaa virheitä, joita monet ohjelmoijat usein tekevät ja jotka voivat johtaa NullReferenceException
-virheeseen.NullReferenceExceptionin
heittäminen aina tarkoittaa samaa asiaa: yrität käyttää viittausta, ja viittausta ei ole alustettu (tai se oli joskus alustettu, mutta sitä ei ole enää alustettu).
Tämä tarkoittaa, että viite on null
, etkä voi käyttää jäseniä (kuten metodeja) null
-viittauksen kautta. Yksinkertaisin tapaus:
string foo = null;
foo.ToUpper();
Tämä heittää NullReferenceException
toisen rivin kohdalla, koska et voi kutsua instanssimetodia ToUpper()
merkkijono-viittauksella, joka osoittaa null
.
Miten löydät NullReferenceException
:n lähteen? Sen lisäksi, että tarkastellaan itse poikkeusta, joka heitetään juuri siinä paikassa, jossa se esiintyy, sovelletaan Visual Studion yleisiä debuggaussääntöjä: aseta strategisia taukopisteitä ja tarkastele muuttujia joko viemällä hiiri niiden nimien päälle, avaamalla (Quick)Watch-ikkuna tai käyttämällä erilaisia debuggauspaneeleita, kuten Locals ja Autos.
Jos haluat selvittää, missä viittaus on tai ei ole asetettu, napsauta sen nimeä hiiren kakkospainikkeella ja valitse "Find All References". Voit sitten asettaa pysäytyskohdan jokaiseen löydettyyn kohtaan ja ajaa ohjelmasi debuggeri liitettynä. Joka kerta, kun debuggeri pysähtyy tällaiseen taukopisteeseen, sinun on määritettävä, odotatko, että viittaus ei ole nolla, tarkastettava muuttuja ja ja tarkistettava, että se osoittaa instanssiin silloin, kun odotat sen osoittavan.
Seuraamalla ohjelman kulkua tällä tavoin voit löytää kohdan, jossa instanssin ei pitäisi olla nolla, ja miksi se ei ole oikein asetettu.
Joitakin yleisiä skenaarioita, joissa poikkeus voidaan heittää:
ref1.ref2.ref3.member
Jos ref1 tai ref2 tai ref3 on nolla, saat NullReferenceException
. Jos haluat ratkaista ongelman, selvitä, mikä niistä on nolla, kirjoittamalla lauseke uudelleen sen yksinkertaisemmaksi vastineeksi:
var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member
Erityisesti HttpContext.Current.User.Identity.Name
-kohdassa HttpContext.Current
voi olla null, tai User
-ominaisuus voi olla null, tai Identity
-ominaisuus voi olla null.
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.
}
}
Jos haluat välttää lapsen (Person) nollaviittauksen, voit alustaa sen vanhemman (Book) objektin'n konstruktorissa.
Sama pätee myös sisäkkäisiin objektien alustajiin:
Book b1 = new Book { Author = { Age = 45 } };
Tämä tarkoittaa
Book b1 = new Book();
b1.Author.Age = 45;
Vaikka avainsanaa new
käytetään, se luo vain uuden Book
-olion, mutta ei uutta Person
-oliota, joten Author
-ominaisuus on edelleen null
.
public class Person {
public ICollection<Book> Books { get; set; }
}
public class Book {
public string Title { get; set; }
}
Sisäkkäisten kokoelmien alustajat käyttäytyvät samoin:
Person p1 = new Person {
Books = {
new Book { Title = "Title1" },
new Book { Title = "Title2" },
}
};
Tämä tarkoittaa
Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });
new Person
luo vain Person
-instanssin, mutta Books
-kokoelma on edelleen null
. Kokoelman alustus-syntaksi ei luo kokoelmaa.
kokoelmaa p1.Books
:lle, vaan se vastaa vain p1.Books.Add(...)
-lauseita.
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
}
}
Jos olet nimennyt kentät eri tavalla kuin paikalliset kentät, olet ehkä huomannut, ettet ole koskaan alustanut kenttää.
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);
}
}
Tämä voidaan ratkaista noudattamalla käytäntöä, jonka mukaan kentät nimetään alkuosaan alaviivalla:
private Customer _customer;
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!";
}
}
// if the "FirstName" session value has not yet been set,
// then this line will throw a NullReferenceException
string firstName = Session["FirstName"].ToString();
Jos poikkeus tapahtuu, kun viitataan ominaisuuteen @Model
ASP.NET MVC -näkymässä, sinun on ymmärrettävä, että Model
asetetaan toimintamenetelmässäsi, kun palautat
näkymän. Kun palautat tyhjän mallin (tai mallin ominaisuuden) ohjaimesta, poikkeus tapahtuu, kun näkymät käyttävät sitä:
// 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-ohjaimet luodaan InitializeComponent
-kutsun aikana siinä järjestyksessä, jossa ne näkyvät näkymäpuussa. NullReferenceException
herätetään, jos ohjaimet on luotu liian aikaisin ja niissä on tapahtumankäsittelijöitä jne. jotka käynnistyvät InitializeComponent
-ohjelman aikana ja jotka viittaavat myöhään luotuihin ohjaimiin.
Esimerkiksi :
<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>
Tässä comboBox1
on luotu ennen label1
. Jos comboBox1_SelectionChanged
yrittää viitata `label1:een, sitä ei ole vielä luotu.
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
label1.Content = comboBox1.SelectedIndex.ToString(); // NullReference here!!
}
Muuttamalla XAML:n ilmoitusten järjestystä (eli listaamalla label1
ennen comboBox1
, suunnittelufilosofian kysymykset huomioimatta, voitaisiin ainakin ratkaista NullReferenceException
tässä tapauksessa.
as
var myThing = someObject as Thing;
Tämä ei aiheuta InvalidCastExceptionia, mutta palauttaa null
, kun cast epäonnistuu (ja kun someObject on itse null). Ole siis tietoinen siitä.
Yksinkertaiset versiot First()
ja Single()
heittävät poikkeuksia, kun mitään ei ole. "OrDefault" -versiot palauttavat tässä tapauksessa nollan. Ole siis tietoinen siitä.
foreach
heittää, kun yrität iteroida nollakokoelmaa. Yleensä johtuu odottamattomasta null
-tuloksesta metodeista, jotka palauttavat kokoelmia.
List<int> list = null;
foreach(var v in list) { } // exception
Realistisempi esimerkki - valitse solmut XML-dokumentista. Heittää, jos solmuja ei löydy, mutta alustava virheenkorjaus osoittaa, että kaikki ominaisuudet ovat kelvollisia:
foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))
null
ja jätä nolla-arvot huomiotta.Jos oletat, että viite on joskus nolla, voit tarkistaa, että se on null
ennen kuin käytät instanssin jäseniä:
void PrintName(Person p) {
if (p != null) {
Console.WriteLine(p.Name);
}
}
null
ja anna oletusarvo.Metodikutsut, joiden odotat palauttavan instanssin, voivat palauttaa null
esimerkiksi silloin, kun etsittyä objektia ei löydy. Voit halutessasi palauttaa oletusarvon, kun näin käy:
string GetCategory(Book b) {
if (b == null)
return "Unknown";
return b.Category;
}
null
metodikutsujen perusteella ja heitä mukautettu poikkeus.Voit myös heittää mukautetun poikkeuksen, mutta vain ottaa sen kiinni kutsuvassa koodissa:
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
, jos arvo ei saisi koskaan olla null
, jotta saat ongelman kiinni aikaisemmin kuin poikkeus tapahtuu.Kun tiedät kehityksen aikana, että metodi voi ehkä palauttaa null
, mutta sen ei pitäisi koskaan palauttaa null
, voit käyttää Debug.Assert()
:a, jotta voit keskeyttää mahdollisimman pian, kun se tapahtuu:
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.
}
Tosin tämä tarkistus ei päädy julkaisukehitykseesi, jolloin se heittää NullReferenceException
:n uudelleen, kun book == null
on ajonaikana julkaisutilassa.
GetValueOrDefault()
nollattavissa oleville arvotyypeille antamaan oletusarvo, kun ne ovat 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
??
[C#] tai If()
[VB].Lyhennelmä oletusarvon antamiselle, kun null
kohdataan:
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;
}
?.
tai ?[x]
matriiseille (käytettävissä C# 6:ssa ja VB.NET 14:ssä):Tätä kutsutaan joskus myös nimellä turvallinen navigointi tai Elvis (sen muodon mukaan) -operaattori. Jos operaattorin vasemmalla puolella oleva lauseke on nolla, oikeaa puolta ei evaluoida, vaan sen sijaan palautetaan null. Tämä tarkoittaa seuraavanlaisia tapauksia:
var title = person.Title.ToUpper();
Jos henkilöllä ei ole titteliä, tämä heittää poikkeuksen, koska se yrittää kutsua ToUpper
-operaattoria ominaisuudelle, jonka arvo on nolla.
C# 5:ssä ja sitä uudemmissa versioissa tämä voidaan suojata seuraavalla tavalla:
var title = person.Title == null ? null : person.Title.ToUpper();
Nyt title-muuttuja on nolla sen sijaan, että se heittäisi poikkeuksen. C# 6 ottaa käyttöön lyhyemmän syntaksin tätä varten:
var title = person.Title?.ToUpper();
Tämä johtaa siihen, että title-muuttuja on null
, ja kutsua ToUpper
ei tehdä, jos person.Title
on null
.
Tietenkin sinun on siltikin tarkistettava, onko title
nolla, tai käytettävä nollaehto-operaattoria yhdessä nollan sulauttamisoperaattorin (??
) kanssa oletusarvon antamiseksi:
// 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;
Vastaavasti voit käyttää ?[i]
-merkkiä matriisien kohdalla seuraavasti:
int[] myIntArray=null;
var i=5;
int? elem = myIntArray?[i];
if (!elem.HasValue) Console.WriteLine("No value");
Tämä tekee seuraavaa: Jos myIntArray on nolla, lauseke palauttaa nollan ja voit tarkistaa sen turvallisesti. Jos se sisältää joukon, se tekee saman kuin:
elem = myIntArray[i];
ja palauttaa ith elementin.
C# 8:ssa käyttöön otetut null-konteksti't ja nullable-viittaustyypit suorittavat muuttujien staattisen analyysin ja antavat kääntäjälle varoituksen, jos arvo voi olla mahdollisesti nolla tai jos se on asetettu nollaksi. Nollattavien viitetyyppien avulla voidaan nimenomaisesti sallia tyyppien olevan nolla. Nullable-merkintäkonteksti ja nullable-varoituskonteksti voidaan asettaa projektille csproj-tiedoston Nullable-elementillä. Tämä elementti määrittää, miten kääntäjä tulkitsee tyyppien nollattavuuden ja mitä varoituksia luodaan. Voimassa olevat asetukset ovat:
?
.C# tukee "iteraattorilohkoja" (joita kutsutaan "generaattoreiksi" joissakin muissa suosituissa kielissä). Nollan dereferenssipoikkeuksia voi olla erityisen hankala korjata iteraattorilohkoissa viivästetyn suorituksen vuoksi:
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) { ... }
Jos mitä tahansa
johtaa null
, niin MakeFrob
heittää. Nyt saatat ajatella, että oikein olisi tehdä näin:
// 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();
}
Miksi tämä on väärin? Koska iteraattorilohko ei oikeastaan juokse ennen foreach
! Kutsu GetFrobs
yksinkertaisesti palauttaa objektin, joka iteraation jälkeen suorittaa iteraattorilohkon.
Kirjoittamalla nollatarkistus tällä tavoin estät nollan poistamisen, mutta siirrät nolla-argumenttia koskevan poikkeuksen Iteraation kohtaan, etkä kutsun kohtaan, ja se on hyvin hämmentävää vianetsintää.
Oikea korjaus on:
// 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();
}
Eli tehdään yksityinen apumetodi, jossa on iteraattorilohkologiikka, ja julkinen pintametodi, joka tekee nollatarkastuksen ja palauttaa iteraattorin. Nyt kun GetFrobs
kutsutaan, nollatarkistus tapahtuu välittömästi, ja sitten GetFrobsForReal
suoritetaan, kun sekvenssiä iteroidaan.
Jos tarkastelet LINQ to Objects -ohjelman lähdeviitteitä, huomaat, että tätä tekniikkaa käytetään kaikkialla. Se on hieman kömpelömpi kirjoittaa, mutta se tekee nollavirheiden vianetsinnästä paljon helpompaa. Optimoi koodisi kutsujan, älä kirjoittajan mukavuuden vuoksi.
C#:ssa on "vaarallinen" tila, joka on nimensä mukaisesti erittäin vaarallinen, koska tavanomaisia turvamekanismeja, jotka takaavat muisti- ja tyyppiturvallisuuden, ei käytetä. Sinun ei pitäisi kirjoittaa vaarallista koodia, ellei sinulla ole perusteellista ja syvällistä ymmärrystä siitä, miten muisti toimii. Epävarmassa tilassa sinun tulisi olla tietoinen kahdesta tärkeästä seikasta:
Se tarkoittaa, että kyseinen muuttuja ei osoita mitään. Voisin luoda tämän näin:
SqlConnection connection = null;
connection.Open();
Tämä aiheuttaa virheen, koska vaikka olen ilmoittanut muuttujan "connection
", se ei osoita mihinkään. Kun yritän kutsua jäsentä "Open
", siinä ei ole mitään viittausta, jota se voisi ratkaista, ja se heittää virheen.
Tämän virheen välttämiseksi:
object == null
.JetBrains' Resharper-työkalu tunnistaa jokaisen kohdan koodissasi, jossa on nollaviittausvirheen mahdollisuus, jolloin voit laittaa nollatarkastuksen. Tämä virhe on virhelähde numero yksi, IMHO.
Se tarkoittaa, että koodisi käytti objektiviitemuuttujaa, joka oli asetettu nollaksi (eli se ei viitannut todelliseen objektin instanssiin).
Virheen estämiseksi objektit, jotka voivat olla nolla, tulisi testata nollan varalta ennen käyttöä.
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
}