Programovací jazyky pod pokličkou – výjimky

Ostatní Programovací jazyky pod pokličkou – výjimky

try {

Objevují se ve většině moderních jazyků a pravděpodobně je používáte tak často, že se staly každodenní záležitostí. Ano, mluvíme o výjimkách – programátorské technice a technologii.

V dnešním článku bych vám rád vysvětlil, co to vlastně výjimky jsou, a proč se tak často používají. Také se společně podíváme na to, jak byste mohli napsat své vlastní zpracování výjimek, pokud vám to jazyk dovolí. Díky tomu pronikneme do černé skříňky, kterou jsou ve většině jazyků obaleny, a která nedovolí nahlédnout dovnitř.

} catch {

Tento článek patří pod kolekci "pokročilé programovací techniky" a předem upozorňuji, že je vyžadována znalost jazyka C. Začátečníkům navíc může způsobit noční můry.

}

Motivace

Pojďme začít definicí: Co jsou to výjimky a co je zpracování výjimek?

Dokážete na tuto otázku odpovědět jednou větou? Pokud ano, napište si svoji definici a srovnejte ji s definicí na konci této kapitoly.

První příklad – motto: Jednoduchost

Začněme jednoduchým příkladem.

Naprogramujeme aplikaci, která řeší rovnici x2 = a. Konstantu na pravé straně získáme od uživatele vstupem z klávesnice.

Pro začátek budeme programovat 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átký a jednoduchý. Jenže se nám začínají rýsovat problémy. Co když uživatel nezadá číslo? Nebo když uživatel zadá záporné číslo?

Druhý příklad – motto: Bezchybnatost

Kód je sice krátký a jednoduchý, ale současně se nestará o chybové stavy. V praxi bychom s takovým přístupem zřejmě daleko nedošli, a proto musíme ošetřit všechny chyby, které se mohou stát.

Náš zdrojový kód po úpravě vypadá následujícím způsobem.

#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 se tedy trochu rozrostl a funguje tak, jak bychom si představovali. Ovšem mimo naše představy o eleganci zcela jistě patří zdrojový kód.

Pokusíme se shrnout výsledek. Na každý příkaz, který jsme použili, přibyla kontrola výsledku, výpis chyby a ukončení programu. To je šest řádek na jednu funkční instrukci. Výsledný program je prakticky pětkrát delší pouze proto, abychom zabránili chybám, které zřejmě nenastanou?!

Třetí příklad – motto: Rozděl a panuj

Chtěli jsme náš program ochránit před chybami, ale v důsledku toho se rozrostl tak, že s vysokou pravděpodobností bude chyba v samotném zpracování chyb. Ve chvíli, kdy funkční část programu tvoří 20% celkového kódu, se program navíc stává naprosto nečitelným.

Pojďme to tedy provést trochu přehledněji.

#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;
}

Třetí verze našeho programu je z funkčního hlediska zcela stejná jako verze předchozí. Ovšem díky tomu, že jsme zpracování chyb přesunuli na konec programu, je už zase trochu vidět, co program dělá. Jinak řečeno, program jsme rozdělili na dvě části – první se stará o samotnou funkcionalitu, zatímco druhá slouží ke zpracování chyb.

Mimochodem, pokud si tento příklad pozorně prohlédnete, nepřipomíná vám tak trochu něco známého? Že by to bylo zpracování výjimek?

Čtvrtý příklad – motto: Pouze od 18 let

Další zjednodušení můžeme provést, pokud se námi volané funkce dokážou samy postarat o skok na chybovou sekci. Ale abychom to mohli udělat, budeme potřebovat dvě věci:

  1. Náš jazyk rozšíříme o takzvaný vzdálený skok. K tomu účelu slouží dvě knihovní funkce: setjmp() a longjmp(). Funkce setjmp() si uloží aktuální stav zásobníku a instrukční ukazatel, zatímco funkce longjmp() provede navrácení těchto hodnot do původního stavu a odskok na místo, odkud byla zavolaná funkce setjmp().
  2. Kvůli přehlednosti dočasně skryjeme všechny další používané funkce do definic, abychom nemuseli opakovaně psát if, if, if… Tento krok si ale vysvětlíme až v další části.
#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 jak vypadá výsledek?

Tak trochu jako Masakr motorovou pilou, ale určité pozitivní znaky se už začínají rýsovat. Všimněte si, že druhá část programu nazvaná TRY BLOCK už opět vypadá jako náš vůbec první příklad. Kód je krátký a snadno čitelný, radost pohledět. Také předchozí část nazvaná CATCH BLOCK je poměrně jasná a přehledná.

Starosti nám budou dělat definice na začátku programu, které vůbec nevypadají vábně, ale to vyřešíme snadno v následující části.

Pátý příklad – motto: Co oči nevidí, to srdce nebolí

V této části už opustíme jazyk C a dále budeme pokračovat ve fiktivním jazyku C/EX. Potřebujeme totiž ještě dořešit nevábně vypadající kód z předchozí části.

Schválně, napadá vás, jak se zbavit definic našich vlastních funkcí ex_XXX?

Správná odpověď se vám bude moc líbit: „Necháme to na někom jiném.“ Jmenovitě, ať to za nás naprogramuje tým, který píše kompilátor jazyka C/EX a jeho knihovny. Každá z funkcí prostě provede zavolání longjmp() s parametrem, který bude identifikovat, co se stalo špatně.

Jediné, co musíme udělat my, je zavolat speciální funkci catch(), která provede volání 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 */
}

Šestý příklad – motto: Od programovací techniky k technologii

Díky tomu, že jsme náš problém hodili na tvůrce jazyka, se už o všechno budou starat knihovny. Proč ale nezajít ještě o kousek dál a nerozšířit syntaxi jazyka? Provedeme to následujícím způsobem, který vám už zcela jistě není cizí.

Zavedeme do jazyka C/EX klíčová slova TRY, CATCH a THROW, která budou následována blokem kódu. Jejich význam je následující:

  • TRY je takzvaná chráněná část kódu; Na základě explicitního příkazu se okamžitě přeruší běh programu a provede se odskok do části CATCH
  • CATCH je podmíněná část kódu; Tento kód proběhne pouze na základě explicitního příkazu THROW v části TRY
  • THROW způsobí přerušení běhu v chráněné sekci a odskok do podmíněné části

Implementace těchto klíčových slov je v našem případě poměrně jednoduchá:

  • TRY zajistí volání funkce setjmp() na začátku bloku CATCH; poté vrátí instrukční pointer na začátek bloku svázaného s TRY
  • CATCH sekce je umístěna mimo běh programu; sem se lze dostat pouze voláním funkce longjmp()
  • THROW zavolá longjmp()

Tohle je chvíle, kdy se z techniky stává technologie. Zpracování chybového stavu aplikace použitím výjimek je programátorská technika. Ovšem podpora v jazyce a na platformě (ať už Java nebo C#) je technologická záležitost.

Díky snaze vývojového týmu jazyka C/EX se konečně dostáváme k finální verzi našeho 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 motivace k výsledku

Vraťme se opět k naší definici ze začátku článku: Co jsou to výjimky a co je zpracování výjimek?

Výjimky a jejich zpracování slouží k oddělení části kódu na zpracování chyb od samotné funkční části kódu. Takže umožňují zpřehlednit a zjednodušit zdrojový kód rozdělením na logické celky – části, které provádí funkcionalitu podle očekávaného scénáře, a části, které se starají o výjimečné stavy a jejich nápravu.

Není v tom žádná magie, ale je za tím obrovská práce. Pojďme si udělat krátkou rekapitulaci:

  • Kompilátor musí umět uložit aktuální běhový stav [vlákna] a později ho vrátit zpět
  • Knihovní funkce se musí starat o kontrolu platných hodnot na vstupu i na výstupu
  • Knihovní funkce se musí starat o explicitní vyvolání výjimky příkazem THROW/longjump()
  • Výjimka není pro interpret výjimečná situace, jedná se pouze o bezpečně provedený vzdálený skok do nadřazené podmíněné sekce
  • Vzdálený skok musí být proveden opatrně s ohledem na uvolnění zdrojů, které mohly být vyhrazeny v nadřazených sekcích

A závěrem…

O výjimkách by toho bylo ještě mnoho, o čem bychom mohli mluvit – FINALLY bloky, typování výjimek, deklarované vs. nedeklarované výjimky, zpracování paměti, vnořené bloky, atd., atd.

Já jsem vám chtěl ovšem zpřístupnit samotné srdce výjimek, protože vypsání několika definic a příkladů vám dá jen malý vhled do této problematiky. Pokud rozumíte všem příkladům uvedeným výše, měli byste být schopni porozumět i tomu, jak výjimky fungují ve vašem jazyce. A hlavně byste měli vědět, jak máte sami zpracování výjimek používat ve vlastních programech.


 

  Aktivity (1)

Článek pro vás napsal coells
Avatar

Jak se ti líbí článek?
Celkem (21 hlasů) :
55555


 


Miniatura
Předchozí článek
Autentizace jménem a heslem
Miniatura
Všechny články v sekci
Články nejen o programování

 

 

Komentáře
Zobrazit starší komentáře (1)

Avatar
Luboš Běhounek (Satik):

Taky 5* - musíme nějak vyrovnat hodnocení (až přijde Kit a dá jednu hvězdičku za GOTO :) )

Odpovědět  +2 3.12.2013 12:37
:)
Avatar
vodacek
Redaktor
Avatar
 
Odpovědět 3.12.2013 12:44
Avatar
coells
Redaktor
Avatar
Odpovídá na vodacek
coells:
:-D

Ve "vyšších" programovacích jazycích goto zakázali, jenže pak zjistili, že je to docela potřebná věc. Takže ho vrátili zpět, a aby to nevypadalo hloupě, dali mu nový název:

  • break = goto
  • continue = goto
  • throw = long goto

Jenže ani to nestačilo a existují případy, kdy je goto preferovanou variantou, takže v Javě navíc zavedli labeled break a labeled continue. Vtipné na tom je, že kvůli jednomu procentu případů tak museli zavést podivnou syntaxi labeled cyklů do jazyka.

Každopádně díky za hodnocení.

 
Odpovědět 3.12.2013 12:56
Avatar
Kit
Redaktor
Avatar
Odpovídá na Luboš Běhounek (Satik)
Kit:

Zatím jsem to jenom přelítl, vyvalil jsem oči a nechal si to na večer, až na to bude klid. Zatím mám pocit, že by ten článek měli číst programátoři od 18 let ... praxe.

Odpovědět 3.12.2013 12:59
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
Kit
Redaktor
Avatar
Odpovídá na coells
Kit:

Téměř všechna goto a break se dají nahradit returnem. Throw je jen jiný (chybový) return.

Odpovědět 3.12.2013 13:02
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
coells
Redaktor
Avatar
Odpovídá na Kit
coells:

Téměř všechny řídící struktury se dají nahradit returnem, ale nebylo by to správné a navíc se tu o tom nebavíme. Throw je mnohem víc než jen return.

Editováno 3.12.2013 15:17
 
Odpovědět  +1 3.12.2013 15:16
Avatar
Kit
Redaktor
Avatar
Odpovídá na coells
Kit:

Ten poslední příklad bych si představoval spíš takto:

:
} CATCH EX_INVALID_STREAM {
    printf("Invalid input stream\n");
    break;
} CATCH EX_INVALID_NUMBER
    printf("Invalid number on input\n");
    break;
} CATCH EX_NAN
    printf("Result is not a number\n");
    break;
}

Ten výstupní string by měl vznikat už v místě chyby, protože jinak je to ošetřování poměrně komplikované.

Odpovědět  -1 3.12.2013 15:29
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
coells
Redaktor
Avatar
Odpovídá na Kit
coells:

Ty možná ano, ale já jsem to schválně takhle nenapsal ze dvou důvodů:

  1. nefungovalo by to
  2. nefungovalo by to

Zdá se to jako jeden důvod, ale zdál se mi tak důležitý, že jsem ho uvedl dvakrát.

Bonus pro toho, kdo přijde na důvod, proč je v tomhle případě větvení na úrovni CATCH špatně ;-)

 
Odpovědět  +1 3.12.2013 21:43
Avatar
vitamin
Člen
Avatar
vitamin:

Veľmi zaujímavý článok, ale v praxi je to nepoužiteľné. setjump/longjmp sú pomalé funkcie ktoré spomalujú normálny beh programu preto sa na spracovávanie výnimiek nepoužívajú(v niektorých prípadoch ich treba napr pri odchytávanie výnimiek z win knižníc ktoré používajú SEH). Tie tvoje "výnimky" nemôžu opustiť funkciu kde boli "vyhodené" a množstvo ďalších obmedzení.

 
Odpovědět  -1 4.12.2013 9:13
Avatar
Šimon Raichl
Redaktor
Avatar
 
Odpovědět 24.8.2014 22:07
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 11. Zobrazit vše