Imam nekaj kode in ko se izvede, vrže izjemo NullReferenceException
, ki pravi:
Sklic na objekt ni nastavljen na primerek objekta.
Kaj to pomeni in kaj lahko storim, da odpravim to napako?
Poskušate uporabiti nekaj, kar je null
(ali Nič
v VB.NET). To pomeni, da ste ga nastavili na null
ali pa ga sploh niste nastavili na nič.
Tako kot vse drugo se tudi null
prenaša naokoli. Če je null
v metodi "A", se lahko zgodi, da je metoda "B" posredovala null
v metodo "A".
null
ima lahko različne pomene:
NullReferenceException
.null
, da označi, da ni na voljo nobene smiselne vrednosti. Upoštevajte, da ima C# za spremenljivke koncept ničelnih podatkovnih tipov (kot imajo lahko tabele podatkovne zbirke ničelna polja) - lahko jim pripišete null
, da označite, da v njih ni shranjene nobene vrednosti, na primer int? a = null;
kjer vprašalnik pomeni, da je v spremenljivki a
dovoljeno shraniti ničlo. To lahko preverite z if (a.HasValue) {...}
ali z if (a==null) {...}
. Ničelne spremenljivke, kot je a
v tem primeru, omogočajo izrecni dostop do vrednosti prek a.Value
ali kot običajno prek a
. a.Value
vrže InvalidOperationException
namesto NullReferenceException
, če je a
null
- preverjanje morate opraviti vnaprej, tj. če imate drugo spremenljivko int b;
, ki jo je mogoče izničiti, potem morate opraviti naloge, kot je if (a.HasValue) { b = a.Value; }
ali krajše if (a != null) { b = a; }
.
Preostanek tega članka je podrobnejši in prikazuje napake, ki jih veliko programerjev pogosto dela in lahko privedejo do izjeme NullReferenceException
.Izpust NullReferenceException
vselej pomeni isto: poskušate uporabiti referenco, referenca pa ni inicializirana (ali pa je bila kdaj inicializirana, vendar ni več inicializirana).
To pomeni, da je referenca null
, do članov (kot so metode) pa ne morete dostopati prek reference null
. Najpreprostejši primer:
string foo = null;
foo.ToUpper();
To bo v drugi vrstici vrglo izjemo NullReferenceException
, ker ne morete poklicati metode primerka ToUpper()
na referenci string
, ki kaže na null
.
Kako poiščete izvor izjeme NullReferenceException
? Poleg tega, da si ogledate samo izjemo, ki se bo vrgla točno na mestu, kjer se pojavi, veljajo splošna pravila razhroščevanja v Visual Studiu: postavite strateške točke prekinitve in preglejte svoje spremenljivke, bodisi z miško nad njihovimi imeni, odprite okno (Quick)Watch ali uporabite različne plošče za razhroščevanje, kot sta Locals in Autos.
Če želite ugotoviti, kje je referenca nastavljena ali ne, z desno tipko miške kliknite njeno ime in izberite "Find All References". Nato lahko na vsako najdeno mesto postavite točko prekinitve in zaženete program s priloženim razhroščevalnikom. Vsakič, ko se razhroščevalnik prekine na takšni točki prekinitve, morate ugotoviti, ali pričakujete, da referenca ni ničelna, pregledati spremenljivko in preveriti, ali kaže na primerek, ko to pričakujete.
S takšnim sledenjem toku programa lahko najdete mesto, kjer instanca ne bi smela biti ničelna, in zakaj ni pravilno nastavljena.
Nekaj pogostih scenarijev, v katerih se lahko vrže izjema:
ref1.ref2.ref3.member
Če je ref1 ali ref2 ali ref3 ničelna, potem boste dobili izjemo NullReferenceException
. Če želite rešiti težavo, potem ugotovite, kateri je ničelni, tako da izraz prepišete v preprostejši ekvivalent:
var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member
Natančneje, v HttpContext.Current.User.Identity.Name
je lahko HttpContext.Current
nič, ali lastnost User
nič, ali lastnost Identity
nič.
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.
}
}
Če se želite izogniti ničelni referenci podrejenega objekta (Person), jo lahko inicializirate v konstruktorju nadrejenega objekta (Book).
Enako velja za vgnezdene inicializatorje objektov:
Book b1 = new Book { Author = { Age = 45 } };
To se prevede v
Book b1 = new Book();
b1.Author.Age = 45;
Čeprav je uporabljena ključna beseda new
, ustvari le nov primerek Book
, ne pa tudi novega primerka Person
, zato je lastnost Author
še vedno null
.
public class Person {
public ICollection<Book> Books { get; set; }
}
public class Book {
public string Title { get; set; }
}
Inicializatorji ugnezdenih zbirk se obnašajo enako:
Person p1 = new Person {
Books = {
new Book { Title = "Title1" },
new Book { Title = "Title2" },
}
};
To se prevede v
Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });
new Person
ustvari le primerek Person
, zbirka Books
pa je še vedno null
. Sintaksa inicializatorja zbirke ne ustvari zbirke
za p1.Books
, temveč se prevede le v stavek 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
}
}
Če ste polja poimenovali drugače kot lokalna, ste morda ugotovili, da polja nikoli niste inicializirali.
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);
}
}
To lahko rešite tako, da upoštevate konvencijo, po kateri je treba poljem dodati podčrtanko:
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();
Če se izjema pojavi pri sklicevanju na lastnost @Model
v pogledu ASP.NET MVC, morate razumeti, da se Model
nastavi v vaši akcijski metodi, ko povrnete
pogled. Ko iz kontrolerja vrnete prazen model (ali lastnost modela), se izjema pojavi, ko do njega dostopajo pogledi:
// 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 -->
Krmilniki WPF se ustvarijo med klicem InitializeComponent
v vrstnem redu, kot so prikazani v vizualnem drevesu. V primeru predčasno ustvarjenih kontrolnih elementov z izvajalci dogodkov itd. bo sproženo izvzetje NullReferenceException
. ki se sprožijo med InitializeComponent
in se sklicujejo na pozno ustvarjene kontrole.
Na primer :
<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>
Tukaj je comboBox1
ustvarjen pred label1
. Če se comboBox1_SelectionChanged
poskuša sklicevati na label1
, ta še ne bo ustvarjen.
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
label1.Content = comboBox1.SelectedIndex.ToString(); // NullReference here!!
}
Sprememba vrstnega reda deklaracij v XAML (tj. navedba label1
pred comboBox1
, brez upoštevanja vprašanj filozofije oblikovanja) bi vsaj rešila NullReferenceException
v tem primeru.
as
var myThing = someObject as Thing;
To ne vrže izjeme InvalidCastException, ampak vrne null
, kadar je cast neuspešen (in kadar je someObject sam null). Zato bodite pozorni na to.
Preprosti različici First()
in Single()
mečeta izjeme, kadar ni ničesar. Različice "OrDefault" v tem primeru vrnejo ničlo. Zato bodite pozorni na to.
foreach
vrže, ko poskušate iterirati ničelno zbirko. Običajno je to posledica nepričakovanega rezultata null
iz metod, ki vračajo zbirke.
List<int> list = null;
foreach(var v in list) { } // exception
Bolj realističen primer - izbira vozlišč iz dokumenta XML. Izvrši se, če vozlišča niso najdena, vendar začetno razhroščevanje pokaže, da so vse lastnosti veljavne:
foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))
null
, in ne upoštevajte ničelnih vrednosti.Če pričakujete, da bo referenca včasih ničelna, lahko pred dostopom do članov instance preverite, ali je null
:
void PrintName(Person p) {
if (p != null) {
Console.WriteLine(p.Name);
}
}
null
in zagotovite privzeto vrednost.Klic metode, za katero pričakujete, da bo vrnila instanco, lahko vrne null
, na primer kadar iskanega predmeta ni mogoče najti. V tem primeru lahko vrnete privzeto vrednost:
string GetCategory(Book b) {
if (b == null)
return "Unknown";
return b.Category;
}
null
pri klicih metod in vrzite izjemo po meri.Prav tako lahko vržete lastno izjemo, vendar jo ujamete šele v klicni kodi:
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
, če vrednost nikoli ne sme biti null
, da težavo ujamete prej, preden se pojavi izjema.Kadar med razvojem veste, da metoda morda lahko, vendar nikoli ne sme vrniti vrednosti null
, lahko uporabite Debug.Assert()
, da čim prej prekinete, ko se to zgodi:
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.
}
Čeprav to preverjanje ne bo končalo v vaši sestavi za izdajo, kar povzroči, da se ob izvajanju v načinu izdaje ponovno vrže izjema NullReferenceException
, ko je knjiga == null
.
GetValueOrDefault()
za tipe vrednosti, ki se lahko izničijo, da zagotovite privzeto vrednost, kadar so 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#] ali If()
[VB].Kratica za zagotavljanje privzete vrednosti, ko naletimo na null
:
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;
}
?.
ali ?[x]
za polja (na voljo v C# 6 in VB.NET 14):Včasih ga imenujemo tudi varna navigacija ali Elvisov operator (po obliki). Če je izraz na levi strani operatorja nič, se desna stran ne ovrednoti in se namesto tega vrne nič. To pomeni primere, kot je ta:
var title = person.Title.ToUpper();
Če oseba nima naziva, bo to vrglo izjemo, ker poskuša poklicati ToUpper
na lastnost z ničelno vrednostjo.
V C# 5 in nižjih je to mogoče zaščititi z:
var title = person.Title == null ? null : person.Title.ToUpper();
Zdaj bo spremenljivka naslov ničelna, namesto da bi se vrgla izjema. C# 6 za to uvaja krajšo sintakso:
var title = person.Title?.ToUpper();
Zaradi tega bo spremenljivka naslov null
, klic ToUpper
pa se ne izvede, če je person.Title
null
.
Seveda morate še vedno preveriti, ali je naslov
ničen, ali pa uporabiti operator ničelnega pogoja skupaj z operatorjem ničelnega združevanja (??
), da zagotovite privzeto vrednost:
// 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;
Podobno lahko za polja uporabite ?[i]
na naslednji način:
int[] myIntArray=null;
var i=5;
int? elem = myIntArray?[i];
if (!elem.HasValue) Console.WriteLine("No value");
To bo naredilo naslednje: Če je myIntArray nič, izraz vrne nič in ga lahko varno preverite. Če vsebuje polje, bo naredil enako kot:
elem = myIntArray[i];
in vrne ith element.
V C# 8 so bili uvedeni ničelni konteksti in referenčne vrste, ki jih je mogoče izničiti, izvajajo statično analizo spremenljivk in zagotavljajo opozorilo za prevajalnik, če je vrednost lahko potencialno ničelna ali če je bila nastavljena na nič. Referenčne vrste, ki jih je mogoče izničiti, omogočajo, da je tipom izrecno dovoljeno, da so ničelni. Kontekst anotacije nullable in kontekst opozorila nullable lahko za projekt nastavite z elementom Nullable v datoteki csproj. Ta element konfigurira, kako prevajalnik razlaga ničelnost tipov in katera opozorila se generirajo. Veljavne nastavitve so:
?
.C# podpira "bloke iteratorjev" (v nekaterih drugih priljubljenih jezikih se imenujejo "generatorji"). Zaradi odloženega izvajanja je lahko odpravljanje napak pri izjemah ničelnega sklicevanja v blokih iteratorjev še posebej zapleteno:
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) { ... }
Če je rezultat česa
null
, bo MakeFrob
vrgel. Zdaj morda mislite, da je prav, da naredite to:
// 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();
}
Zakaj je to narobe? Ker se blok iteratorja dejansko ne izvede, dokler se ne izvede foreach
! Klic GetFrobs
preprosto vrne objekt, ki bo po iteraciji zagnal blok iteratorja.
S takšnim zapisom preverjanja ničelnosti preprečimo null dereferenciranje, vendar pa izjemo ničelnega argumenta premaknemo na točko iteracije in ne na točko klica, kar je zelo zmedeno pri odpravljanju napak.
Pravilna rešitev je:
// 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();
}
To pomeni, da naredite zasebno pomožno metodo, ki ima logiko bloka iteratorja, in javno površinsko metodo, ki preveri ničelnost in vrne iterator. Zdaj se ob klicu GetFrobs
preverjanje ničelnosti izvede takoj, nato pa se GetFrobsForReal
izvede ob iteraciji zaporedja.
Če si ogledate referenčni vir za LINQ to Objects, boste videli, da je ta tehnika uporabljena povsod. Napisati jo je nekoliko bolj nerodno, vendar je odpravljanje napak zaradi ničnosti veliko lažje. Optimizirajte kodo za udobje klicatelja in ne za udobje avtorja.
C# ima način "unsafe", ki je, kot pove že ime, zelo nevaren, saj se običajni varnostni mehanizmi, ki zagotavljajo varnost pomnilnika in tipov, ne izvajajo. Nevarne kode ne smete pisati, če nimate temeljitega in poglobljenega razumevanja delovanja pomnilnika. V nevarnem načinu se morate zavedati dveh pomembnih dejstev:
To pomeni, da je zadevna spremenljivka usmerjena v nič. To bi lahko ustvaril na naslednji način:
SqlConnection connection = null;
connection.Open();
To bo vrglo napako, ker čeprav sem deklariral spremenljivko "povezava
", ni usmerjena na nič. Ko poskušam poklicati član "Open
", ni reference, ki bi jo lahko razrešil, in bo vrgel napako.
Da bi se izognil tej napaki:
object == null
.Orodje JetBrains' Resharper bo prepoznalo vsako mesto v vaši kodi, kjer obstaja možnost napake ničelne reference, in vam omogočilo, da vstavite preverjanje ničelnosti. Ta napaka je IMHO najpomembnejši vir napak.
To pomeni, da je vaša koda uporabila spremenljivko reference na objekt, ki je bila nastavljena na nič (tj. ni se sklicevala na dejanski primerek objekta).
Da bi preprečili to napako, je treba pred uporabo objektov, ki so lahko ničelni, preveriti, ali so ničelni.
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
}