Programovacie jazyky pod pokrievkou - výnimky
Try {
Objavujú sa vo väčšine moderných jazykov a pravdepodobne ich používate tak často, že sa stali každodennou záležitosťou. Áno, hovoríme o výnimkách - programátorské technike a technológii.
V dnešnom článku by som vám rád vysvetlil, čo to vlastne výnimky sú, a prečo sa tak často používajú. Tiež sa spoločne pozrieme na to, ako by ste mohli napísať svoje vlastné spracovanie výnimiek, ak vám to jazyk dovolí. Vďaka tomu prenikáme do čiernej skrinky, ktorú sú vo väčšine jazykov obalené, a ktorá nedovolí nahliadnuť dovnútra.
} Catch {
Tento článok patrí pod kolekciu "pokročilé programovacie techniky" a vopred upozorňujem, že je vyžadovaná znalosť jazyka C. Začiatočníkom navyše môže spôsobiť nočné mory.
} *
Motivácia
Poďme začať definíciou: Čo sú to výnimky a čo je spracovanie výnimiek?
Dokážete na túto otázku odpovedať jednou vetou? Ak áno, napíšte si svoju definíciu a porovnajte ju s definíciou na konci tejto kapitoly.
Prvý príklad - motto: Jednoduchosť
Začnime jednoduchým príkladom.
Naprogramujeme aplikáciu, ktorá rieši rovnicu x 2 = a. Konštantu na pravej strane získame od užívateľa vstupom z klávesnice.
Pre začiatok budeme programovať v jazyku C.
#include <stdio.h> #include <stdlib.h> #include <math.h> void main() { char text[50]; char *textend; double a, x; printf("solver of equation: x*x = a\n? a="); scanf("%s", text); a = strtod(text, &textend); x = sqrt(a); printf("sqrt(%lf) = %lf\n", a, x); }
Program je krátky a jednoduchý. Lenže sa nám začínajú rysovať problémy. Čo keď používateľ nezadá číslo? Alebo keď používateľ zadá záporné číslo?
Druhý príklad - motto: Bezchybnatost
Kód je síce krátky a jednoduchý, ale súčasne sa nestará o chybové stavy. V praxi by sme s takýmto prístupom zrejme ďaleko nedošli, a preto musíme ošetriť všetky chyby, ktoré sa môžu stať.
Náš zdrojový kód po úprave vyzerá nasledovne.
#include <stdio.h> #include <stdlib.h> #include <math.h> void main() { char text[50]; char *textend; double a, x; printf("solver of equation: x*x = a\n? a="); if (scanf("%s", text) != 1) { printf("Invalid input stream\n"); return; } a = strtod(text, &textend); if (text == textend) { printf("Invalid number on input\n"); return; } x = sqrt(a); if (errno) { printf("Result is not a number\n"); return; } printf("sqrt(%lf) = %lf\n", a, x); }
Program sa teda trochu rozrástol a funguje tak, ako by sme si predstavovali. Ovšem mimo naše predstavy o eleganciu celkom iste patrí zdrojový kód.
Pokúsime sa zhrnúť výsledok. Na každý príkaz, ktorý sme použili, pribudla kontrola výsledku, výpis chyby a ukončenie programu. To je šesť riadok na jednu funkčnú inštrukciu. Výsledný program je prakticky päťkrát dlhší len preto, aby sme zabránili chybám, ktoré zrejme nenastanú?!
Tretí príklad - motto: Rozdeľ a panuj
Chceli sme náš program ochrániť pred chybami, ale v dôsledku toho sa rozrástol tak, že s vysokou pravdepodobnosťou bude chyba v samotnom spracovaní chýb. Vo chvíli, keď funkčná časť programu tvorí 20% celkového kódu, sa program navyše stáva absolútne nečitateľným.
Poďme to teda urobiť trochu prehľadnejšie.
#include <stdio.h> #include <stdlib.h> #include <math.h> void main() { char text[50]; char *textend; double a, x; printf("solver of equation: x*x = a\n? a="); if (scanf("%s", text) != 1) goto ex_invalid_stream; a = strtod(text, &textend); if (text == textend) goto ex_invalid_number; x = sqrt(a); if (errno) goto ex_nan; printf("sqrt(%lf) = %lf\n", a, x); return; ex_invalid_stream: printf("Invalid input stream\n"); return; ex_invalid_number: printf("Invalid number on input\n"); return; ex_nan: printf("Result is not a number\n"); return; }
Tretia verzia nášho programu je z funkčného hľadiska úplne rovnaká ako verzia predchádzajúce. Avšak vďaka tomu, že sme spracovanie chýb presunuli na koniec programu, je už zase trochu vidieť, čo program robí. Inak povedané, program sme rozdelili na dve časti - prvá sa stará o samotnú funkcionalitu, zatiaľ čo druhá slúži na spracovanie chýb.
Mimochodom, ak si tento príklad pozorne prezriete, nepripomína vám tak trochu niečo známeho? Že by to bolo spracovanie výnimiek?
Štvrtý príklad - motto: Iba od 18 rokov
Ďalšie zjednodušenie môžeme vykonať, ak sa nami volané funkcie dokážu samy postarať o skok na chybovú sekciu. Ale aby sme to mohli urobiť, budeme potrebovať dve veci:
- Náš jazyk rozšírime o takzvaný vzdialený skok. K tomu účelu slúžia dve knižničný funkcie: setjmp () a longjmp (). Funkcia setjmp () si uloží aktuálny stav zásobníka a inštrukčnú ukazovateľ, zatiaľ čo funkcia longjmp () vykoná vrátenie týchto hodnôt do pôvodného stavu a odskok na miesto, odkiaľ bola zavolaná funkcia setjmp ().
- Kvôli prehľadnosti dočasne skryjeme všetky ďalšie používané funkcie do definícií, aby sme nemuseli opakovane písať if, if, if ... Tento krok si ale vysvetlíme až v ďalšej časti.
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <setjmp.h> #define EX_INVALID_STREAM 1 #define EX_INVALID_NUMBER 2 #define EX_NAN 3 #define ex_scanf(txt, arg) if (scanf(txt, arg) != 1) longjmp(_exception, EX_INVALID_STREAM); #define ex_strtod(txt, end) strtod(txt, end); if (txt == *end) longjmp(_exception, EX_INVALID_NUMBER); #define ex_sqrt(a, x) sqrt(a); if (errno) longjmp(_exception, EX_NAN); void main() { char text[50]; char *textend; double a, x; jmp_buf _exception; int _exc; /* CATCH BLOCK */ if ((_exc = setjmp(_exception))) { switch (_exc) { case EX_INVALID_STREAM: printf("Invalid input stream\n"); break; case EX_INVALID_NUMBER: printf("Invalid number on input\n"); break; case EX_NAN: printf("Result is not a number\n"); break; } return; } /* CATCH BLOCK */ /* TRY BLOCK */ printf("solver of equation: x*x = a\n? a="); ex_scanf("%s", text); a = ex_strtod(text, &textend); x = ex_sqrt(a); printf("sqrt(%lf) = %lf\n", a, x); /* TRY BLOCK */ }
A ako vyzerá výsledok?
Tak trochu ako Masaker motorovou pílou, ale určité pozitívne znaky sa už začínajú rysovať. Všimnite si, že druhá časť programu nazvaná TRY BLOCK už opäť vyzerá ako náš vôbec prvý príklad. Kód je krátky a ľahko čitateľný, radosť pozerať. Tiež predchádzajúca časť nazvaná CATCH BLOCK je pomerne jasná a prehľadná.
Starosti nám budú robiť definície na začiatku programu, ktoré vôbec nevyzerajú vábne, ale to vyriešime ľahko v nasledujúcej časti.
Piaty príklad - motto: Čo oči nevidia, to srdce nebolí
V tejto časti už opustíme jazyk C a ďalej budeme pokračovať vo fiktívnom jazyku C / EX. Potrebujeme totiž ešte doriešiť nevábne vyzerajúce kód z predchádzajúcej časti.
Schválne, napadá vás, ako sa zbaviť definícií našich vlastných funkcií ex_XXX?
Správna odpoveď sa vám bude veľmi páčiť: "Necháme to na niekom inom." Konkrétne, nech to za nás naprogramuje tím, ktorý píše kompilátor jazyka C / EX a jeho knižnice. Každá z funkcií jednoducho vykoná zavolanie longjmp () s parametrom, ktorý bude identifikovať, čo sa stalo zle.
Jediné, čo musíme urobiť my, je zavolať špeciálnu funkciu catch (), ktorá vykoná volanie setjmp ().
#include <stdio.h> #include <stdlib.h> #include <math.h> void main() { char text[50]; char *textend; double a, x; int _exc; /* CATCH BLOCK */ if ((_exc = catch())) { switch (_exc) { case EX_INVALID_STREAM: printf("Invalid input stream\n"); break; case EX_INVALID_NUMBER: printf("Invalid number on input\n"); break; case EX_NAN: printf("Result is not a number\n"); break; } return; } /* CATCH BLOCK */ /* TRY BLOCK */ printf("solver of equation: x*x = a\n? a="); ex_scanf("%s", text); a = ex_strtod(text, &textend); x = ex_sqrt(a); printf("sqrt(%lf) = %lf\n", a, x); /* TRY BLOCK */ }
Šiesty príklad - motto: Od programovacie techniky k technológii
Vďaka tomu, že sme náš problém hodili na tvorcov jazyka, sa už o všetko budú starať knižnice. Prečo ale nezájsť ešte o kúsok ďalej a nerozšíriť syntax jazyka? Vykonáme to nasledujúcim spôsobom, ktorý vám už celkom iste nie je cudzie.
Zavedieme do jazyka C / EX kľúčové slová TRY, CATCH a THROW, ktoré budú nasledovaná blokom kódu. Ich význam je nasledovný:
- TRY je takzvaná chránená časť kódu; Na základe explicitného príkazu sa okamžite preruší beh programu a vykoná sa odskok do časti CATCH
- CATCH je podmienená časť kódu; Tento kód prebehne len na základe explicitného príkazu THROW v časti TRY
- THROW spôsobí prerušenie behu v chránenej sekcii a odskok do podmienené časti
Implementácia týchto kľúčových slov je v našom prípade pomerne jednoduchá:
- TRY zaistí volanie funkcie setjmp () na začiatku bloku CATCH; potom vráti inštrukčnú pointer na začiatok bloku zviazaného s TRY
- CATCH sekcia je umiestnená mimo beh programu; sem sa dá dostať iba volaním funkcie longjmp ()
- THROW zavolá longjmp ()
Toto je chvíľa, kedy sa z techniky stáva technológie. Spracovanie chybového stavu aplikácie použitím výnimiek je programátorská technika. Avšak podpora v jazyku a na platforme (či už Java alebo C #) je technologická záležitosť.
Vďaka snahe vývojového tímu jazyka C / EX sa konečne dostávame k finálnej verzii nášho programu.
#include <stdio.h> #include <stdlib.h> #include <math.h> void main() { char text[50]; char *textend; double a, x; TRY { printf("solver of equation: x*x = a\n? a="); ex_scanf("%s", text); a = ex_strtod(text, &textend); x = ex_sqrt(a); printf("sqrt(%lf) = %lf\n", a, x); } CATCH _exc { switch (_exc) { case EX_INVALID_STREAM: printf("Invalid input stream\n"); break; case EX_INVALID_NUMBER: printf("Invalid number on input\n"); break; case EX_NAN: printf("Result is not a number\n"); break; } } }
Od motivácie k výsledku
Vráťme sa opäť k našej definícii zo začiatku článku: Čo sú to výnimky a čo je spracovanie výnimiek?
Výnimky a ich spracovanie slúži k oddeleniu časti kódu na spracovanie chýb od samotnej funkčné časti kódu. Takže umožňujú sprehľadniť a zjednodušiť zdrojový kód rozdelením na logické celky - časti, ktoré vykonáva funkcionalitu podľa očakávaného scenára, a časti, ktoré sa starajú o výnimočné stavy a ich nápravu.
Nie je v tom žiadna mágia, ale je za tým obrovská práce. Poďme si urobiť krátku rekapituláciu:
- Kompilátor musí vedieť uložiť aktuálny rozbehový stav [vlákna] a neskôr ho vrátiť späť
- Knižničný funkcie sa musí starať o kontrolu platných hodnôt na vstupe aj na výstupe
- Knižničný funkcie sa musí starať o explicitné vyvolanie výnimky príkazom THROW / longjump ()
- Výnimka nie je pre interpret výnimočná situácia, jedná sa iba o bezpečne vykonaný vzdialený skok do nadradenej podmienené sekcie
- Vzdialený skok musí byť vykonaný opatrne vzhľadom na uvoľnenie zdrojov, ktoré mohli byť vyhradené v nadradených sekciách
A záverom ...
O výnimkách by toho bolo ešte veľa, o čom by sme mohli hovoriť - Finally bloky, typovanie výnimiek, deklarované vs. nedeklarovaný výnimky, spracovanie pamäti, vnorené bloky, atď., atď.
Ja som vám chcel však sprístupniť samotné srdce výnimiek, pretože vypísanie niekoľkých definícií a príkladov vám dá len malý vhľad do tejto problematiky. Ak rozumiete všetkým príkladom uvedeným vyššie, mali by ste byť schopní porozumieť aj tomu, ako výnimky fungujú vo vašom jazyku. A hlavne by ste mali vedieť, ako máte sami spracovanie výnimiek používať vo vlastných programoch.