Search

Search all blog posts and tutorials by any word or term

← NewsAugust 5, 20268 min read

Delphi

RTTI Teil 3: Methoden aufrufen — und ein echtes Werkzeug

  • Delphi
  • RTTI
  • Reflection
  • RTL
  • Object Pascal

Wir haben einen weiten Weg zurückgelegt. Teil 1 hat erklärt, was RTTI ist — Reflection, also Code, der Typen inspiziert, die er zur Compile-Zeit nicht kannte — und TRttiContext und TValue eingeführt. Teil 2 hat die Properties, Felder und Attribute eines Objekts gelesen und Werte per Name mit GetValue/SetValue zurückgeschrieben. Ein Verb fehlt noch, und es ist das spektakulärste: eine Methode über ihren Namen aufrufen — als String.

Wenn sich das Lesen einer Property per Name wie ein kleiner Zaubertrick anfühlte, ist das Aufrufen einer Methode per Name die ganze Show. Es bedeutet, dass Sie DoThing auf einem Objekt aufrufen können, wenn der String 'DoThing' erst zur Laufzeit eintrifft — aus einem Skript, einer Konfigurationsdatei, einer Nachricht von einem Server, einem Plugin. Genau so funktionieren Command-Dispatcher, Scripting-Brücken und Test-Runner.

Teil 3 behandelt die InvocationTRttiMethod.Invoke, das Übergeben von Argumenten und das Auslesen von Rückgabewerten als TValue — und dann, um die ganze Serie abzurunden, bauen wir mit allem Gelernten ein kleines, wirklich nützliches Werkzeug: einen generischen Objekt-zu-CSV-Exporter, der mit jeder Liste jeder Klasse funktioniert — allein mit Reflection. Bringen wir es stark zu Ende.

Eine Methode über ihren Namen aufrufen

Der Aufbau spiegelt Teil 2 exakt wider: Context öffnen, den Typ holen — aber diesmal fragen wir nach einer Methode und rufen sie per Invoke auf. Angenommen, wir haben eine Klasse mit einer Methode, die etwas tut und ein Ergebnis zurückgibt:

type
  TCalculator = class
  public
    function Add(A, B: Integer): Integer;
    procedure Reset;
  end;

So rufen Sie Add(2, 3) auf, wenn 'Add' ein Laufzeit-String ist — beachten Sie, dass die Argumente als Array von TValue hineingehen und das Ergebnis als TValue zurückkommt:

uses
  System.Rtti;
 
var
  Ctx: TRttiContext;
  Calc: TCalculator;
  Method: TRttiMethod;
  Res: TValue;
begin
  Calc := TCalculator.Create;
  Ctx := TRttiContext.Create;
  try
    Method := Ctx.GetType(TCalculator).GetMethod('Add');
    Res := Method.Invoke(Calc, [2, 3]);      // Add(2, 3) aufrufen
    ShowMessage(Res.AsInteger.ToString);      // '5'
  finally
    Ctx.Free;
    Calc.Free;
  end;
end;

Betrachten Sie die Signatur, die uns die RTL gibt: Invoke(Instance: TObject; const Args: array of TValue): TValue. Jedes Argument ist ein TValue (die Integer 2 und 3 boxen sich selbst über die impliziten Operatoren aus Teil 1), und der Rückgabewert ist ein TValue, den Sie mit AsInteger, AsString oder dem jeweils passenden Accessor auspacken. Bei einer Prozedur wie Reset, die nichts zurückgibt, ignorieren Sie das (leere) Ergebnis einfach.

Invoke: Name + geboxte Argumente gehen hinein, ein geboxtes Ergebnis kommt zurück

Das Diagramm ist der gesamte Mechanismus: Ein Methodenname plus geboxte Argumente gehen hinein, die Methode läuft auf Ihrem lebenden Objekt, und ein geboxtes Ergebnis kommt zurück. Ein Command-Dispatcher ist genau dieser Aufruf, bei dem Name und Argumente aus einer eingehenden Nachricht stammen.

Das Finale: ein generischer CSV-Exporter

Setzen wir alle drei Teile für etwas ein, das Sie tatsächlich ausliefern könnten. Das Ziel: jede TObjectList<T> nach CSV exportieren — Kopfzeile aus den Property-Namen, eine Zeile pro Objekt — mit einer einzigen Routine, die nichts über die Klassen weiß, die man ihr übergibt. Das ist genau die Art kleiner Gewinn, für die Reflection perfekt ist.

Wir lassen eine Klasse eine Property sogar per Custom-Attribut vom Export ausnehmen — ein Rückgriff auf Teil 2. Zuerst das Attribut:

type
  // Markiert eine Property, die der Exporter überspringen soll (Attribute aus Teil 2)
  NoExportAttribute = class(TCustomAttribute);
 
  TEmployee = class
  private
    FName: string;
    FSalary: Currency;
    FPasswordHash: string;
  public
    property Name: string read FName write FName;
    property Salary: Currency read FSalary write FSalary;
    [NoExport]                                   // niemals exportieren
    property PasswordHash: string read FPasswordHash write FPasswordHash;
  end;

Nun der Exporter. Er läuft einmal über die Properties, um die Kopfzeile zu bauen (und überspringt alles, was mit [NoExport] markiert ist), dann über die Werte jedes Objekts, um die Zeilen zu erzeugen — mit GetProperties, GetValue, HasAttribute<T> und TValue.ToString aus der ganzen Serie:

uses
  System.Rtti, System.SysUtils, System.Classes, System.Generics.Collections;
 
function ExportToCsv<T: class>(const Items: TObjectList<T>): string;
var
  Ctx: TRttiContext;
  Typ: TRttiType;
  Props: TArray<TRttiProperty>;
  Prop: TRttiProperty;
  Item: T;
  Line: TArray<string>;
  SB: TStringBuilder;
 
  // Property nur aufnehmen, wenn sie lesbar und nicht mit [NoExport] markiert ist
  function Exported(const P: TRttiProperty): Boolean;
  begin
    Result := P.IsReadable and not P.HasAttribute<NoExportAttribute>;
  end;
 
begin
  Ctx := TRttiContext.Create;
  SB := TStringBuilder.Create;
  try
    Typ := Ctx.GetType(TClass(T));
    Props := Typ.GetProperties;
 
    // 1. Kopfzeile — Property-Namen
    Line := [];
    for Prop in Props do
      if Exported(Prop) then
        Line := Line + [Prop.Name];
    SB.AppendLine(string.Join(',', Line));
 
    // 2. Eine Zeile pro Objekt — Property-Werte per Reflection
    for Item in Items do
    begin
      Line := [];
      for Prop in Props do
        if Exported(Prop) then
          Line := Line + [Prop.GetValue(Item).ToString];
      SB.AppendLine(string.Join(',', Line));
    end;
 
    Result := SB.ToString;
  finally
    SB.Free;
    Ctx.Free;
  end;
end;

Geben Sie ihm eine Liste von Mitarbeitern, und Sie erhalten:

Name,Salary
Ada Lovelace,95000
Grace Hopper,102000

PasswordHash fehlt — das [NoExport]-Attribut hat seinen Dienst getan — und, entscheidend: Diese Routine exportiert TCustomer, TInvoice oder jede Klasse, die Sie nächstes Jahr schreiben, ohne eine einzige Änderung. Das ist das ganze Versprechen der Reflection, eingelöst in rund vierzig Zeilen: den Walker einmal schreiben, für immer profitieren. Hier der Ablauf auf einen Blick.

Ein generischer Walker: Property-Namen werden zur Kopfzeile, Werte zu Zeilen

Das Diagramm ist das Werkzeug in einer Zeile: Eine Liste unbekannter Objekte geht hinein, der Reflection-Walk macht aus Property-Namen eine Kopfzeile und aus Property-Werten Zeilen, und CSV kommt heraus — nirgendwo klassenspezifischer Code.

Die andere Seite: Ist Reflection hier die richtige Wahl?

Ein Versprechen an die Leserin und den Leser — auch bei einem schönen Werkzeug: Fragen Sie, ob Reflection ihren Platz verdient. Für einen CSV-Exporter, der beliebige Klassen quer durch Ihre Codebasis verarbeiten muss: ja. Die Alternative wäre ein handgeschriebener Exporter pro Klasse — genau der Boilerplate, den Reflection ausradieren soll. Wenn Sie aber nur je eine bekannte Klasse exportieren, ist eine schlichte handgeschriebene Funktion einfacher, schneller und sicherer — kein TValue-Boxing, volle Compile-Zeit-Prüfung. Die Regel aus Teil 1 gilt bis zum Schluss: Reflektieren Sie, wenn Ihr Code viele Typen abdecken muss, die Sie nicht kontrollieren; schreiben Sie direkten Code, wenn Sie den Typ kennen. Der Exporter qualifiziert sich, weil „jede Klasse" schon in seiner Signatur steht.

Fazit — die ganze Serie

Drei Teile haben uns von „Was ist RTTI" zu einem funktionierenden Werkzeug geführt. Das vollständige Bild:

  • Eine Methode per Name aufrufen mit TRttiMethod.Invoke(Instance, [args]) — Argumente gehen als TValue hinein, das Ergebnis kommt als TValue zurück. Die Overloads erlauben sogar den reflektiven Aufruf von Konstruktoren und Klassenmethoden (das Herz von DI-Containern).
  • Reflection verliert die Compile-Zeit-Prüfung: Falsche Argumente lösen zur Laufzeit einen EInvocationError aus. Sichern Sie Invocations ab, die aus externen Eingaben gebaut werden.
  • Der Exporter als Finale zeigt den Ertrag: GetProperties + GetValue + HasAttribute<T> ergeben einen CSV-Writer, der mit jeder Klasse funktioniert, einmal geschrieben — die Essenz jedes Serializers und Mappers.
  • Der rote Faden aller drei Teile: Reflektieren Sie, wenn Ihr Code Typen abdecken muss, die Sie nicht kontrollieren; schreiben Sie direkten Code, wenn Sie den Typ kennen.

Die Invocation vervollständigt das Trio — Lesen (GetValue), Schreiben (SetValue) und Aufrufen (Invoke) jedes Members per Name — und macht aus „Typen als Daten" funktionierende Werkzeuge wie einen klassenunabhängigen CSV-Exporter in wenigen Dutzend Zeilen.

Damit schließt die RTTI-Serie. Wenn Sie die Grundlagen noch einmal brauchen: Teil 1 ist das „Was und Warum", und Teil 2 behandelt Properties und Attribute. Sie verstehen jetzt die Maschinerie hinter Delphis Serializern, ORMs und DI-Containern — und Sie können Ihre eigene bauen, wenn der Bedarf wirklich Ihrer ist. Reflektieren Sie mal drüber.

Free to read, not free to make. If this article saved you time or taught you something, there's a way to give back.

How to support