PORADY

Porady > Programowanie > Delphi > Tajniki RTTI

Tajniki RTTI

Spis treści

Czym jest RTTI i na co pozwala?

RTTI, to skrót od Run-time Type Information, czyli informacji, które zbierane są przez kompilator o klasach w kodzie I dołączane do tychże klas. Dzięki temu możliwe jest odczytywanie i modyfikowane ich podczas wykonywania kodu. Ponadto daje też dostęp do informacji o typach używanych w programie. W szczególności RTTI pozwala na takie operacje, jak:

  • Uzyskiwanie nazw klas, a także - w najnowszych wersjach, jak Delphi 12 - także nazw zmiennych czy typów (NameOf)
  • Listowanie metod (wraz z parametrami), pól i właściwości klas
  • Odczytywanie i zapisywanie wartości pól i właściwości
  • Wywoływanie metod, w tym przekazywanie i odbieranie wartości parametrów
  • Dostarczanie informacji o rekordach
  • Uzyskiwanie nazw wartości typów wyliczeniowych
  • Uzyskiwanie informacji o typach porządkowych

Jak pozyskać informacje?

Wykorzystując RTTI nieodłączne stają się dwa moduły: System.Rtti, który pomaga w przetwarzaniu informacji o klasach i ich zawartości, a także zawiera typy pomocne przy odczycie i nadawaniu wartości oraz System.TypInfo, dzięki któremu można bezpośrednio odwoływać się do wartości obiektów lub np. nazw typów wyliczeniowych.

W najprostszym scenariuszu wystarczy tylko utworzyć kontekst RTTI, aby zyskać dostęp do informacji. Choć sam typ TRttiContext jest rekordem, to zachowuje się nieco jak obiekt przy użytkowaniu:

var ctx: TRTTIContext; info: TRttiType; begin ctx := TRttiContext.Create; try Info := ctx.GetType(TObject); finally ctx.Free; end; end;

Powyższy przykład pobiera informacje o klasie TObject. Ale oczywiście ten kod jeszcze niczego nie wyświetli. Aby pozyskać więcej informacji warto przyjrzeć się najczęściej używanym metodom i właściwościom dla poszczególnych typów.

Aby móc wyciągać te informacje, warto zapoznać się z następującymi typami:

  • TRttiContext - umożliwia dostęp do wszystkich informacji
  • TRttiType - zawiera informacje o typie danych; typ ten ma wiele odrębnych podtypów w zależności od rodzaju danych
  • TRttiField - informacje o polu klasy
  • TRttiProperty - informacje o właściwości klasy
  • TRttiMethod - informacje o metodzie klasy
  • TRttiParameter - informacje o parametrze metody
  • TRttiPackage - informacje o załadowanych paczkach

Prawie każdy element w RTTI na swoją nazwę, którą można pobrać z właściwości Name. Zatem chcąc wyświetlić nazwę przekazanej klasy wystarczy dodać po pobraniu informacji:

Writeln(Info.Name);

Z jakich jeszcze elementów zmiennej takiego typu najczęściej będzie się korzystać?

Aby sprawdzić, z czym mamy do czynienia, należy wywołać TypeKind. Pozwala ona określić, czy mamy do czynienia z klasą, rekordem, prostą zmienną liczbową, tekstową, wyliczeniową, czy może wskazywana jest procedura lub metoda. Jeśli określimy, że mamy na pewno do czynienia z klasą lub rekordem, to dla nich wszak można pobierać szczegółowe informacje związane z zawartością poprzez następujące metody:

  • GetProperties - zwraca listę informacji o właściwościach
  • GetIndexedProperties - zwraca listę informacji o właściwościach z dostępem indeksowanym (od Delphi XE2)
  • GetMethods - zwraca listę informacji o metodach
  • GetFields - zwraca listę informacji o polach

Oczywiście istnieją też możliwości pobrania informacji o wskazanym po nazwie obiekcie - metody nazywają się podobnie (lecz w liczbie pojedynczej) i mają parametr tekstowy. Nadmienić trzeba, że GetMethods również posiada opcjonalny parametr - służy wówczas do pobierania listy przeładowanych metod (czyli wielu o tej samej nazwie).

Istnieją też te same metody z dopisanym po Get słowem Declared - w takim przypadku zwracane są wyłącznie te elementy, które wprost znajdują się w analizowanej klasie.

Wszystkie te metody zwracają jeden z typów wspomnianych już wcześniej. Każdy z nich, poza oczywiście nazwą, ma np. taką właściwość jak Visibility, która mówi, w jakiej części klasy odpowiedzialnej za widoczność jest ten element umieszczony. Dodatkowo dla właściwości można odnaleźć informacje, czy jest ona do odczytu (IsReadable) lub do zapisu (IsWritable), a dla metod będą to np. informacje o parametrach (metoda GetParameters) lub zwracanym typie dla funkcji (ReturnType). Te ponownie są określonego typu, więc można rekurencyjnie przeglądać dalsze informacje.

Kolejnym przydatnym narzędziem może być sprawdzanie, czy coś, z czym pracujemy, nie jest dokładnie pewnym elementem. Owszem, możliwe jest sprawdzenie po prostu nazwy. Ale znacznie prostsze i szybsze okaże się korzystanie z funkcji systemowej (nie wymaga użycia żadnego specjalnego unitu) TypeInfo(…), która zwraca wskaźnik do informacji o danym elemencie. Ten sam znaleźć można w typach zwracanych przez RTTI pod właściwością Handle.

Ostatnim przydatnym elementem jest właściwość BaseType, która - zgodnie z tym, co podpowiada intuicja - zwraca informacje o typie nadrzędnym. Przykładowo dla klasy będą to informacje o klasie, z której dziedziczy, a np. dla typu TDateTime będzie to liczba zmiennoprzecinkowa, gdyż właśnie jest on jej rozwinięciem.

Wyposażeni w tą wiedzę przyjrzyjmy się poniższemu modułowi, który wyświetla szereg informacji o przekazanym dowolnym typie RTTI:

unit uRTTI; interface procedure PrintRTTIInformation(const aClass: TClass); var ExcludeInherited: Boolean = True; ListTObject: Boolean = False; implementation uses System.SysUtils, System.RTTI, system.TypInfo; function ParamFlagsToStr(const aFlags: TParamFlags): String; begin Result := ''; if pfVar in aFlags then Result := Result + 'var '; if pfConst in aFlags then Result := Result + 'const '; if pfArray in aFlags then Result := Result + 'array '; if pfAddress in aFlags then Result := Result + 'addr '; if pfReference in aFlags then Result := Result + 'ref '; if pfOut in aFlags then Result := Result + 'out '; if pfResult in aFlags then Result := Result + 'res '; end; procedure PrintRttiTypeInfo(const aType: TRttiType; const aLvl: Integer = 1); const StrBool: array[False..True] of String = ('-', '+'); StrVis: array[Low(TMemberVisibility)..High(TMemberVisibility)] of String = ('PRIVATE', 'PROTECTED', 'PUBLIC', 'PUBLISHED'); StrMethod: array[Low(TMethodKind)..High(TMethodKind)] of String = ('PROCEDURE', 'FUNCTION', 'CONSTRUCTOR', 'DESTRUCTOR', 'CLASS PROCEDURE', 'CLASS FUNCTION', 'CLASS CONSTRUCTOR', 'CLASS DESTRUCTOR', 'OPERATOR OVERLOAD', 'SAFE PROCEDURE', 'SAFE FUNCTION'); var prefix: String; fields: TArray>TRttiField>; fieldInfo: TRttiField; props: TArray>TRttiProperty>; propInfo: TRttiProperty; meths: TArray>TRttiMethod>; methInfo: TRttiMethod; parInfo: TRttiParameter; begin if (aType = nil) then begin writeln('?'); Exit; end; if (aLvl > 6) then Exit; prefix := StringOfChar(' ', aLvl * 2); writeln(aType.Name); if (aType.Handle = TypeInfo(TObject)) and not ListTObject then Exit; if (aType.TypeKind = tkClass) or (aType.TypeKind = tkRecord) then begin if ExcludeInherited then fields := aType.GetDeclaredFields else fields := aType.GetFields; for fieldInfo in fields do begin if not ListTObject and (fieldInfo.Parent.Handle = TypeInfo(TObject)) then Continue; write(prefix + StrVis[fieldInfo.Visibility] + ' FIELD ' + fieldInfo.Name + ': '); PrintRttiTypeInfo(fieldInfo.FieldType, aLvl + 2); end; if ExcludeInherited then props := aType.GetDeclaredProperties else props := aType.GetProperties; for propInfo in props do begin if ExcludeInherited and (propInfo.Parent <> aType) then Continue; if not ListTObject and (propInfo.Parent.Handle = TypeInfo(TObject)) then Continue; write(prefix + StrVis[propInfo.Visibility] + ' PROPERTY ' + propInfo.Name + ' ['); if propInfo.IsReadable and propInfo.IsWritable then write('R/W') else if propInfo.IsReadable then write('RO') else write('WO'); write(']: '); PrintRttiTypeInfo(propInfo.PropertyType, aLvl + 1); end; if ExcludeInherited then meths := aType.GetDeclaredMethods else meths := aType.GetMethods; for methInfo in meths do begin if ExcludeInherited and (methInfo.Parent <> aType) then Continue; if not ListTObject and (methInfo.Parent.Handle = TypeInfo(TObject)) then Continue; writeln(prefix + StrVis[methInfo.Visibility] + ' ' + StrMethod[methInfo.MethodKind] + ' ' + methInfo.Name); for parInfo in methInfo.GetParameters do begin write(prefix + ParamFlagsToStr(parInfo.Flags) + ' parameter ' + parInfo.Name + ': '); PrintRttiTypeInfo(ParInfo.ParamType, aLvl + 1); end; if methInfo.ReturnType <> nil then begin write(prefix + ' returns ' + methInfo.ReturnType.Name + ': '); PrintRttiTypeInfo(methInfo.ReturnType, aLvl + 1); end; end; end else if aType.TypeKind = tkArray then begin writeln(prefix + ' dimessions: ' + IntTostr(TRttiArrayType(aType).DimensionCount)); writeln(prefix + ' element type:'); PrintRttiTypeInfo(TRttiArrayType(aType).ElementType, aLvl + 1); end else writeln(prefix + ' type: ' + GetEnumName(TypeInfo(TTypeKind), Ord(aType.TypeKind))); if aType.IsOrdinal then begin writeln(prefix + ' Min value: ' + IntToStr(TRttiOrdinalType(aType).MinValue)); writeln(prefix + ' Max value: ' + IntToStr(TRttiOrdinalType(aType).MaxValue)); end; if aType.TypeKind = tkEnumeration then begin writeln(prefix + ' Enums: ' + String.Join(', ', TRttiEnumerationType(aType).GetNames)); end; if aType.BaseType <> nil then begin writeln(''); write(prefix + 'Base on '); PrintRttiTypeInfo(aType.BaseType, aLvl + 2); end; end; procedure PrintRTTIInformation(const aClass: TClass); var ctx: TRTTIContext; begin ctx := TRttiContext.Create; try PrintRttiTypeInfo(ctx.GetType(aClass)); finally ctx.Free; end; end; end.

Funkcja PrintRTTIInformation przyjmuje za punkt startowy wskazaną przez nas klasę. Ale najistotniejsze jest to, co dzieje się w PrintRttiTypeInfo, która wywoływana jest rekurencyjnie.

Warto zwrócić uwagę na kilka fragmentów. Jako, że działa ona rekurencyjnie, to oczywiście dodano zabezpieczenie przed głębokością - inaczej, gdyby poddać jej działaniu klasę, której metoda parametr tej samej klasy, to doszłoby do zapętlenia.

Ponadto w pliku tym znajduje się zmienna ExcludeInherited, która mówi, czy należy przetwarzać dane należące do klas, z której dziedziczy dana klasa. Ponieważ wszystkie publiczne i chronione element z klasy rodzica zawsze przechodzą do klasy potomnej, przez co każda następna jest już na starcie tak samo bogata, jak jej rodzic. Jeśli użyte są metody bez słowa Declared, to wówczas uzyskuje się informacje wraz ze wszystkimi przejętymi po rodzicach informacjami. Użycie wersji z tym słowem sprawia, że zwracane są tylko deklarowane wprost w danej klasie.

Procedura ta może wykluczać też przetwarzanie klasy TObject poprzez ustawienie zmiennej ListTObject. To w tym miejscu wykorzystane jest porównanie wskaźnika z informacjami:

if (aType.Handle = TypeInfo(TObject)) and not ListTObject then Exit;

Kolejnymi rzeczami, jakie się tu wyszczególniają, to fakt, że w zależności od typu mamy do czynienia z rozbudowanymi wersjami klasy TRttiType, które posiadają dodatkowe właściwości. I tak dla tablic (tkArray) zwracany typ jest w rzeczywistości typem TRttiArrayType, a ten posiada np. takie właściwości jak liczba wymiarów tablicy (DimensionCount) oraz typ jej elementów (ElementType). Ten element można oczywiście rekurencyjnie poddać dalszemu badaniu (por. linie 129-133).

Dla typu wyliczeniowego z kolei można pobrać nazwy jego elementów (por. linia 144-146).

Oczywiście, nie są to wciąż wszystkie możliwe do pozyskania informacje, ale z pewnością jedne z częściej wykorzystywanych.

W tym miejscu warto zwrócić też uwagę na pewne niedomagania RTTI. Jeśli typ złożony znajdzie się wprost w definicji, to nie będzie miał on swojego odwzorowania w RTTI.

TMyClass = class strict private fValues: array[0..5] of Integer; fSub: 1..100; fProc: procedurę of object; end;

Aby obejść ten problem, trzeba "otypować", czyli nadać nazwy takim typom:

T6IntArray = array[0..5] of Integer; TSub1To100 = 1..100; TProcedure = procedureof object; TMyClass = class strict private fValues: T6IntArray; fSub: TSub1To100; fProc: TProcedure; end;

Podobny problem spotyka typy wyliczeniowe z narzuconymi wartościami, jeśli nie są one zgodne z naturalnymi nadawanymi przez kompilator:

{$SCOPEDENUMS ON} // pozwala zastosować te same nazwy elementów wyliczeniowych, ale wymaga poprzedzenia nazwą typu TMyEnum1 = (First, Last); TMyEnum2 = (First = 0, Last = 1); TMyEnum3 = (First = 1, Last = 2); TEnumerationClass = class public fEnum1: TMyEnum1; // ok fEnum2: TMyEnum2; // jeszcze ok., bo się pokrywa fEnum3: TMyEnum3; // brak informacji end;

Obejścia tego problemu są różne - można utworzyć helper dokonujący konwersji, można zbudować tablicę lub można opisać wartości atrybutami.

Co ciekawe, wartości enumeratora umieszczone wprost w definicji zmiennej, bez tworzenia typu, są obsługiwane!

Dyrektywy kompilatora sterujące informacjami RTTI

Jednak przeglądając informacje nie ma danych o właściwościach i metodach z sekcji innych, niż publiczne lub publikowane. Wynika to z domyślnych ustawień kompilatora. Domyślnie zamieszczane są informacje o publicznych i publikowanych metodach i właściwościach oraz polach ze wszystkich sekcji.

Aby zmienić ten stan rzeczy można posłużyć się dyrektywą kompilatora {$RTTI dziedziczenie, element([widoczność])}.

Dziedziczenie może przyjmować jedną z dwóch wartości:

  • INHERITED - bez podawania innych parametrów sprawi, że ustawione zostaną wszystkie identycznie, jak dla klasy nadrzędnej (z której dziedziczy).
  • EXPLICIT - redefiniuje na nowo parametry widoczności elementów klasy

Do sterowania oczywiście oddane są 3 elementy: FIELDS dla pól, PROPERTIES dla właściwości oraz METHODS dla metod.

W zbiorze będącym parametrem elementu można wymieniać zakresy widoczności: vcPrivate - elementy z sekcji prywatnych, vcProtected - chronionych, vcPublic - publicznych oraz vcPublished - publikowanych (szczególnie znaczących dla komponentów).

Przyjrzyjmy się zatem dwóm wariantom przedstawionym niżej:

{$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])} {$RTTI EXPLICIT METHODS([vcPrivate, vcProtected, vcPublic, vcPublished]) PROPERTIES([vcPrivate, vcProtected, vcPublic, vcPublished]) FIELDS([vcPrivate, vcProtected, vcPublic, vcPublished])}

Pierwszy z nich usuwa wszystkie informacje o polach, właściwościach i metodach. Drugi z kolei umieszcza absolutnie wszystkie możliwe dane.

Należy zaznaczyć, że każdy unit ma niewidoczne ustawienie na początku nakazujące użycie widoczności jak dla klasy bazowej, czyli {$RTTI INHERIT}. Ma to miejsce od wersji XE2. Wcześniej wystarczyło zdefiniowane na poziomie projektu, czyli w pliku dpr. Dlatego chcąc zmienić te informacje, stosowna dyrektywa musi znaleźć się w każdym z unitów. Przy czym może ona być równie dobrze dodana na początku jeszcze przed częścią interfejsową, jak i bezpośrednio przed poszczególnymi klasami.

Jakie elementy zatem będą widoczne dla tak zdefiniowanych klas:

unit Klasy; {$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])} interface type TMyClass1 = class strict private fField1: Integer; property Prop1: Integer; procedure Method1; strict protected fField2: Integer; property Prop2: Integer; procedure Method2; public fField3: Integer; property Prop3: Integer; procedure Method3; end; {$RTTI INHERIT} TMyClass2 = class strict private fField1: Integer; property Prop1: Integer; procedure Method1; strict protected fField2: Integer; property Prop2: Integer; procedure Method2; public fField3: Integer; property Prop3: Integer; procedure Method3; end; {$RTTI EXPLICIT METHODS([vcPublic]) PROPERTIES([vcProtected]) FIELDS([])} TMyClass3 = class(TMyClass1) strict private fFieldA: Integer; property PropA: Integer; procedure MethodA; strict protected fFieldB: Integer; property PropB: Integer; procedure MethodB; public fFieldC: Integer; property PropC: Integer; procedure MethodC; end;

Dla klasy TMyClass1 nie zobaczymy nic, gdyż leży ona w obszarze wycięcia wszystkich informacji.

Dla klasy TMyClass2 zobaczymy pola fField1, fField2 oraz fField3, a także właściwość Prop3 i metodę Method3, gdyż takie są domyślne widoczności dla TObject, z której (niejawnie) dziedziczy.

Dla ostatniej klasy TMyClass3 zobaczymy tylko właściwość PropB i metodę MethodC, gdyż ponownie nastąpiła redefinicja.

Kolejne dwie dyrektywy odnoszą się do całego projektu.

Pierwszą jest {$WEAKLINKRTTI ON} (w szczególności używana właśnie z podanym parametrem). Powoduje ona usunięcie informacji z RTTI o typach, które nie są jawnie w kodzie używane.

Druga to {$STRONGLINKTYPES ON, która wymusza umieszczenie wszystkich typów, jakie występują w dołączonych do projektach unitach. Jest to też domyślna dyrektywa dla plików BPL (które po prostu muszą dostarczać wszystkie klasy).

Aby lepiej zobrazować posłużmy się następującymi kodami. Zdefiniujmy w unicie 3 klasy:

unit p2_c1; interface type TMyClass1 = class end; TMyClass2 = class(TMyClass1) end; TMyClass3 = class public procedure Start; end; implementation { TMyClass3 } procedure TMyClass3.Start; begin TMyClass1.Create.Free; end; end.

Mamy tu następujące zależności: klasa TMyClass2 dziedziczy z TMyClass1, zaś klasa TMyClass3 wykorzystuje klasę TMyClass1.

Wykonajmy też listowanie dostępnych klas prostym kodem:

procedure ListClasses; var ctx: TRTTIContext; &type: TRttiType; begin ctx := TRttiContext.Create; try for &type in ctx.GetTypes do begin if (&type.TypeKind = tkClass) then writeln(&type.Name); end; finally ctx.Free; end; end;

Dodajmy też w kodzie odwołanie do klasy TMyClass3 poprzez jej utworzenie i zwolnienie:

TMyClass3.Create.Free

Jeśli nie użyjemy żadnych dyrektyw kompilatora, to poza klasami systemowymi wynikającymi z użycia samego RTTI, otrzymamy w wyniku klasy TMyClass3 (z racji użycia) oraz TMyClass1 (gdyż do niej odwołuje się ona w swoim kodzie ta pierwsza).

Dodanie dyrektywy {$WEAKLINKRTTI ON} sprawi, że z wyniku zniknie TMyClass1 ponieważ kod, który się do niej odwołuje, nie jest używany.

Jeśli z kolei użyjemy dyrektywy {$STRONGLINKTYPES ON}, to pojawią się wszystkie 3 klasy, gdyż ta dyrektywa zmusza do dołączenia informacji o wszystkich klasach z kompilowanego kodu nawet, jeśli są one nieużywane. To ustawienie jest silniejsze od poprzedniego, więc poprzednie traci w takiej sytuacji swoją moc.

Bardziej obrazowo przedstawi to poniższa tabelka:

Atrybuty

Atrybuty pozwalają na umieszczanie dodatkowych obiektów (i informacji, jakie one przechowują) w informacjach RTTI kolekcjonowanych przez kompilator. Umieszcza się je w nawiasach kwadratowych przed danym elementem (klasą, polem, właściwością, metodą czy nawet parametrem - w tym ostatnim przypadku dopiero od Delphi XE7) zgodnie z konwencją, jaką dopuszcza konstruktor atrybutu. Może być to atrybut bezparametrowy (wówczas samo jego istnienie może nieść informację), ale może też posiadać wiele parametrów podobnie jak i wiele przeładowanych konstruktorów.

W odróżnieniu od większości typów, atrybuty posługują się inną konwencją nazewnictwa. Po pierwsze nie poprzedza się ich zwyczajowo wyróżnikiem T. Po drugie, jeśli jego nazwa zakończy się słowem Attribute, to to słowo może być pomijane podczas opisywania kodu tym atrybutem.

Atrybut jest tworzony w momencie, gdy poprzez RTTI zażądamy listy atrybutów. To oznacza, że jeśli obiekt podczas tworzenia zwróci wyjątek, to dowiemy się o nim dopiero w tym momencie, a nie, jak można by się spodziewać, na etapie kompilacji.

Przyjrzyjmy się definicji atrybutów - bezparametrowego i z pojedynczym parametrem:

interface type ZnacznikAttribute = class(TCustomAttribute) end; OpisAttribute = class(TCustomAttribute) strict private fOpis: String; public constructor Create(const aOpis: String); reintroduce; property Opis: String read fOpis; end; implementation uses System.SysUtils; { OpisAttribute } constructor OpisAttribute.Create(const aOpis: String); begin inherited Create; fOpis := aOpis; end;

Aby wykorzystać, wystarczy teraz umieścić w sekcji uses odwołanie do tej definicji (w przeciwnym wypadku kompilator wypluje ostrzeżenie W1025 Unsupported language feature: 'custom attribute') oraz opisać atrybutami kod. Warto zwrócić uwagę, że dany atrybut może pojawiać się wiele razy:

type [Opis('To jest moja klasa')] TMyClass = class strict private [Opis('To jest pole')] [Opis('To pole przechowuje wartość')] [Znacznik] fVal: Integer; public [Opis('To jest konstruktor')] constructor Create([Opis('To jest wartość na początek')] const aValue: Integer); [Opis('Ta funkcja zwiększa wartość o 1 i zwraca wynik')] function Inc: Integer; end;

Aby pobrać listę atrybutów, każdy z typów RTTI opisujących element możliwy do opisania atrybutami posiada metodę GetAttributes:

var a: TCustomAttribute; begin … for a in (…).GetAttributes do if a is OpisAttribute then writeln('Opis: ' + OpisAttribute(a).Opis) else writeln('Atrybut klasy ' + a.ClassName); end;

W tym miejscu sprawdza się warunkowanie, czy atrybut jest danego typu. Ale możliwe jest też... Ponowne wykorzystanie RTTI w odniesieniu do atrybutów w celu np. sprawdzenia nazwy czy poznania właściwości. Choć częściej jednak wykorzysta się pierwszą z możliwości.

Ograniczenia atrybutów

Niestety, ze względu na sposób działania atrybutów, posiadają one też pewne swoje ograniczenia w odniesieniu do parametrów kontruktorów. Oczywistym jest, że można stosować w nich wyłącznie stałe wartości. Zatem odpadają wszelkie obiekty (pomimo tego, że atrybut jest tworzony dopiero w momencie jego pobrania) czy zmienne.

Ale na tym ograniczenia się nie kończą. RTTI, jak już wcześniej wspomniałem, nie radzi sobie z typami wyliczeniowymi z narzuconymi wartościami. Stąd parametr z taką wartością wyliczeniową będzie powodował błąd o treści EInsufficientRtti: Insufficient RTTI available to support this operation (por. kod poniżej, linia 10). Jednak można temu niec zaradzić - wystarczy zdefiniować parametr typu liczbowego i dokonać rzutowania podczas oznaczania kodu atrybutem czyli zdefiniować jako [Fail(Integer(NotFound))].

Innym, być może mniej intuicyjnym, ale za to dającym błędy już na etapie kompilacji w momencie użycia takiego konstruktora, jest chęć użycia tablicy stałych, badź to dynamicznej (array of ...), bądź statycznej (array[0..1] of ...). Choć z punktu kodu jest z góry określony, to nie wypełnia on definicji stałej dla kompilatora. Jeśli zależy nam na zmiennej ilości parametrów, a wiemy, że nie będzie ich więcej niż np. 3, można poradzić sobie kolejnymi przeładowanymi wersjami konstruktora lub używając wartości domyślnych, jeśli to możliwe (por. kod poniżej, linie 15-18).

Dokładnie ten sam problem, co wcześniej dotyka rekordy. Także w tym przypadku próba użycia atrybutu z parametrem rekordu skończy się błędem kompilatora E2026 Constant expression expected.

Kolejnym typem, który jest podobnym do wcześniejszych jest PChar, który de facto jest wskaźnikiem, a nie stałą wartością.

Nie ma możliwości także używania typów wariantowych (Variant), choć w tym przypadku kompilacja przebiegnie poprawnie. Jednak próba dobrania się do parametru skończy się wyjątkiem EVariantInvalidOpError: Invalid variant operation.

W starszych wersjach Delphi, sprzed 10.3 problemem były też rozległe zbiory, np. set of char. Próba użycia tego typu w parametrze kontruktora atrybutu kończyła się podobnie, jak ma to miejsce w przypadku wszelkich danych dynamicznych. W nowszych wersjach jednak nie jest już to problemem.

Poniżej przedstawiono kod z opisywanymi wcześniej parametrami w konstruktorach atrybutów wraz z opisem. Warto zwrócić uwagę, że sam w sobie jest w pełni kompilowalny.

type TMyEnum = (NotFound = 404, ServerError = 500); TArr = array[0..3] of Integer; TMyRec = record a, b: Integer; end; FailAttribute = class(TCustomAttribute) public constructor Create(const aEnum: TMyEnum); overload; // typ wyliczeniowy z narzuconymi wartościami spowoduje bład przy próbie odczytu atrybutu constructor Create(const aArr: array of Integer); overload; // to spowoduje błąd kompilacji przy próbie zdefiniowana atrybutu z tym kontruktorem constructor Create(const aArr: TArr); overload; // ten tak samo // Obejście: constructor Create(const aElem1: Integer); overload; // na 1 element constructor Create(const aElem1, aElem2: Integer); overload; // na 2 elementy constructor Create(const aElem1, aElem2, aElem3: Integer); overload; // na 3 elementy constructor Create(const aElem1, aElem2, aElem3, aElem4: Integer; const aElem5: Integer = 0; const aElem6: Integer = 0); overload; // na 4-6 elementów //---------- constructor Create(const aRec: TMyRec); overload; // to także nie zadziała przy próbie użycia constructor Create(const aStr: PChar); overload; // to wskaźnik, a nie stała wartość constructor Create(const aVar1, aVar2: Variant); overload; // taki atrybut zgłosi błąd przy dostępie end;

Odczytywanie i nadawanie wartości oraz wywoływanie metod

Aby móc przejść dalej, nieodzownym staje się zaznajomienie z typem TValue, który pojawił się wraz z nową wersją RTTI w Delphi 2010. Jest on dość podobny do dobrze znanego typu Variant, ale na pewno nie jest z nim równoważny i nie należy go wykorzystywać do swobodnego zamieniania typów danych, mimo że pozwala na przechowywanie różnych typów i dla typów kompatybilnych umożliwia konwersję. Ale po określeniu typu nie można już tego zmieniać. Przeznaczony jest do przenoszenia danych do i z systemu RTTI. Przyjrzyjmy się poniższemu przykładowi, który obazuje, co można, czego nie można i jak tworzyć nawet złożone typy:

type TMyRec = record Val: Integer; Arr: array[0..1] of Char; end; var v: TValue; i: Integer; r, r2: TMyRec; begin v := 123; i := v; // tak nie można! i := v.AsInteger; // tak można v := v.Cast<Byte>; // tak można zmienić typ na kompatybilny if v.IsType then writeln('V jest typu Byte'); r.Val := 1; r.Arr[0] := 'a'; r.Arr[b] := 'b'; TValue.Make(@r, TypeInfo(TMyRec), v); r2 := v.AsType>TMyRec>; end;

W pierwszych wierszach widać różnicę między TValue a Variant. Do rzutowania typu TValue na określony typ służy metoda generyczna Cast<T>. Do określania kompatybilności można posłużyć się IsType<T>.

Bez wątpienia ciekawym jest ostatni przykład pokazujący, jak w zmiennej TValue można przechować bardziej złożone struktury danych przy użyciu klasowej metody Make oraz jak takie dane odczytać do zmiennej poprzez AsType.

Mając już wiedzę na temat działania typu TValue łatwo można przejść zarówno do odczytywania wartości pól i właściwości, nadawania a także wywoływania metod wraz z parametrami.

Zacznijmy od najprostszego przypadku, a mianowicie odczytu wartości z pól. Wystarczy do tego prosty kod:

procedure TExecutor.ListProperties(const aObj: TObject); var ctx: TRttiContext; rt: TRttiType; pt: TRttiProperty; attr: TCustomAttribute; s: String; i: Integer; begin ctx := TRttiContext.Create; try rt := ctx.GetType(aObj.ClassType); for pt in rt.GetProperties do begin if not pt.IsReadable then Continue; s := ''; for attr in pt.GetAttributes do if attr is OpisAttribute then s := OpisAttribute(attr).opis; s := s + ' [' + pt.Name + '] = ' + pt.GetValue(obj).ToString; writeln(s); end; finally ctx.free; end; end;

Co ma tu miejsce? Z przekazanego obiektu odczytujemy przez RTTI informacje o klasie. Następnie przechodzimy do listowania właściwości. Sprawdzamy, czy właściwość pozwala na odczyt - jeśli nie jest do odczytu - ignorujemy. Przy okazji, poza nazwą właściwości, szukamy też zdefiniowanego przez nas atrybutu z opisem. Na koniec wystarczy proste pobranie wartości i - w większości przypadków - konwersja dzięki metodzie ToString. Jeśli jednak chcielibyśmy bardziej zaawansowaną obsługę wyświetlania, w tym typów złożonych bądź innych obiektów, jakie by były we właściwościach, trzeba by oczywiście tą część rozbudować. Tak, jak może mieć to miejsce w przypadku ustawiania wartości. Jednak nim do tego przejdziemy, przeanalizujmy kod tworzenia obiektu danej klasy z konstruktorem, który może posiadać parametry. Warto zwrócić uwagę, że często konstruktor może być dziedziczony, dlatego szukając go, lepiej użyć metody GetMethods, a nie GetDeclaredMethods.

function CreateNew(const aClass: TClass): TObject; var ctx: TRttiContext; rt: TRttiType; mt: TRttiMethod; params: TArray>TValue>; begin writeln; writeln('Wybierz obiekt:'); ctx := TRttiContext.Create; try rt := ctx.GetType(aClass); if rt = nil then Exit(nil); // szukamy konstruktora: for mt in rt.GetMethods do if mt.MethodKind = mkConstructor then begin params := GetParameters(mt); Result := mt.Invoke(aClass, params).AsObject; Exit; end; finally ctx.Free; end; end;

To prosta funkcja, która utworzy obiekt przekazanej klasy wywołując pierwszy napotkany konstruktor (można się pokusić o obsługę przeładowanych konstruktorów).

Używa ona funkcji pobierania parametrów. Zatem jak wygląda wylistowanie i pobranie wartości?

function GetParameters(const aMethod: TRttiMethod): TArray>TValue>; var p: TRttiParameter; attr: TCustomAttribute; s: String; paramsList: TList>TValue>; begin paramsList := TList>TValue>.Create; try // sprawdzamy parametry: for p in aMethod.GetParameters do begin s := p.Name; limitAttr := nil; //szukamy atrybutu opisu for attr in p.GetAttributes do if attr is OpisAttribute then s := OpisAttribute(attr).Opis; s := s + ': ' + p.ParamType.Name; write(s + ' = '); repeat try readln(s); paramsList.Add(StrToVal(s, p.ParamType)); Break; except on E: Exception do writeln(E.Message); end; until False; end; Result := paramsList.ToArray; finally paramsList.Free; end; end;

Ta funkcja z kolei używa następnej, pozwalającej na konwersję tekstu, jakim podamy parametr do wartości TValue. To właśnie kluczowa funkcja związana z koniecznością odpowiedniej konwersji między typami i poprawnym nadaniem wartości:

function StrToVal(const aParamStr: String; const aParamType: TRttiType): TValue; var LElemType, LElemType2: TRttiType; lValArray: TArray>TValue>; lStrArray: TArray>string>; i: Integer; ctx: TRttiContext; EnumOrd: Integer; SetIntVal: Int64; begin case aParamType.TypeKind of tkInteger: Result := StrToInt(aParamStr); tkChar, tkWChar: Result := aParamStr[1]; tkEnumeration: if SameText(aParamType.Name, 'Boolean') then Result := SameText(aParamStr, 'True') or SameText(aParamStr, '1') else begin ctx := TRttiContext.Create; try lElemType := ctx.GetType(aParamType.Handle); if (lElemType <> nil) and (lElemType is TRttiEnumerationType) then begin EnumOrd := GetEnumValue(aParamType.Handle, aParamStr); if EnumOrd < 0 then raise EIncorrectValue.CreateFmt('Wrong enum name (%s)', [aParamStr]); Result := TValue.FromOrdinal(aParamType.Handle, EnumOrd); end; finally ctx.Free; end; end; tkFloat: if SameText(aParamType.Name, 'TDateTime') or SameText(aParamType.Name, 'TDate') then Result := System.DateUtils.ISO8601ToDate(aParamStr, False) else if SameText(aParamType.Name, 'TTime') then Result := StrToTime(aParamStr) else Result := StrToFloat(aParamStr); tkString, tkLString, tkUString, tkWString: Result := aParamStr; tkInt64: Result := StrToInt64(aParamStr); tkSet: begin LElemType := aParamType.AsSet.ElementType; lStrArray := aParamStr.Split([',']); if (lElemType <> nil) and (lElemType is TRttiEnumerationType) then begin SetIntVal := 0; for i := 0 to High(lStrArray) do begin EnumOrd := GetEnumValue(lElemType.Handle, lStrArray[i].Trim); if EnumOrd < 0 then raise EIncorrectValue.CreateFmt('Wrong enum name (%s)', [lStrArray[i]]); SetIntVal := SetIntVal or (1 shl EnumOrd); end; Tvalue.Make(@SetIntVal, aParamType.Handle, Result); end; end; tkArray, tkDynArray: begin if aParamType is TRttiDynamicArrayType then LElemType := TRttiDynamicArrayType(aParamType).ElementType else if aParamType is TRttiArrayType then LElemType := TRttiArrayType(aParamType).ElementType else raise EConvertError.Create('Nieobsługiwany typ tablicy'); lStrArray := aParamStr.Split([',']); SetLength(lValArray, Length(lStrArray)); for i := 0 to High(lStrArray) do case LElemType.TypeKind of tkInteger: lValArray[I] := StrToInt(lStrArray[i].Trim); tkChar: lValArray[I] := lStrArray[i].Trim[1]; tkEnumeration: if SameText(LElemType.Name, 'Boolean') then lValArray[I] := SameText(lStrArray[i].Trim, 'True') or (StrToIntDef(lStrArray[i].Trim, 0) > 0) else begin ctx := TRttiContext.Create; try lElemType2 := ctx.GetType(lElemType.Handle); if (lElemType2 <> nil) and (lElemType2 is TRttiEnumerationType) then begin EnumOrd := GetEnumValue(lElemType.Handle, lStrArray[i].Trim); if EnumOrd < 0 then raise EIncorrectValue.CreateFmt('Wrong enum name (%s)', [lStrArray[i].Trim]); lValArray[I] := TValue.FromOrdinal(lElemType.Handle, EnumOrd); end; finally ctx.Free; end; end; tkFloat: if SameText(LElemType.Name, 'TDateTime') or SameText(LElemType.Name, 'TDate') then lValArray[I] := System.DateUtils.ISO8601ToDate(lStrArray[i].Trim, False) else if SameText(LElemType.Name, 'TTime') then lValArray[I] := StrToTime(lStrArray[i].Trim) else lValArray[I] := StrToFloat(lStrArray[i].Trim); tkInt64: lValArray[I] := StrToInt64(lStrArray[i].Trim); else lValArray[I] := lStrArray[i].Trim; end; Result := TValue.FromArray(aParamType.Handle, lValArray); end; else raise EConvertError.Create('Nieobsługiwany typ zmiennej'); end; end;

Choć ta funkcja nie obsługuje wciąż wszystkich typów (wykluczono np. inne obiekty czy rekordy, których sposób wprowadzania musiałby być bardziej złożony, np. w postaci zapisu JSON lub XML), to przedstawia pracę z większością podstawowych typów, w tym z tablicami typów prostych. Warto też zwrócić uwagę na fakt traktowanie typu Boolean jako wyliczeniowego oraz konwersję miedzy nazwami a wartościami typów wyliczeniowych.

Gdzie RTTI ma zastosowanie?

Użycie RTTI z pewnością pozwala na lepszą segregację zadań. Programista zmieniając tylko jeden, swój plik, może wprowadzać daleko idące zmiany, a jednocześnie nie musi przy tym ingerować w inne pliki. Na tej samej zasadzie można dynamicznie tworzyć UI dostarczając wyłącznie obiekty obliczeniowe odpowiednio opisane.

RTTI pozwala także na bardzo swobodną serializację i deserializację danych wg dowolnych standardów czyniąc też taki kod uniwersalnym, mogącym pracować z typami, o których nie ma się pojęcia na etapie projektowania kodu. Dzięki temu kod pozostaje niezmienny, a zmieniają się tylko dane.

Niewątpliwą swobodę daje też przy tworzeniu testów jednostkowych (co wykorzystuje np. framework DUnitX). Ponownie, dobrze opisana i stworzona klasa automatycznie podda się testom bez potrzeby generowania odrębnego kodu testującego.

RTTI pomoże także w prowadzeniu przeglądu kodu (z ang. code rewiev) pomagając zweryfikować trzymania się pewnych założeń, np. dotyczących nazewnictwa, używania określonych typów, konwencji i innych wytycznych szczególnie istotnych przy pracach zbiorowych.

Bez wątpienia daje też ogromny potencjał przy budowie API wraz z automatycznym tworzeniem dokumentacji kodu, co eliminuje zarówno potrzebę dwukrotnej pracy jak i zapewnia znacznie lepszą zgodność i odporność na błędy w dokumentacji (choćby takie dotyczące literówek czy opisu nieistniejących funkcji, albo braku tych, które zostały utworzone), gdyż programista tworząc i opisując kod skupia się wciąż w jednym obszarze.

Innym pomocnym elementem może być stworzenie rozszerzonego narzędzia do logowania błędów, które nie tylko będzie już wyposażone w komunikat czy stos wywołań, ale może dołączać całą konfigurację obiektu, w którym powstaje wyjątek i to niezależnie od miejsca wywołania takiej funkcji logującej.

Przykłady do pobrania

Dostępne są do pobrania przykłady kodu źródłowego obrazujące poruszone powyżej zaganienia. W plikach dpr na początku umieszczono w komentarzach krótki opis, na czym skupia się dany projekt. Dostępnych jest 6 różnych projektów.

Większość projektów bazuje na tych samych plikach np. do pozyskiwania informacji RTTI. Dodatkowo w pliku ConsoleUtils.pas są funkcje, które automatycznie maksymalizują okno konsoli, zmieniają czcionkę czy pozwalają na kolorowanie znaków w systemie Windows.

Informacje o poradzie
Poradę czytano:102 razy
Ocena porady:
brak
(Kliknij właściwą gwiazdkę, by oddać głos)

Wróć

Komentarze (0)


Ładowanie komentarzy... Trwa ładowanie komentarzy...

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-2025 Dawid Najgiebauer. Wszelkie prawa zastrzeżone.
Ostatnia aktualizacja podstrony: 11.10.2025 09:04
Wszystkie czasy dla strefy czasowej: Europe/Warsaw