We've come a long way. Part 1 explained what RTTI is — reflection, code that inspects types it didn't know at compile time — and introduced TRttiContext and TValue. Part 2 read an object's properties, fields, and attributes, and wrote values back by name with GetValue/SetValue. There's one verb left, and it's the most dramatic: calling a method by its name, as a string.
If reading a property by name felt like a small magic trick, invoking a method by name is the whole show. It means you can call DoThing on an object when the string 'DoThing' only arrives at runtime — from a script, a config file, a message from a server, a plugin. It's how command dispatchers, scripting bridges, and test runners work.
Part 3 covers invocation — TRttiMethod.Invoke, passing arguments and reading return values as TValue — and then, to tie the whole series together, we'll build one small, genuinely useful tool with everything we've learned: a generic object-to-CSV exporter that works on any list of any class, using nothing but reflection. Let's finish strong.
Calling a method by name
The setup mirrors Part 2 exactly: open a context, get the type, but this time ask for a method and Invoke it. Suppose we have a class with a method that does something and returns a result:
type
TCalculator = class
public
function Add(A, B: Integer): Integer;
procedure Reset;
end;Here's how you call Add(2, 3) when 'Add' is a runtime string — note that the arguments go in as an array of TValue, and the result comes back as a TValue:
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]); // call Add(2, 3)
ShowMessage(Res.AsInteger.ToString); // '5'
finally
Ctx.Free;
Calc.Free;
end;
end;Look at the signature the RTL gives us: Invoke(Instance: TObject; const Args: array of TValue): TValue. Every argument is a TValue (the integers 2 and 3 box themselves via the implicit operators from Part 1), and the return value is a TValue you unwrap with AsInteger, AsString, or whichever accessor fits. For a procedure like Reset that returns nothing, you simply ignore the (empty) result.
The diagram is the entire mechanism: a method name plus boxed arguments go in, the method runs on your live object, and a boxed result comes back. A command dispatcher is just this call with the name and args pulled from an incoming message.
The capstone: a generic CSV exporter
Let's put all three parts to work on something you might actually ship. The goal: export any TObjectList<T> to CSV — header row from the property names, one line per object — with a single routine that knows nothing about the classes it's handed. This is the kind of small win reflection is perfect for.
We'll even let a class opt a property out of the export with a custom attribute, tying in Part 2. First the attribute:
type
// Mark a property to be skipped by the exporter (Part 2's attributes)
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] // never export this
property PasswordHash: string read FPasswordHash write FPasswordHash;
end;Now the exporter. It walks the properties once to build the header (skipping anything marked [NoExport]), then walks each object's values to build the rows — using GetProperties, GetValue, HasAttribute<T>, and TValue.ToString from across the series:
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;
// include a property only if it's readable and not marked [NoExport]
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. Header row — property names
Line := [];
for Prop in Props do
if Exported(Prop) then
Line := Line + [Prop.Name];
SB.AppendLine(string.Join(',', Line));
// 2. One row per object — property values via 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;Feed it a list of employees and you get:
Name,Salary
Ada Lovelace,95000
Grace Hopper,102000
PasswordHash is absent — the [NoExport] attribute did its job — and, crucially, this routine will export TCustomer, TInvoice, or any class you write next year, with zero changes. That's the whole promise of reflection delivered in about forty lines: write the walker once, reap it forever. Here's the flow at a glance.
The diagram is the tool in one line: a list of unknown objects goes in, the reflection walk turns property names into a header and property values into rows, and CSV comes out — no per-class code anywhere.
The other side: is reflection the right call here?
Guarantee to the reader — even for a nice tool, ask whether reflection earns its place. For a CSV exporter that must handle arbitrary classes across your codebase, yes: the alternative is a hand-written exporter per class, which is exactly the boilerplate reflection exists to erase. But if you only ever export one known class, a plain hand-written function is simpler, faster, and safer — no TValue boxing, full compile-time checking. The rule from Part 1 holds all the way to the end: reflect when you're writing code that must span many types you don't control; write direct code when you know the type. The exporter qualifies because "any class" is in its very signature.
Takeaways — the whole series
Three parts took us from "what is RTTI" to a working tool. The complete picture:
- Invoke a method by name with
TRttiMethod.Invoke(Instance, [args])— arguments go in asTValue, the result comes back asTValue. The overloads even let you call constructors and class methods reflectively (the heart of DI containers). - Reflection loses compile-time checking: wrong arguments raise
EInvocationErrorat runtime. Guard invocations built from external input. - The capstone exporter shows the payoff:
GetProperties+GetValue+HasAttribute<T>produce a CSV writer that works on any class, written once — the essence of every serializer and mapper. - The through-line of all three parts: reflect when your code must span types you don't control; write direct code when you know the type.
Invocation completes the trio — read (
GetValue), write (SetValue), and call (Invoke) any member by name — turning "types as data" into working tools like a class-agnostic CSV exporter in a few dozen lines.
That closes the RTTI series. If you want the foundations again, Part 1 is the "what and why" and Part 2 is properties and attributes. You now understand the machinery behind Delphi's serializers, ORMs, and DI containers — and you can build your own when the need is genuinely yours. Go reflect on something.