Hlavní navigace

Squeak: návrat do budoucnosti (13)

4. 5. 2004
Doba čtení: 6 minut

Sdílet

Dnes se podíváme na to, jak Smalltalk pracuje s výjimkami. Rozhodně se nemá za co stydět. Přesto nebo právě proto, že výjimky nejsou součástí jeho syntaxe, jsou velmi silným a rychlým nástrojem pro tvorbu kvalitnějších programů.

Strukturované zpracování výjimek v dnešní době k moderním imperativním programovacím jazykům neodmyslitelně patří. Dokáže výrazně zjednodušit a zobecnit kód při zvýšení jeho bezpečnosti. Smalltalk samozřejmě nezůstává pozadu. Naopak kvůli dynamické typové kontrole tvoří výjimky jeho neodmyslitelnou součást a tomu odpovídají i možnosti, které Smalltalk programátorům při práci s nimi nabízí.

Podobně, jako je tomu u řídících struktur, i s výjimkami se pracuje pomocí zpráv zasílaných blokům, takže jejich zápis se v podstatě příliš neliší od toho, na co můžete být zvyklí třeba z C++. Na rozdíl např. od Javy nebo C# se o zpracování výjimek nemusí starat virtuální stroj. Jsou sice z části optimalizovány pomocí primitivních metod, nicméně by tomu tak být nemuselo, protože práce s procesy, kam výjimky také patří, je řešena na úrovni image Smalltalku a nikoliv jeho virtuálního stroje.

Implementace výjimek bývá většinou velmi komplikovaná a náročná. Smalltalk se však díky čistě objektovému návrhu s touto problematikou vypořádal velmi dobře. Jejich zpracování je např. oproti konkurenční platformě .Net minimálně o řád rychlejší.

Odchyt všech chyb

Zdaleka nejjednodušší a nejčastěji používanou konstrukcí je odchyt všech výjimek, které se v bloku kódu mohou vygenerovat. Používá se k tomu metoda ifError: třídy BlockContext. Zašleme ji ošetřovanému bloku a jako parametr jí předáme blok, který má být při výskytu chyby vykonán.

[
  "toto je blok, ve kterem je vygenerovana vyjimka"
  1 / 0.
] ifError: [ Transcript show: 'Chyba v programu' ]

V našem případě bylo provedeno dělení nulou. Blok byl násilně ukončen a byl proveden náhradní, který vypsal chybové hlášení do Transcirptu.

Protože jako celek se jedná o výraz, velmi často se používá tato konstrukce jako pravá strana přiřazení. Při bezchybném provedení je vrácen výsledek ošetřovaného bloku, tedy poslední výraz ze sekvence výrazů, které ho tvoří. V případě výskytu chyby je výsledkem hodnota dodaná náhradním blokem.

| var result |
var := 0.
result := [ 42 / var ] ifError: [ Float infinity ].

V tomto příkladu je při dělení nulou výsledkem nekonečno.

Náhradní blok může mít až dva parametry. Prvním je popis výjimky a druhým je příjemce výjimky.

[ 1 / 0 ] ifError: [:error :receiver |
  self notify: ('Pri praci s objektem ', receiver asString, ' se vyskytla chyba ', error).] 

Je vhodné poznamenat, že metoda notify třídy Objectvyvolávající okno s varováním rovněž generuje výjimku. Vyvolávání a zpracovávání výjimek při ošetřování jiných výjimek tedy není problém.

[
  [ 1/0 ] ifError: [ [2/0] ifError: [ 3/0 ] ]
] ifError: [:e :receiver | receiver ]

Výsledkem tohoto výrazu je hodnota 3.

Selektivní zpracování výjimek

Pro sofistikovanější odchyt výjimek se používá zpráva on:do:třídy BlockContext přijímající dva parametry. Prvním je množina výjimek a druhým je náhradní blok, který je vyhodnocen při výskytu některé výjimky z této množiny.

Množiny výjimek jsou instance třídy ExceptionSet obsahující třídy výjimek. Tvoří se stejně jako ostatní množiny, např.

ExceptionSet with: ZeroDivide with: MessageNotUnderstood. 

Elegantnějšího zápisu docílíme jejich vytvořením pomocí binární zprávy , (čárka).

ZeroDivide, MessageNotUnderstood, Error

Náhradní blok přijímá jako (nepovinný) parametr objekt popisující výjimku. Celý výraz zpracovávající výjimku pak vypadá např. takto:

[ 1 / 0 ] on: ZeroDivide do: [ Float infinity ].

[ 1 / 0 ] on: ZeroDivide do: [ :exception | self notify: exception description ].

[ 1 add: 2 ] on: MessageNotUnderstood do: [ "do nothing" ].

[ self notify: 'Upozorneni'. ] on: Notification, Warning do: [ "do nothing" ] 

Pokud je třeba odchytávat výjimku MessageNotUnder­stood pouze pro určitý selektor, můžete tak učinit pomocí zprávy onDNU:do:

| var |
var := 2.
[ 1 add: 2 ] onDNU: #add: do: [:e | e receiver + var ]. 

Výsledkem posledního příkladu je hodnota 3.

Více bloků pro zpracování výjimky nelze na rozdíl od C++ použít. Je možné tedy buď odchytávání výjimek zanořit, nebo rozlišovat výjimky pomocí metody handles:

[ 1 / 0 ] on: Exception do: [:e |
  (Warning, Notification handles: e)
    ifTrue: [ Transcript show: e messageText. e return ].
  (ZeroDivide handles: e)
    ifTrue: [ Transcript show: 'Dividing by zero'. e return ].
  e pass.
] 

Generování výjimek

Základní třídou pro výjimky je třída Exception. Z ní je odvozena celá řada tříd výjimek a je samozřejmě možné si vytvářet vlastní.

Nejčasněji používané odvozené třídy výjimek jsou např. Error,Warning, Notification, Halt apod. Výjimky se generují metodou signal (sloužící rovněž jako konstruktor).

Error signal.
Error new signal.
Error signal: 'Text chyby'.
Warning signal: 'Text varovani'.
Notification signal: 'Text upozorneni'.

Výjimky třídy Notification nejsou standardně v prostředí ošetřeny otevřením okna. Velmi často se používají metody třídyObject usnadňující generování těchto výjimek.

self error: 'Plánovač procesů nenalezen'.
self notify: 'Pozemšťané! Věnujte pozornost tomuto
  hlášení. Hovoří k vám Prostenik Vogon Jelc
  z Galaktického úřadu pro plánování hyperprostorové
  dopravy. Jak nepochybně víte, plány pro rozvoj
  okrajových oblastí Galaxie vyžadují vybudování
  nové expresní dálnice, která má vést vaší sluneční
  soustavou. Vaše planeta je bohužel jedna z těch,
  které jsou určené k demolici. Celá akce bude trvat
  necelé dvě pozemské minuty. Děkuji vám.'.
self notifyWithLabel: 'Image je na nic, poslouchej žízeň' 

Naleznete zde i další užitečné metody. Pokud jste zvyklí používat v C/C++ např. makro ASSERT (a měli byste), pak jistě přivítáte metodu assert:

| param |
param := -10.
self assert: param > 0

V případě, že parametr je false, vygeneruje se výjimka třídyAssertion­Failure, která standardně otevře debugger. Můžete si ji samozřejmě libovolně odchytnout a zpracovat jinak.

Za zmínku stojí ještě metoda confirm: otevírající dialogové okno s textem a vracející true nebo false dle volby uživatele (yes/no).

Smalltalk
  snapshot: (self confirm: 'Přejete si uložit image?')
  andQuit: true. 

Finalizační blok

Občas potřebujeme, aby k nějaké akci došlo, ať se děje, co se děje. Typicky například vyžadujeme zavření souboru nebo přístupu k databázi. Pokud by při práci se souborem došlo k chybě, nemusel by být korektně zavřen. Tuto situaci lze ošetřit metodou ensure: třídyBlockContext přijímající finalizační blok.

| file |
[
  file := (StandardFileStream readOnlyFileNamed: 'list.txt').
  file contents linesDo: [ :line |
    | items |
    items := line findTokens: ';'.
    #(IP login name surname description)
      with: items do: [:label :item |
        Transcript show: (label, ': ', item asString); cr.
      ].
  Transcript cr.
  ]
] ensure: [ file close.] 

Tento příklad načte obsah souboru list.txt, obsah každého řádku rozparseruje do kolekce items podle oddělujících středníků a vypíše do Transcriptu s patřičným popisem (kvůli demonstraci zprávyensure: jsem nepoužil volání metody contentsOfEnti­reFile).

Nezaměňujte tuto konstrukci s finalizací objektů před jejich odstraněním garbage collectorem.

Práce s výjimkou

S již vygenerovanou výjimkou lze dle potřeby pracovat. Pro její obnovování, opětovné generování apod. máte k dispozici celou kolekci užitečných metod.

resume

Některé výjimky umožňují návrat, což se zjišťuje pomocí zprávy isResumeable. K návratu slouží zpráva resume, která vrací jako výsledek hodnotu nil.

[
  1 add: 2.  "DNU"
  1 + 2.  "Provede se po návratu"
] on: Error do: [:ex | ex resume.]

resume:

Je obdobou předešlé zprávy. Liší se od ní v tom, že přijímá návratovou hodnotu.

[ | var |
  var := 1 / 0.  "ZeroDivide"
  var + 1.  "Provede se po návratu, = 3"
] on: Error do: [:ex | ex resume: 2 ]

retry:

Zkusí znovu zopakovat ošetřovaný blok.

| var |
var := 0.
[
  1 / var.
] on: Error do: [:ex | var := 2. ex retry ]

retryUsing:

Provede náhradní blok.

| var |
var := 0.
[
  1 / var.
] on: Error do: [:ex | ex retryUsing: [ var := 2. 1 / var ] ] 

pass

Propustí výjimku dále. Viz příklad k on:do:

return

Ukončení provádění náhradního bloku.

return:

Ukončení provádění náhradního bloku. Parametr je použit jako výsledek ošetřovaného bloku

| var |
var := [ 1 / 0 ] on: Exception do: [:e |
  (Warning, Notification handles: e)
    ifTrue: [ Transcript show: e messageText. e return ].
  (ZeroDivide handles: e)
    ifTrue: [ e return: Float infinity ].
  e pass.
] 

resignalAs:

Nahradí výjimkový signál.

[ self error: 'Chyba' ] on: Error do: [:ex |
  ex resignalAs: (Warning new messageText: 'Varování').
]. 

Tvorba vlastní výjimky

Při tvorbě vlastní výjimky se můžete inspirovat již existujícími třídami. Je vhodné přetížit instanční metodu defaultAction obsahující kód provedený, pokud není výjimka nijak odchycena.

Velkým lákadlem je možnost vytvářet si vlastní zprávy třídy BlockContext. Přidáme-li si do ní např. metodu

ict ve školství 24

dbSafely
  ^ self on: DBException do: [:e | DatabaseErrorDlg from: e ] 

nic nám nebude bránit v naší aplikaci napsat třeba konstrukci

[ database := DBConnection from: connectionString ] dbSafely 

Díky veliké flexibilitě Smalltalku jistě snadno přizpůsobíte zpracování výjimek vlastním potřebám a zvyklostem, pokud vám jejich standardní implementace nebude vyhovovat.