PORADY

Porady > Programowanie > Delphi > Pułapki kodu Delphi

Pułapki kodu Delphi

Artykuł ten poświęcony jest nieoczekiwanym zachowaniom zarówno samego kodu wykonywalnego, jak i debuggera w Delphi w wersji do Delphi 10 Seattle (choć możliwe, że późniejsze wciąż będą działały tak samo). Przedstawię sytuacje, w których teoretycznie poprawny i kompilujący się bez błędów kod działa w sposób nieintuicyjny i niezamierzony, a poszukiwanie przyczyn nieprawidłowości w działaniu może być uciążliwe i trudne.

Na chwilę obecną szczęśliwie jest to krótki tekst, lecz niewykluczone, że w przyszłości rozbuduje się o nowe pułapki, z jakimi uda mi się spotkać i opisać.

Spis treści

Reprezentacja danych w debugerze

Zapewne każdy, kto poddawał swój pod debugowaniu, zetknął się z nieoczekiwaną reprezentacją danych przez debuger, co z kolei mogło powodować szukanie błędów tam, gdzie ich nie ma. Dość powiedzieć, że systemowe narzędzie nie obsługuje choćby danych spod klauzuli with. Pół biedy, gdy żadna dana nam się nie pokaże. Ale jeśli nazwa pola pokrywa się z nazwą pola klasy, w której zastosowano zapis, to prezentowana dana wcale nie będzie pochodzić z tego miejsca, z którego się spodziewamy.

Dane pokazywane przez debuger Delphi

Jednak kwestia związana z prostymi zmiennymi jest jeszcze do ogarnięcia. Jednak debugger wyposażony jest także w tzw. wizualizery wielu standardowych klas, np. TStringList. Jednak, wbrew intuicji, nie stosuje on tych samych parametrów, jak klasa, której dane chcemy podejrzeć. To może być źródłem problemów, gdy w taki sposób próbujemy sprawdzić zawartość zmiennych. Jednak bez obaw - kod działa poprawnie!

Dane pokazywane przez debuger Delphi

Niekonsekwencja(?) funkcji inline

Funkcje inline w Delphi są świetnym narzędziem do skracania kodu bez szkody w wydajności działania. Są bardziej jak makra, gdzie wywołanie takiej funkcji podczas kompilacji zostanie zastąpione jej ciałem. Więcej na ich temat można znaleźć w poradzie Optymalny, wysokowydajny kod w Delphi.

Jedną z bardziej popularnych jest na pewno funkcja IfThen z modułów System.Math (dla liczb) i System.StrUtils (dla ciagów tekstowych). Przyjrzyjmy się przykładowej: function IfThen(AValue: Boolean; const ATrue: Integer; const AFalse: Integer = 0): Integer; overload; inline; begin if AValue then Result := ATrue else Result := AFalse; end;

Napiszmy teraz prosty program korzystający z takiej funkcji: var o: TControl; r: Integer; begin o := nil; r := IfThen(o = nil, 0, 1); writeln(r); end; Gdyby funkcja nie była typu inline (lub stosowanie tej właściwości zostało wyłączone w opcjach kompilatora) skompilowany kod w miejscu jej użycia wyglądałby następująco: r := IfThen(o = nil, 0, 1); cmp dword ptr [ebp-$04],$00 setz al mov ecx,$00000001 xor edx,edx call IfThen mov [ebp-$08],eax Widać tu inicjację rejestrów w celu przekazania do wywołania funkcji IfThen oraz jej wywołanie (call).

Jednakże dzięki temu, że funkcja jest zdefiniowana jako inline, wówczas generowany kod jest następujący: r := IfThen(o = nil, 0, 1); cmp dword ptr [ebp-$04],$00 jnz +$09 xor eax,eax mov [ebp-$0c],eax jmp +$09 mov [ebp-$0c],$00000001 mov eax,[ebp-$0c] mov [ebp-$08],eax Nie ma tutaj żadnego wywoływania funkcji, a kod jest praktycznie identyczny, jak gdyby zapisać go w następujący sposób: if (o = nil) then r := 0 else r := 1; Prawda, że pierwszy zapis jest wygodniejszy, czytelniejszy i znacznie skraca kod źródłowy? A jednocześnie nic nie straciliśmy na wydajności (nadmieniam, że wywołania funkcji są jednymi z bardziej czasochłonnych operacji, gdyż wiąże się to z kilkoma poleceniami więcej w ramach wejścia i wyjścia funkcji; więcej na ten temat można znaleźć w przytoczonej już wcześniej poradzie).

Gdzie zatem czai się niebezpieczeństwo? Zmieńmy tylko zwracaną wartość tak, aby w przypadku istnienia obiektu, była zwracana wartość pola Tag, a jeśli obiekt nie jest utworzony - 0: r := IfThen(o = nil, 0, o.Tag); Zgodnie z logiką i rozwinięciem funkcji inline, kod jest poprawny. Jednak, gdy podejrzymy skompilowaną wersję, zobaczymy coś takiego: r := IfThen(o = nil, 0, o.Tag); mov eax,[ebp-$04] mov eax,[eax+$0c] mov [ebp-$0c],eax cmp dword ptr [ebp-$04],$00 jnz $005c6cdd xor eax,eax mov [ebp-$10],eax jmp $005c6ce3 mov eax,[ebp-$0c] mov [ebp-$10],eax mov eax,[ebp-$10] mov [ebp-$08],eax Bardziej doświadczeni dostrzegą tu jeden istotny szczegół: jeszcze przed wykonaniem porównania następuje odwołanie do pola Tag, a przecież obiekt nie istnieje. I tak, taki kod skończy się błędem dostępu! Nie jest więc w żaden sposób równoważny kodowi: if (o = nil) then r := 0 else r := o.Tag;

Niestety, w takim przypadku nie możemy skorzystać z dobrodziejstw funkcji inline. Skąd takie zachowanie? Choć wydawało się, że funkcje inline są czymś podobnym do makr, gdzie w miejsce ich wywołania kompilator podstawia kod, wcale tak nie jest! Związane jest to z faktem, że w przypadku zwykłych funkcji wszystkie parametry muszą być wyliczone wcześniej przed przekazaniem. A co, gdyby Tag był właściwością, w której getter nie byłby prostym pobraniem wartości pola, lecz funkcją dodatkowo zmieniającą cokolwiek w klasie? Stąd właśnie wczesne wywołanie gettera. Kompilator broni się przed różnym działaniem w zależności od ustawień. I bardzo dobrze, że tak się dzieje, bo dzięki temu program działa niezmiennie niezależnie od parametrów. Ale trzeba pamiętać, że funkcje inline nie są do końca makrami, lecz hybrydą funkcji i makra, a największą wydajność (a w tym przypadku i poprawność działania) zapewni tylko własne rozwinięcie kodu bez korzystania z dobrodziejstw modyfikatora inline. Warto przyjąć, że funkcje inline sprawdzą się tak samo, jak ich rozwinięcie, jeśli przekazywać będziemy im stałe lub proste zmienne. W przypadku jakichkolwiek wartości, które muszą być pobrane/wyliczone (czyli własności klas, ich metody, funkcje), a nie zostaną w każdym przypadku użyte wewnątrz funkcji (jak jest to w przypadku IfThen) lepiej zastanowić się, czy warto ją zastosować.

Procedury anonimowe i wyciek pamięci

W nowszych wersjach Delphi wreszcie istnieją procedury lub funkcje anonimowe. Niesamowicie upraszczają pisanie kodu, jednocześnie często pozwalając na rezygnację z funkcji zagnieżdżonych (których użycie od zawsze wiązało się z drastycznym spadkiem wydajności podczas wywołania takiej funkcji) lub niepotrzebnej deklaracji na poziomie klasy. Ot, choćby przypadek sortowania list przy użyciu własnej metody porównywania. Poniżej przykład funkcji mieszającej: var o: TList<Integer>; i: Integer; begin o := TList<Integer>.Create(); try o.AddRange([1, 2, 3, 4, 5]); //mieszaj: o.Sort(TComparer<Integer>.Construct(function(const Left, Right: Integer): Integer begin Result := Random(3) - 1; end)); for i in o do writeln(i); finally o.Free; end; end; Wszystko fajnie i ładnie. Co jednak się stanie, jeśli jedną funkcję anonimową zechcemy użyć w innej? type TLos = reference to function: Integer; var o: TList<Integer>; i: Integer; Los: TLos; begin o := TList<Integer>.Create(); try o.AddRange([1, 2, 3, 4, 5]); //mieszaj: Los := function: Integer begin Result := Random(3) - 1; end; o.Sort(TComparer<Integer>.Construct(function(const Left, Right: Integer): Integer begin Result := Los; end)); for i in o do writeln(i); finally o.Free; end; end; Wcześniej zdefiniowaliśmy anonimową funkcję Los, której użyliśmy w kolejnej anonimowej funkcji będącej parametrem konstruktora klasy TComparer<T>. Wszystko wydaje się działać, lecz monitor wycieków pamięci (można do włączyć przez określenie zmiennej ReportMemoryLeaksOnShutdown := True) pokaże raport:

Unexpected Memory Leak
An unexpected memory leak has occurred. The unexpected small block leaks are:

21 - 28 bytes: test$0$ActRec x 1

Ten wyciek właśnie pochodzi z niezwolnionej funkcji anonimowej. Jak sobie z tym poradzić? A no w takim przypadku po każdym użyciu, gdy funkcja anonimowa przestaje być potrzebna, należy wyzerować zmienną z nią związaną. Z jakiegoś powodu kompilator Delphi nie potrafi zapanować sam nad tym zagadnieniem. type TLos = reference to function: Integer; var o: TList<Integer>; i: Integer; Los: TLos; begin o := TList<Integer>.Create(); try o.AddRange([1, 2, 3, 4, 5]); //mieszaj: Los := function: Integer begin Result := Random(3) - 1; end; o.Sort(TComparer<Integer>.Construct(function(const Left, Right: Integer): Integer begin Result := Los; end)); Los := nil; for i in o do writeln(i); finally o.Free; end; end; Teraz wszystko będzie ok. Dlaczego nie umieszczono przypisania nil zaraz po przypisaniu do Result wewnątrz funkcji sortującej? Wszak tam jest jej ostatnie użycie. Odpowiedź jest prosta: Ponieważ funkcja porównująca wywoływana jest kilkukrotnie. Należy zawsze głęboko zastanowić się, w którym momencie takie zwolnienie wywołać, szczególnie pisząc aplikacje wielowątkowe. Niech podpowiedzią będzie fragment kodu: if (chbAnyc.Checked) then TTask.Run( procedure begin TThread.Synchronize(nil, proc); proc := nil; end) else if (Sender is TThread) then begin TThread(Sender).Synchronize(TThread(Sender), proc); proc := nil; end else begin proc(); proc := nil; end; Anonimowa procedura proc jest jedna, ale w zależności od sytuacji może być użyta w osobnym zadaniu, w ramach synchronizacji wątku lub bezpośrednio w wątku, który ją wywołał. Kod jest krótki, ale trzeba pamiętać o wyzerowaniu w momencie, gdy przestaje być użyteczna. Nie można w powyższym przykładzie zrobić jednego przypisania nil na koniec, gdyż w przypadku zadania (TTask) nie mamy kontroli nad tym, kiedy zostanie ono wykonane i zakończone.

Zdradziecki TListBoxStrings

Zapewne wielu wykorzystywało własność Object typów dziedziczących po TStrings do przechowywania nie obiektów, a prostych wartości liczbowych. Wszak gdy potrzebujemy przechować tylko liczbę typu NativeInt (czyli Integer w 32-bitowych aplikacjach) wpisanie wprost jej wartości jest najszybsze zarówno pod względem tworzenia kodu jak i wydajności jego działania.

Przeanalizujmy prosty kod dodający pozycje do komponentu TComboBox: for i := -5 to 5 do ComboBox1.Items.AddObject('10^' + IntToStr(i), Pointer(i)); Następnie w zdarzeniu OnSelect można wykorzystać to w następujący sposób: SpinEdit2.Value := Round(SpinEdit1.Value * IntPower(10, Integer(ComboBox1.Items.Objects[ComboBox1.ItemIndex]))); Widać, że wybór pozycji z listy spowoduje przemnożenie liczby w jednym komponencie przez 10 podniesione do wybranej potęgi i zapisze w drugim. Wszystko fajnie, ładnie, cieszymy się bardzo prostym, krótkim i szybkim kodem, na dodatek w prosty sposób dającym zmieniać zakres potęg... Aż do wyboru wartości -1. W tym momencie dostaniemy komunikat List index out of bounds (4). Ale jak to? Przecież elementów jest 11! Jak można przekroczyć zakres? To samo stanie się, jeśli np. spróbujemy znaleźć pozycję wartości przy użyciu IndexOfObject. Co więcej, przecież takie same przypisania np. w klasach TList lub TStringList działają nie powodując problemów.

Odpowiedzią jest typ TListBoxStrings używany przez sporą część komponentów. Choć formalnie dziedziczy on po TStrings (i takim typem przedstawiają się klasy, lecz w rzeczywistości jest to właśnie wspomniany wcześniej typ), to jednak wprowadza inną obsługę wartości. A tam znajduje się kod, dla którego wartość -1 (a ściślej: $FFFFFFFF - w przypadku kompilacji 32-bitowych) jest wartością szczególną, która właśnie zwraca taki wyjątek (zainteresowanym polecam przyjrzeć się definicji metody TListBoxStrings.Get w System.StdCtrls).

Niestety, niespodzianka może zaskoczyć z czasem, skutecznie unikając testów. Co można zrobić? Zastosować styl lbVirtual lub lbVirtualOwnerDraw i zdarzenie OnData. Inny sposób polega na tworzeniu prawdziwych obiektów, które zostaną przypisane (adres $FFFFFFFF nigdy nie będzie miał miejsca), lecz trzeba pamiętać o ich późniejszym zwolnieniu. Trzecia opcja polega na posłużeniu się towarzyszącymi tablicami, czyli osobną zmienną, którą niestety trzeba utrzymywać wraz ze zmianami w liście Items komponentów - w przypadku używania np. sortowania staje się to jednak kłopotliwe.

Informacje o poradzie
Powiązane linki:Wróć
[Komentarze] | Share Podziel się
Poradę czytano:26 razy
Ocena porady:
brak
(Kliknij właściwą gwiazdkę, by oddać głos)

Uwaga! Wszystkie porady na tej stronie są mojego autorstwa i objęte są prawami autorskimi! Kopiowanie, publikowanie lub cytowanie całego tekstu bez wiedzy autora - ZABRONIONE! [Dowiedz się więcej o prawach autorskich]

Strona istnieje od 25.01.2001
Ta strona używa plików Cookie.
Korzystając z niej wyrażasz zgodę na przetwarzanie danych a zakresie podanym w Polityce Prywatności.
 
archive To tylko kopia strony wykonana przez robota internetowego! Aby wyświetlić aktualną zawartość przejdź do strony.

Optymalizowane dla przeglądarki Firefox
© Copyright 2001-2018 Dawid Najgiebauer. Wszelkie prawa zastrzeżone.
Ostatnia aktualizacja podstrony: 15.07.2018 16:37
Wszystkie czasy dla strefy czasowej: Europe/Warsaw