Agenda
● Więcej oklasach: properties
● Moduły i include files
● Więcej o klasach: widoczność
● Jak działają forms, frames, components w Delphi (ćwiczenie 3)
● Wyjątki
● Zarządzanie pamięcią: notyfikacje (słabe referencje) (ćwiczenie 4)
● Zagnieżdżone identyfikatory w klasach
● Metody klas, właściwości i pola klas
● Referencje klas
● Pomocnicy klas (class helpers)
● Klasy generyczne (generics) (ćwiczenie 5)
● Callbacks
● Interfejsy
● I/O na TStream
Properties
● Wyglądają jakzmienne…
● …ale ich zachowanie (get, set) może być kontrolowane metodami.
○ Przykładowe zastosowanie:
■ property jest "proxy" do get/set na innym, wewnętrznym obiekcie
■ obliczenie wartości w getter jest "cache'owane"
■ setter wykonuje coś dodatkowego, ale naturalnego, np. "schedule repaint"
● Zalecam używanie ich w sytuacjach gdy zachowują się mniej-więcej jak zmienne, np.
○ Ustawianie wartości X := 10 powinno (przynajmniej zazwyczaj) skutkować tym że X zmienia się na 10
○ Niezwiązane zmiany, np. Y (raczej) nie powinny zmieniać automatycznie wartości X
○ Jak widać, powyżej są pewne nagięcia zasad: "przynajmniej", "raczej". Różne biblioteki i programiści traktują
powyższe zasady mniej lub bardziej surowo.
■ Delphi VCL i FMX np. uaktualniają niektóre wartości aby odzwierciedlić aktualny rozmiar elementu.
■ Dla odmiany, Castle Game Engine ma oddzielne Width, Height i EffectiveWidth, Height. Więc
pokazanie UI wielokrotnie zawsze zaczyna od tego samego stanu, nasze "Width, Height" to "pożądany
rozmiar".
● Jeżeli to coś "nie zachowuje się jak zmienna", to explicit metoda/metody są pewnie bardziej
naturalne.
5.
Properties
● Tradycyjnie, propertyw section published są dostępne przez RTTI, co oznacza że są wyświetlane w
Object Inspector oraz są serializowane (w DFM, FMX)
○ Sprawdzimy to zaraz!
○ Note: W nowszych Delphi, także zmienne, także w sekcji public, są dostępne przez RTTI, see {$rtti xxx} .
Nadal, najprostszym sposobem aby coś zobaczyć w Object Inspector jest aby było to published property,
● Properties mogą mieć sekcje "default", "stored".
○ Mają znaczenie dla serializacji.
○ Samo "defaut XXX" nie oznacza że dana wartość faktycznie jest domyślna po konstrukcji. To nasze zadanie
aby w konstruktorze zrobić "FMyProp := Xxxx". "default XXX" mówi tylko "nie ma potrzeby serializować jeśli
jest równe XXX".
○ String always has default = ''. Use "nodefault" to avoid it.
○ Single cannot have default in Delphi.
○ Jeśli jest zarówno "default" jak i "stored", oba warunki muszą przejść aby property zostało zapisanem tzn.
"(wartość musi być <> default) and IsStored".
6.
Typowo, setter jestzabezpieczony ani nie robić
nic gdy nowa wartość = stara
● Polecam to robić, od początku!
● Bo optymalizacja często ma sens.
● A dodanie później, podczas developmentu, takiego testu jest ryzykowne -- a co jeśli coś polega na
"efekcie ubocznym" zrobienia "X := X"? Nic nie powinno, ale jeśli coś polega, a my dodamy test "if
FXxx = Xxx then Exit" -> może to prowadzić do trudnych do "złapania" błędów.
7.
Array properties
● Dostęp(get. set) do property może wyglądać jak dostęp do tablicy
○ property Voices[const Index: Integer]: String read GetVoices;
● Z "default": można pominąć nazwę property, efektywnie cała instancja wygląda trochę jak tablica
○ property Children[const Index: Integer]: TChildCreature read GetChildren; default;
● Note: caller nie ma jak poznać Count, więc typowo Count jest zwracany przez dodatkowe
read-only property albo funkcję.
● Używać warto wtedy kiedy traktowanie tego jako tablica jest "naturalne". To syntactic sugar, ma
sens kiedy ulepsza czytelność.
Jak czysto podzielićprojekt na wiele plików?
● Moduły (units)
● Jasno zdefiniowany interfejs (wpływa na używających go) i implementacja (nie ma wpływu).
○ Note: Kompilator także polega na powyższych założeniach (chociaż pewne language features jak inline i
generics wprowadzają pewne wyjątki, ale nevermind)
● Wiele modułów może zdefiniować ten sam identyfikator
○ Wygrywa ostatni na liście "uses"
○ Chociaż można kwalifikować odwołanie explicite nazwą modułu, jak "MyUnit.MyProcedure"
○ Nadal, bezpieczniej nie mieć konflików w ogóle, bo mogą być confusing
● Circular dependencies
○ Moduły mają 2 sekcje "uses", w interface i implementation
○ Moduły mogą się nawzajem używać, ale nie może być circular dependency w interfejsach
● initialization / finalization modułu
10.
Jak "nieczysto" rozdzielićkod na wiele plików?
● Include files
● Po prostu {$I myinclude.inc}
● To powoduje bezpośrednie wstawienie pliku include w miejsce {$I xxx.inc}
● Raczej unikamy, chociaż mam przykłady (także w Castle Game Engine) użycia które IMHO jest
prawidłowe.
○ Czasem chcemy mieć wiele klas w jednym module, aby zmuszać użytkownika to zbyt olbrzymiej listy w "uses".
○ Jest też popularne aby definiować w bibliotekach include file używany przez wszystkie moduły, aby ustawić
wspólne opcje kompilacji, parsowania w danej bibliotece, wspólne symbole (dla {$ifdef XXX} etc..
● Raczej unikamy, bo nie jest konstrukcją językową z jasno zdefiniowanym interfejsem (jak moduły).
○ Unikamy także ze względu na słaby support w Delphi IDE (brak code completion, bo ignoruje "{%MainUnit
xxx.pas}")
Wszystko w klasiema swoją widoczność
Przede wszystkim:
● private (niewidoczne dla wszyskich, poza aktualnym modułem)
○ Nie tylko "aktualną klasa", a "aktualny moduł". To pozwala klasom w obrębie tego samego moduły współpracować ze
sobą za pomocą pól / metod wewnętrznych. W C++ "friends" służy do tego.
● strict private (niewidoczne dla wszystkich, poza aktualną klasą)
● protected (widoczne w tym samym module oraz w podklasach)
○ Wygodne aby zakomunikować że coś jest "nie do normalnego skorzystania"... ale w praktyce oznacza że dostęp z
zewnątrz jest, więc należy traktować jako cześć API którego złamanie wpływa na users.
● public (widoczne dla wszystkich)
● published (jak public, ale jest w RTTI i starym i nowym, i Object Inspector pokazuje, i serializowane
automatycznie)
Oraz:
● strict protected
○ IMHO niezbyt użyteczne, see https://castle-engine.io/coding_conventions#no_strict_protected
● automated (Win32 automation technology)
Jak poznane koncepcje(klasy etc.) mapują się
na tworzenie programów GUI
● Formularz to nowa klasa
○ Z sekcją published (implicit to "published" bo dziedziczy z TPersistent które ma {$M+})
● Ta klasa ma instancję
○ Jeśli form jest auto-created, to po prostu znaczy że główny program tworzy instancję klasy i ustawia tą
zmienną.
■ "Application.CreateForm(TFormAutoCreated, FormAutoCreated)" ->
■ "FormAutoCreated := TFormAutoCreated.Create(Application)" (mniej-więcej, z pewnymi sztuczkami)
○ Jeśli form nie jest auto-created, tą zmienną można zignorować, usunąć z kodu, nie musimy jej używać.
● Możemy sami stworzyć form (bez względu na to czy jest auto-created czy nie). Można mieć tyle
instancji ile zechcemy. Przechowujemy je w zmiennych globalnych lub nie, jak chcemy.
15.
Dziedziczenie formularzy
● Skoroformularz to klasa, to mamy dziedziczenie?
○ Tak, "File -> New -> Other -> Inheritable Items"
○ See also DFM, defines only "difference" from ancestor
Komponenty i properties
●Wszystko co widzimy na form (lub data module, lub frame) to instancja klasy która dziedziczy z
TComponent
● Wszystko co widzimy w Object Inspector to published property
18.
Ćwiczenie 3
Nowa klasa,zarejestrowana w Delphi IDE, używana w data module:
● New Component
○ Z TComponent (w Classes)
○ FMX
○ Install to New Package
● Pamiętaj: Delphi IDE nie jest aplikacją konsolową, Writeln jest niedopuszczalne
● Zdefiniuj klasę, "Install"
○ Kompiluje BPL, który jest jak DLL ale z dodatkami DLL aby łatwo udostępniać klasy i inne elementy Pascala
○ Instalacja po prostu dodaje go do Delphi IDE, wywołując metodę "Register" modułu która
■ dodaje komponent do palette
■ pozwala do serializować / deserializować do DFM, XFM…
● W projekcie, dodaj data module.
○ W projekcie konsolowym data module nie jest stworzony? Ale już wiemy że stworzenie go ręcznie to żadna "czarna
magia":
■ "DataModule1 := TDataModule1.Create(nil);"
● Zdefiniuj w ten sposób możliwe przedmioty w grze, jak Key, Sword, Armor. Użyj ich w grze.
○ Np. Owns: Boolean, damage dla broni etc.
● IOW, użyj Delphi IDE jako projektanta elementów które użyjesz w grze.
Wyjątki
● Zgłoszony (raise)wyjątek wychodzi z bloków kodu, aż zostanie złapany (try .. except).
● Świetnia metoda zgłaszania błędów z których program może wyjść ale nie da się kontynuować
obecnego kodu.
○ Np. nie da otworzyć pliku.
○ Więc nie da się też odczytać z niego danych.
○ Więc nie da się też stworzyc np. instancji reprezentującej te dane.
○ … ale być może da się kontynuować program (może z warningiem) nie mając tych danych.
● Wyjątek to po prostu klasa.
○ Konwencja to zgłaszać wyjątek z (pod)klasy Exception (z prefixem E, nie T), ale to tylko konwencja.
○ Dowolna klasa z TObject jest OK.
○ Zgłoszony wyjątek zostanie automatycznie zwolniony.
● raise
● klasa Exception, Message, CreateFmt
21.
Wyjątki
● własne klasywyjątków z własnymi danymi
● try except
○ re-raise
○ modyfikacja E.Message
● try finally
○ bez względu na wyjątki (oraz Exit, Break, Continue) zrób coś
○ Np. finaliazację, zwalnianie pamięci.
○ Uwaga: Wszelkie finalizacje, destruktory warto pisać bezpiecznie, aby działały nawet w na-wpół dobrym
stanie, aby "awaryjne" zwalnianie nie spowodowało nowych problemów.
● Wynik funkcji nie ma jak być przekazany gdy ta funkcja kończy się z exception
○ Więc pamiętaj zwolnić Result, przez try..except
● Pamiętaj aby nie zwalniać nieprawidłowych wskaźników! Free, FreeAndNil - tylko na nil albo
gwarantowanych OK wskaźnikach, nie przypadkiem dangling pointers.
○ Wiele zagnieżdżonych try..finally można "collapse", ale pamiętaj zagwarantować że wskaźniki są nil, żeby nie
zrobić free na dangling pointers.
22.
Wyjątki w konstruktorze
●Wyjątek może wydarzyć się w konstruktorze klasy, jak wszędzie.
○ Przerywa konstrukcję
○ i automatycznie wywołuje destructor (bez tego "automatycznie", nie byłoby jak sfinalizować na-wpół
stworzonej klasy, bo przecież nie dostaniemy jej referencji w kodzie który robi "C := TMyClass.Create" gdy
"TMyClass.Create" zgłasza wyjątek).
○ Dlatego destructor musi być przygotowany poradzić sobie z na-wpół stworzoną klasą.
○ Dlatego też gwarancja "wszystkie pola klasy zaczynają od zera" jest przydatna, bez niej byłoby to bardzo
trudne.
Zagnieżdżone klasy
● Wśrodku klasy można zadeklarować… więcej klas
● Wygodne gdy klasa używana jest tylko jako typ w sekcji private
● Note że klasa wewnętrzna nie ma żadnego specjalnego dostępu do nadrzędnej.
○ Jeśli go potrzebujesz, zdefiniuj i ustaw nowe pole klasy podrzędnej do teo celu.
25.
Pomocnicy klas (classhelpers)
● Czasem nie możemy zmienić danej klasy, ale chcielibyśmy dodaj do niej metodę.
● Można przecież zawsze zrobić to globalną procedurą/funkcją, "procedure MyProc(const C:
TCreature);" jest zupełnie podobne do dodania "MyProc" to "TCreature"
● "class helpers" to syntactic sugar który robi takie "procedure MyProc(const C: TCreature);" , ale
pozwala używać składni podobnej jak definiowanie normalnej metody
○ Ergo, nie można dodawać nowych pól lub metod wirtualnych. Ale można dodawać nie-wirtualne metody.
● Główne zastosowanie: dodaj "utilities" do klasy które polegają na tych zadeklarowanych po
zdefiniowaniu danej klasy.
26.
● Słabe (weakreferences) oznaczają że mamy wskaźnik z A do B, ale ta referencja z A nie "wymusza
istnienia B"
○ W przypadku zarządzania z reference counting, oznacza to referencję która nie podbija ref count
○ W naszym przypadku, to tylko fancy nazwa na "property jest instancją klasy, ale zachowa się OK jeśli tą
instancję zwolni coś innego".
● Skorzystamy z TComponent.FreeNotification
● Możemy zostać poinformowani, w jednym TComponent, o zwolnieniu innego
● To nie jest zupełnie trywialne, bo musimy pamiętać aby "odpiąć" nasze notyfikacje
○ od poprzednich wartości
○ kiedy obserwator jest niszczony
Notyfikacje (słabe referencje)
27.
Ćwiczenie 4
Tym razemrebus, trochę trudny.
Uruchom program w 250_weak_ref_trap ,
https://github.com/michaliskambi/modern_pascal_course/blob/master/250_weak_ref_trap/weak_ref_tra
p.dpr
Dlaczego output jest dziwny (patrz komentarz na poczatku)?
Hint: 2 razy FreeNotification nie oznacza 2 razy RemoveFreeNotification.
Jak to naprawić?
Metody, pola, właściwościklas
● Możemy zdefiniować metody, pola, właściwości które dotyczą klasy (nie jej instancji)
● Możemy zdefiniować nawet konstruktory i destruktory klas
○ Wywołane raz na cały program.
○ Są wywołane automatycznie,
○ Użyteczne aby zainicjować coś w klasie co jest strict private, więc nie ma do tego dostępu z initialization /
finalization modułu. Np. zmienną klasy w strict private
○ Trochę zastępują ("wpychają do środka obiektów") zmienne w implementacji modułu, initialization,
finalization modułu.
30.
Referencje klas
● Klasy(nie tylko instancje klas!) także są czymś co może być pamiętane w zmiennych.
● Czyli też zmieniane, at runtime.
● Typowy przykład to różnego rodzaju "fabryki" instancji, których zadaniem jest tworzenie instancji
czegoś.
● Czyli metody wirtualne klas mają sens.
○ Tak samo jak metody wirtualne instancji, ale teraz klasa nie jest znana at runt-time.
● W tym też konstruktory wirtualne mają sens.
Zastosowanie
● "type TMyClass<T>= class … end"
● Definicja typu (klasy, rekordu, metody, callback) zależy od innego typu.
● Chcemy być type-safe, szybcy, ale też pozwolić na różne zastosowania w zależności od T.
● Zastosowana:
○ Np. lista, stos, kolejka z elementów typu T.
■ Dla porównania, kontenery z Contnrs, bez generics, wymagają typecasts, kompilator nas nie pilnuje.
○ Albo słownik T1 -> T2
○ To już jest gotowe, zaimplementowane w Generics.Collections,
https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.Generics.Collections
○ Albo Nullable<T>, czyli wartość T albo informacja że takiej wartości nie ma
■ Internet znajduje przynajmniej 3 implementacje :)
33.
Notes
● Ograniczenie: kompilatormusi mieć gwarancję, w momencie parsowania typy generycznego (nie
specjalizacji) że definicja jest poprawna.
○ constraints tutaj się przydają: <T: TMyClass>
○ To ogólnie uniemożliwia (niestety) niektóre zastosowania w Delphi, np.
■ nie można użyć typu T do indeksowania tablicy (bo nie ma jak powiedzieć że jest ordinal)
■ albo robienia + * na typie (bo nie ma na to constraint).
■ albo porównania < > (chociaż kolekcje w Generics.Collections robią pewne sztuczki aby zdefiniować
comparer automatycznie).
● Default(T) użyteczne aby zainicjować element.
● Typy w środku też mogą zależeć od T, "array of T", "SizeOf(T)", "record X: T end;" są Ok
34.
Ćwiczenie 5
Zaimplementuj klasęTGrid2D<T> do użyku do różnych gier 2D
TGrid2D<T> = class
constructor Create(const AWidth, AHeight: Cardinal);
property Width: Integer read …;
property Height: Integer read …;
property Items[const X, Y: Integer]: T read … write …; // setting also makes HasItems[x,y]=true
property HasItems[const X, Y: Integer]: Boolean read ..;
procedure ClearItem(const X, Y: Integer);
end;
// E.g. T = TChessPiece = record ChessType: (Knight, Rook, …); IsWhite: Boolean; end;
// E.g. T = TCheckersPiece = Boolean // is white
// E.g. T = TShip = (ShipHit ShipHealthy);
Wskaźniki na funkcje,procedury, metody
● Potężna metoda konfigurowania zachowania.. czegokolwiek.
○ Kod, a dokładnie: funkcja, procedura, metoda, możemy przekazywać jako argumenty.
● type TMyFunction = function(const X: Integer): String;
○ bez "of object": globalna funkcja / procedura
● type TMyEvent = function(const X: Integer): String of object;
○ z "of object": metoda
○ Nie kompatybilne ze sobą (bo metoda ma dodatkowy ukryty parametr, instancja jaką dostaje, przekazana w
Self)
○ Beware: = jest troche zepsute w Delphi, porównuje tylko wskaźnik na kod, nie instancję, to zazwyczaj nie to
co chcemy. See SameMethods for solution.
● type TMyReference = reference to function(const X: Integer): String;
○ Kompatybilne z oboma powyżej
37.
Przypisywanie
● Mogą byćporównane i przypisane "nil" (chociaż "of object" to de facto 2 wskaźniki).
● Assigned(X) też działa, generalnie jak X <> nil ale unika nie-1-znaczności (co jest X jest funkcją bez
parametrów)
● W Delphi, @X oznacza faktycznie zawartość zmiennej typu proceduralnego, samo X oznacza jego
wywołanie. Można użyć () aby wywołanie było explicite.
● Śmiesznie, @@X w rezultacie w Delphi ma sens (to adres zmiennej przechowującej callback).
Interfejsy
● Niezwiązane zesobą klasy implementują to samo API
● Wymagany GUID aby działało testowanie "is" było OK
● Używanie interfejsów oznacza też reference counting, i wymaga implementacji kilku metod.
○ Dziedzicz z TInterfacedObject aby mieć to łatwo zrobione.
○ Albo dziedzicz z TComponent aby ominąć reference counting.
■ See https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Not_Using_Reference_Counting
Uwaga na propertieszwracające wartości
złożone, jak rekordy
Wynik (może) być tymczasową wartością której modyfikacja nie ma sensu.
https://castle-engine.io/coding_traps