1
    type
      TMyRecord = record
      private
        class constructor create;
      public
        class var MyField1: string;                
        class var MyField2: integer;            
        class var MyField3: extended;             
        class function ToString: string; static;
      end;

    class constructor TMyRecord.Create;
    begin
        TMyRecord.MyField1 := 'Hello, world!';
        TMyRecord.MyField2 := 123;
        TMyRecord.MyField3 := 3.1415927;
    end;  

    class function TMyRecord.ToString: string;
    var
      RecType: TRTTIType;
      RecFields: TArray<TRttiField>;
      I: integer;
    begin
      RecType := TRTTIContext.Create.GetType(TypeInfo(TMyRecord));
      Result := RecType.ToString;
      RecFields := RecType.GetFields;
      for I := 0 to High(RecFields) do
        Result := Result + Format('%s: %s = %s', [RecFields[I].Name, RecFields[I].FieldType.ToString, RecFields[I].GetValue(@TMyRecord).ToString]) + sLineBreak;
    end;

I am trying to get TMyRecord.ToString to return:

    TMyRecord
    MyField1: string = Hello, world!
    MyField2: integer = 123;
    MyField3: extended = 3.1415927;

However, I get a compiler error on GetValue(@TMyRecord) - E2029 '(' expected but ')' found

Normally GetValue should be called with the address of the 'instance' of the record. But in this case the record is static. I do not want to convert this record to a normal record, create an instance etc. I'm trying to solve this for a static record. How can I get the address that should be passed to GetValue?

Rob
  • 155
  • 1
  • 12
  • Why use RTTI, when you just can get the field value directly? (MyField1,MyField2,MyField3). – LU RD Mar 29 '17 at 08:42
  • 1. Because I am not only interested in the field value, but also the field name and field type. 2. Because this is only an example. In my actual code the record contains 974 fields. Iterating through those fields takes 5 lines of code. Accesssing them one by one would take 974 lines of code. 3. Because when new fields are added to the record, they will be automatically included in the output of ToString. etc. etc. – Rob Mar 29 '17 at 10:03
  • Thanks for explaining the motivation. See my answer which will take care of your problems. – LU RD Mar 29 '17 at 13:03

3 Answers3

3

To the best of my knowledge, class fields are not accessible through RTTI.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Damn, I was rather hoping for a different answer. But if that's the way it is... Thanks for answering! – Rob Mar 29 '17 at 10:05
  • 1
    Depending on why you want to use static records, you can of course make a normal record look like a static record to other units. – Dsm Mar 29 '17 at 12:38
2

As said in another answer, it is not possible to get access to class var fields with RTTI.

But with a small adjustment it is still possible to get the wanted solution:

program TestClassVar;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,System.RTTI;

type
  TMyRecord = record
    type
      TIR = record
        MyField1: string;
        MyField2: integer;
        MyField3: extended;
      end;
  private
    class constructor create;
  public
    class var r: TIR;
    class function ToString: string; static;
  end;

class constructor TMyRecord.Create;
begin
  TMyRecord.r.MyField1 := 'Hello, world!';
  TMyRecord.r.MyField2 := 123;
  TMyRecord.r.MyField3 := 3.1415927;
end;

class function TMyRecord.ToString: string;
var
  RecType: TRTTIType;
  RecFields: TArray<TRttiField>;
  I: integer;
  firstFieldAdr: Pointer;
begin
  RecType := TRTTIContext.Create.GetType(TypeInfo(TMyRecord));
  Result := RecType.ToString + sLineBreak;
  RecType := TRTTIContext.Create.GetType(TypeInfo(TIR));
  RecFields := RecType.GetFields;

  firstFieldAdr := Addr(TMyRecord.r);
  for I := 0 to High(RecFields) do
    Result :=
      Result +
      Format('%s: %s = %s',
        [RecFields[I].Name,
        RecFields[I].FieldType.ToString,
        RecFields[I].GetValue(firstFieldAdr).ToString])
        +
      sLineBreak;    
end;

begin
  WriteLn(TMyRecord.ToString);
  ReadLn;
end.

The fields are put into an inner record and the address of the first field is resolved via the Addr() function.

The drawback is that the access to the fields will have an extra r. specifier.


Actual output:

TMyRecord
MyField1: string = Hello, world!
MyField2: Integer = 123
MyField3: Extended = 3.1415927 
LU RD
  • 34,438
  • 5
  • 88
  • 296
0

Thank you LU RD for your contribution! Quite clever, using a nested record and declaring a local class var 'r', allowing access to both the type as well as an instance.

In the mean time I had come up with another solution, although that uses a class (with an external instance) instead of a record, which I didn't want to do, but I didn't see another way, so thanks for your solution!

type
  TMyRecord = class
  private
    constructor create;
  public
    MyField1: string;                
    MyField2: integer;            
    MyField3: extended;             
    function ToString: string; 
  end;
var
  MyRecord: TMyRecord;

constructor TMyRecord.Create;
begin
  MyField1 := 'Hello, world!';
  MyField2 := 123;
  MyField3 := 3.1415927;
end;  

function TMyRecord.ToString: string;
var
  Field: TRttiField;
begin
  Result := sLineBreak;
  for Field in TRTTIContext.Create.GetType(Self.ClassType).GetFields do
    Result := Result + Format('%s: %s = %s', [Field.Name, Field.FieldType.ToString, Field.GetValue(Self).ToString]) + sLineBreak;
end;
Rob
  • 155
  • 1
  • 12