Undtagelseshåndtering er en programmeringssprogsmekanisme designet til at beskrive et programs reaktion på køretidsfejl og andre mulige problemer ( undtagelser ), der kan opstå under programafvikling og føre til umuligheden (meningsløsheden) af yderligere behandling af programmet af dets grundlæggende algoritme. På russisk bruges også en kortere form af udtrykket: " undtagelseshåndtering ".
Under afviklingen af et program kan der opstå situationer, hvor tilstanden af eksterne data, input-output-enheder eller computersystemet som helhed gør yderligere beregninger i overensstemmelse med den underliggende algoritme umulige eller meningsløse. Klassiske eksempler på sådanne situationer er givet nedenfor.
Undtagelsessituationer, der opstår under programdrift, kan opdeles i to hovedtyper: synkron og asynkron, hvor principperne for respons er væsentligt forskellige.
Nogle typer undtagelser kan klassificeres som enten synkrone eller asynkrone. For eksempel bør en divider-med-nul-instruktion formelt resultere i en synkron undtagelse, da den logisk forekommer præcis, når den givne instruktion udføres, men på nogle platforme, på grund af dyb pipelining , kan undtagelsen faktisk vise sig at være asynkron.
I mangel af en indbygget undtagelseshåndteringsmekanisme for applikationer, er den mest almindelige reaktion på enhver undtagelse at afbryde eksekveringen øjeblikkeligt og bede brugeren om en besked om undtagelsens art. Vi kan sige, at i sådanne tilfælde bliver operativsystemet den eneste og universelle undtagelsesbehandler. For eksempel er Dr. Watson , som indsamler oplysninger om en ubehandlet undtagelse og sender den til en speciel Microsoft -server .
Det er muligt at ignorere undtagelsen og fortsætte, men denne taktik er farlig, fordi den fører til fejlagtige programresultater eller fejl senere. For eksempel, ved at ignorere fejlen ved at læse fra datablokfilen, vil programmet ikke modtage de data, det skulle læse, men nogle andre til sin rådighed. Det er umuligt at forudsige resultaterne af deres brug.
Håndtering af ekstraordinære situationer af programmet selv består i, at når en ekstraordinær situation opstår, overføres kontrollen til en foruddefineret handler - en kodeblok, en procedure, en funktion, der udfører de nødvendige handlinger.
Der er to fundamentalt forskellige mekanismer for, hvordan undtagelsesbehandlere fungerer.
Der er to muligheder for at forbinde en undtagelseshåndtering til et program: strukturel og ikke-strukturel undtagelseshåndtering.
Ikke-strukturel undtagelseshåndtering er implementeret som en mekanisme til registrering af funktioner eller kommandobehandlere for hver mulig type undtagelse. Programmeringssproget eller dets systembiblioteker giver programmøren mindst to standardprocedurer: registrering af en behandler og afregistrering af en behandler. Kald den første "binder" handleren til en specifik undtagelse, kalder den anden ophæver denne "binding". Hvis der opstår en undtagelse, afbrydes udførelsen af hovedprogramkoden øjeblikkeligt, og udførelsen af behandleren begynder. Efter afslutning af handleren overføres kontrollen enten til et forudbestemt punkt i programmet eller tilbage til det punkt, hvor undtagelsen opstod (afhængigt af den specificerede behandlingsmetode - med eller uden retur). Uanset hvilken del af programmet, der kører i øjeblikket, bliver en bestemt undtagelse altid reageret på af den sidst registrerede behandler. På nogle sprog forbliver en registreret handler kun gyldig inden for den aktuelle kodeblok (procedurer, funktioner), så er afregistreringsproceduren ikke påkrævet. Nedenfor er et betinget fragment af programkoden med ikke-strukturel undtagelseshåndtering:
SetHandler(ErrorDB, Goto ErrorDB) // En handler er indstillet til undtagelsen "DB Error" - kommandoen "GoTo Error DB". ... // Her er operatørerne til at arbejde med databasen JumpTo ClearErrorDB // Ubetinget jump-kommando - omgå undtagelseshåndtering ErrorDB: // label - overgangen vil finde sted her i tilfælde af en databasefejl ifølge den installerede handler ... // DB undtagelsesbehandler Fjern OshDB: // label - hop vil forekomme her, hvis den kontrollerede kode udføres uden en databasefejl. ClearHandler (DB-fejl) // Håndter fjernetIkke-strukturel behandling er praktisk talt den eneste mulighed for at håndtere asynkrone undtagelser, men det er ubelejligt for synkrone undtagelser: du skal ofte kalde kommandoer for at installere/fjerne handlere, der er altid en fare for at bryde programmets logik ved at springe registreringen over eller afmelding af handleren.
Strukturel undtagelseshåndtering kræver obligatorisk støtte fra programmeringssproget - tilstedeværelsen af specielle syntaktiske konstruktioner. En sådan konstruktion indeholder en blok med kontrolleret kode og en(e) undtagelsesbehandler(e). Den mest generelle form for en sådan konstruktion (betinget):
Startblok ... // Kontrolleret kode ... hvis (betingelse) så CreateException Exception2 ... Håndterundtagelse 1 ... // Håndterkode for Undtagelse1 Håndterundtagelse 2 ... // Håndterkode for Undtagelse2 HandlerRaw ... // Kode til håndtering af tidligere ubehandlede undtagelser EndBlockHer er "StartBlock" og "EndBlock" nøgleord , der afgrænser blokken med kontrolleret kode, og "Handler" er begyndelsen af blokken til håndtering af den tilsvarende undtagelse. Hvis der opstår en undtagelse inde i blokken, fra begyndelsen til den første handler, så vil der være en overgang til den handler, der er skrevet til den, hvorefter hele blokken afsluttes og udførelsen fortsætter med kommandoen efter den. Nogle sprog har ikke specielle nøgleord til at begrænse en blok af kontrolleret kode, i stedet kan undtagelsesbehandler(e) indbygges i nogle eller alle syntaktiske konstruktioner, der kombinerer flere udsagn. Så for eksempel i Ada-sproget kan enhver sammensat sætning (begynde - slutning) indeholde en undtagelsesbehandler.
En "RawHandler" er en undtagelseshåndtering, der ikke matcher nogen af dem, der er beskrevet ovenfor i denne blok. Undtagelsesbehandlere kan i virkeligheden beskrives på forskellige måder (én behandler for alle undtagelser, én behandler for hver type undtagelse), men i princippet fungerer de på samme måde: når der opstår en undtagelse, er den første tilsvarende behandler placeret i denne blok, dens kode udføres, hvorefter udførelsesblokken slutter. Undtagelser kan forekomme både som følge af programmeringsfejl og ved eksplicit at generere dem ved hjælp af den relevante kommando (i eksemplet "CreateException"-kommandoen). Fra handlernes synspunkt er sådanne kunstigt skabte undtagelser ikke forskellige fra andre.
Undtagelseshåndteringsblokke kan gentagne gange indlejre sig i hinanden, enten eksplicit (tekstuelt) eller implicit (for eksempel kaldes en procedure i en blok, der selv har en undtagelseshåndteringsblok). Hvis ingen af behandlerne i den aktuelle blok kan håndtere undtagelsen, afsluttes udførelsen af denne blok med det samme, og kontrollen overføres til den næste passende behandler på et højere hierarkiniveau. Dette fortsætter, indtil en behandler er fundet og håndterer undtagelsen, eller indtil undtagelsen forlader programmer-specificerede behandlere og videregives til standard systemhandleren, der bryder programmet ned.
Nogle gange er det ubelejligt at fuldføre behandlingen af en undtagelse i den aktuelle blok, det vil sige, det er ønskeligt, at når der opstår en undtagelse i den aktuelle blok, udfører handleren nogle handlinger, men undtagelsen fortsætter med at blive behandlet på et højere niveau ( normalt sker dette, når behandleren af denne blok ikke helt håndterer undtagelsen, men kun delvist). I sådanne tilfælde genereres en ny undtagelse i undtagelsesbehandleren eller genoptages ved hjælp af en speciel kommando, som tidligere er stødt på. Behandlerkoden er ikke beskyttet i denne blok, så en undtagelse, der er smidt i den, vil blive håndteret i blokke på højere niveau.
Ud over kontrollerede kodeblokke til undtagelseshåndtering kan programmeringssprog understøtte garanterede færdiggørelsesblokke. Deres brug viser sig at være praktisk, når det i en bestemt blok kode, uanset om der er opstået fejl, er nødvendigt at udføre visse handlinger før dens afslutning. Det enkleste eksempel: hvis en procedure dynamisk opretter et lokalt objekt i hukommelsen, skal objektet destrueres (for at undgå hukommelseslækager), før det afsluttes, uanset om der opstod fejl efter dets oprettelse eller ej. Denne funktion er implementeret af kodeblokke i formen:
Startblok ... // Hovedkode Færdiggørelse ... // Slutkode EndBlockUdsagn (hovedkode) indesluttet mellem nøgleordene "StartBlock" og "End" udføres sekventielt. Hvis der ikke kastes nogen undtagelser under deres eksekvering, så udføres sætningerne mellem nøgleordene "End" og "EndBlock" (termineringskode). Hvis der opstår en undtagelse (enhver) under udførelse af hovedkoden, udføres exit-koden straks, hvorefter hele blokken er fuldført, og den opståede undtagelse fortsætter med at eksistere og udbredes, indtil den fanges af en undtagelse på højere niveau håndteringsblok.
Den grundlæggende forskel mellem en blok med garanteret fuldførelse og behandling er, at den ikke håndterer undtagelsen, men kun garanterer udførelse af et bestemt sæt af operationer, før behandlingsmekanismen aktiveres. Det er let at se, at en blok med garanteret fuldførelse let implementeres ved hjælp af den sædvanlige strukturerede behandlingsmekanisme (for dette er det nok at sætte kommandoen til at kaste en undtagelse umiddelbart før færdiggørelsen af den kontrollerede blok og skrive handlerkoden korrekt) , men tilstedeværelsen af en separat konstruktion giver dig mulighed for at gøre koden mere gennemsigtig og beskytter mod utilsigtede fejl. .
De fleste moderne programmeringssprog som Ada , C++ , D , Delphi , Objective-C , Java , JavaScript , Eiffel , OCaml , Ruby , Python , Common Lisp , SML , PHP , alle .NET platformsprog osv. har native support strukturel undtagelseshåndtering . På disse sprog, når en sprogunderstøttet undtagelse opstår, rulles opkaldsstakken ud til den første undtagelsesbehandler af den passende type, og kontrollen overføres til behandleren.
Bortset fra mindre syntaksforskelle er der kun et par undtagelseshåndteringsmuligheder. I de mest almindelige af dem genereres en undtagelse af en speciel operatør ( throweller raise), og undtagelsen i sig selv, set fra programmets synspunkt, er en slags dataobjekt . Det vil sige, at genereringen af en undtagelse består af to trin: oprettelse af et undtagelsesobjekt og rejsning af en undtagelse med dette objekt som parameter . Samtidig medfører konstruktionen af en sådan genstand ikke i sig selv en undtagelse. På nogle sprog kan et undtagelsesobjekt være et objekt af enhver datatype (inklusive en streng, et tal, en pointer og så videre), på andre kun en foruddefineret undtagelsestype (oftest har den navnet Exception) og evt. , dens afledte typer (typer -børn, hvis sproget understøtter objektfunktioner).
Omfanget af handlerne starter med et specielt nøgleord tryeller blot en blokstartsprogmarkør (for eksempel begin) og slutter før beskrivelsen af handlerne ( catch, except, resque). Der kan være flere behandlere, én efter én, og hver kan specificere den type undtagelse, den håndterer. Som regel bliver der ikke valgt den mest passende handler, og den første handler, der er typekompatibel med undtagelsen, udføres. Derfor er rækkefølgen af handlerne vigtig: Hvis en handler, der er kompatibel med mange eller alle typer undtagelser, vises i teksten før specifikke handlere for specifikke typer, så vil specifikke handlere slet ikke blive brugt.
Nogle sprog tillader også en speciel blok ( else), der udføres, hvis ingen undtagelse er blevet kastet i det tilsvarende omfang. Mere almindeligt er muligheden for garanteret fuldførelse af en kodeblok ( finally, ensure). En bemærkelsesværdig undtagelse er C++, hvor der ikke er en sådan konstruktion. I stedet bruges et automatisk kald til objektdestruktorer . Samtidig er der ikke-standard C++ udvidelser, der også understøtter funktionalitet finally(for eksempel i MFC ).
Generelt kan undtagelseshåndtering se sådan ud (på et abstrakt sprog):
prøv { line = konsol . readLine (); if ( line . length () == 0 ) throw new EmptyLineException ( "Linjen læst fra konsollen er tom!" ); konsol . printLine ( "Hej %s!" % linje ); } catch ( EmptyLineException undtagelse ) { konsol . printLine ( "Hej!" ); } catch ( Undtagelse undtagelse ) { konsol . printLine ( "Fejl: " + undtagelse . besked ()); } andet { konsol . printLine ( "Programmet kørte uden undtagelse" ); } endelig { konsol . printLine ( "Programmet afsluttes" ); }Nogle sprog har muligvis kun én handler, der håndterer forskellige typer undtagelser alene.
Fordelene ved at bruge undtagelser er især mærkbare, når man udvikler biblioteker af procedurer og softwarekomponenter , der er orienteret mod massebrug. I sådanne tilfælde ved udvikleren ofte ikke præcis, hvordan undtagelsen skal håndteres (når du skriver en universel procedure for læsning fra en fil, er det umuligt at forudse reaktionen på en fejl på forhånd, da denne reaktion afhænger af, at programmet bruger proceduren), men han har ikke brug for dette - det er nok at kaste en undtagelse A, hvis handler er leveret til at implementere af brugeren af komponenten eller proceduren. Det eneste alternativ til undtagelser i sådanne tilfælde er at returnere fejlkoder, som er tvunget til at blive sendt langs kæden mellem flere niveauer af programmet, indtil de når frem til behandlingsstedet, hvilket roder koden og reducerer dens forståelighed. Brug af undtagelser til fejlkontrol forbedrer kodelæsbarheden ved at adskille fejlhåndtering fra selve algoritmen og gør det nemmere at programmere og bruge tredjepartskomponenter. Og fejlhåndtering kan centraliseres i .
Desværre er implementeringen af undtagelseshåndteringsmekanismen meget sprogafhængig, og selv kompilatorer af det samme sprog på den samme platform kan have betydelige forskelle. Dette tillader ikke, at undtagelser overføres gennemsigtigt mellem dele af et program, der er skrevet på forskellige sprog; for eksempel er biblioteker, der understøtter undtagelser, generelt uegnede til brug i programmer på andre sprog end dem, de er designet til, og endnu mere på sprog, der ikke understøtter en undtagelseshåndteringsmekanisme. Denne tilstand begrænser markant muligheden for at bruge undtagelser, for eksempel i UNIX og dets kloner og under Windows, da det meste af systemsoftwaren og lavniveaubibliotekerne i disse systemer er skrevet på C-sproget, som ikke understøtter undtagelser. For at arbejde med API'en for sådanne systemer, der anvender undtagelser, skal man følgelig skrive wrapper-biblioteker, hvis funktioner ville analysere returkoderne for API-funktioner og, om nødvendigt, ville generere undtagelser.
Undtagelsesstøtte komplicerer sproget og compileren. Det reducerer også programmets hastighed, da omkostningerne ved at håndtere en undtagelse normalt er højere end omkostningerne ved at håndtere en fejlkode. På hastighedskritiske steder i et program anbefales det derfor ikke at hæve og håndtere undtagelser, selvom det skal bemærkes, at i applikationsprogrammering er tilfælde, hvor forskellen i hastigheden af behandling af undtagelser og returkoder virkelig er betydelige, meget sjældne. .
Det kan være svært at implementere undtagelser korrekt på sprog med automatisk destruktoropkald . Når der opstår en undtagelse i en blok, er det nødvendigt automatisk at kalde destruktorerne af objekter, der er oprettet i denne blok, men kun dem, der endnu ikke er blevet slettet på den sædvanlige måde. Derudover er kravet om at afbryde den aktuelle operation, når der opstår en undtagelse, i konflikt med kravet om obligatorisk automatisk sletning på sprog med autodestruktorer: hvis der opstår en undtagelse i destruktoren, vil compileren enten blive tvunget til at slette et ufuldstændigt frigivet objekt , eller objektet forbliver eksisterende, det vil sige, at der opstår en hukommelseslækage . Som et resultat er generering af ufangede undtagelser i destruktorer simpelthen forbudt i nogle tilfælde.
Joel Spolsky mener, at kode designet til at håndtere undtagelser mister sin linearitet og forudsigelighed. Hvis der i klassisk kode kun findes udgange fra en blok, procedure eller funktion, hvor programmøren eksplicit har angivet dem, så kan der i kode med undtagelser (potentielt) forekomme en undtagelse i enhver sætning, og det er umuligt at finde ud af præcis, hvor undtagelser kan forekomme ved at analysere selve koden. I kode, der er designet til undtagelser, er det umuligt at forudsige, hvor kodeblokken vil afslutte, og enhver erklæring skal betragtes som potentielt den sidste i blokken, som et resultat heraf øges kodekompleksiteten, og pålideligheden falder. [en]
Også i komplekse programmer er der store "dynger" af operatører try ... finallyog try ... catch( try ... except), hvis du ikke bruger aspekter.
Oprindeligt (f.eks. i C++) var der ingen formel disciplin til beskrivelse, generering og håndtering af undtagelser: enhver undtagelse kan rejses hvor som helst i programmet, og hvis der ikke er nogen handler for den i opkaldsstakken, er programafviklingen unormalt afbrudt. Hvis en funktion (især en biblioteksfunktion) kaster undtagelser, så skal programmet, der bruger den, fange dem alle for at være stabile. Når en af de mulige undtagelser af en eller anden grund ikke bliver behandlet, vil programmet uventet gå ned.
Sådanne effekter kan bekæmpes ved organisatoriske foranstaltninger: at beskrive mulige undtagelser, der forekommer i biblioteksmoduler, i den relevante dokumentation. Men på samme tid er der altid en chance for at springe den nødvendige handler over på grund af en utilsigtet fejl eller inkonsistens i dokumentationen med koden (hvilket slet ikke er ualmindeligt). For fuldstændigt at eliminere tabet af undtagelseshåndtering, skal du specifikt tilføje en "hver anden" undtagelseshåndteringsgren til dine handlere (som garanteret vil fange alle, selv tidligere ukendte undtagelser), men denne vej ud er ikke altid optimal. Desuden kan skjule alle mulige undtagelser føre til en situation, hvor alvorlige og svære at finde fejl er skjult.
Senere introducerede en række sprog, såsom Java, kontrollerede undtagelser . Essensen af denne mekanisme er at tilføje følgende regler og begrænsninger til sproget:
Eksternt (på Java-sproget) ser implementeringen af denne tilgang sådan ud:
int getVarValue ( String varName ) kaster SQLException { ... // metodekode, der muligvis indeholder kald, der kunne afgive en SQLException } // Kompileringsfejl - ingen undtagelse blev erklæret eller fanget int eval1 ( String expression ) { ... int a = prev + getVarValue ( "abc" ); ... } // Korrekt - en undtagelse er blevet erklæret og vil blive videregivet på int eval2 ( String expression ) throws SQLException { ... int a = prev + getVarValue ( "abc" ); ... } // Korrekt - undtagelsen er fanget inde i metoden og går ikke uden for int eval3 ( String expression ) { ... try { int a = prev + getVarValue ( "abc" ); } catch ( SQLException ex ) { // Håndter undtagelsen } ... }Her erklæres getVarValue-metoden til at kaste en SQLException. Derfor skal enhver metode, der bruger det, enten fange undtagelsen eller erklære, at den er smidt. I dette eksempel vil eval1-metoden resultere i en kompileringsfejl, fordi den kalder getVarValue-metoden, men ikke fanger eller erklærer undtagelsen. eval2-metoden erklærer undtagelsen, og eval3-metoden fanger og håndterer den, som begge er korrekte til at håndtere undtagelsen fra getVarValue-metoden.
Markerede undtagelser reducerer antallet af situationer, hvor en undtagelse, der kunne være blevet håndteret, forårsagede en kritisk fejl i programmet, fordi compileren holder styr på tilstedeværelsen af handlere . Dette er især nyttigt for kodeændringer, hvor en metode, der ikke tidligere kunne give en undtagelse af X-type, begynder at gøre det; compileren vil automatisk spore alle forekomster af dens brug og tjekke for passende handlere.
En anden nyttig kvalitet ved kontrollerede undtagelser er, at de bidrager til en meningsfuld skrivning af behandlere: programmøren ser tydeligt den komplette og korrekte liste over undtagelser, der kan forekomme et givet sted i programmet, og kan i stedet skrive en meningsfuld behandler for hver af dem. at skabe en "just in case" En fælles handler for alle undtagelser, der reagerer lige meget på alle unormale situationer.
Afkrydsede undtagelser har også ulemper.
På grund af disse mangler omgås denne mekanisme ofte, når der bruges kontrollerede undtagelser. For eksempel erklærer mange biblioteker, at alle metoder kaster en generel klasse af undtagelser (for eksempel Exception), og behandlere oprettes kun for denne type undtagelse. Resultatet er, at compileren tvinger dig til at skrive undtagelsesbehandlere, selv hvor de objektivt set ikke er nødvendige, og det bliver umuligt at bestemme, uden at læse kilderne, hvilke underklasser af de erklærede undtagelser, der kastes af metoden for at hænge forskellige behandlere på dem. En mere korrekt tilgang er at opsnappe nye undtagelser inde i metoden, genereret af den kaldte kode, og om nødvendigt videregive undtagelsen - "ombryde" den til en undtagelse, der allerede er returneret af metoden. For eksempel, hvis en metode ændres, så den begynder at få adgang til databasen i stedet for filsystemet, så kan den selv fange SQLExceptionog smide en nyoprettet en i stedet IOExceptionfor, hvilket angiver den oprindelige undtagelse som årsagen. Det anbefales normalt at indledningsvis erklære præcis de undtagelser, som opkaldskoden skal håndtere. Lad os sige, at hvis metoden henter inputdata, så er det tilrådeligt at den erklærer IOException, og hvis den fungerer med SQL-forespørgsler, så skal den, uanset fejlens art, erklære SQLException. Under alle omstændigheder skal det sæt af undtagelser, som en metode giver, overvejes nøje. Hvis det er nødvendigt, giver det mening at oprette dine egne undtagelsesklasser, der stammer fra Exception eller andre passende kontrollerede undtagelser.
Det er umuligt at gøre alle undtagelser kontrollerbare generelt, da nogle ekstraordinære situationer i sagens natur er sådan, at deres forekomst er mulig på et hvilket som helst eller næsten ethvert sted i programmet, og programmøren er ikke i stand til at forhindre dem. Samtidig er det meningsløst at angive sådanne undtagelser i funktionsbeskrivelsen, da dette skulle gøres for hver funktion uden at gøre programmet mere overskueligt. Dybest set er disse undtagelser relateret til en af to typer:
Det er ulogisk og ubelejligt at tage sådanne fejl ud af undtagelseshåndteringssystemet, om ikke andet fordi de nogle gange stadig bliver fanget og behandlet. Derfor er nogle undtagelsestyper i systemer med kontrollerede undtagelser fjernet fra kontrolmekanismen og fungerer på traditionel vis. I Java er disse undtagelsesklasser arvet fra java.lang.Error - fatale fejl og java.lang.RuntimeException - runtime-fejl, normalt relateret til kodningsfejl eller utilstrækkelige kontroller i programkoden (dårligt argument, adgang med nul-reference, udgang uden for arrayets grænser , forkert skærmtilstand osv.).
Grænsen mellem en "korrigerbar" og en "fatal" fejl er meget vilkårlig. For eksempel kan en I/O-fejl i et desktop-program normalt "korrigeres", og det er muligt at informere brugeren om det og fortsætte med at køre programmet. I et webscript er det også "fatalt" - hvis det skete, skete der noget slemt med eksekveringsmiljøet, og du skal stoppe med at vise en besked.
Datatyper | |
---|---|
Ufortolkelig | |
Numerisk | |
Tekst | |
Reference | |
Sammensatte | |
abstrakt | |
Andet | |
relaterede emner |