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 obrade 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 dobro napisan.
Međutim, jasno je (još od samog početka), da korisnički unos ne mora biti ispravan, da se tokom izvršavanja programa mogu pojaviti anomalije i da stoga, u opštem smislu, postoji potreba da pravilno odreagujemo u takvim okolnostima.
Recimo, ukoliko pišemo funkciju koja vraća obim pravougaonika (za unete stranice a i b), sve će biti u redu ukoliko unesemo vredosti kao što su 3.5 i 7.2, međutim, šta ako smo uneli 12.4 i 0, ili 5 i -5?
U prvom slučaju, dobićemo rezultat (24.8) koji deluje dobro (ali, 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 (kao i mnogim drugim) okolnostima, dobro dođe mehanizam koji proverava unete vredsnoti, obustavlja dalje izvršavanje 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 i kraća diskusija o situacijama koje se mogu rešiti bez upotrebe izuzetaka
Pre nego što se detaljnije pozabavimo izuzecima (koji su glavna tema članka), razmotrićemo, preko jednog jednostavnog primera, da postoje i situacije u kojima nisu potrebni izuzeci da bismo dobili sve informacije o obradi koja je izvršena (korektan rezultat, ili poruku o grešci, u zavisnosti od ulaznih podataka).
Funkcija za pronalaženje prve pojave niske s2
unutar niske s1
, verovatno je najuobičajeniji primer funkcije koja se može implementirati bez 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
, dok u slučaju niski "Beograd" i "Beo", funkcija vraća 0
.
U pitanju su očekivane vrednosti za situacije kada dolazi do poklapanja, međutim, šta funkcija treba da vrati ukoliko poklapanja nema (s obzirom 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
).
Takva vrednost (vrlo očigledno) ne predstavlja poziciju u nizu znakova, pa je stoga (kao takva) 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, ovde ćemo, zarad preglednosti, ipak izostaviti):
int poklapanjeNiski(string s1, string s2)
{
int p = -1;
/* -- OBRADA -- */
// Ukoliko pronadjemo poklapanje,
// p će dobiti vrednost indeksa
// na kome poklapanje počinje,
// dok će u suprotnom zadržati
// početnu vrednost
return p;
}
Sa druge strane, postoje (reklo bi se znatno brojniji) primeri gde prikazani pristup ne možemo koristiti, a verovatno najtipičniji primer (koji čitaoci mogu razumeti čak i ukoliko nemaju previše iskustva sa bazama podataka), predstavljaju funkcije za povezivanje sa bazama podataka na udaljenom serveru (dostupne u brojnim programskim jezicima; većina programera ih koristi gotovo svakodnevno).
Funkcije za povezivanje sa bazom obično imaju četiri parametra: ime servera, korisničko ime za pristup serveru, lozinku i ime baze, a za povratnu vrednost imaju objekat preko koga se ostvaruje veza sa bazom (i nadalje, čitanje i obrada podataka).
Naravno, ukoliko su parametri korektno predati i veza uspešno uspostavljena; u suprotnom - funkcija vraća izuzetak.
Izuzetak je specijalno formatirana poruka koja sadrži informaciju o tome da je došlo do greške, kao i (što je važnije) - dodatne informacije o prirodi greške.
U prethodnom primeru (povezivanje sa bazom), uz korišćenje izuzetaka - ukoliko dođe do greške - dobijamo informaciju o tome šta je tačno "krenulo naopako" pri povezivanju (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" (pri čemu ne bismo znali šta konkretno), nije u redu, ili - što bi bilo mnogo gore (ali, što se ipak retko sreće u pravilno napisanim programima) - da nam program uopšte ni ne sopšti da je do greške 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, pa ćemo pogledati odmah jednostavan primer upotrebe.
Kreiranje izuzetaka u programskom jeziku C++
Ukoliko dođe do greške, program, preko funkcije throw
, prosleđuje poruku 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;
}
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 ni jedna 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 "pući" (da budemo precizni, prestaće sa izvršavanjem).
Blok try-catch - Obrada izuzetaka
Da bi izuzeci koje smo definisali (u funckiji racunanjeObima
, a naravno i inače), mogli da se koriste na pravi način, potrebno je koristiti blok try-catch
.
U pitanju je svojevrsno grananje u programu, sa sintaksom nalik na standardni blok if-else
, sa kojim ste se sretali mnogo puta do sad.
Pogledajmo primer:
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 . . .
Blok try-catch
je specifično namenjen obradi izuzetaka i funkcioniše na sledeći način:
- u odeljku
try
, navode se opšte naredbe obrade podataka ("ono što treba da se desi", ukoliko ne dođe do grešaka) - u odeljku
catch
, navode se naredbe koje treba izvršiti u slučaju da do grešaka dođe - ukoliko ni u jednom pozivu unutar bloka
try
ne dolazi do grešaka, blokcatch
se (uopšte) neće izvršavati - 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
Primer koji smo videli, sledi navedena pravila:
- 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 stranice, poziv metoderacunajeObima
i, na kraju, naredba ispisa za obim - prvi blok naredbi izvršava se na očekivan način
- u drugom bloku, dolazi do naredbi 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 funckiji
racunanjeObima
- drugu opštu poruku koju smo naveli
Poruka "--DODATNA OBRADA--", koju ste mogli videti, sugeriše da je (naravno), u situacijama kada se izuzeci pojave (a "uhvatimo" ih preko bloka catch
), moguće napisati naredbe koje će, po potrebi, program usmeriti na odgovarajući način (zahtevati novi unos podataka, obustaviti izvršavanje programa ili 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, ovakav mehanizam ima nekoliko 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 pravougaonik sa neodgovarajućim stranicama
.... što ostavlja mesta za dalja uapređenja.
Drugi navedeni nedostatak lako možemo rešiti kreiranjem zasebnih try-catch
blokova za svaki pravougaonik (prethodno smo stavili sva tri pravougaonika u isti blok, da bismo prikazali osnovni način funkcionisanja try-catch
bloka), dok ćemo za ispis svih poruka morati malo više da se potrudimo.
Kao što u strujnim instalacijama postoje spori i brzi osigurači (prvi štite robusne industrijske uređaje koji imaju izvesnu toleranciju prema pojavi struja većih od dozvoljenih, dok drugi štite relativno osetljive uređaje kakve srećemo u domaćinstvima, koji nemaju preveliku toleranciju prema pojavi struja i napona većih od dozvoljenih), tako i, kada su izuzeci u pitanju, razlikujemo dve situacije:
- prvu, u kojoj je bitno da što pre prekinemo dalje izvršavanje, čim se pojavi bilo kakava anomalija u funkcionisanju programa (da time prekinemo dalje "rasipanje" resursa)
- drugu, u kojoj 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, nije sve tako "crno i belo" i postoji veliki broj "međuslučajeva", ali, u nastavku ćemo izuzetke definisati na sledeći način: zanima nas koji sve parametri pravougaonika nisu bili 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];
}
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.ImaIzuzetaka = true;
izuzetak.GenerisanjeIzuzetka(s);
}
if (a == 0) {
string s = "Duzina stranice a ne sme biti 0.\n";
izuzetak.ImaIzuzetaka = true;
izuzetak.GenerisanjeIzuzetka(s);
}
if (b < 0) {
string s = "Duzina stranice b ne sme biti negativan broj.\n";
izuzetak.ImaIzuzetaka = true;
izuzetak.GenerisanjeIzuzetka();
}
if (b == 0) {
string s = "Duzina stranice a ne sme biti 0.\n";
izuzetak.ImaIzuzetaka = true;
izuzetak.GenerisanjeIzuzetka();
}
if (izuzetak.ImaIzuzetaka) {
throw izuzetak;
}
return 2 * a + 2 * b;
}
Kao što smo i naveli, dosledno skupljamo sve informacije, pre nego što metoda generiše izuzetak (naravno, ako za tim ima potrebe).
Sada će i catch
odeljak 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 ćemo pozivati metodu za ispis, umesto da "hvatamo" i ispusjemo niske koje je funkcija racunanjeObima
vratila kao izuzetak.)
Sada, kada razumemo da funkcije mogu vraćati izuzetke koji su objekti klasa, pogledaćemo još jednu šemu.
Blok try-catch sa višestrukim catch odeljkom
Vraćajući se na primer sa povezivanjem sa bazom, pogledaćemo šemu koja predstavlja razgranati try-catch blok koji bismo mogli upotrebiti u ovakvoj 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 catch
blokove i udestiti da svaki od njih bude namenjen obradi specifičnog tipa izuzetka.
I naravno, svaku od navedenih klasa (IzuzetakServer
, IzuzetakKorisnik
i IzuzetakBaza
) bilo bi potrebno definisati unapred (to ćemo ovoga puta izostaviti), ali, jasno je da navedeni pristup omogućava visok nivo fleksibilnosti u radu sa izuzecima (naravno, uz neizostavno povećanje kompleksnosti programskog koda).
Za kraj ....
U ovom članku dotakli smo se najvažnijih tehnika vezanih za izuzetke 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 doslovno ne postoji, treba biti racionalan i ne treba trošiti vreme na kreiranje delova koda koji geerišu i obrađuju zadatke).
Za vežbu, probajte (na primer) da sami implementirate klasu Izuzeci
, specifično namenjenu obradi grešaka do kojih može doći u radu sa ulančanim listama, o kojima smo govorili u prethodnom članku.