Uvod
Pedagoški pristup u izučavanju programiranja, obično podrazumeva da se u početnim stadijumima ne obraća prevelika pažnja na određene anomalije koje se mogu javiti u toku izvršavanja programa (ukoliko korisnik unese podatke koji dovode u pitanje korektnost izvršavanja programa, ili ako se takvi podaci pojave u toku obrade).
Podrazumeva se - u (navedenim) okolnostima u kojima je prioritet da se polaznici upoznaju sa osnovnim mehanizmima za obradu podataka - da će korisnički unos biti ispravan i da će samim tim i program uvek vraćati korektan rezultat (naravno, ukoliko je program uopšte pravilno osmišljen i zapisan).
Međutim, jasno je da, van konteksta pisanja osnovnih programa u cilju upoznavanja sa tehnikama programiranja (prosto rečeno, u realnim situacijama), korisnički unos ne mora biti ispravan, jasno je da se tokom izvršavanja programa mogu pojaviti anomalije i da stoga (i te kako) postoji potreba da se pravilno odreaguje u nepovoljnim okolnostima.
Recimo, ukoliko pišemo funkciju koja vraća obim pravougaonika (za unete stranice a i b), sve će biti u redu ukoliko unesemo vrednosti kao što su 3.5 i 7.2, međutim - kako (tačno) program treba da odreaguje ako unesemo vrednosti 12.4 i 0, ili 5 i -5?
U prvom slučaju, dobićemo rezultat (24.8), koji deluje dobro, ali - u pitanju je rezultat do koga smo došli pod "sumnjivim okolnostima", dok će u drugom slučaju i sama vrednost koju funkcija vraća (0), biti krajnje diskutabilna.
U navedenim okolnostima (kao i mnogim drugim), * dobro dođe mehanizam koji proverava unete vrednosti, obustavlja dalje izvršavanje problematičnih delova koda ukoliko vrednosti nisu pravilno unete ** i naravno - obaveštava programere (a po potrebi i krajnje korisnike), o tome da je do grešaka došlo.
Pojam izuzetka
Pre nego što se detaljnije pozabavimo izuzecima (što naravno ostaje glavna tema članka), razmotrićemo - preko jednostavnih primera - razliku između situacija u kojima nije potrebno koristiti izuzetke da bismo dobili sve informacije o obradi koja je izvršena * (korektan rezultat, ili poruku o grešci, u zavisnosti od ulaznih podataka) - i situacija u kojima jeste potrebno koristiti izuzetke.
Kraća diskusija o situacijama koje se mogu rešiti bez upotrebe izuzetaka
Funkcija za pronalaženje prve pojave niske s2
unutar niske s1
, verovatno je najuobičajeniji primer funkcije u kojoj se "sigurnosni mehanizmi" mogu implementirati bez korišćenja izuzetaka.
int poklapanjeNiski(string s1, string s2)
{
}
Ukoliko se niska s2
pojavljuje unutar niske s1
, funkcija će vratiti indeks prvog poklapanja: na primer, za niske "Beograd" i "grad", funkcija vraća 3
(poklapanje počinje od 4. znaka), dok u slučaju niski "Beograd" i "Beo", funkcija vraća 0
(poklapanje počinje od 1. znaka).
U pitanju su povratne vrednosti za situacije u kojima dolazi do poklapanja, međutim, šta funkcija treba da vrati ukoliko poklapanja nema (s obzirom na to da je povratni tip funkcije celobrojna vrednost)?
Uz (sasvim) malo promišljanja i snalaženja, lako ćemo zaključiti da je dovoljno da u navedenim okolnostima vratimo bilo koju negativnu vrednost (tipično -1
).
Negativna celobrojna vrednost (vrlo očigledno) ne predstavlja indeks u niski znakova, pa se stoga može protumačiti kao svojevrstan "signal" da do poklapanja nije došlo.
Funkciju sada možemo implementirati na jednostavan način (doduše, prikazaćemo samo šematski prikaz; konkretnu implementaciju koja podrazumeva korišćenje nekog od algoritma za proveru poklapanja niski ćemo na ovom mestu, zarad preglednosti, ipak izostaviti):
int poklapanjeNiski(string s1, string s2)
{
int p = -1;
/* -- OBRADA -- */
// Ukoliko funkcija pronađe poklapanje,
// promenljiva p će dobiti vrednost
// indeksa od koga počinje poklapanje,
// dok će u suprotnom promenljiva p
// zadržati početnu vrednost
return p;
}
Sa druge strane, postoje (reklo bi se znatno brojniji) primeri, u kojima se prikazani pristup ne može koristiti, a verovatno najtipičniji primer (koji je idejno blizak čak i čitaocima koji nemaju iskustva sa web programiranjem), predstavljaju funkcije za povezivanje sa bazama podataka na udaljenom serveru.
Funkcije za povezivanje sa bazom obično imaju četiri parametra: ime servera, korisničko ime za pristup serveru, lozinku i ime baze, a povratna vrednost je objekat preko koga se ostvaruje veza sa bazom (i nadalje, čitanje i obrada podataka).
Naravno, to je ono što se dešava ukoliko su parametri korektno predati i ukoliko je veza uspešno uspostavljena, dok u suprotnom - funkcija vraća izuzetak.
Definicija
Izuzetak je specijalno formatirana poruka koja sadrži informaciju o grešci koja se pojavila.
U najosnovnijem obliku, izuzeci se javljaju u vidu prostih tekstualnih poruka o tome da je došlo do greške, ali, mogu se formatirati i tako da sadrže detaljan opis okolnosti koje su dovele do pojave greške (to jest, dodatne informacije o prirodi greške).
U "konkretnijem" primeru kao što je povezivanje sa bazom (koji smo pominjali), tipično su predviđeni sledeći mehanizmi: ukoliko dođe do greške, možemo - uz korišćenje prikladno formatiranih izuzetaka - dobiti informaciju o tome šta je tačno "krenulo naopako" pri povezivanju sa bazom podataka (da li je predato pogrešno korisničko ime, ili naziv baze, ili se desilo nešto sasvim treće), što je svakako mnogo bolje nego da nam program samo saopšti da "nešto" nije u redu (pri čemu ne bismo znali šta konkretno nije u redu), ili - što bi bilo mnogo gore - da program 'sakrije' da je do greške uopšte došlo! *
U nastavku, bavićemo se izuzecima (s tim što ćemo se vratiti na jednostavniji primer sa obimom pravougaonika, koji lako možete isprobavati samostalno).
Kreiranje izuzetaka
Kreiranje izuzetaka nije ni izdaleka komplikovana procedura (ni u idejnom, ni u tehničkom smislu), pa ćemo pogledati odmah jednostavan primer upotrebe.
Kreiranje izuzetaka u programskom jeziku C++
Ukoliko dođe do greške, unutar (korisničke) funkcije racunanjeObima
, poziva se specijalizovana funkcija throw
. U pitanju je mehanizam koji obustavlja dalje izvršavanje funkcije (slično kao da je navedena naredba return
) - pri čemu se prosleđuje poruka o grešci:
double racunanjeObima(double a, double b)
{
if (a < 0) {
throw "Dužina stranice a ne sme biti negativan broj.";
}
if (a == 0) {
throw "Dužina stranice a ne sme biti 0.";
}
if (b < 0) {
throw "Dužina stranice b ne sme biti negativan broj.";
}
if (b == 0) {
throw "Dužina stranice b ne sme biti 0.";
}
return 2 * a + 2 * b;
}
Kao što vidimo, naizgled smo 'pokrili' sve okolnosti, međutim, ukoliko pozovemo sledeći kod:
double obim = racunanjeObima(-2.32, 12.2);
cout << "Da li vidimo ovaj tekst?!";
.... na ekranu se neće pojaviti nijedna od prethodno navedenih poruka (koje smo definisali u funkciji racunanjeObima
), kao ni poruka "Da li vidimo ovaj tekst?!".
Kada program naiđe na izuzetak za koji nije predviđena odgovarajuća obrada - jednostavno će prestati sa izvršavanjem ("pući", "bez objašnjenja"), što: niti je u skladu sa principima dobrog programiranja, niti je elegantno .....
Blok try-catch - Obrada izuzetaka
Da bi izuzeci koje smo definisali (u funkciji racunanjeObima
, a naravno i inače), mogli da se koriste na pravi način - potrebno je koristiti blok try-catch
.
U pitanju je, naizgled, svojevrsno grananje u programu, sa sintaksom nalik na standardni blok if-else
(sa kojim ste se sretali mnogo puta do sad), ali je zapravo u pitanju sintaksa specijalizovana za prihvat i obradu izuzetaka.
Pogledajmo šemu izvršavanja bloka try-catch
:
try {
naredba_1;
naredba_2;
naredba_3;
}
catch (tip_podatka greska) {
ispis_greske(greska);
}
Princip izvršavanja koda je sledeći
- u odeljku
try
, navode se opšte naredbe za obradu podataka (u praktičnom smislu, u pitanju je: "ono što treba da se desi - ukoliko ne dođe do grešaka"), međutim, podrazumeva se da među navedenim naredbama, postoji programski kod koji, u slučaju greške - generiše izuzetak - u odeljku
catch
, navode se naredbe koje treba izvršiti u slučaju da je neka od naredbi iz odeljkatry
, generisala izuzetak - ukoliko ni u jednom pozivu unutar bloka
try
ne dođe do greške, blokcatch
se (uopšte) neće izvršavati (biće preskočen) - ukoliko bilo koja od naredbi iz bloka
try
vrati izuzetak, momentalno prestaje izvršavanje naredbi iz blokatry
(pojava naredbethrow
je u tom smislu veoma slična pojavi naredbereturn
) - i prelazi se na odeljakcatch
Vraćamo se na računanje obima (uz korišćenje bloka try-catch
):
double a, b, obim;
try {
// 1.
a = 4.5;
b = 6.2;
cout << "Stranice pravougaonika: " + a + " i " + b << endl;
obim = racunanjeObima(a, b);
cout << "Obim: " << obim << endl;
// 2.
a = -3.32;
b = 2.6;
cout << "Stranice pravougaonika: " + a + " i " + b << endl;
obim = racunanjeObima(a, b);
cout << "Obim: " << obim << endl;
// 3.
a = 4.2;
b = 6.4;
cout << "Stranice pravougaonika: " + a + " i " + b << endl;
obim = racunanjeObima(a, b);
cout << "Obim: " << obim << endl;
}
catch (const char* greske) {
cout << "Pri računanju obima pravougaonika, ";
cout << "došlo je do sledećih grešaka:" << endl;
cout << greske;
cout << "--DODATNA OBRADA!--" << endl;
}
Ovoga puta, program se izvršava na sledeći način:
Stranice pravougaonika: 4.5 i 6.2
Obim: 21.4
Stranice pravougaonika: -3.32 i 2.6
Pri računanju obima pravougaonika,
došlo je do sledećih grešaka:
Dužina stranice a ne sme biti negativan broj.
--DODATNA OBRADA--
--------------------------------
Process exited after 0.0728 seconds with return value 0
Press any key to continue . . .
Primer koji smo videli, sledi pravila koja smo naveli na početku odeljka:
- blok
try
podeli smo na tri (gotovo) identična dela - u svakom od delova nalaze se dve naredbe dodele (za stranice
a
ib
), naredba ispisa (za vrednosti stranica), poziv metoderacunanjeObima
i na kraju, naredba ispisa preko koje se ispisuje vrednost obima - prvi blok naredbi izvršava se na očekivan način
- u drugom bloku se izvršava naredba dodele, prva naredba ispisa se takođe izvršava, ali - poziv funkcije
racunanjeObima
generiše izuzetak! - izvršavanje bloka
try
se momentalno prekida i prelazi se na izvršavanje blokacatch
Blok catch
ispisuje:
- opštu poruku koju smo naveli
- tekst izuzetka koji je definisan u funkciji
racunanjeObima
- drugu opštu poruku koju smo naveli
Poruka "--DODATNA OBRADA--", koju ste mogli videti, sugeriše da je, u situacijama kada se izuzeci pojave (a pri tom ih "uhvatimo" preko bloka catch
), moguće napisati naredbe koje će po potrebi program usmeriti na odgovarajući način (zahtevaće se novi unos podataka, obustaviće se izvršavanje programa ili će se desiti "nešto treće").
Kompleksniji primer izuzetka sa više poruka
U prethodnom slučaju, kreirali smo funkcionalan mehanizam provere koji obustavlja dalje izvršavanje programa i vraća poruku o grešci, međutim, prikazani mehanizam ima i nekoliko (potencijalnih) nedostataka:
- nećemo biti obavešteni o svim greškama, već samo o prvoj grešci koja se pojavi
- program prekida izvršavanje čim naiđe na prvi pravougaonik sa neodgovarajućim stranicama
.... što ostavlja mesta za dalja unapređenja.
Drugi navedeni nedostatak, lako možemo rešiti kreiranjem zasebnih blokova try-catch
za svaki pravougaonik (prethodno smo stavili sva tri pravougaonika u isti blok, da bismo prikazali osnovni način funkcionisanja bloka try-catch
), dok ćemo za ispis svih poruka morati malo više da se potrudimo.
Osvrnimo se za trenutak na koristan primer iz druge grane industrije.
U strujnim instalacijama, postoje "spori" i "brzi" osigurači: prvi štite robusne industrijske uređaje (koji imaju izvesnu toleranciju prema pojavi struja koje su veće od dozvoljenih), dok drugi štite relativno osetljive uređaje kakvi se sreću u domaćinstvima (i takvi osigurači nemaju preveliku toleranciju prema pojavi struja koje su veće od dozvoljenih).
Kada su u pitanju izuzeci - takođe razlikujemo (nalik prethodnom primeru), dve situacije:
- nekada je najbitnije da prekinemo dalje izvršavanje čim se pojavi bilo kakva anomalija u funkcionisanju programa (i time prekinemo dalje "rasipanje resursa", pojavu "još većih grešaka" i sl)
- u drugim situacijama je najbitnije da prikupimo što više informacija o okolnostima koje su dovele do nepravilnog funkcionisanja programa (da se greške ne bi ponavljale u budućnosti i sl)
(Naravno, postoje i razni "međuslučajevi".)
U nastavku ćemo se osvrnuti na sledeći primer: prepravićemo funkciju racunanjeObima
tako da - u slučaju pojave izuzetka - poruka sadrži podatke o svim parametrima pravougaonika koji nisu dobro navedeni.
Za početak, pripremićemo klasu preko koje se mogu skladištiti sve informacije o izuzecima:
#include<vector<
class Izuzetak
{
private:
int BrojIzuzetaka;
vector<string> SpisakPoruka;
bool ImaIzuzetaka;
public:
Izuzetak()
{
ImaIzuzetaka = false;
BrojIzuzetaka = 0;
}
bool ImaLiIzuzetaka()
{
return ImaIzuzetaka;
}
void GenerisanjeIzuzetka(string s)
{
SpisakPoruka.push_back(s);
BrojIzuzetaka++;
ImaIzuzetaka = true;
}
string IspisIzuzetaka()
{
string s = "";
for (int i = 0; i < this->BrojIzuzetaka; i++) {
s += SpisakPoruka[i] + "\n";
}
return s;
}
};
Detaljniju analizu klase ostavljamo vama, ali, napomenućemo da su sva polja privatna (kao što i treba da bude u ovakvoj klasi) i da se njihova vrednost menja isključivo preko metode GenerisanjeIzuzetaka
(u suprotnom, moglo bi doći do povećih nepravilnosti).
Sada možemo prepraviti metodu za računanje obima:
double racunanjeObima(double a, double b)
{
Izuzetak izuzetak;
if (a < 0) {
string s = "Duzina stranice a ne sme biti negativan broj.\n";
izuzetak.GenerisanjeIzuzetka(s);
}
if (a == 0) {
string s = "Duzina stranice a ne sme biti 0.\n";
izuzetak.GenerisanjeIzuzetka(s);
}
if (b < 0) {
string s = "Duzina stranice b ne sme biti negativan broj.\n";
izuzetak.GenerisanjeIzuzetka(s);
}
if (b == 0) {
string s = "Duzina stranice b ne sme biti 0.\n";
izuzetak.GenerisanjeIzuzetka(s);
}
if (izuzetak.ImaLiIzuzetaka()) {
throw izuzetak;
}
return 2 * a + 2 * b;
}
Kao što smo ranije naveli - dosledno prikupljamo sve informacije pre nego što metoda generiše izuzetak (naravno, ako je bilo grešaka).
Sada će i odeljak catch
biti nešto drugačiji ....
try {
....
}
catch (Izuzetak& izuzeci) {
cout << "Pri racunanju obima pravougaonika, ";
cout << "doslo je do sledecih gresaka:" << endl;
cout << izuzeci.IspisIzuzetaka();
cout << "--DODATNA OBRADA--" << endl;
}
.... u tom smislu da pozivamo metodu za generisanje ispisa, umesto da "hvatamo" i ispisujemo niske koje je funkcija racunanjeObima
vratila kao izuzetak.
Pošto smo se 'usput' upoznali sa tim da funkcije mogu vraćati izuzetke koji su objekti klasa, pogledaćemo još jedan "šematski prikaz".
Blok try-catch sa višestrukim odeljkom catch
Vraćajući se na primer koji se tiče povezivanja sa bazom podatka, razmotrićemo šemu koja predstavlja razgranati blok try-catch
koji bismo mogli upotrebiti u navedenoj situaciji:
try {
// Pokušaj povezivanja
// sa bazom
}
catch(IzuzetakServer izuzetakServer) {
// Ispis informacija o grešci
// pri povezivanju sa serverom
}
catch(IzuzetakKorisnik izuzetakKorisnik) {
// Ispis informacija o grešci
// pri unosu korisničkog imena
// i/ili lozinke (pogrešno ime,
// pogrešna lozinka i sl.)
}
catch(IzuzetakBaza izuzetakBaza) {
// Ispis informacija o grešci
// pri povezivanju sa bazom
// (pogrešan naziv baze, korisnik
// nema pravo pristupa bazi i sl.)
}
Dakle, možemo nadovezivati odeljke catch
i udesiti da svaki od odeljaka bude namenjen obradi specifičnog tipa izuzetka.
Za kraj ....
U ovom članku, dotakli smo se najvažnijih detalja koji su vezani za implementaciju izuzetaka, i nadamo se da sada imate bolju predstavu o tome šta je potrebno učiniti ukoliko postoji mogućnost da funkcionalnost određenih delova programskog koda bude narušena zbog pogrešnog korisničkog unosa ili drugih okolnosti (dok - ukoliko takva mogućnost u praktičnom smislu ne postoji - treba biti racionalan i ne treba trošiti vreme na kreiranje delova koda koji generišu i obrađuju izuzetke).
Za vežbu, probajte (na primer), da sami implementirate klasu Izuzeci
koja je specifično namenjena obradi grešaka do kojih može doći u radu sa ulančanim listama, o kojima smo pisali u prethodnom članku.