Dlaczego zwalnianie obiektów za pomocą metody Free jest sugerowane?

1

Od dawna się nad tym zastanawiam i nadal nie rozumiem. Istotą metody Free jest to, że najpierw sprawdza czy referencja jest nilem i jeśli nie to wywoływany jest destruktor Destroy. Dzięki temu można bezpiecznie zwolnić dany obiekt kilka razy. Tyle tylko, że normalnie używa się Free, a nie FreeAndNil, więc takie zabezpieczenie o dupę rozbić, bo w typowym kodzie po prostu nie zadziała — drugie wywołanie Free spowoduje rzucenie wyjątku AV/SIGSEGV.

Według mnie — kolejne zwolnienie tego samego obiektu powinno spowodować wyrzucenie wyjątku o naruszeniu pamięci, bo to nie jest normalne, że się kilka razy zwalnia ten sam blok pamięci. Jeśli tak jest, to — znów, według mnie — programista popełnił błąd i napisał nieprawidłowy kod, który powinien naprawić usuwając nadmiarowe instrukcje zwalniające.

A tu w dokumentacji taki kwiatek:

It is bad programming practice to call Destroy directly. It is better to call the Free method, because that one will check first if Self is different from Nil.

Nie dość, że sugerują Free, to wołanie Destroy nazywają złą praktyką. :|

Czyli podsumowując, kod projektu powinien działać tak samo prawidłowo nawet jeśli wymieni się w nim wszystkie Free na Destroy. Kiedyś sprawdziłem to w projekcie Richtris i niestety nie zadziałało — poleciał wyjątek. Problem w tym, że w nim używam komponentów wizualnych i bug jeden wie co z nimi robi klasa formularza. Natomiast teraz zrobiłem to samo w Fairtrisie i wszystko działa prawidłowo — nie ma żadnych wyjątków, a więc każdy utworzony obiekt jest zwalniany tylko raz (i tak być powinno).

Może mi ktoś powiedzieć dlaczego Free jest promowany i dlaczego wielokrotne zwalnianie danego obiektu nie jest powszechnie uznawane za błąd w projekcie? O czymś nie wiem, czy po prostu jest to przyzwolenie na delikatne niechlujstwo, aby nieco łatwiej było pisać kod zarządzający obiektami?

1

dlaczego wielokrotne zwalnianie danego obiektu nie jest powszechnie uznawane za błąd

A Ty nigdy nie wróciłeś się do domu bo nie pamiętałeś, czy zamknąłeś drzwi? :P Wydaje mi się, że zwolnienie czegoś, czego nie ma, nie powinno być traktowane jako błąd.

Przykładowo - masz jakąś tablicę obiektów. Na bieżąco obiekty w poszczególnych wierszach tworzysz i kasujesz. Potem, po zakończeniu działania chcesz posprzątać. I, zamiast przeglądać wszystkie wiersze tabeli, ewentualnie usuwać je w inny sposób, to lecisz i wszędzie z automatu dajesz Free(). Oczywiście - przykład trochę siłowy, ale chodzi o zasadę.

O wiele groźniejsze są odwrotne sytuacje - czyli albo odwołanie się do czegoś, czego nie ma/co nie zostało jeszcze stworzone, albo pozostawienie śmieci w pamięci. Pierwsza opcja wywali zonka, za to druga może spowodować wycieki pamięci. I o ile przy odpaleniu apki na chwilę nic się nie stanie (a jakby się tak działo, to znaczy że wyciek mamy naprawdę konkretny), ale czasem się apkę odpala na dłużej i wtedy taki brak zwolnienia pamięci może być problemem.

Ale zwolnienie czegoś, czego nie ma? To jakbyś wysłał kumpla z poleceniem "idź do mojego biurka, zobacz czy w drugiej szufladzie są dokumenty i jeśli tak, to daj je do niszczarki". Kumpel idzie, otwiera szufladę i krzyczy "niczego tu nie ma", na co Ty ze spokojem "OK, dzięki za info. W takim razie zapisuję sobie w notatniku, że druga szuflada jest pusta". Nic złego się nie stało. Ewentualnie, jakbyś był pewien, że dokumenty tam są, to wtedy możesz zacząć się wkurzać. Ale wtedy mamy inną sytuacje, o której pisałem w poprzednim akapicie - czyli odwołanie się do czegoś, co nie zostało jeszcze stworzone, albo co zostało za wcześnie zniszczone. OK, można takie podejście/przyzwolenie na zwalnianie pustych obiektów nazwać przyzwoleniem na niechlujstwo, ale wydaje mi się, że konsekwencji tutaj (poza Twoim niesmakiem) nie ma żadnych - więc nie widzę w czym problem :P

1
cerrato napisał(a):

Przykładowo - masz jakąś tablicę obiektów. Na bieżąco obiekty w poszczególnych wierszach tworzysz i kasujesz. Potem, po zakończeniu działania chcesz posprzątać. I, zamiast przeglądać wszystkie wiersze tabeli, ewentualnie usuwać je w inny sposób, to lecisz i wszędzie z automatu dajesz Free(). Oczywiście - przykład trochę siłowy, ale chodzi o zasadę.

No właśnie o tym mowa. Normalnie wszędzie daje się Free, bo tak się utarło i wszyscy to promują, jednak Free nie niluje referencji (zmiennej), więc jeśli wywoła się drugi raz Free na już zwolnionym obiektcie, to dostaniemy wyjątek SIGSEGV. Więc żadna różnica czy wowołamy go więcej niż raz czy nie wywołamy go w ogóle, albo poleci wyjątek, albo skończymy z wyciekiem pamięci.

Skoro Free ma być bezpieczne, to musi również ustawiać zmienną na nil. Ona sprawdza Self i na tej podstawie określa czy obiekt jest zwolniony czy nie. Ale nie przypisuje nil do Self, więc kolejne wywołanie metody będzie oznaczało kolejną próbę zwolnienia pamięci, a więc poleci wyjątek. Aby tego uniknąć wymyślono FreeAndNil, która jest zwykłą procedurą — może wywołać destruktor a także ustawić zmienną na nil, bo jest ona przekazywana przez referencję.

I tu przechodzimy do meritum. Wymyślono Free aby zapobiec wyjątkom wielokrotnego zwolnienia, ale tym wyjątkom nie zapobiega, bo nie niluje referencji (uj wie czemu). Zupełnie błędnie twierdzi się, że Free jest bezpieczne, podczas gdy w praktyce niczym się nie różni od Destroy, którego jawne używanie uznaje się za złą praktykę. Jeszcze takiego bullshitu w Pascalu nie wiedziałem. :D


IMO to właśnie jawne używanie Destroy jest dobrą praktyką, bo to jedyna możliwość określenia czy poprawnie zarządzamy pamięcią czy nie. Zgubimy gdzieś destruktor to dostaniemy wyciek pamięci (łatwe do namierzenia), wywołamy drugi raz to dostaniemy SIGSEGV (IDE nam wskaże gdzie dokładnie wywołano destruktor drugi raz). Poza tym używanie Destroy jest intuicyjne, bo to właśnie destruktor nadpisuje się w klasach, a nie metodę Free.

Free to taki wzorzec-wydmuszka.

2

TL;DR
W Delphi masz NIE skierowane grafy komponentów i zdarzenia. Bywa ze nie wiesz dokładnie skąd była zainicjowana akcja (albo nie chcesz tego badać latami) dlatego:

  • używasz FreeAndNil żeby zwolnic i zapewnić ze powtórne zwolnienie się nie wywali
  • możesz odpalić metodę na obiekcie wskazanym przez nil i dopóki nie dobiera się ona do atrybutów czy metod to się nie wywali
  • i dlatego działa Free - na nil to no-op

No i jeszcze jedno - zastosowanie Free, FreeAndNil i Destroy jest trochę inne:

  • FreeAndNil - w zasadzie tylko do pól komponentów
  • Free - do wszystkiego innego (np. zmienne lokalne)
  • Destroy - w zasadzie nie ma potrzeby używania
1

A to nie będzie jedno z nieszczęść przeskoku proceduralny -> obiektowy pascal ?

O ile bardzo szanuję (historycznie) proceduralny pascal, w czasie powstania o wiele lepszy język niż C (tak, to ta sama epoka), to Object Pascal jest jakimś koszmarnym koszmarem (stargetowany pod jedną jedyną implementacją której celem było klikanie).
edit: to jeszcze lokomotywą był Niklaus Wirth, czy już personalnie kto inny?

Ekosystemy Javy i C# mają (fakt, czego nie mają: Free *) , ale mają bardzo jasno udokumentowane odpowiedniki Destroy . Da się udokumentować i wychować społeczność? Da się.

*) taki stary framework GUI z kręgu Eclipse p.n. SWT ma obowiązek zwalniania, ale tam chodzi o natywne obiekty GUI. Wiec nigdy nie mów nigdy.

ps. wydaje się, że (co dotyczy obiektów trzymających zewnętrzne zasoby np pliki) ekosystem C# nieco lepiej wychowuje adeptów co do zamykania, choć Java dość wcześnie podkradła (nazwijmy dyplomatycznie backport) idę try with resources, na świeżym kodzie maja to równoważne.

@furious programming:

Zgodzę się. Nienullowana referencja / wskażnik wymagała by kosztownych zabezpieczeń, prowadzących do tego że tzreba "jakoś" jednak utrzymywać przy życiu przynajmniej częściową informację o zwolnionych blokach. Horror.

0

stargetowany pod jedną jedyną implementacją której celem było klikanie

Możesz wyjaśnić, co masz na myśli? W sensie - za bardzo nie rozumiem tego zarzutu/stwierdzenia.

0
cerrato napisał(a):

stargetowany pod jedną jedyną implementacją której celem było klikanie

Możesz wyjaśnić, co masz na myśli? W sensie - za bardzo nie rozumiem tego zarzutu/stwierdzenia.

Bardziej targetem było stworzenie klikalnych komponentów, niż eleganckiego, przemyślanego języka obiektowego.
Wyszło jak nieheblowana deska.

Coś a'la javowski Bean - ideą było osadzenie tego w IDE, a na dziesiątki lat został może nie koszmar, ale o wiele poniżej możliwości jak to można było umieścić w ekosystemie.

0

No ale przecież w obiektowych Pascalach możesz korzystać z całego OOP całkowicie w oderwaniu od VCL czy innych podobnych tworów. Możesz mieć nawet apkę obiektową konsolową, która nie ma nic wspólnego z tymi klikalnymi komponentami.

0
cerrato napisał(a):

No ale przecież w obiektowych Pascalach możesz korzystać z całego OOP całkowicie w oderwaniu od VCL czy innych podobnych tworów. Możesz mieć nawet apkę obiektową konsolową, która nie ma nic wspólnego z tymi klikalnymi komponentami.

podobnie jak z Beanów

Do Object Pascala mam najbardziej to, że (sądząc po efektach) celem był jak najmniej patchowac "proceduralny" kompilator (tuż przed deadline ???).
Po drugie, na pewno jakoś potrzebne, ale koszmarne relacje pomiędzy częścią publiczą a prywatną modułu ("uses" vs "implements" )

Stąd moje pytanie, czy Wirth z wysoką elegancja swoich rozwiązań jeszcze przy tym był.

1

@furious programming: Jak dla mnie Free jest ok. Gdyby Free miało wywalać błąd w tych przypadkach, to równie dobrze Clear powinno walić błędem, jeśli np. lista jest pusta;-)
Programując w C# brakuje mi Free, czasem na siłę sprawdzam czy można użyć Dispose().

1

Zwracam uwagę ze obiektowych Pascali jest kilka, m.in.:

  • Turbo Pascal (Turbo Vision)
  • Object Pascal (Delphi)
  • Free Pascal

Niklaus Wirth - watpie czy mial cos z tym wspolnego.
On raczej nie wyszedl poza ramy swoich jezykow:

  • Pascal
  • Modula
  • Oberon

Pascal ten podstawowy jest wbrew pozorom bardzo różny od Object Pascala. Ma wiele ograniczeń i w zasadzie nie nadaje sie do pracy (jest to jezyk typowo edukacyjny). W Object Pascalu robi sie normalne aplikacje, w tym toole jak Double Commander czy aplikacje bankowe.

Narzekanie na uses/implements mozna tylko rozpatrywac jako forme trollingu jako ze jest to jedna z mocniejszych stron Pascala w porownaniu do np. C/C++.

1

No i @ZrobieDobrze zrobił off-top. ;)

Nie chodzi mi tutaj o to jak jest zaprojektowana obiektówka w języku — OOP we Free Pascalu jest w porządku. Chodzi mi o to, że sugeruje się używanie metody Free ze względów bezpieczeństwa, pomimo tego, że ta metoda bezpieczeństwa żadnego nie daje. A używanie Destroy, pomimo bycia właściwym i jedynym destruktorem, uznawane jest za złą praktykę, choć jednoznacznie pozwala właściwie pisać kod (raz alokować, raz dealokować).

Kristof napisał(a):

@furious programming: Jak dla mnie Free jest ok. Gdyby Free miało wywalać błąd w tych przypadkach […]

No tyle tylko, że właśnie wywala błąd — nie da rady bezkarnie dwa razy wywołać Free na obiekcie. Przykład:

uses
  Classes;
var
  List: TStringList;
begin
  List := TStringList.Create();
  List.Free();
  List.Free();
end.

Przy drugim Free leci wyjatek:

screenshot-20220406164919.png

No więc po co mi używać Free, skoro do rzucenia wyjątku i tak dojdzie? Jeśli w kodzie projektu taki wyjątek się pojawia, to jest to jednoznaczny sygnał świadczący o tym, że spartaczyłem zarządzanie pamięcią i muszę doprowadzić kod do takiego stanu, aby tylko raz zwalniać pamięć (tak jak to wygląda w C/C++).

Normalnie w projektach wykorzystywałem do tej pory Free i nigdy nie miałem problemów z zarządzaniem pamięcią — w końcu nie trudno jest robić jedną alokację i jedną dealokację, to prosta zasada, do ogarnięcia przez każdego. Używałem Free (i nigdy FreeAndNil), choć nie dawało mi niczego więcej ponad Destroy.

Teraz piszę API swojej gry i wszędzie jawnie używam Destroy, bo to żadna zła praktyka. Przy czym tutaj mam logikę w zewnętrznych procedurach, więc destrukcja obiektu zawsze oznacza wywołanie Destroy oraz nilowanie zmiennej, tak aby kolejna próba zwolnienia rzuciła wyjątek — dzięki temu będę mógł poprawnie napisać kod zarządzania pamięcią.

0

Różnica jest taka, że jakbyś po pierwszym free dał coś w stylu List:= nil; to wtedy nie ma errora. A destroy się wywali i tak czy siak. Jedyn WTF które widzę to sugerowanie używania free zamiast freeAndNil. Ale poza tym nie wiem za bardzo w czym problem - masz free, które sprawdza czy kasowane coś rzeczywiście istnieje - jest to raczej dobre zabezpieczenie. Jakby samo nilowało, to by się niczym nie różniło od freeAndNil.

0

No tylko po co miałbym drugi raz wołać Free na już zwolnionym obiekcie? Taka próba jasno pokazuje, że nie ogarniam własnego kodu i chcę robić rzeczy, które łamią podstawowe prawa zarządzania pamięcią. Po to mam Assigned czy zwykłe porównanie do nil, aby sobie sprawdzić zmienną i dopiero potem wywołać Destroy.

0

No a po co masz kontrolowanie, czy nie odwołujesz się do indeksu tablicy poza zakresem? Jeśli chcesz skorzystać z 180 elementu 40-elementowej tablicy, to znaczy, że nie ogarniasz swojego kodu. No i po co mamy typowane zmienne? Skoro chcesz podstawić liczbę do ciągu, to znaczy, że nie ogarniasz... i tak dalej :P

Jest to po prostu jedno z zabezpieczeń przed wywaleniem aplikacji w kosmos. Ty tego nie potrzebujesz - to super, nie musisz korzystać. Ale czy dzieje Ci się z tego powodu jakaś krzywda, że komuś takie sprawdzenie robione przez free może pomóc?

1
cerrato napisał(a):

No a po co masz kontrolowanie, czy nie odwołujesz się do indeksu tablicy poza zakresem?

Po to aby naprawić kod i poprawnie zapisać odwoływanie się do jej elementów. Ta kontrola jest włączona domyślnie w trybie debug i nie oznacza ukrycia problemu, a powoduje wyrzucenie wyjątku ERangeError. To pozwala mi spojrzeć na kod, znaleźć błąd i go wyeliminować. I tego samego oczekuję od zwalniania obiektów — żądam wyjątku podczas kolejnej próby zwolnienia, abym był świadom tego, że skopałem kod.

Jest to po prostu jedno z zabezpieczeń przed wywaleniem aplikacji w kosmos.

Jeśli robi się rzeczy z d**y to program ma się wykrzaczyć, zamiast ukrywać problem przed koderem.

Ale czy dzieje Ci się z tego powodu jakaś krzywda, że komuś takie sprawdzenie robione przez free może pomóc?

Dzieje mi się krzywda, bo mnie od zawsze okłamywano i nadal okłamuje się mnie pisząc bzdury w dokumentacji. Wmawia się nam, że Free jest bezpieczne, choć nie jest i w większości istniejących projektów wywali się tak samo jak Destroy. No i wmawia się nam, że używanie Destroy jest złą praktyką, a tu wychodzi na to, że jest jedyną słuszną. ;)


A jeśli o te tablice i indeksy chodzi, to w kodzie swojego API mam np. procedurkę usuwającą element z listy:

procedure Delete<T>(AList: TList<T>; AIndex: TGameSInt32);
begin
  AList.Count -= 1;

  if AIndex < AList.Count then
    AList.Items[AIndex] := AList.Items[AList.Count];
end;

Nie ma tu żadnej kontroli indeksów — podam indeks spoza zakresu i dostanę wyjątek. Musi polecieć wyjątek, bo jeśli podałem nieprawidłowy indeks to muszę o tym się dowiedzieć i naprawić kod wywołujący tę procedurę. I tak będzie ze wszystkim, za każdym razem gdy zrobię bubel, mam być o tym poinformowany.

1

Padł postulat:
kumaty programista wie, kiedy zwalnia i nie ma problemu.

Ja przychylam się, że jest problem. Nie jest trudno wymyślić nieco większy projekt, że dobry programista będzie miał niewiedzę "zwolniłem czy nie"
.

1

Free jest bezpieczniejsze od Destroy gdyż bo ponieważ:

  • jeśli odpaliłeś wcześniej FreeAndNil to Free nic nie robi
  • jeśli odpaliłeś wcześniej samo Free lub Destroy to właśnie znaczy że nie kontrolujesz zmiennych lokalnych (o ile stosujesz FreeAndNil do atrybutów)
  • Destroy nie odróżnia nil od już zwolnionego obiektu, więc jak się wywali to możesz mieć problem z interpretacją
  • FreeAndNil może też służyć jako narzędzie strategii "fail fast" - po wywołaniu FreeAndNil każde następne wywołanie metod czytających atrybuty obiektu się wywali, po samym Free/Destroy niekoniecznie (o ile pamięć nie jest natychmiastowo czyszczona w pamięci może pozostać nietknięta zawartość obiektu)
0
ZrobieDobrze napisał(a):

Nie jest trudno wymyślić nieco większy projekt, że dobry programista będzie miał niewiedzę "zwolniłem czy nie".

Jesli programista używający języka bez GC (a takim właśnie są Pascale) ma problem z niewiedza na temat tego czy zwolnił czy nie, to w tym momencie akurat jego najmniejszym problemem jest wybór pomiędzy Free a Destroy, bo nie kontroluje zarządzania pamięcią i łata problemy używając metody, która zwalnia go z myślenia. To nie jest logiczne podejście.


vpiotr napisał(a):

Free jest bezpieczniejsze od Destroy gdyż bo ponieważ: […]

No nie przekonałeś mnie, bo w dalszym ciągu pozostaje problem z tym co się robi z referencjami.

Jeśli się zmiennej używa po wywołaniu destruktora (nie ma znaczenia jaką metodą), nieważne czy próbuje się z niej coś czytać czy ponownie zwolnić, to jest to błąd ze strony programisty. Wskaźnika, który przestał wskazywać na zaalokowaną pamięć się po prostu nie używa. A przecież referencje są niczym innym jak zwykłymi wskaźnikami.

1

Na tym polega caly ambaras zeby wykrywac jak najszybciej bledne odwolanie do zwolnionego obiektu. Najlepiej w czasie developmentu.
W C++ masz do tego odpowiednie narzedzia czasu wykonania wkompilowywane w exe, w Object Pascalu masz bieda-rozwiazanie FreeAndNil.

1
vpiotr napisał(a):

Na tym polega caly ambaras zeby wykrywac jak najszybciej bledne odwolanie do zwolnionego obiektu. Najlepiej w czasie developmentu.

No tyle że jak mam to wywołanie namierzyć, skoro Free nie będzie mi go sygnalizowało? Destroy wywali wyjątek i dowiem się od razu kiedy i w jakich okolicznościach zwolniono drugi raz — a w przypadku gdy sprawdzam runtime pod debuggerem, wykonanie zostanie przerwane i IDE pokaże mi w której linijce wystąpił problem, więc niczego nie będę musiał szukać. W razie czego są jeszcze testy.

vpiotr napisał(a):

W C++ masz do tego odpowiednie narzedzia czasu wykonania wkompilowywane w exe, w Object Pascalu masz bieda-rozwiazanie FreeAndNil.

W FPC mamy moduł HeapTrc, który robi dokładnie to samo — wypluwa do konsoli (lub w formie okienek błędów, jeśli apka nie używa konsoli) wszystkie informacje na temat wycieków pamięci. Moduł ten jest automatycznie używany w buildzie debug, ale można go włączyć w opcjach projektu, dla dowolnego projektu i dowolnego buildu. Można go też normalnie dodać do uses, bo jest w bibliotece standardowej. Zawsze go używam, aby wiedzieć czy coś zmaściłem czy nie.


Szybki test, analogiczny do tego spod linku:

uses
  HeapTrc;
const
  HELLO_WORLD = 'Hello World!'#0;
var
  Buffer: Pointer;
begin
  Buffer := AllocMem(20);
  Move(HELLO_WORLD[1], Buffer^, Length(HELLO_WORLD));

  WriteLn(PChar(Buffer));
  FreeMem(Buffer);
end.

Wyjście:

C:\Users\furious programming\Desktop\test>htrc
Hello World!
Heap dump by heaptrc unit of C:\Users\furious programming\Desktop\test\htrc.exe
1 memory blocks allocated : 20/24
1 memory blocks freed     : 20/24
0 unfreed memory blocks : 0
True heap size : 98304 (160 used in System startup)
True free heap : 98144

I bubel — wyciek pamięci:

uses
  HeapTrc;
const
  HELLO_WORLD = 'Hello World!'#0;
var
  Buffer: Pointer;
begin
  Buffer := AllocMem(20);
  Move(HELLO_WORLD[1], Buffer^, Length(HELLO_WORLD));

  WriteLn(PChar(Buffer));
end.

Wyjście:

C:\Users\furious programming\Desktop\test>htrc
Hello World!
Heap dump by heaptrc unit of C:\Users\furious programming\Desktop\test\htrc.exe
1 memory blocks allocated : 20/24
0 memory blocks freed     : 0/0
1 unfreed memory blocks : 20
True heap size : 98304 (160 used in System startup)
True free heap : 97888
Should be : 97928
Call trace for block $00000000000B5D20 size 20
  $000000010000F7AB
  $00000001000055EB
  $000000010000161C  main,  line 8 of htrc.lpr
  $0000000100001686
  $000000010000A540
  $00000001000015F0
  $00007FFB5DE87034
  $00007FFB5E782651
  $6577415C736E6F69
  $53206D75696D6F73
  $362E362E315C4B44
  $6E6F6D6D6F43005C
  $466D6172676F7250
  $5C3A433D73656C69
  $206D6172676F7250
  $6F435C73656C6946

Jest informacja w którym module i w której linijce jest alokacja, której wyciek dotyczy — niczego szukać nie muszę. Pozostaje tylko znaleźć odpowiednie miejsce i dopisać kod dealokacji zasobów, które ciekną.

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