Nowoczesny obiektowy Pascal:
część 2 (czwartek)
Michalis Kamburelis
https://www.bsc.com.pl/szkolenie-nowoczesny-obiektowy-pascal/
https://github.com/michaliskambi/modern_pascal_course
https://castle-engine.io/modern_pascal
Agenda
● Więcej o klasach: 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
Więcej o klasach: properties
Properties
● Wyglądają jak zmienne…
● …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.
Properties
● Tradycyjnie, property w 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".
Typowo, setter jest zabezpieczony 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.
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ść.
Moduły oraz include files
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
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}")
Więcej o klasach: Widoczność
Wszystko w klasie ma 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 działają forms, frames, data
modules w Delphi
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.
Dziedziczenie formularzy
● Skoro formularz to klasa, to mamy dziedziczenie?
○ Tak, "File -> New -> Other -> Inheritable Items"
○ See also DFM, defines only "difference" from ancestor
Zawieranie (kompozycja)?
● Tak, to sa frames!
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
Ć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
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
Wyjątki
● własne klasy wyją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.
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.
Więcej o klasach
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.
Pomocnicy klas (class helpers)
● 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.
● Słabe (weak references) 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)
Ćwiczenie 4
Tym razem rebus, 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ć?
Referencje klas
Metody, pola, właściwości klas
● 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.
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.
Klasy generyczne
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 :)
Notes
● Ograniczenie: kompilator musi 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
Ć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 (callbacks, events) na
procedury, funkcje i metody oraz
funkcje anonimowe
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
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).
Anonimowe funkcje
Każda zachowuje kontekst swojego wywołania, "niesie" ze sobą zmienne w jej kontekście.
Interfejsy
Interfejsy
● Niezwiązane ze sobą 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
I/O używając TStream,
TFileStream
Strumienie (TStream) pozwalają łatwo
operować na dowolnych danych - plikach,
strumieniach z Internetu, obszarach pamięci…
TStream
● TMemoryStream
● TStringStream
● TFileStream
Test:
● TNetHttpClient
● https://github.com/michaliskambi/delphi-http-download/blob/master/test_net_http_client.dpr
Rekordy
Uwaga na properties zwracają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
Dziękuję!
I pozostajemy w kontakcie:
Discord
michalis@castle-engine.io

Nowoczesny obiektowy Pascal (2) - kurs BSC

  • 1.
    Nowoczesny obiektowy Pascal: część2 (czwartek) Michalis Kamburelis https://www.bsc.com.pl/szkolenie-nowoczesny-obiektowy-pascal/ https://github.com/michaliskambi/modern_pascal_course https://castle-engine.io/modern_pascal
  • 2.
    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
  • 3.
  • 4.
    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ść.
  • 8.
  • 9.
    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}")
  • 11.
    Więcej o klasach:Widoczność
  • 12.
    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)
  • 13.
    Jak działają forms,frames, data modules w Delphi
  • 14.
    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
  • 16.
  • 17.
    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.
  • 19.
  • 20.
    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.
  • 23.
  • 24.
    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ć?
  • 28.
  • 29.
    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.
  • 31.
  • 32.
    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);
  • 35.
    Wskaźniki (callbacks, events)na procedury, funkcje i metody oraz funkcje anonimowe
  • 36.
    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).
  • 38.
    Anonimowe funkcje Każda zachowujekontekst swojego wywołania, "niesie" ze sobą zmienne w jej kontekście.
  • 39.
  • 40.
    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
  • 41.
  • 42.
    Strumienie (TStream) pozwalająłatwo operować na dowolnych danych - plikach, strumieniach z Internetu, obszarach pamięci… TStream ● TMemoryStream ● TStringStream ● TFileStream Test: ● TNetHttpClient ● https://github.com/michaliskambi/delphi-http-download/blob/master/test_net_http_client.dpr
  • 43.
  • 44.
    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
  • 45.
    Dziękuję! I pozostajemy wkontakcie: Discord michalis@castle-engine.io