Posts tagged ‘finally’

Obsługa strumienia w Javie

2011-07-26 20:37

W językach kompilowanych mechanizmy wejścia-wyjścia opiera się niemal zawsze o koncepcję strumienia (stream), czyli abstrakcyjnego obiektu z którego możemy czytać dane i/lub je do niego zapisywać. Ponieważ strumień jest opakowaniem na jakiś zewnętrzny zasób – plik, połączenie sieciowe, itp. – należy generalnie dbać o jego poprawne i szybkie zamknięcie, gdy nie jest już potrzebny. Dotyczy to także zwłaszcza języków z zarządzaną pamięcią, gdzie osierocony obiekt strumienia może nie być posprzątany przez bardzo długi czas, zajmując zewnętrzne, niezarządzane zasoby systemowe.

Poprawny sposób postępowania z obiektem strumienia, który chciałem dzisiaj omówić, dotyczy konkretnie języka Java, gdyż tam cała sprawa jest co najmniej nietrywialna. Dzieje się tak z trzech powodów:

  • W Javie każdy wyjątek musi zostać albo złapany (catch), albo zadeklarowany jako opuszczający funkcję (throws). Jest to sprawdzane podczas kompilacji.
  • Metoda close, zamykająca strumień, deklaruje potencjalne wyrzucanie wyjątku IOException.
  • Java poniżej wersji 7 nie posiada odpowiednika konstrukcji with (obecnej np. w C# i Pythonie), która automatycznie posprzątałaby po obiekcie strumienia w momencie opuszczenia jej zasięgu.

Brak instrukcji with sprawia, iż do dyspozycji pozostaje nam wyłącznie try-catch lub try-finally. Naiwne zastosowanie któregoś z nich nie daje jednak pożądanych efektów:

  1. try {
  2.     InputStream is = new FileInputStream("file.txt");
  3.     // ...
  4. } finally {
  5.     is.close(); // ups!
  6. }

Takim efektem byłby na przykład fakt kompilowania się kodu :) W tej wersji jest to jednak niemożliwe (o ile funkcja nie deklaruje wyrzucania IOException), bowiem wyjątek ten może zostać rzucony przez metodę close… A przynajmniej taka jest teoria, którą kompilator niestety pedantycznie sprawdza.

W rzeczywistości ten kod ma przynajmniej jeszcze jeden błąd, którego nie wyeliminuje otoczenie wywołania close odpowiednim blokiem try-catch. Jego znalezienie pozostawiam aczkolwiek jako – ahem – ćwiczenie dla czytelnika ;) W zamian pokażę dla odmiany nieco lepszy sposób na obejście zaprezentowanych problemów.
Polega on na zastosowaniu dwóch zagnieżdżonych bloków try: jednego z catch do złapania IOException i drugiego z finally do zamknięcia strumienia. W całości prezentuje się to następująco:

  1. try {
  2.     InputStream is = new FileInputStream("file.txt");
  3.     try {
  4.         // (czytamy ze strumienia)
  5.     } finally {
  6.         is.close();
  7.     }
  8. } catch (IOException e) {
  9.     // ...
  10. }

Przy zastosowaniu takiej konstrukcji wszystkie miejsca, w których wyjątek I/O może wystąpić, są otoczone blokiem try-catch, więc kompilator nie będzie miał powodów do narzekań. Nadal też gwarantujemy, że strumień zostanie zawsze zamknięty, co z kolei zapewnia blok try-finally.

A że wygląda to wszystko cokolwiek nieestetycznie? Cóż… Java :)

Tags: , , , ,
Author: Xion, posted under Programming » 8 comments

Bolączki C++ #3 – RAII vs finally

2007-09-13 9:23

Wyjątki są sposobem na zasygnalizowanie nietypowych i niespodziewanych błędów, które poważnie zaburzają działanie programu. I właśnie to, że potencjalnie wyjątek może wystąpić w bardzo wielu miejscach w kodzie, rodzi pewne kłopoty. Problemami są chociażby zasoby: coś, co się pozyskuje, wykorzystuje, a następnie zwalnia, gdyż w przeciwnym razie doszłoby do wycieku. Typowym zasobem jest chociażby dynamicznie alokowana pamięć – jeżeli jej nie zwolnimy, nastąpi klasyczny wyciek, jako że C++ nie posiada garbage collectora, który mógłby się tym zająć za nas.

W C++ zalecanym rozwiązaniem tego problemu jest technika znana jako RAII (Resource Acquision Is Initialization – pozyskanie zasobu jest inicjalizacją). Korzysta ona z faktu, że w naszym ulubionym języku programowania możemy tworzyć obiekty lokalne z konstruktorami i destruktorami. Te drugie wywołają się zawsze przy opuszczaniu danego bloku kodu – niezależnie od tego, czy stało się z powodu wyjątku czy tez normalnego przebiegu programu. Pomysł polega więc na tym, by tworzyć obiekt specjalnie przygotowanej klasy w momencie pozyskania zasobu, zaś destruktor tego obiektu zajmie się już jego zwolnieniem, niezależnie od powodu.

  1. try
  2. {
  3.    // 'wskaźnik lokalny' - chroni przed wyciekiem pamięci
  4.    std::auto_ptr<CFoo> pFoo(new CFoo(...));i
  5.  
  6.    // strumień plikowy - automatycznie zamyka otwarty plik
  7.    std::fstream FS("file.txt", std::ios::out);
  8. }
  9. catch (...) { /* ... */ }

Dopóki korzystamy z pamięci albo z plików, wszystko jest w porządku; odpowiednie klasy (jak auto_ptr) posiada bowiem Biblioteka Standardowa. Gorzej jeśli chcemy skorzystać z innego rodzaju zasobów. Jeśli odpowiednia klasa realizująca technikę RAII nie istnieje, nie pozostaje nam nic innego, jak samemu ją sobie zapewnić (czytaj: napisać). I tak dla każdego rodzaju niestandardowych zasobów, które używamy. Po niedługim czasie można by z tych klas ułożyć własną “bibliotekę standardową” ;)

Alternatywą dla RAII jest dodanie trzeciego bloku (po try i catch) do konstrukcji łapiącej wyjątki. Jest on zwykle nazywany finally. Instrukcje zawarte w tym bloku są wykonywane zawsze po tych z bloku try – niezależnie od tego czy wyjątek wystąpił czy nie. Jest to więc bardzo dobre miejsce na wszelki kod zwalniający pozyskane wcześniej zasoby, np.:

  1. import java.io.*;
  2.  
  3. try
  4. {
  5.    FileReader fr = new FileReader("file.txt");
  6.  
  7.    // (czytanie pliku)
  8. }
  9. finally { fr.close(); }

Co ciekawe, posiadają go języki, które jeden z najważniejszych zasobów – pamięć – mają zarządzaną przez odśmiecacz, który praktycznie wyklucza możliwość powstania wycieków. Rzecz jednak w tym, że niektóre zasoby, jak chociażby otwarte pliku, nie mogą sobie czekać na to, aż odśmiecacz przypomni sobie o nich, gdyż wtedy byłyby blokowane stanowczo zbyt długo.

Czy C++ też potrzebuje instrukcji finally? Na pewno nie jest to bardzo paląca potrzeba, jako że technika RAII zapewnia komplet potrzebnej tutaj funkcjonalności. To drugie, alternatywne rozwiązanie ma jednak szereg zalet:

  • Brak konieczności opakowywania każdego wykorzystywanego zasobu w specjalną klasę.
  • Większa przejrzystość kodu, w którym zarówno operację pozyskania, jak i zwolnienia zasobu.
  • finally można też pożytecznie wykorzystać nawet wówczas, gdy w grę nie wchodzi możliwość pojawienia się wyjątku. Jeżeli na przykład w jakiejś skomplikowanej funkcji mamy wiele miejsc, w których może nastąpić jej zakończenie, a przy każdej okazji może być potrzeba wykonania jeszcze jakichś czynności końcowych. Wtedy moglibyśmy zamknąć całą treść funkcji w try, a owe czynności umieścić w sekcji finally. Trzeba by się było jednak liczyć z tym, że blok try nie jest darmowy i jego użyciu nakłada pewien narzut.

I przede wszystkim: RAII i finally nie wykluczają się nawzajem. Dlatego obecność tego drugiego mechanizmu w C++ na pewno by nam nie zaszkodziła :)

Tags: , ,
Author: Xion, posted under Programming » Comments Off on Bolączki C++ #3 – RAII vs finally
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.