Programmering

Er kontrollerede undtagelser gode eller dårlige?

Java understøtter kontrollerede undtagelser. Denne kontroversielle sprogfunktion er elsket af nogle og hadet af andre, til det punkt, hvor de fleste programmeringssprog undgår kontrollerede undtagelser og kun understøtter deres ukontrollerede kolleger.

I dette indlæg undersøger jeg kontroversen omkring kontrollerede undtagelser. Jeg introducerer først undtagelseskonceptet og beskriver kort Java's sprogstøtte til undtagelser for at hjælpe begyndere med bedre at forstå kontroversen.

Hvad er undtagelser?

I en ideel verden ville computerprogrammer aldrig støde på problemer: filer ville eksistere, når de skulle eksistere, netværksforbindelser ville aldrig lukke uventet, der ville aldrig være et forsøg på at påberåbe en metode via nulreferencen, heltal-division-by -nul-forsøg ville ikke forekomme og så videre. Imidlertid er vores verden langt fra ideel; disse og andre undtagelser til ideel programudførelse er udbredt.

Tidlige forsøg på at genkende undtagelser omfattede returnering af specielle værdier, der indikerer fejl. For eksempel C-sprog fopen () funktion vender tilbage NUL når den ikke kan åbne en fil. Også PHP'er mysql_query () funktion vender tilbage FALSK når der opstår en SQL-fejl. Du skal kigge andre steder efter den faktiske fejlkode. Selvom det er let at implementere, er der to problemer med denne tilgang til "returner særlig værdi" til at genkende undtagelser:

  • Særlige værdier beskriver ikke undtagelsen. Hvad gør NUL eller FALSK virkelig mener? Det hele afhænger af forfatteren af ​​funktionaliteten, der returnerer den specielle værdi. Desuden, hvordan relaterer du en særlig værdi til programmets kontekst, da undtagelsen opstod, så du kan præsentere en meningsfuld besked til brugeren?
  • Det er for let at ignorere en særlig værdi. For eksempel, int c; FIL * fp = fopen ("data.txt", "r"); c = fgetc (fp); er problematisk, fordi dette C-kodefragment udføres fgetc () at læse et tegn fra filen, selv når fopen () vender tilbage NUL. I dette tilfælde, fgetc () lykkes ikke: vi har en fejl, der kan være vanskelig at finde.

Det første problem løses ved hjælp af klasser til at beskrive undtagelser. En klasses navn identificerer typen af ​​undtagelse, og dens felter aggregerer passende programkontekst til at bestemme (via metodeopkald) hvad der gik galt. Det andet problem løses ved at lade kompilatoren tvinge programmøren til enten at svare direkte på en undtagelse eller angive, at undtagelsen skal håndteres andetsteds.

Nogle undtagelser er meget alvorlige. For eksempel kan et program forsøge at allokere noget hukommelse, når der ikke er ledig hukommelse tilgængelig. Grænseløs rekursion, der udtømmer stakken, er et andet eksempel. Sådanne undtagelser er kendt som fejl.

Undtagelser og Java

Java bruger klasser til at beskrive undtagelser og fejl. Disse klasser er organiseret i et hierarki, der er rodfæstet i java.lang. kan kastes klasse. (Grunden til Kan kastes blev valgt til at navngive denne specielle klasse vil snart fremgå.) Direkte nedenunder Kan kastes er de java.lang.Undtagelse og java.lang.Error klasser, der beskriver henholdsvis undtagelser og fejl.

For eksempel inkluderer Java-biblioteket java.net.URISyntaxException, som strækker sig Undtagelse og indikerer, at en streng ikke kunne parses som en Uniform Resource Identifier-reference. Noter det URISyntaxException følger en navngivningskonvention, hvor et undtagelsesklassens navn slutter med ordet Undtagelse. En lignende konvention gælder for fejlklassens navne, f.eks java.lang.OutOfMemoryError.

Undtagelse er underklasseret af java.lang.RuntimeException, som er superklassen af ​​de undtagelser, der kan kastes under den normale drift af Java Virtual Machine (JVM). For eksempel, java.lang.ArithmeticException beskriver aritmetiske fejl såsom forsøg på at dividere heltal med heltal 0. Også java.lang.NullPointerException beskriver forsøg på at få adgang til objektmedlemmer via null-referencen.

En anden måde at se på RuntimeException

Afsnit 11.1.1 i Java 8 Language Specification hedder: RuntimeException er superklassen af ​​alle de undtagelser, der kan kastes af mange grunde under evaluering af udtryk, men hvorfra genopretning stadig kan være mulig.

Når en undtagelse eller fejl opstår, et objekt fra det relevante Undtagelse eller Fejl Underklassen oprettes og sendes til JVM. Handlingen med at passere objektet er kendt som kaster undtagelsen. Java leverer kaste erklæring til dette formål. For eksempel, smid ny IOException ("ude af stand til at læse fil"); skaber et nyt java.io.IOUndtagelse objekt, der er initialiseret til den angivne tekst. Dette objekt kastes efterfølgende til JVM.

Java leverer prøve erklæring til afgrænsning af kode, hvorfra en undtagelse kan kastes. Denne erklæring består af nøgleord prøve efterfulgt af en afstivningsafgrænset blok. Følgende kodefragment demonstrerer prøve og kaste:

prøv {metode (); } // ... ugyldig metode () {smid ny NullPointerException ("noget tekst"); }

I dette kodefragment kommer udførelse ind i prøve blokere og påberåbe sig metode(), som kaster en forekomst af NullPointerException.

JVM modtager kastbar og søger op i metoden-opkaldstakken for en handler til at håndtere undtagelsen. Undtagelser, der ikke er afledt af RuntimeException håndteres ofte runtime undtagelser og fejl håndteres sjældent.

Hvorfor fejl sjældent håndteres

Fejl håndteres sjældent, fordi der ofte ikke er noget, som et Java-program kan gøre for at gendanne fejlen. For eksempel, når ledig hukommelse er opbrugt, kan et program ikke allokere yderligere hukommelse. Men hvis tildelingsfejlen skyldes, at man holder fast i en masse hukommelse, der skal frigøres, kan en afleverer forsøge at frigøre hukommelsen med hjælp fra JVM. Selvom en handler måske synes at være nyttig i denne fejlkontekst, kan forsøget muligvis ikke lykkes.

En handler er beskrevet af en fangst blok, der følger prøve blok. Det fangst blok indeholder en overskrift, der viser de typer undtagelser, som den er klar til at håndtere. Hvis den kastbare type er medtaget på listen, sendes den kastbare til fangst blokere hvis kode udføres. Koden reagerer på årsagen til fiasko på en sådan måde, at programmet fortsætter eller muligvis afsluttes:

prøv {metode (); } fange (NullPointerException npe) {System.out.println ("forsøg på at få adgang til objektmedlem via null reference"); } // ... ugyldig metode () {smid ny NullPointerException ("noget tekst"); }

I dette kodefragment har jeg tilføjet en fangst blokere til prøve blok. Når NullPointerException genstand kastes fra metode(), JVM lokaliserer og videregiver udførelse til fangst blok, der udsender en besked.

Endelig blokerer

EN prøve blok eller dens endelige fangst blok kan efterfølges af en langt om længe blok, der bruges til at udføre oprydningsopgaver, såsom frigivelse af erhvervede ressourcer. Jeg har ikke mere at sige om langt om længe fordi det ikke er relevant for diskussionen.

Undtagelser beskrevet af Undtagelse og dens underklasser undtagen RuntimeException og dens underklasser er kendt som kontrollerede undtagelser. For hver kaste erklæring, kompilatoren undersøger undtagelsesobjektets type. Hvis typen angiver markeret, kontrollerer kompilatoren kildekoden for at sikre, at undtagelsen håndteres i den metode, hvor den kastes eller erklæres behandlet længere op i metoden-opkaldstakken. Alle andre undtagelser er kendt som ukontrollerede undtagelser.

Java giver dig mulighed for at erklære, at en kontrolleret undtagelse håndteres længere oppe i metoden-opkaldstakken ved at tilføje en kaster klausul (nøgleord kaster efterfulgt af en komma-afgrænset liste over kontrollerede undtagelsesklassenavne) til en metodeoverskrift:

prøv {metode (); } fange (IOException ioe) {System.out.println ("I / O-fejl"); } // ... ugyldig metode () kaster IOException {kast ny IOException ("noget tekst"); }

Fordi IOUndtagelse er en kontrolleret undtagelsestype, skal kastede forekomster af denne undtagelse håndteres i den metode, hvor de kastes eller erklæres behandlet længere oppe i metoden-opkaldstakken ved at tilføje en kaster klausul til hver berørt metodes overskrift. I dette tilfælde a kaster IOException klausul er vedhæftet metode()header. Den kastede IOUndtagelse objekt sendes til JVM, som lokaliserer og overfører udførelse til fangst handler.

Argumenterer for og imod kontrollerede undtagelser

Kontrollerede undtagelser har vist sig at være meget kontroversielle. Er de et godt sprog, eller er de dårlige? I dette afsnit præsenterer jeg sagerne for og imod kontrollerede undtagelser.

Kontrollerede undtagelser er gode

James Gosling oprettede Java-sproget. Han inkluderede kontrollerede undtagelser for at tilskynde til oprettelse af mere robust software. I en samtale fra 2003 med Bill Venners påpegede Gosling, hvor let det er at generere buggykode på C-sproget ved at ignorere de specielle værdier, der returneres fra Cs filorienterede funktioner. For eksempel forsøger et program at læse fra en fil, der ikke blev åbnet for læsning.

Alvorligheden ved ikke at kontrollere returværdier

Ikke at kontrollere returværdier kan virke som ingen big deal, men denne sløvhed kan have liv eller død konsekvenser. Tænk for eksempel på sådan buggysoftware, der styrer missilstyringssystemer og førerløse biler.

Gosling påpegede også, at programmeringskurser på college ikke tilstrækkeligt diskuterer fejlhåndtering (skønt det kan have ændret sig siden 2003). Når du går gennem college, og du laver opgaver, beder de dig bare om at kode den ene sande sti [udførelse, hvor fiasko ikke er en overvejelse]. Jeg har bestemt aldrig oplevet et college-kursus, hvor fejlhåndtering overhovedet blev diskuteret. Du kommer ud af college, og det eneste du har haft at gøre med er den eneste rigtige vej.

At kun fokusere på den ene sande sti, dovenskab eller en anden faktor har resulteret i, at der er skrevet en masse buggy-kode. Kontrollerede undtagelser kræver, at programmøren overvejer kildekodens design og forhåbentlig opnår mere robust software.

Kontrollerede undtagelser er dårlige

Mange programmører hader kontrollerede undtagelser, fordi de er tvunget til at beskæftige sig med API'er, der overanvender dem eller forkert specificerer afkrydsede undtagelser i stedet for ukontrollerede undtagelser som en del af deres kontrakter. For eksempel sendes en metode, der indstiller en sensors værdi, til et ugyldigt tal og kaster en markeret undtagelse i stedet for en forekomst af det ukontrollerede java.lang.IllegalArgumentException klasse.

Her er et par andre grunde til ikke at kunne lide afkrydsede undtagelser; Jeg har uddraget dem fra Slashdots interviews: Spørg James Gosling om Java og Ocean Exploring Robots-diskussion:

  • Kontrollerede undtagelser er lette at ignorere ved at genkaste dem som RuntimeException tilfælde, så hvad er meningen med at have dem? Jeg har mistet antallet af gange, jeg har skrevet denne blok med kode:
    prøv {// gør ting} fangst (Irriterende afkrydsetException e) {kast ny RuntimeException (e); }

    99% af tiden kan jeg ikke gøre noget ved det. Endelig gør blokke enhver nødvendig oprydning (eller i det mindste de burde).

  • Kontrollerede undtagelser kan ignoreres ved at sluge dem, så hvad er meningen med at have dem? Jeg har også mistet antallet af gange, jeg har set dette:
    prøv {// gør ting} fangst (AnnoyingCheckedException e) {// gør ingenting}

    Hvorfor? Fordi nogen skulle håndtere det og var doven. Var det forkert? Jo da. Er det tilfældet? Absolut. Hvad hvis dette i stedet var en ukontrolleret undtagelse? Appen ville lige være død (hvilket er at foretrække frem for at sluge en undtagelse).

  • Kontrollerede undtagelser resulterer i flere kaster klausulerklæringer. Problemet med kontrollerede undtagelser er, at de tilskynder folk til at sluge vigtige detaljer (nemlig undtagelsesklassen). Hvis du vælger ikke at sluge den detalje, skal du fortsætte med at tilføje kaster erklæringer på tværs af hele din app. Dette betyder 1) at en ny undtagelsestype vil påvirke mange funktionsunderskrifter, og 2) du kan gå glip af en bestemt forekomst af den undtagelse, du faktisk vil-fange (sig, at du åbner en sekundær fil for en funktion, der skriver data til en Den sekundære fil er valgfri, så du kan ignorere dens fejl, men fordi signaturen kaster IOUndtagelse, det er let at overse dette).
  • Kontrollerede undtagelser er ikke rigtig undtagelser. Sagen ved kontrollerede undtagelser er, at de ikke rigtig er undtagelser ved den sædvanlige forståelse af konceptet. I stedet er de API-alternative returværdier.

    Hele ideen med undtagelser er, at en fejl kastet et sted langt ned i opkaldskæden kan boble op og håndteres af kode et eller andet sted længere op, uden at den mellemliggende kode behøver at bekymre sig om det. Afkrydsede undtagelser kræver på den anden side hvert niveau af kode mellem kasteren og fangeren for at erklære, at de kender til alle former for undtagelser, der kan gå igennem dem. Dette er i praksis lidt anderledes end hvis afkrydsede undtagelser simpelthen var specielle returværdier, som den, der ringer op, skulle kontrollere.

Derudover har jeg stødt på argumentet om, at applikationer skal håndtere et stort antal afkrydsede undtagelser, der genereres fra de mange biblioteker, som de får adgang til. Imidlertid kan dette problem overvindes gennem en smart designet facade, der udnytter Java's kædede undtagelsesfacilitet og undtagelsesomlægning for i høj grad at reducere antallet af undtagelser, der skal håndteres, samtidig med at den oprindelige undtagelse, der blev kastet, bevares.

Konklusion

Er kontrollerede undtagelser gode, eller er de dårlige? Med andre ord, skal programmerere tvinges til at håndtere afkrydsede undtagelser eller have mulighed for at ignorere dem? Jeg kan godt lide ideen om at håndhæve mere robust software. Men jeg tror også, at Java's undtagelseshåndteringsmekanisme skal udvikles for at gøre det mere programmørvenligt. Her er et par måder at forbedre denne mekanisme på: