4

What I want to achieve is just like the dummy code:

type
  CommandSetOne = (Command1, Command2, Command3);
  CommandSetTwo = (Command4, Command5, Command6);

  TRobot = class
    procedure RegisterCommands(anyEnumerationType : TRttiEnumerationType);
    procedure ExecuteCommands(anEnumeration : theEnumerationType);
  end;

Which I may have multiple set of command, and any command in command set is replaceable.

TRobot has a procedure can take a enumeration type as parameter, and he will save this type, use this type for the ExecuteCommands procedure.

About passing any enumeration type as parameter, I found out a way to do that is to use TRttiEnumerationType, in the call side it should looks like:

var
  rttiContext : TRttiContext;
  typeref : TRttiType;
  RobotA : TRobot;
begin
  rttiContext := TRttiContext.Create();
  RobotA := TRobot.Create();
  RobotA.RegisterCommands(rttiContext.GetType(TypeInfo(CommandSetOne)));
end;

but I got stuck on passing a Command like Command1. I have tried Variant for theEnumerationType but seems I can not pass Command1 to this.

I know if I use something like TStringList for this is a much easier way to do what I want, but I would like to have a check by delphi at complies time in case I mistype some command(use TstringList I can add code to check at runtime)

so the real problem is:

  1. which type should I use for theEnumerationType?

  2. if it's not possible to to this, any other solution to use Enumeration?

  3. or any solution can provide a complies time check as well as a flexible structure?

EDIT:

thanks for David suggested, I should use both Rtti things, so to make it clear, I add the implementation for RegisterCommands

implementation
  procedure TRobot.RegisterCommands(anyEnumerationType : TRttiEnumerationType);
    begin
    theEnumerationType := anyEnumerationType;
    end;
  procedure TRobot.ExecuteCommands (anyEnumerationValueoftheType : ???);
    begin
    //do something with the command
    end;

so what should fit for any enumeration value for the type?

for example, if I use CommandSetOne in RegisterCommands, how can delphi accept Command1 or Command2 or Command3?

more specifically, can delphi limit the room only for Command1 or Command2 or Command3? means if I put Command4 it give me an compile error?

Mengchao
  • 81
  • 7
  • 1
    Why are you using RTTI at all? This sounds like a job for Generics instead. – Remy Lebeau Aug 31 '16 at 07:15
  • What is `theEnumerationType`? – David Heffernan Aug 31 '16 at 08:08
  • @DavidHeffernan theEnumerationType is either CommandSetOne or CommandSetTwo or any Enumeration type I Register to Trobot – Mengchao Aug 31 '16 at 08:53
  • @RemyLebeau it's kind of Generics, because the type send to RegisterCommands may be changed – Mengchao Aug 31 '16 at 08:54
  • @Mengchao But `ExecuteCommands` cannot ever compile. – David Heffernan Aug 31 '16 at 08:57
  • @DavidHeffernan that's actually the first question, I wonder the type I should put here. – Mengchao Aug 31 '16 at 09:11
  • Generics are surely the answer here. For both functions. You don't want the calling code to have to frab around with RTTI. Btw, you can remove the `rttiContext := TRttiContext.Create();` line which serves no purpose at all. – David Heffernan Aug 31 '16 at 09:12
  • 1
    You could check this link, http://stackoverflow.com/questions/11110704/pass-a-mixture-of-differend-enums-types-in-delphi – shyambabu Aug 31 '16 at 09:55
  • Regarding your edit that's the exact opposite of what I suggested – David Heffernan Aug 31 '16 at 19:40
  • I recall in a skill sprint being told that generics do not deal with enums well. Actually i found that within limits they handle them pretty well. But I struggle trying to understand what you are trying to achieve. How is ExecuteCommands meant to work? If you can explain that I may have some ideas. – Dsm Aug 31 '16 at 20:50
  • @DavidHeffernan Hmmm... what is the opposite to enumeration? – Mengchao Sep 01 '16 at 00:10
  • @Dsm The ExecuteCommands does nothing special, actually he will just run an event to let call side decide what thing to do. as I said I can use stringlist to do this, which it's element is string, but that provide no type check. – Mengchao Sep 01 '16 at 00:14

2 Answers2

2

Whenever you find yourself wanting to pass the type of something as a parameter the goto solution is generics.

We're going to abuse the fact that an enumeration is really an integer underneigh.
Let's assume you have the actual command encoded in the string representation of the enum label.
e.g.

TCommands = (Left, Right, Up, Down);

TRobot = class
private
  FRegisteredCommands: TDictionary<integer, string>;
public  
  procedure RegisterCommand<E: record>(Enum : E);
  procedure ExecuteCommand<E: record>(Enum : E);
end;

procedure TRobot.RegisterCommand<E: record>(Enum: E);
var
  Key: integer absolute Enum;  //typesafe, because of the if below.  
  Info: PTypeInfo;
begin
  if GetTypeKind(E) <> tkEnumeration then raise Exception.Create('Enum is not an enum');
  //Added type safety:
  if     not(TypeInfo(E) = TypeInfo(TRobotCommand1)) 
      or not(TypeInfo(E) = TypeInfo(TRobotCommend2)) then raise ....
  Info:= TypeInfo(Enum);
  FRegisteredCommands.Add(Key, GetEnumName(Info, Key));
end;

The compiler will remove all this if code if these checks are true and only generate the code if these checks are false, because GetTypeKind is a compiler intrinsic routine This means that it will take zero runtime to execute these checks
Note that you can use the if TypeInfo(E) = TypeInfo(TMyCommandSet) compiler intrinsics trick to hardcode the command if crazy fast performance is your thing.

Note that on early Delphi's the absolute directive causes a compiler internal error (in Seattle it works 100% fine). In that case change the code like so:

procedure TRobot.RegisterCommand<E: record>(Enum: E);
var
  Key: integer;
  Info: PTypeInfo;
begin
  ....
  Key:= PInteger(@Enum)^;
  .....

If a given TRobot descendent only accepts a single type of command then I'd move the generic type to TRobot like so:

TBaseRobot<E: record> = class(TObject)
   constructor Create; virtual;
   procedure RegisterCommand(Enum: E);  //only implement once, see above.
   procedure ExecuteCommand(Enum: E); virtual; abstract; //implement in descendents.
....

constructor TBaseRobot<E>.Create;
begin
  inherited Create;
  if GetTypeKind(E) <> tkEnumeration then raise('error: details');  
end;

TRobotA = class(TBaseRobot<TMyEnum>)
  procedure ExecuteCommand(Enum: TMyEnum); override;
end;
....

EDIT
Instead of doing the check in the constructor, you can do it in a class constructor. The benefit of this is that any error will fire as soon as your app starts and not at some random time which may never happen in your testing.

Remove the constructor and replace it with a class constructor like so:

//You should never name a class constructor `create`. class constructor don't create anything, they init stuff.
class constructor TBaseRobot<E>.Init;  
begin
  if GetTypeKind(E) <> tkEnumeration then raise('error: details');  
end;
Johan
  • 74,508
  • 24
  • 191
  • 319
  • Interesting approach, I really appreciate the TypeInfo method. I will make an approach by a mixture of TypeInfo and Rtti – Mengchao Sep 02 '16 at 02:19
1

thanks to @DavidHeffernan and @Johan, I make a mixture of the two approach, and it works perfectly. sadly I can still not perform a complies time check on that.

unit Unit3;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Typinfo, Vcl.StdCtrls;

type
  TRobot = class
    private
      FPTypeInfo : PTypeInfo;
    public
      procedure RegisterCommands<E : record>;
      procedure ExecuteCommands<E : record>(anEnumeration : E);
  end;

  TForm3 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    private
      RobotA : TRobot;
    end;

  CommandSetOne = (Command1, Command2, Command3);
  CommandSetTwo = (Command4, Command5, Command6);

var
  Form3: TForm3;

implementation

{$R *.dfm}

{ TRobot }

procedure TRobot.ExecuteCommands<E>(anEnumeration: E);
  begin
  if (TypeInfo(E) = FPTypeInfo) then
    begin
    showMessage('correct command type');
    end
  else
    begin
    raise Exception.Create('Enum type not correct');
    end;
  end;

procedure TRobot.RegisterCommands<E>;
  begin
  FPTypeInfo := TypeInfo(E);
  end;

procedure TForm3.Button1Click(Sender: TObject);      
  begin
  RobotA := TRobot.Create();
  RobotA.RegisterCommands<CommandSetOne>;
  end;

procedure TForm3.Button2Click(Sender: TObject);
  begin
  RobotA.ExecuteCommands(command1);
  end;

procedure TForm3.Button3Click(Sender: TObject);
  begin
  RobotA.ExecuteCommands(command4);
  end;

end.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Mengchao
  • 81
  • 7