
av Christian Neumanns
Hva er betydningen avnull
? Hvordan ernull
implementert? Når bør du brukenull
i kildekoden, og når bør duikkebruk det?
Introduksjon
null
er et grunnleggende konsept i mange programmeringsspråk. Det er allestedsnærværende i alle typer kildekode skrevet på disse språkene. Så det er viktig å fullt ut forstå ideen omnull
. Vi må forstå dens semantikk og implementering, og vi må vite hvordan vi skal brukenull
i kildekoden vår.
Kommentarer i programmererfora avslører noen ganger litt forvirring mednull
. Noen programmerere prøver til og med å unngå detnull
. Fordi de tenker på det som "million-dollar feilen", et begrep laget av Tony Hoare, oppfinneren avnull
.
Her er et enkelt eksempel: Anta at Aliceepostadresse
poeng tilnull
. Hva betyr dette? Betyr det at Alice ikke har en e-postadresse? Eller at e-postadressen hennes er ukjent? Eller at det er hemmelig? Eller betyr det rett og slett detepostadresse
er 'udefinert' eller 'uinitialisert'? La oss se. Etter å ha lest denne artikkelen, bør alle kunne svare på slike spørsmål uten å nøle.
Merk:Denne artikkelen er programmeringsspråknøytral - så langt det er mulig. Forklaringer er generelle og ikke knyttet til et spesifikt språk. Se bruksanvisningene for programmeringsspråket for spesifikke råd omnull
. Imidlertid inneholder denne artikkelen noen enkle kildekodeeksempler vist i Java. Men det er ikke vanskelig å oversette dem til favorittspråket ditt.
Run-time Implementering
Før vi diskuterer betydningen avnull
, vi må forstå hvordannull
implementeres i minnet under kjøring.
Merk:Vi skal ta en titt på entypiskImplementering avnull
. Den faktiske implementeringen i et gitt miljø avhenger av programmeringsspråket og målmiljøet, og kan avvike fra implementeringen vist her.
Anta at vi har følgende kildekodeinstruksjon:
Strengnavn = "Bob";
Her erklærer vi en variabel av typenString
og med identifikatorenNavn
som peker på strengen"Bob"
.
Å si «peker på» er viktig i denne sammenhengen, fordi vi forutsetter at vi jobber medreferansetyper(og ikke medverdityper). Mer om dette senere.
For å gjøre ting enkelt, vil vi gjøre følgende forutsetninger:
- Instruksjonen ovenfor utføres på en 16-bits CPU med et 16-bits adresserom.
- Strenger er kodet som UTF-16. De avsluttes med 0 (som i C eller C++).
Følgende bilde viser et utdrag av minnet etter å ha utført instruksjonen ovenfor:
Minneadressene i bildet ovenfor er valgt vilkårlig og er irrelevante for vår diskusjon.
Som vi kan se, strengen"Bob"
er lagret på adresse B000 og opptar 4 minneceller.
VariabelNavn
ligger på adresse A0A1. Innholdet i A0A1 er B000, som er startminnet til strengen"Bob"
. Derfor sier vi: VariabelenNavn
poeng til "Bob"
.
Så langt så bra.
Anta nå at du, etter å ha utført instruksjonen ovenfor, utfører følgende:
navn = null;
NåNavn
poeng tilnull
.
Og dette er den nye tilstanden i minnet:
Vi kan se at ingenting har endret seg for strengen"Bob"
som fortsatt er lagret i minnet.
Merk: Minnet som trengs for å lagre strengen"Bob"
kan senere bli utgitt hvis det er en søppeloppsamler og ingen andre referanser peker på"Bob"
, men dette er irrelevant i vår diskusjon.
Det som er viktig er at innholdet i A0A1 (som representerer verdien av variabelNavn
) er nå 0000. Så variabelNavn
peker ikke på"Bob"
lenger. Verdien 0 (alle biter på null) er en typisk verdi som brukes i minnet for å anginull
. Det betyr at det finnesingen verdi knyttet tilNavn
. Du kan også tenke på det somfraværet av dataeller rett og slettingen data.
Merk: Den faktiske minneverdien som brukes til å anginull
er implementeringsspesifikk. For eksempelJava Virtual Machine-spesifikasjonstår på slutten av avsnittet2.4. "Referansetyper og verdier:"
Java Virtual Machine-spesifikasjonen krever ikke en konkret verdikodingnull
.
Huske:
Hvis en referanse peker pånull
, det betyr ganske enkelt at det er det ingen verdi knyttet til det.
Teknisk sett inneholder minneplasseringen som er tildelt referansen verdien 0 (alle biter på null), eller en hvilken som helst annen verdi som angirnull
i det gitte miljøet.
Opptreden
Som vi lærte i forrige avsnitt, operasjoner som involverernull
er ekstremt raske og enkle å utføre under kjøring.
Det er bare to typer operasjoner:
- Initialiser eller angi en referanse til
null
(f.eks.navn = null
): Det eneste du kan gjøre er å endre innholdet i én minnecelle (f.eks. sette den til 0). - Sjekk om en referanse peker på
null
(f.eks.hvis navn == null
): Det eneste du må gjøre er å sjekke om minnecellen til referansen har verdien 0.
Huske:
Operasjoner pånull
er ekstremt raske og billige.
Referanse vs verdityper
Så langt har vi antatt å jobbe medreferansetyper. Grunnen til dette er enkel:null
eksisterer ikke forverdityper.
Hvorfor?
Som vi har sett tidligere, er en referanse enpekerentil en minneadresse som lagrer en verdi (f.eks. en streng, en dato, en kunde, hva som helst). Hvis en referanse peker pånull
, da er ingen verdi knyttet til den.
På den annen side er en verdi per definisjon selve verdien. Det er ingen peker involvert. En verditype lagres som selve verdien. Derfor konseptet mednull
finnes ikke for verdityper.
Følgende bilde viser forskjellen. På venstre side kan du se igjen minnet ved variabelNavn
være en referanse som peker på "Bob". Høyre side viser minnet i tilfelle variabelNavn
være en verditype.
Som vi kan se, i tilfelle av en verditype, lagres selve verdien direkte på adressen A0A1 som er assosiert med variabelNavn
.
Det vil være mye mer å si om referanse kontra verdityper, men dette er utenfor rammen av denne artikkelen. Vær også oppmerksom på at noen programmeringsspråk kun støtter referansetyper, andre støtter kun verdityper, og noen (f.eks. C# og Java) støtter begge.
Huske:
Konseptet avnull
eksisterer kun forhenvisningtyper. Det finnes ikke forverdityper.
Betydning
Anta at vi har en typeperson
med et feltepostadresse
. Anta også at for en gitt person som vi vil kalle Alice,epostadresse
poeng tilnull
.
Hva betyr dette? Betyr det at Alice ikke har en e-postadresse? Ikke nødvendigvis.
Som vi allerede har sett, er det vi kan hevdeingen verdi er knyttet til e-postadresse.
MenHvorforer det ingen verdi? Hva er grunnen tilepostadresse
peker pånull
? Hvis vi ikke kjenner konteksten og historien, kan vi bare spekulere. Grunnen tilnull
kunnevære:
Alice har ingen e-postadresse. Eller…
Alice har en e-postadresse, men:
- den er ennå ikke lagt inn i databasen
- det er hemmelig (uavslørt av sikkerhetsgrunner)
- det er en feil i en rutine som lager et personobjekt uten innstillingsfelt
epostadresse
- og så videre.
I praksis kjenner vi ofte bruken og konteksten. Vi knytter intuitivt en presis mening tilnull
. I en enkel og feilfri verden,null
ville ganske enkelt bety at Alice faktisk ikke har en e-postadresse.
Når vi skriver kode, er årsakenHvorforen referanse peker pånull
er ofte irrelevant. Vi bare sjekker etternull
og ta passende tiltak. Anta for eksempel at vi må skrive en løkke som sender e-post for en liste over personer. Koden (i Java) kan se slik ut:
for ( Person person: personer ) { if ( person.getEmailAddress() != null ) { // kode for å sende e-post } else { logger.warning("Ingen e-postadresse for " + person.getName()); }}
I løkken ovenfor bryr vi oss ikke om årsaken tilnull
. Vi bare erkjenner det faktum at det ikke er noen e-postadresse, logger en advarsel og fortsetter.
Huske:
Hvis en referanse peker pånull
da betyr det alltid at det er det ingen verdi knyttet til det.
I de fleste tilfeller,null
har enmer spesifikk betydning som avhenger av konteksten.
Hvorfor er detnull
?
Noen ganger deterviktig å viteHvorforen referanse peker pånull
.
Tenk på følgende funksjonssignatur i en medisinsk applikasjon:
Liste getAllergiesOfPatient ( String patientId )
I dette tilfellet tilbakenull
(eller en tom liste) er tvetydig. Betyr det at pasienten ikke har allergi, eller betyr det at en allergitest ennå ikke er utført? Dette er to semantisk svært forskjellige saker som må håndteres forskjellig. Ellers kan utfallet være livstruende.
Bare anta at pasienten har allergier, men en allergitest er ennå ikke utført og programvaren forteller legen at "det er ingen allergier". Derfor trenger vi tilleggsinformasjon. Vi trenger å viteHvorforfunksjonen returnerernull
.
Det ville være fristende å si: Vel, for å skille, kommer vi tilbakenull
dersom det ennå ikke er utført en allergitest, og vi returnerer en tom liste dersom det ikke er allergier.
IKKE GJØR DETTE!
Dette er dårlig datadesign av flere grunner.
De forskjellige semantikkene for returnull
versus å returnere en tom liste må være godt dokumentert. Og som vi alle vet, kan kommentarer være feil (dvs. inkonsistente med koden), utdaterte, eller de kan til og med være utilgjengelige.
Det er ingen beskyttelse for misbruk i klientkode som kaller opp funksjonen. For eksempel er følgende kode feil, men den kompileres uten feil. Dessuten er feilen vanskelig å få øye på for en menneskelig leser. Vi kan ikke se feilen bare ved å se på koden uten å vurdere kommentaren tilgetAllergiesOfPatient:
List allergies = getAllergiesOfPatient ( "123" );if ( allergier == null ) { System.out.println ( "Ingen allergier" ); // <-- FEIL!} else if ( allergies.isEmpty() ) { System.out.println ( "Test ikke utført ennå" ); // <-- FEIL!} else { System.out.println ( "Det er allergier" );}
Følgende kode ville også være feil:
List allergies = getAllergiesOfPatient ( "123" );if ( allergier == null || allergies.isEmpty() ) { System.out.println ( "Ingen allergier" ); // <-- FEIL!} else { System.out.println ( "Det er allergier" );}
Hvisnull
/tom-logikk avgetAllergiesOfPatient
endringer i fremtiden, så må kommentaren oppdateres, samt all klientkode. Og det er ingen beskyttelse mot å glemme noen av disse endringene.
Hvis det senere er et annet tilfelle som skal skilles ut (f.eks. venter en allergitest - resultatene er ikke tilgjengelige ennå), eller hvis vi ønsker å legge til spesifikke data for hvert tilfelle, så står vi fast.
Så funksjonen må returnere mer informasjon enn bare en liste.
Det er forskjellige måter å gjøre dette på, avhengig av hvilket programmeringsspråk vi bruker. La oss ta en titt på enmuligløsning i Java.
For å differensiere sakene definerer vi en overordnet typeAllergitestresultat
, samt tre undertyper som representerer de tre tilfellene (Ikke ferdig
,Avventer
, ogFerdig
):
grensesnitt AllergyTestResult {}
grensesnitt NotDoneAllergyTestResult utvider AllergyTestResult {}
grensesnitt PendingAllergyTestResult utvider AllergyTestResult { public Date getDateStarted();}
interface DoneAllergyTestResult extends AllergyTestResult { public Date getDateDone(); offentlig liste getAllergies(); // null hvis ingen allergier // ikke-tom hvis det er // allergier}
Som vi kan se, kan vi for hvert enkelt tilfelle ha spesifikke data knyttet til det.
I stedet for bare å returnere en liste,getAllergiesOfPatient
returnerer nå enAllergitestresultat
gjenstand:
AllergyTestResult getAllergiesOfPatient( String patientId )
Klientkoden er nå mindre utsatt for feil og ser slik ut:
AllergyTestResult allergyTestResult = getAllergiesOfPatient("123");
if (allergyTestResult-forekomst av NotDoneAllergyTestResult) { System.out.println ( "Test ikke utført ennå" ); } else if (allergyTestResult-forekomst av PendingAllergyTestResult) { System.out.println ("Test venter"); } else if (allergyTestResult-forekomst av DoneAllergyTestResult) { List list = ((DoneAllergyTestResult) allergyTestResult).getAllergies(); if (liste == null) { System.out.println ( "Ingen allergier" ); } else if (list.isEmpty()) { assert false; } else { System.out.println ( "Det er allergier" ); }} annet { påstå falsk;}
Merk: Hvis du synes at koden ovenfor er ganske detaljert og litt vanskelig å skrive, så er du ikke alene. Noen moderne språk lar oss skrive konseptuelt lik kode mye mer kortfattet. Og nullsikre språk skiller mellom nullbare og ikke-nullbare verdier på en pålitelig måte ved kompilering - det er ikke nødvendig å kommentere nullbarheten til en referanse eller å sjekke om en referanse som er erklært å være ikke-null ved et uhell har blitt satt tilnull
.
Huske:
Hvis vi trenger å vite hvorfor det ikke er noen verdi knyttet til en referanse, datilleggsdata må gis for å skille de mulige tilfellene.
Initialisering
Vurder følgende instruksjoner:
String s1 = "foo";String s2 = null;String s3;
Den første instruksen erklærer enString
variabels1
og tildeler den verdien"foo"
.
Den andre instruksjonen tildelernull
tils2
.
Den mer interessante instruksjonen er den siste. Ingen verdi er eksplisitt tildelts3
. Derfor er det rimelig å spørre: Hva er tilstanden tils3
etter dens erklæring? Hva vil skje hvis vi skrivers3
til OS-utgangsenheten?
Det viser seg at tilstanden til en variabel (eller klassefelt) deklarert uten å tildele en verdi, avhenger av programmeringsspråket. Dessuten kan hvert programmeringsspråk ha spesifikke regler for forskjellige tilfeller. For eksempel gjelder forskjellige regler for referansetyper og verdityper, statiske og ikke-statiske medlemmer av en klasse, globale og lokale variabler og så videre.
Så vidt jeg vet, er følgende regler typiske variasjoner som oppstår:
- Det er ulovlig å deklarere en variabel uten også å tildele en verdi
- Det er en vilkårlig verdi lagret i
s3
, avhengig av minneinnholdet på kjøringstidspunktet - det er ingen standardverdi - En standardverdi blir automatisk tilordnet
s3.
For en referansetype er standardverdiennull.
Når det gjelder en verditype, avhenger standardverdien av variabelens type. For eksempel0
for heltall,falsk
for en boolsk og så videre. - staten av
s3
er 'udefinert' - staten av
s3
er 'uinitialisert', og ethvert forsøk på å brukes3
resulterer i en kompileringsfeil.
Det beste alternativet er det siste. Alle andre alternativer er utsatt for feil og/eller upraktiske - av grunner vi ikke vil diskutere her, fordi denne artikkelen fokuserer pånull
.
Som et eksempel bruker Java det siste alternativet for lokale variabler. Følgende kode resulterer derfor i en kompileringstidsfeil på den andre linjen:
String s3;System.out.println ( s3 );
Kompilatorutgang:
feil: variabel s3 er kanskje ikke initialisert
Huske:
Hvis en variabel er deklarert, men ingen eksplisitt verdi er tilordnet den,da avhenger tilstanden av flere faktorer som er forskjellige på forskjellige programmeringsspråk.
På noen språk,null
er standardverdien for referansetyper.
Når du skal brukenull
(Og når du ikke skal bruke det)
Grunnregelen er enkel:null
bør bare tillates når det er fornuftig at en objektreferanse har 'ingen verdi knyttet til seg'. (Merk: en objektreferanse kan være en variabel, konstant, egenskap (klassefelt), input/output argument, og så videre.)
Anta for eksempel typeperson
med feltNavn
ogdateOfFirstMarriage
:
grensesnitt Person { public String getName(); offentlig Date getDateOfFirstMarriage();}
Hver person har et navn. Derfor gir det ikke mening for feltNavn
å ha 'ingen verdi knyttet til det'. FeltNavn
erikke nullbar. Det er ulovlig å tildelenull
til det.
På den annen side feltdateOfFirstMarriage
representerer ikke en påkrevd verdi. Ikke alle er gift. Derfor gir det mening fordateOfFirstMarriage
å ha 'ingen verdi knyttet til det'. DerfordateOfFirstMarriage
er ennullbarfelt. Hvis en personsdateOfFirstMarriage
feltet peker pånull
da betyr det ganske enkelt at denne personen aldri har vært gift.
Merk: Dessverre skiller de fleste populære programmeringsspråk ikke mellom nullbare og ikke-nullbare typer. Det er ingen måte å si det pålitelig pånull
kan aldri tilordnes en gitt objektreferanse. På noen språk er det mulig å bruke merknader, for eksempel de ikke-standardiserte merknadene @Nullable og @NonNullable i Java. Her er et eksempel:
grensesnitt Person { public @Nonnull String getName(); offentlig @Nullable Date getDateOfFirstMarriage();}
Imidlertid brukes ikke slike merknader av kompilatoren for å sikre null-sikkerhet. Likevel er de nyttige for den menneskelige leseren, og de kan brukes av IDE-er og verktøy som statiske kodeanalysatorer.
Det er viktig å merke seg detnull
skal ikke brukes til å angi feiltilstander.
Tenk på en funksjon som leser konfigurasjonsdata fra en fil. Hvis filen ikke eksisterer eller er tom, bør en standardkonfigurasjon returneres. Her er funksjonens signatur:
public Config readConfigFromFile (filfil)
Hva skal skje i tilfelle en fillesefeil?
Bare returnernull
?
NEI!
Hvert språk har sin egen standardmåte for å signalisere feiltilstander og gi data om feilen, for eksempel en beskrivelse, type, stabelsporing og så videre. Mange språk (C#, Java osv.) bruker en unntaksmekanisme, og unntak bør brukes i disse språkene for å signalisere kjøretidsfeil.readConfigFromFile
skal ikke returnerenull
for å angi en feil. I stedet bør funksjonens signatur endres for å gjøre det klart at funksjonen kan mislykkes:
public Config readConfigFromFile ( File file ) kaster IOException
Huske:
Tillatenull
bare hvis det er fornuftig at en objektreferanse har 'ingen verdi knyttet til seg'.
Ikke bruknull
for å signalisere feiltilstander.
Null-sikkerhet
Tenk på følgende kode:
Strengnavn = null;int l = navn.lengde();
Ved kjøretid resulterer koden ovenfor i den beryktedenull-pekerfeil, fordi vi prøver å utføre en metode for en referanse som peker pånull
. I C#, for eksempel, aNullReferenceException
er kastet, i Java er det enNullPointerException
.
Nullpekerfeilen er ekkel.
Det er denhyppigste feili mange programvareapplikasjoner, og har vært årsaken til utallige problemer i programvareutviklingens historie. Tony Hoare, oppfinneren avnull
, kaller det 'milliardfeilen'.
Men Tony Hoare (Turing Award-vinner i 1980 og oppfinner av Quicksort-algoritmen), gir også et hint til en løsning i sintale:
Nyere programmeringsspråk ... har introdusert erklæringer for ikke-nullreferanser. Dette er løsningen, som jeg avviste i 1965.
I motsetning til noen vanlig tro, er den skyldige ikke detnull
per se. Problemet ermangel på støtte tilnull
håndteringpå mange programmeringsspråk. For eksempel, i skrivende stund (mai 2018), er ingen av de ti beste språkene iTiobe indeksskiller naturlig mellom nullbare og ikke-nullbare typer.
Derfor gir noen nye språk kompileringstid null-sikkerhet og spesifikk syntaks for praktisk håndteringnull
i kildekoden. På disse språkene vil koden ovenfor resultere i en kompileringsfeil. Programvarekvalitet og pålitelighet øker betraktelig, fordi null-pekerfeilen forsvinner.
Null-sikkerhet er et fascinerende emne som fortjener en egen artikkel.
Huske:
Når det er mulig, bruk et språk somstøtter null-sikkerhet ved kompilering.
Merk: Noen programmeringsspråk (for det meste funksjonelle programmeringsspråk som Haskell) støtter ikke konseptetnull
. I stedet bruker deKanskje/valgfritt mønster å representere "fraværet av en verdi". Kompilatoren sikrer at "ingen verdi"-saken blir eksplisitt behandlet. Derfor kan null-pekerfeil ikke oppstå.
Sammendrag
Her er et sammendrag av viktige punkter å huske:
- Hvis en referanse peker på
null
, det betyr alltid at det er detingen verdi knyttet til det. - I de fleste tilfeller,
null
har en mer spesifikk betydning som avhenger av konteksten. - Hvis vi trenger å viteHvorfordet er ingen verdi knyttet til en referanse, da må tilleggsdata gis for å skille de mulige tilfellene.
- Tillate
null
bare hvis det er fornuftig at en objektreferanse har 'ingen verdi knyttet til seg'. - Ikke bruk
null
for å signalisere feiltilstander. - Konseptet av
null
eksisterer kun for referansetyper. Det finnes ikke for verdityper. - På noen språk
null
er standardverdien for referansetyper. null
operasjonene er ekstremt raske og billige.- Når det er mulig, bruk et språk som støtter compile-time-null-safety.
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
ANNONSE
Hvis denne artikkelen var nyttig, .
Lær å kode gratis. freeCodeCamps åpen kildekode-pensum har hjulpet mer enn 40 000 mennesker med å få jobb som utviklere.Kom i gang
ANNONSE