IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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.


 

Všechny články v sekci
Články nejen o programování
Článek pro vás napsal coells
Avatar
Uživatelské hodnocení:
25 hlasů
Aktivity