FIGYELEM! Ez a dokumentum kizárólag az ELTE IK hallgatók számára oktatási célra készült! Félkész munka, dolgozunk rajta! Terjeszteni, felhasználni máshol a szerzők engedélye nélkül tilos! |
|
Kivételek, dobás és elkapás
Eddig Kényelemországban írtuk a programunkat - egy csodálatos helyen, ahol mindig minden jól megy. Minden könyvtárhívásunk sikeres volt, a felhasználó nem adott meg helytelen adatot, valamint sok és olcsó erőforrás állt rendelkezésünkre. Nos, ez változni fog. Üdvözöllek a való világban!
A való világban történnek hibák. Jó programok (és programozók) számolnak velük, és a kényelmesen kezelhetőségük érdekében csoportokba osztják őket. Ez azonban nem olyan egyszerű, mint amilyennek lennie kéne. Gyakran a hibát felismerő kód nem rendelkezik megfelelő információval az illetően, hogy mit is tegyen vele. Például, egy nem létező file megnyitása elfogadható bizonyos körülmények között, míg más esetekben végzetes hiba is lehet. Mit tegyen hát a file-ok kezelését végző modulunk?
A hagyományos megközelítés a visszatérési kódok használata. Az open metódus egy különleges értéket ad vissza, ha hibát akar jelezni. Ez az érték ezután visszagörgetődik a meghívási rutinok különböző logikai rétegein keresztül mindaddig, amíg valaki felelősséget nem vállal érte.
Ennek a megközelítésnek legnagyobb problémája, hogy a hibakódok karbantartása igen keserves lehet. Ha egy függvény egymás után meghívja az open , a read , és végül a close metódusokat, és mindhárom visszaadhat egy hibakódot, akkor a függvényünk hogyan jelzi ezt a saját hívójának?
Ezt a problémát a kivételek egész magas szinten megoldhatják. A kivételek lehetővé teszik az információ objektumba csomagolását. Ez a kivételobjektum adódik vissza a hívási vermen keresztül autómatikusan mindaddig, amíg a futási rendszer meg nem találja azt a kódrészletet, amely pontosan felismeri, és lekezelni az adott kivételtípust.
Az Exception (kivétel) osztály
Az a csomag, amely egy kivétellel kapcsolatos információkat tartalmaz, vagy az Exception osztály, vagy annak leszármazottjának egy objektuma. A Ruby új, rendezett hierarchiát alakított ki a kivételek számára. Mint azt később látni fogjuk, ez a hierarchia a kivételek kényelmesebb kezelését teszi lehetővé.
Ha egy kivételt ki kell váltanunk, akkor használhatjuk a beépített Exception osztályokat, vagy megalkothatjuk a sajátunkat is. Ha az utóbbi mellett döntünk, akkor valószínüleg a SandardError osztály, vagy annak valamely leszármazottjából fogjuk származtatni a sajátunkat. Ha nem, akkor a kivételünket alapból nem fogjuk tudni elkapni.
Minden Exception rendelkezik egy hozzá kapcsolódó üzenet szöveggel, és egy visszautalással a verem megfelelő helyére. A saját kivételünk definiálásánel ezeket újabb információkkal bővíthetjük.
Kivételek kezelése
A zenegépünk egy TCP socket használatával az Interneten keresztül tölti le a számokat. Az alap kód egyszerű:
opFile = File.open(opName, "w")
|
|
while data = socket.read(512)
|
|
opFile.write(data)
|
|
end
|
|
|
Mi történik, ha egy végzetes hibát kapunk a letöltés felénél? Természetesen nem akarunk egy befejezetlen dalt a listában tárolni.
Adjunk hát hozzá némi kivételkezelő kódot, és nézzük meg, hogyan is segít nekünk. A kivételt kiváltható kódot egy begin/end blokkba zárjuk, és rescue esetekkel adjuk meg a Rubynak, hogy milyen típusú kivételekkel akarunk foglalkozni. Ez esetben a SystemCallError típusú kivételek (és így annak leszármazottai) érdekelnek minket, tehát ez jelenik meg a rescue sorában. A hiba kezelő blokkjában jelentjük a hibát, lezárjuk és töröljük a kimeneti file-t, majd kivételt váltunk ki.
opFile = File.open(opName, "w")
|
|
begin
|
|
# Ezen kód által kiváltott kivételeket
|
|
# az ezt követő "rescue" blokk kezeli le
|
|
while data = socket.read(512)
|
|
opFile.write(data)
|
|
end
|
|
|
|
|
|
rescue SystemCallError
|
|
$stderr.print "IO failed: " + $!
|
|
opFile.close
|
|
File.delete(opName)
|
|
raise
|
|
end
|
|
|
Amikor egy, minden következő kivételkezeléstől független kivétel váltódik ki, a Ruby az adott kivétel Exception objektumára mutató referenciát helyez el $! globális változóban. Az előző példánkban is használtuk ezt a változót a hibaüzenet megformázásához.
A file zárása és törlése után meghívjuk a raise metódust paraméter nélkül, amely egy $! kivétel kiváltását jelenti. Ez a hasznos technika lehetővé teszi számunkra, hogy magasabb szintre továbbítsuk azon kivételeket, amelyeket nem tudtunk lekezelni. Ez majdnem olyan, mint egy öröklődési hierarchia implementálása hibafeldolgozás céljára.
Egy begin blokkban több rescue kifejezésünk is lehet, és minden egyes rescue több kivételtípust is elkaphat. A rescue kifejezés végén megadhatjuk azt a lokális változót, amelyben az elkapott kivételt tárolni szeretnénk. Sokan ezt olvashatóbbnak tartják a $! folyamatos használatánnál.
begin
|
|
eval string
|
|
rescue SyntaxError, NameError => boom
|
|
print "String doesn't compile: " + boom
|
|
rescue StandardError => bang
|
|
print "Error running script: " + bang
|
|
end
|
|
|
Hogyan dönti el a Ruby, hogy melyik rescue klózt hajtsa végre? Ez nagyon hasonlít a case utasítás feldolgozására. A Ruby rescue kifejezések közül az első illeszkedésnél végrehajtja az ott meghatározott utasításokat, és leállítja a keresést. Az illeszkedés felderítéséhez a $!.kind_of?(parameter) metódust használja, amely sikeres, ha a paraméter ugyanolyan típusú, mint a kivétel, vagy a kivétel egy őse. Ha egy paraméter nélküli rescue kifejezést adunk meg, akkor a paraméter alapértelmezésként a StandardError .
Ha kiváltott kivétel egyik rescue kifejezéssel sem egyezik, vagy a begin/end blokkon kívül váltódott ki, akkor a Ruby a hivóban keres kivételkezelőt, majd a hívó hívójában, és így tovább.
Habár általában a rescue kifejezések paraméterei az Exception osztályok nevei, lehetnek olyan kifejezések metódushívások is, amelyek egy Exception osztályt adnak vissza.
Rendrakás
Néha garantlálnunk kell azt, hogy egy bizonyos számítás befejeződik egy blokknyi kód végére, függetlenül attól, hogy kiváltódott-e kivétel. Például, ha van egy file megnyitásunk a blokk elején, bizosak akarunk lenni benne, hogy a blokk végére azt le is zárjuk.
Az ensure (biztosítani) kifejezés pontosan ezt teszi. Az ensure a rescue kifejezést követi, és olyan kódrészletet tartalmaz, amely a blokk terminálásakor mindenképp végrehajtódik, függetlenül attól, hogy normális kilépés volt-e, vagy esetleg egy lekezelt, vagy ismeretlen kivétel megjelenése okozta a blokk terminálását.
f = File.open("testfile")
|
|
begin
|
|
# .. számítás
|
|
rescue
|
|
# .. hibakezelés
|
|
ensure
|
|
f.close unless f.nil?
|
|
end
|
|
|
Az else kifejezés hasonló, bár kevésbé hasznos konstrukció. Ha van, akkor a rescue és az ensure között a helye. Az else törzse csak akkor kerül végrehajtásra, ha a fő blokk törzse nem váltott ki semmilyen kivételt.
f = File.open("testfile")
|
|
begin
|
|
# .. számítás
|
|
rescue
|
|
# .. hibakezelés
|
|
else
|
|
puts "Gratulálok-- nincs hiba!"
|
|
ensure
|
|
f.close unless f.nil?
|
|
end
|
|
|
Játsszuk újra
Néha képesnek kell lennünk egy kivétel okának kijavítására. Ezekben az esetekben használhatjuk a retry utasítást egy rescue blokkon belül, és ekkor az egész begin/end blokk újra végrehajtásra kerül. Láthatjuk, hogy itt igen nagy esélyünk van arra, hogy az egész programot belekergessük egy végtelen ciklusba. Szóval ezt a funkciót különös körültekintéssel kezeljük (és egy ujjunkat tartsuk a megszakító gombon...)!
Egy kivétel esetén újra próbálkozó blokk példájaként vegyünk Minero Aoki net/smtp.rb könyvtárából egy részletet.
@esmtp = true
|
|
|
|
begin
|
|
# Először egy kiterjesztett login-nal próbálkozunk. Ha sikertelen,
|
|
# mert a szerver nem támogatja, akkor normál login-nal
|
|
# próbálkozunk.
|
|
|
|
if @esmtp then
|
|
@command.ehlo(helodom)
|
|
else
|
|
@command.helo(helodom)
|
|
end
|
|
|
|
rescue ProtocolError
|
|
if @esmtp then
|
|
@esmtp = false
|
|
retry
|
|
else
|
|
raise
|
|
end
|
|
end
|
|
|
Ez a kód először az EHLO paranccsal próbál az SMTP szerverhez csatlakozni, ami általában nem támogatott. Ha a kapcsolódás hibát jelez, a kód a @esmtp változót false -ra állítja, és újra megpróbál kapcsolódni. Ha ismét sikertelen, akkor kivételt jelez a hívónak.
Kivételek kiváltása
Ezidáig védekező játékot játszottunk, azaz a mások által kiváltott kivételeket próbáltuk lekezelni. Ideje kipróbálni a másik oldalt is.
Kivételeket a Kernel::raise metódus használatával jelezhetünk.
raise
|
|
raise "bad mp3 encoding"
|
|
raise InterfaceException, "Keyboard failure", caller
|
|
|
Az első forma egyszerűen újra kiváltja az aktuális kivételt (vagy egy RuntimeError -t, ha nincs aktuális kivétel). Ezt kivételkezelőkben használjuk, ha egy kivételt fel kell deríteni, mielőtt továbbhaladunk.
A második forma egy új RuntimeError kivételt hoz létre, és az üzenetét az adott szövegre állítja be. Ezt a kivételt ezután eggyel magasabb továbbítja a hívási veremben.
A harmadik forma első argumentumát egy kivétel létrehozására használja, majd annak üzenetét beállítja a második argumentumban megadott szövegre. Általában az első argumentum vagy Exception hierarchiabeli osztály neve, vagy ezen osztályok egy objektumára mutató referencia. A hívási veremben keresés általában a Kernel::caller metódus használatával történik.
Vegyünk néhány tipikus példát a raise használatára:
raise
|
|
|
|
raise "Missing name" if name.nil?
|
|
|
|
if i >= myNames.size
|
|
raise IndexError, "#{i} >= size (#{myNames.size})"
|
|
end
|
|
|
|
raise ArgumentError, "Name too big", caller
|
|
|
Az utolsó példában eltávolítjuk az aktuális rutint a veremből, amely általában könyvtár moduloknál hasznos. De ezt tovább is vihetjük: a következő kód 2 rutint távolít el:
raise ArgumentError, "Name too big", caller[1..-1]
|
|
|
Információ hozzáadása kivételekhez
Definiálhatunk saját kivételeket, amelyek tartalmazzák a számunkra fontos információkat egy hiba fellépéséről. Például: bizonyos típusú hálózati hibák - a körülményektől függően - csak pillanatokig tartanak. Ha egy ilyen hiba lép fel, és a körülmények jók, beállíthatjuk a kivételben, hogy a kivételkezelő tudomására hozzuk, a művelet végrehajtását megérné újra megpróbálni.
class RetryException < RuntimeError
|
|
attr :okToRetry
|
|
def initialize(okToRetry)
|
|
@okToRetry = okToRetry
|
|
end
|
|
end
|
|
|
Valahol a kód mélyebb részein egy ilyen pillanatnyi hiba lép fel.
def readData(socket)
|
|
data = socket.read(512)
|
|
if data.nil?
|
|
raise RetryException.new(true), "transient read error"
|
|
end
|
|
# .. normális folyamat
|
|
end
|
|
|
A hívási verem magasabb szintjén pedig lekezeljük a kivételt.
begin
|
|
stuff = readData(socket)
|
|
# .. stuff feldolgozása
|
|
rescue RetryException => detail
|
|
retry if detail.okToRetry
|
|
raise
|
|
end
|
|
|
Dobás és elkapás
Míg a kivételek raise és rescue mechanizmusa megfelelő, ha hiba esetén szeretnénk megszakítani a futtatást, néha azért szebb, ha képesek vagyunk kiugrani egy mélyen beágyazott szerkezetből normális működés közben. Itt lép be a képbe a catch (elkapni) és a throw (dobni).
catch (:done) do
|
|
while gets
|
|
throw :done unless fields = split(/\t/)
|
|
songList.add(Song.new(*fields))
|
|
end
|
|
songList.play
|
|
end
|
|
|
A catch egy adott névvel cimkézett blokkot definiált, amely mindaddig normálisan került futtatásra, amíg egy throw hirtelen meg nem jelenik. A blokk neve lehet String , vagy Symbol (szimbólum).
Amikor a Ruby egy throw -ba ütközik, végigzongorázza a hívási vermet egy megfelelő szimbólummal ellátott catch blokkot keresve. Ha megtalálja, a Ruby visszatörli a vermet az adott pontig, és terminálja az adott blokkot. Ha a throw egy opcionális második paraméterrel kerül meghívásra, akkor annak értéke a catch visszatérési értékeként kerül átadásra. Tehát előző példánkban, ha a bemenet nem tartalmaz megfelelően formázott sorokat, a throw a hozzá tartozó catch végére ugrik, nem csak terminálva ezzel a while ciklust, de kihagyva a dalok listájának lejátszását is.
A következő példában a throw használatával megszakítjuk a felhasználóval való kommunikációt, ha egy ! -et ad bármely kérdésre.
def promptAndGet(prompt)
|
|
print prompt
|
|
res = readline.chomp
|
|
throw :quitRequested if res == "!"
|
|
return res
|
|
end
|
|
|
|
|
|
catch :quitRequested do
|
|
name = promptAndGet("Name: ")
|
|
age = promptAndGet("Age: ")
|
|
sex = promptAndGet("Sex: ")
|
|
# ..
|
|
# információ feldolgozása
|
|
end
|
|
|
Mint az ezen a példán is látszik, a catch -nek nem kell a throw statikus láthatóságán belül lennie.
|