Tworzenie obiektów w połączeniu z blokiem "try finally"

0

Ostatnio przeglądałem dość sporo modułów ze źródeł biblioteki standardowej Free Pascala i biblioteki LCL; Natrafiłem tam na co najmniej dwa odmienne sposoby tworzenia obiektów i zabezpieczania kodu przed wyciekami pamięci, za pomocą bloków Try Finally;

Pierwszy sposób:

someObject := TSomeClass.Create();
try
  // użycie obiektu
finally
  someObject.Free();
end;

Drugi sposób, rzadziej spotykany:

someObject := nil;
try
  someObject := TSomeClass.Create();
  // użycie obiektu
finally
  someObject.Free();
end;

Różnica polega na tym, że w pierwszym przykładzie konstruktor nie jest zabezpieczony i nie wiadomo (przynajmniej ja nie wiem) co stanie się, jeśli konstruktor rzuci wyjątek; W drugim kodzie zmienna najpierw jest **Nil**owana, a konstruktor wywoływany jest już wewnątrz bloku Try Finally, dzięki czemu w razie rzucenia wyjątku przez konstruktor lub dlaszy kod po nim, metoda Free zostanie zawsze wywołana;

I tu moje pytanie - czy ktokolwiek korzysta z tego drugiego sposobu i czy taki zapis (drugi) ma jakiś sens?

Do tej pory wszędzie używałem sposobu z pierwszego przykładu (o ile instancja klasy miała być zwalniana w tym samym bloku kodu, np. w metodzie) i sprawdzał się dobrze; Jednak widząc inne sposoby nabieram wątpliwości co do wady korzystania z tej (pierwszej) konstrukcji; Oczywiście tylko w przypadku, gdy konstruktor wykonuje jakieś niepewne operacje, np. ładowanie pliku, gdzie klasa może rzucić wyjątek.

2

Ja jeśli już korzystam raczej z pierwszego sposobu. Nie pamiętam kto mi to pisał, bo coś już kiedyś o tym na forum dyskutowaliśmy. Ale dawno. Był to albo MisiekD albo @Azarien. ale pewności nie mam. Zdaje się, że tak jak istnieje zasada, żeby zwalniać kiedy zbędne wszystko co stworzyliśmy. Tak zdaje się, jest zasada, że konstruktor powinien być tak napisany aby nie powodował wyjątków. Jakby się zakładało, że nie powinien tego robić.

0

Zacznijmy od tego, że konstruktor nie powinien rzucać wyjątków. Jeśli robi coś, co może rzucić wyjątkiem, to należy to przenieść do metody.

0

A ja spojrzałem na kod VCL od Delphi 7 i klasę TResourceStream, która oczywiście przy tworzeniu rzuca wyjątkiem gdy nie ma danego zasobu. Tam w ogóle prawie nie ma nigdzie w module bloków try ... except. Natomiast jest wspomniane przeniesienie do metody Initialize, później jest w niej odwołanie do procedury Error, która rzuca wyjątkiem. Jest to wyjątek obsługiwany przez klasę do wyjątków bez słów kluczowych.

0

@olesio, @Azarien - w VCL/LCL istnieje trochę klas, które w konstruktorach mogą wyrzucić wyjątek;

Nie wiem jak Delphi, ale we Free Pascalu istnieje zabezpieczenie, które umożliwia zwolnienie obiektu, jeśli konstruktor rzuci wyjątkiem; Kiedyś to sprawdzałem zarówno pod debugerem (ale w wersji którą posiadam jest problem ze wznowieniem debugowania po złapaniu wyjątku - taki błąd debugera) jak i w trybie release obiekt zostaje zwolniony, a zmienna w dalszej części kodu przybiera wartość Nil; Czyli nawet jeśli konstruktor nie jest objęty blokiem Try Finally/Except to i tak obiekt zostaje zwolniony;

Dlatego też zastanawiam się nad sensem tej drugiej konstrukcji, bo choćby nie wiem co - obiekt i tak zostanie zwolniony; Konstruktor w przypadku rzucenia wyjątku sam zwalnia obiekt, którego ten konstruktor dotyczy; No i teraz nie wiem, czy ta dziwna konstrukcja użyta jest celowo (tylko po co?), czy została napisana wcześniej, kiedy takiego samozwalniającego obiekt mechanizmu jeszcze nie było.

0

Różnica polega na tym, że w pierwszym przykładzie konstruktor nie jest zabezpieczony i nie wiadomo (przynajmniej ja nie wiem) co stanie się, jeśli konstruktor rzuci wyjątek;

Stanie się to, że zostanie wywołany destruktor a potem pojawi się wyjątek.

Druga sprawa. A co jak destruktor rzuci wyjątek? DUPA się stanie.

Konstruktor i Destruktor nie powinny rzucać wyjątków...

Jeżeli ktoś jest jednak paranoikiem, to sposób drugi* jest jak najbardziej OK.

0

Stanie się to, że zostanie wywołany destruktor a potem pojawi się wyjątek.

To samo napisałem w poprzednim poście;

Druga sprawa. A co jak destruktor rzuci wyjątek? DUPA się stanie.

Obiekt nie zostanie zwolniony - proste;

Konstruktor i Destruktor nie powinny rzucać wyjątków...

Destruktor nie powinien i nie robi się w nich takich operacji, jednak konstruktor może i wiele klas tak robi przy błędnych danych wejściowych;

Jeżeli ktoś jest jednak paranoikiem, to sposób drugi* jest jak najbardziej OK.

Może i Ok bo działa, jednak wydaje się nieco nadmiarowy i zaciemniający kod;

Zadałem jedynie pytanie czy ktoś z takiej konstrukcji korzysta i czy ma ona większy sens, a Ty piszesz tak, jakbym się tym nie wiadomo jak przejmował; Jest mi to obojętne, dlatego że zawsze korzystam z pierwszego sposobu, ale nadmiar wiedzy nie szkodzi - stąd pytanie.

1

jeśli konstruktor rzuci wyjątek to obiekt nie zostanie utworzony więc wołanie na nim free wywali kolejny wyjątek. Sposób drugi jest uzasadniony tylko w konkretnych przypadkach - np. wspomniana wcześniej klasa TResourceStream czy TFileSTream. Tylko, że tutaj albo powinniśmy sprawdzić sami czy zasób (plik) istnieje i dopiero wtedy tworzyć instancję klasy. Co do drugiego przypadku to poprawną formą będzie

someObject := nil;
try
  someObject := TSomeClass.Create();
  // użycie obiektu
finally
  if someObject <> nil then
    someObject.Free();
end;

A co do pytania to mi się chyba nie zdarzyło użyć powyższej konstrukcji ani się z taką nie spotkałem

0

Co do postu powyżej to wklejam zawartość metody FREE i zachęcam do prostej analizy.

 procedure TObject.Free;
begin
// under ARC, this method isn't actually called since the compiler translates
// the call to be a mere nil assignment to the instance variable, which then calls _InstClear
{$IFNDEF AUTOREFCOUNT}
  if Self <> nil then
    Destroy;
{$ENDIF}
end;
1

Drugi sposób jeśli już powinien wyglądać tak:

someObject := nil;
try
  someObject := TSomeClass.Create();
  // użycie obiektu
finally
  someObject.Free();
end;

i generalnie nie zabezpiecza przed wyjątkiem w konstruktorze.
Wobec czego jest bez sensu.
Nawet jeśli mamy części składowe w TSomeClass które chcielibyśmy zwolnić w finally, to wskazanie w someObject będzie = nil (przypisanie się nie udało), w związku z czym finally nic nie robi.

Także w zasadzie tylko pierwszy sposób jest OK.

Co ciekawe - ktoś może zapytać dlaczego przy TResourceStream i pierwszej wersji nie ma wycieku:
If an exception is raised during the execution of a constructor that was invoked on a class reference, the Destroy destructor is automatically called to destroy the unfinished object.

http://docwiki.embarcadero.com/RADStudio/XE7/en/Methods#Constructors

0
abrakadaber napisał(a)

jeśli konstruktor rzuci wyjątek to obiekt nie zostanie utworzony więc wołanie na nim free wywali kolejny wyjątek.

Z moich testów wynika(ło), że obiekt zostanie utworzony, a metoda Free nie wyrzuci wyjątku; Dzieje się tak zapewne dlatego, że jeśli tworzę jakiś konstruktor, to pierwszą linijką w nim jest wywołanie bazowego konstruktora:

constructor TSomeClass.Create();
begin
  inherited Create();
  // dalsze instrukcje i te, które mogą rzucić wyjątek
end;

I zapewne właśnie wywołanie tego bazowego konstruktora tworzy fizycznie obiekt, a dopiero dalszy kod już po utworzeniu obiektu przerwie jego kod rzuceniem wyjątku;

Natomiast metoda Free to nakładka na goły destruktor, która sprawdza czy obiekt wskazuje na zaalokowaną pamięć i jeśli tak - wywoływana jest metoda Destroy; @danny podał kod metody Free zapewne z Delphi - w bibliotece standardowej FPC jest podobnie:

{****************************************************************************
                               TOBJECT
****************************************************************************}

constructor TObject.Create;
begin
end;

destructor TObject.Destroy;
begin
end;

procedure TObject.Free;
begin
  // the call via self avoids a warning
  if self<>nil then
    self.destroy;
end;

Z czego bazowe konstruktor i destruktor są już zarządzane przez internalsy FPC, a metoda Free jak widać to zwykły wrapper zabezpieczający;

vpiotr napisał(a)

Nawet jeśli mamy części składowe w TSomeClass które chcielibyśmy zwolnić w finally, to wskazanie w someObject będzie = nil (przypisanie się nie udało), w związku z czym finally nic nie robi.

No właśnie - w przypadku pojawienia się wyjątku sekcja Finally jest omijana, więc nie ma sensu; Sprawdziłem różne kody pod Delphi7 i Lazarusem i okazuje się, że w obu przypadkach konstruktor po rzuceniu wyjątku automatycznie wywołuje destruktor; Różnica jest taka, że pod FPC obiekt jest dodatkowo **Nil**owany;

vpiotr napisał(a)

Drugi sposób jeśli już powinien wyglądać tak:

{..}

i generalnie nie zabezpiecza przed wyjątkiem w konstruktorze.
Wobec czego jest bez sensu.

{..}

Także w zasadzie tylko pierwszy sposób jest OK.

Czyli jednak taki zapis jest bez większego sensu; I ja tak myślałem, jednak wolałem temat poddać dyskusji.

1

Postaw sobie breakpointa na free i zobaczysz, że obiekt jest tam nilem wiec zwalnianie go mija się z celem

2
furious programming napisał(a):
abrakadaber napisał(a)

jeśli konstruktor rzuci wyjątek to obiekt nie zostanie utworzony więc wołanie na nim free wywali kolejny wyjątek.

Z moich testów wynika(ło), że obiekt zostanie utworzony, a metoda Free nie wyrzuci wyjątku; Dzieje się tak zapewne dlatego, że jeśli tworzę jakiś konstruktor, to pierwszą linijką w nim jest wywołanie bazowego konstruktora:
...
I zapewne właśnie wywołanie tego bazowego konstruktora tworzy fizycznie obiekt, a dopiero dalszy kod już po utworzeniu obiektu przerwie jego kod rzuceniem wyjątku;

W momencie wyjątku w konstrutorze gdzieś w środku (przypadek 1):

  • obiekt jest już zaalokowany w pamięci
  • nie wszystkie operacje w konstruktorze zostały wykonane (pozostało zrobić to co za wyjątkiem, ale się nie wykona)
  • program wychodzi z konstruktora
  • wywołuje się automatycznie destruktor
  • wskaźnik do obiektu się nie przepisze do zmiennej - będzie ona miała wartość jak przed wywołaniem konstruktora

W C++ (i w Pascalu) konstruktor NIE tworzy fizycznie obiektu - tylko go inicjalizuje.

https://www.safaribooksonline.com/library/view/delphi-in-a/1565926595/re51.html

Nie wiem co chciałeś powiedzieć przez to:

furious programming napisał(a):

No właśnie - w przypadku pojawienia się wyjątku sekcja Finally jest omijana, więc nie ma sensu;

ale finally oczywiście w przypadku wyjątku jest wywoływane - tylko akurat tutaj (przypadek 1) Free nic nie zrobi.

0

Jednak źle robiłem, faktycznie zmienna wskazuje na Nil - musiałem walnąć się i sprawdzić nie zawartość zmiennej, a adres zmiennej, który zawsze jest różny od 0; Czyli jednak wszystko gra - mój błąd;

Nie wiem co chciałeś powiedzieć przez to:

furious programming napisał(a):

No właśnie - w przypadku pojawienia się wyjątku sekcja Finally jest omijana, więc nie ma sensu;

ale finally oczywiście w przypadku wyjątku jest wywoływane - tylko akurat tutaj (przypadek 1) Free nic nie zrobi.

Miałem na myśli pierwszy przypadek, w którym konstruktor jest przed blokiem Try Finally; W drugim przypadku sekcja Finally zostanie wykonana, ale jeśli przypisanie nie udało się to tamtejsza Free nic nie zrobi;
____Zastanawiam się jednak nad tymi słowami @vpiotr:

W C++ (i w Pascalu) konstruktor NIE tworzy fizycznie obiektu - tylko go inicjalizuje.

W takim razie co fizycznie alokuje pamięć dla obiektu? Wiadome, że konstruktor inicjalizuje już utworzony obiekt, ale co go tworzy? Konstruktor klasy TObject? Jego definicja jest pusta, więc musi być zarządzany wewnętrznie.

0
furious programming napisał(a):

W takim razie co fizycznie alokuje pamięć dla obiektu? Wiadome, że konstruktor inicjalizuje już utworzony obiekt, ale co go tworzy? Konstruktor klasy TObject? Jego definicja jest pusta, więc musi być zarządzany wewnętrznie.

Zobacz link z mojego poprzedniego posta.

1 użytkowników online, w tym zalogowanych: 0, gości: 1