Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Unntak, catch og throw



Så langt har vi utviklet kode i Pleasantville, et vidunderlig sted hvor ingenting, absolutt ingenting, går galt. Hver bibliotekkall utføres uten problemer, brukere kommer ikke med inkorrekte data og ressurser er billige og lett tilgjengelige. Vel, det er det nå slutt på. Velkommen til den virkelige verden!

I den virkelige verden forekommer det feil. Gode programmer (og programmerere) forutser dem og tar høyde for dem, slik at de blir håndtert på en pen måte. Dette er ikke alltid så lett som det kunne være. Ofte har ikke koden som oppdager en feil konteksten til å vite hva den skal gjøre med feilen. For eksempel, å prøve å åpne en fil som ikke eksisterer er akseptabelt i noen sammenhenger, og en fatal feil andre ganger. Hva skal din fil-håndterende modul gjøre?

Den tradisjonelle tilnærmingsmetoden er å bruke retur-koder. open-metoden returnerer en spesifikk verdi for å si at den feilet. Denne verdien blir da ført tilbake gjennom lagene av metodekall til noen vil ta ansvaret for den.

Problemet med denne måten er at håndteringen av alle disse feilkodene kan bli slitsomt. Hvis en funksjon kaller open, så read, og til slutt close, og hver kan returnere en feil-indikasjon, hvordan kan funksjonen skille disse feil-kodene i den verdien den returnerer til dens kaller?

Til en stor grad løser unntak dette problemet. Unntak lar deg pakke inn informasjon om en feil i et objekt. Dette unntaksobjektet blir da ført tilbake opp kallstakken automatisk til kjøretidssystemet finner kode som uttrykkelig deklarerer at den vet hvor den skal håndtere den type unntak.

Unntaks-klassen

Pakken som innholder informasjonen om et unntak er et objekt av klassen Exception, eller en av klassen Exceptions mange subklasser. Ruby kommer ferdig med et ryddig hierarki av unntak, vist i figur 8.1 på side 91. Som vi vil se senere gjør dette hierarkiet behandling av unntak betraktelig lettere.

Figur 8.1: Ruby sitt unntakshierarki

Når du trenger å heve et unntak kan du bruke en av de innebygde Exception-klassene, eller du kan lage din egen. Hvis du lager din egen, kan du ønske å gjøre det til en subklasse av StandardError, eller et av dens barn. Hvis du ikke gjør det vil ikke ditt unntak bli fanget som standard.

Hvert Exception-objekt har en tekstbeskjed og en sporing av kallstakken. Hvis du definerer dine egne unntak, kan du legge til tilleggsinformasjon.

Håndtering av unntak

Vår jukeboks laster ned sanger fra Internett ved å bruke en TCP-socket. Den grunnleggende koden er enkel nok.

opFile = File.open(opName, "w")
while data = socket.read(512)
  opFile.write(data)
end

Hva skjer hvis vi får en fatal feil halvveis i nedlastingen. Vi vil helt sikkert ikke lagre en uferdig sang i sang-listen. ``I Did It My *click*''.

La oss legge til noe unntakshåndteringskode som kunne heve et unntak i en begin/end-blokk, og bruke rescue-klausuler for å fortelle Ruby hvilke typer unntak som vi vil håndtere. I dette tilfellet er vi interessert i å fange SystemCallError-unntak (og implisitt alle unntak som er subklasser av SystemCallError), så det er hva som kommer fram på rescue-linjen. I feilbehandlingsblokken rapporterer vi feilen, lukker og sletter output-filer, og så gjenheve unntaket.

opFile = File.open(opName, "w")
begin
  # Unntak hevet av denne kodebiten
  # fanges opp av den påfølgende rescue-klausulen
  while data = socket.read(512)
    opFile.write(data)
  end

rescue SystemCallError   $stderr.print "IO failed: " + $!   opFile.close   File.delete(opName)   raise end

Når et unntak blir hevet, uavhengig av eventuell påfølgende unntaks-håndtering, plasserer Ruby en referanse til Exception-objektet som innekapsler unntaket i den globale variabelen $! (utropstegnet gjenspeiler antagelig vår overraskelse over at vår kode kunne feile). I det forrige eksemplet bruke vi denne variabelen til å formatere vår feilmelding.

Etter å ha lukket og slettet filen, kaller vi raise uten noen parametere, som gjenreiser unntaket i $!. Dette er en brukbar teknikk som tillater deg å skrive kode som filterer unntak, og sender du ikke kan håndtere til høyere nivå. Dette er nesten som å implementere et arve-hierarki for feil-prosessering.

Du kan ha flere rescue-klausuler i en begin-blokk, og hver rescue-klausul kan spesifisere flere unntakstyper som skal fanges. I enden av hver redningsklausul kan du gi Ruby navnet til en lokal variabel som skal motta unntaket. Mange syntes dette er mer leselig enn å bruke $! overalt.

begin
  eval string
rescue SyntaxError, NameError => boom
  print "String doesn't compile: " + boom
rescue StandardError => bang
  print "Error running script: " + bang
end

Hvordan bestemmer Ruby hvilken redningsklausul som skal settes igang? Det viser seg at prosesseringen er ganske lik til det som er brukt av case-utsagnet. For hver rescue-klausul i begin-blokken, sammenligner Ruby det hevede unntaket med hver av parameterene etter tur. Hvis det hevede unntaket passer med en parameter, utfører Ruby kroppen til rescue og avslutter sitt søk. Denne matchen er utført ved å bruke $!.kind_of?(parameter), og så vil få suksess hvis parameteren er av samme klasse som unntaket, eller er en forelderklasse til unntaket. Hvis du skriver en rescue-klausul uten noen parameter-liste, antas parameteren til å være StandardError.

Hvis ingen rescue-klausul passer, eller hvis et unntak er hevet på utsiden av en begin/end-blokk, beveger Ruby seg oppi kallstakken og ser etter unntakshåndteringskode i kalleren, så i kallerens kaller, og så videre.

Selv om parameterene til rescue-klausulen typisk er navn til Exception-klasser, kan de også faktisk være tilfeldige uttrykk (inkluderende metodekall) som returnerer en Exception-klasse. Exception class.

Oppryddning

Noen ganger trenger du å garantere at noe prosessering blir gjort på slutten av en kodeblokk, likegyldig om et unntak ble hevet eller ikke. For eksempel kan du ha en fil åpen ved inngangen til blokken, og du trenger å være sikker på at den blir lukket når blokken slutter.

ensure-klausulen gjør akkurat dette. ensure går etter den siste rescue-klausulen og inneholder et knippe kode som alltid vil bli utført når blokken terminerer. Det gjør ingenting om blokken avslutter normalt, hvis den hever og redder et unntak, eller hvis den blir terminert av et ufanget unntak---ensure-blokken vil bli kjørt.

f = File.open("testfile")
begin
  # .. prosesser
rescue
  # .. håndter feil
ensure
  f.close unless f.nil?
end

else-klausulen er en lignende, om enn en mindre nyttig, konstruksjon. Hvis den er tilstede kommer den etter rescue-klausulen(e) og før en eventuell ensure. Kroppen til en else-klausul blir bare kjørt hvis ingen unntak blir hevet av hovedkroppen til koden.

f = File.open("testfile")
begin
  # .. prosesser
rescue
  # .. håndter feil
else
  puts "Congratulations-- no errors!"
ensure
  f.close unless f.nil?
end

Spill den en gang til

Noen ganger kan du være i stand til å rette på årsaken til et unntak. I noen tilfeller kan du bruke retry-utsagnet inne en rescue-klausul for å repetere hele begin/end-blokken. Det er helt klart store muligheter for å skyte seg selv i foten med uendelige løkker her, så dette er ting som bør benyttes med varsomhet (og med en finger som hviler lett på avbruddstasten.)

Som et eksempel på kode som prøver igjen ved unntak, ta en titt på det følgende, tilpasset fra Minero Aoki's net/smtp.rb-bibliotek.

@esmtp = true

begin   # First try an extended login. If it fails because the   # server doesn't support it, fall back to a normal login

  if @esmtp then     @command.ehlo(helodom)   else     @command.helo(helodom)   end

rescue ProtocolError   if @esmtp then     @esmtp = false     retry   else     raise   end end

Denne koden prøver først å koble seg til en SMTP-server ved å bruke EHLO-kommandoen, som ikke er universelt støttet. Hvis tilkoblingsforsøket feiler, setter koden @esmtp-variabelen til false og prøver igjen å koble seg til. Hvis dette feiler igjen blir unntaket gjenhevet opp til kalleren.

Å heve unntak

Så lang har vi vært på defensiven, og håndtert unntak hevet av andre. Det er tid for å snu på flisa og gå på offensiven. (Det er de som sier at dine høflige forfattere alltid er offensive, men det er en annen bok.)

Du kan sette unntak i din kode med Kernel::raise -metoden.

raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller

Den første formen gjenhever ganske enkelt det nåværende unntak (eller et RuntimeError hvis det ikke er et nåværende unntak). Dette blir benyttet i unntakshåndterere som trenger å samhandle med et unntak før det sendes videre.

Den andre formen lager et nytt RuntimeError-unntak, ved å sette dens beskjed til den gitte strengen. Dette unntaket blir så hevet oppover i kallstakken som vanlig.

Den tredje formen bruker det første argumentet til å lage et unntak og så setter den assosierte beskjeden til det andre argumentet og stakksporingen til det tredje argumentet. Typisk vil det første argumentet være enten navnet på klassen i Exception-hierarkiet eller en referanse til en objekt-instans til en av disse klassene. [Teknisk kan dette argumentet være hvilket som helst objekt som svarer på beskjeden exception ved å returnere et objekt slik at object.kind_of?(Exception) er sann]. Stakksporingen er normalt produsert ved å bruke Kernel::caller -metoden.

Her er noen typiske eksempler på raise i aksjon.

raise

raise "Missing name" if name.nil?

if i >= myNames.size   raise IndexError, "#{i} >= size (#{myNames.size})" end

raise ArgumentError, "Name too big", caller

I det siste eksemplet fjerner vi den nåværende rutinen fra stakk-tilbakesporingen, som er ofte nyttig i bibliotek-moduler. Vi kan dra dette lengre: Den følgende koden fjerner to rutiner fra tilbakesporingen.

raise ArgumentError, "Name too big", caller[1..-1]

Å legge informasjon til unntak

Du kan definere dine egne unntak til å inneholde all informasjon som du trenger å sende ut fra åstedet til en feil. For eksempel kan visse typer av nettverksfeil være forbigående, avhengig av omstendighetene. Hvis en slik feil oppstår, og omstendighetene tilsier at feilen kan være forbigående, kunne du sette et flag i unntaket for å fortelle håndtereren at det ville være verdt å prøve en gang til.

class RetryException < RuntimeError
  attr :okToRetry
  def initialize(okToRetry)
    @okToRetry = okToRetry
  end
end

Et sted i dypet av koden oppstår en forbigående feil.

def readData(socket)
  data = socket.read(512)
  if data.nil?
    raise RetryException.new(true), "transient read error"
  end
  # .. normal prosessering
end

Høyere opp i kallstakken håndterer vi unntaket.

begin
  stuff = readData(socket)
  # .. prosesser ting
rescue RetryException => detail
  retry if detail.okToRetry
  raise
end

Catch og throw

Mens unntaksmekanismen til raise og rescue er perfekt for å avslutte kjøring når ting går galt, er det noen ganger fint å være i stand til å hoppe ut av en dypt nøstet konstruksjon under vanlig prosessering. Dette er hvor catch og throw er hendige.

catch (:done)  do
  while gets
    throw :done unless fields = split(/\t/)
    songList.add(Song.new(*fields))
  end
  songList.play
end

catch definerer en blokk som er merket med det gitte navnet (som kan være et Symbol eller en String). Denne blokken blir kjøres helt normalt til en throw viser seg.

Når Ruby finner en throw suser den tilbake oppover i kallstakken og leter etter en catch med et matchende symbol. Når den finner det, pakker Ruby opp kallstakken tilbake til det punktet og terminerer blokken. Hvis throw blir kalt med det valgfrie andre parameteren blir denne verdien returnert som verdien til catch. Så i det tidligere eksempelet, hvis inputen ikke inneholder korrekt formaterte linjer, vil throw hoppe til slutten på den korresponderende catch, og ikke bare terminere while-løkken, men også hoppe over spillingen av sanglisten.

Det følgende eksemplet bruker en throw til å terminere interaksjonen med bruker hvis ``!'' blir skrevet som respons på hvilket som helst prompt.

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:  ")   # ..   # process information end

Som dette eksemplet illustrerer er throw ikke nødt til å være innenfor det statiske skopet til catch.

( In progress translation to Norwegian by NorwayRUG. $Revision: 1.8 $ )
$Log: tut_exceptions.xml,v $
Revision 1.8  2003/08/31 11:58:58  kent
Fjerner masse KapitaliseringsMani i overskrifter.

Revision 1.7  2002/08/03 10:07:58  kent
Første kjappe gjennomgang ferdig.

Revision 1.6  2002/08/03 09:36:37  kent
Småfiks frem til "Play it again".


Forrige < Innhold ^
Neste >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide".
Translation to norwegian by Norway Ruby User Group.
Copyright for the english original authored by David Thomas and Andrew Hunt:
Copyright © 2001 Addison Wesley Longman, Inc.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).

(Please note that the license for the original has changed from the above. The above is the license of the original version that was used as a foundation for the translation efforts.)

Copyright for the norwegian translation:
Copyright © 2002 Norway Ruby User Group.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.