Search code examples
delphiinheritanceconstantsclass-constants

Accessing Class Constants from a Class Reference variable in Delphi


I'm using Delphi 2007 to maintain an old project, I have a problem accessing class constants from a Class Reference variable, I get always the parent class constant instead of the children one.

Suppose to have a parent class, some child classes, a class reference and finally a const array to store the class references for looping purposes.

take a look at following simple program:

program TestClassConst;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type

  TParent = class
  const
    ClassConst = 'BASE CLASS';
  end;

  TChild1 = class(TParent)
  const
    ClassConst = 'CHILD 1';
  end;

  TChild2 = class(TParent)
  const
    ClassConst = 'CHILD 2';
  end;

  TParentClass = class of TParent;
  TChildClasses = array[0..1] of TParentClass;

const
  ChildClasses: TChildClasses = (TChild1, TChild2);

var
  i: integer;
  c: TParentClass;
  s: string;

begin
  try
    writeln;

    writeln('looping through class reference array');
    for i := low(ChildClasses) to high(ChildClasses) do begin
      c := ChildClasses[i];
      writeln(c.ClassName, ' -> ', c.ClassConst);
    end;

    writeln;

    writeln('accessing classes directly');
    writeln(TChild1.ClassName, ' -> ', TChild1.ClassConst);
    writeln(TChild2.ClassName, ' -> ', TChild2.ClassConst);

  except
    on E: Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.

When it runs I get:

looping through class reference array
TChild1 -> BASE CLASS
TChild2 -> BASE CLASS

accessing classes directly
TChild1 -> CHILD 1
TChild2 -> CHILD 2

I expected to see 'CHILD 1' and 'CHILD 2' also in array loop!

Can anyone explain me why it does not work with class reference?


Solution

  • An untyped class constant is a normal constant with some scoping added.
    A typed class constant is really a class variable that you cannot change.
    The problem is that the class variables are not virtual.

    Hallvard Vassbotn has written about this issue here: Part 1, Part 2

    You cannot access class variables and class constants from a class reference because the language does not have support for virtual class variables.
    When you say s:= TClass1.SomeConst the compiler translates this into s:= SomeGlobalButHiddenConst before moving on with the rest of the compilation.

    class var and class const are nothing more than syntactic sugar.
    As such the link between the class var/const and the actual class only exists during compile-time, it is broken come run-time, much like type-erasure in Java.

    RTTI also does not help: Get constant fields from a class using RTTI
    I guess if you're using D2007 your only option is to declare a virtual function that returns the constant you want:

    Pre D2010 option: virtual method

    TParent = class
      class function Name: string; virtual;
    end;
    
    TChild1 = class(TParent)
      class function name: string; override;
    ....
    class function TParent.name: string;
    begin
      Result:= Self.ClassConst;
    end;
    
    class function TChild1.name: string;
    begin
      Result:= Self.ClassConst;   //Silly copy paste solution
    end;
    

    This is a sad state of affairs, but I don't see another option.

    From Delphi 2010 onwards: use attributes
    A better option is to use attributes, these you can access using RTTI:

    The following code works:

    program TestClassConst;
    
    {$APPTYPE CONSOLE}
    
    uses
      SysUtils, rtti;
    
    type
    
      NameAttribute = class(TCustomAttribute)
      private
        Fname: string;
      public
        constructor Create(const Name: string);
        property Name: string read Fname;
      end;
    
      [Name('Base class')]
      TParent = class
      const
        ClassConst = 'BASE CLASS';
      private
      public
        class function Name: string;
      end;
    
      [Name('Child 1')]
      TChild1 = class(TParent)
      const
        ClassConst = 'CHILD 1';
      end;
    
      [Name('Child 2')]
      TChild2 = class(TParent)
      const
        ClassConst = 'CHILD 2';
      end;
    
      TParentClass = class of TParent;
      TChildClasses = array[0..1] of TParentClass;
    
    const
      ChildClasses: TChildClasses = (TChild1, TChild2);
    
    var
      i: integer;
      c: TParentClass;
      s: string;
    
    { TParent }
    
    class function TParent.Name: string;
    var
      Context: TRttiContext;
      ClassData: TRttiType;
      Attr: TCustomAttribute;
    begin
      Context:= TRttiContext.Create;
      ClassData:= Context.GetType(Self);
      try
        for Attr in ClassData.GetAttributes do begin
          if Attr is NameAttribute then Result:= NameAttribute(Attr).Name;
        end;
      finally
        ClassData.Free;
      end;
    end;
    
    { NameAttribute }
    
    constructor NameAttribute.Create(const Name: string);
    begin
      inherited Create;
      FName:= name;
    end;
    
    begin
      writeln;
    
      writeln('looping through class reference array');
      for i := low(ChildClasses) to high(ChildClasses) do begin
        c := ChildClasses[i];
        writeln(c.ClassName, ' -> ', c.Name);
      end;
    
      writeln;
    
      writeln('accessing classes directly');
      writeln(TChild1.ClassName, ' -> ', TChild1.Name);
      writeln(TChild2.ClassName, ' -> ', TChild2.Name);
      readln;
    end.