可以将枚举类型作为参数传递,并在其他函数中重用该类型吗?

4
我希望您能够像虚拟代码一样实现以下内容:
type
  CommandSetOne = (Command1, Command2, Command3);
  CommandSetTwo = (Command4, Command5, Command6);

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

我可能有多组命令,而且命令集中的任何命令都可以替换。

TRobot有一个过程可以将枚举类型作为参数,并保存此类型,用于ExecuteCommands过程。

关于传递任何枚举类型作为参数,我发现可以使用TRttiEnumerationType来实现,在调用方面应该如下所示:

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

但是我在传递像Command1这样的命令时遇到了问题。我尝试过使用Variant类型来代替EnumerationType,但似乎无法将Command1传递给它。

我知道如果我使用类似TStringList的东西,这是更容易实现我想要的功能的方法,但是我希望Delphi在编译时进行检查,以防我输错命令(使用TstringList,我可以添加代码在运行时检查)。

所以真正的问题是:

  1. 我应该使用哪种类型来代替EnumerationType?

  2. 如果这不可能,是否有其他解决方案可以使用枚举?

  3. 或者,是否有任何解决方案可以提供编译时检查和灵活的结构?

编辑:

感谢David的建议,我应该同时使用Rtti,因此为了使其清晰,我添加了RegisterCommands的实现。

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

那么对于该类型的任何枚举值应该适用什么?

例如,如果我在RegisterCommands中使用CommandSetOne,那么delphi如何接受Command1Command2Command3

更具体地说,delphi能否仅限于Command1Command2Command3?这意味着如果我放置Command4,它会给我一个编译错误吗?


1
你为什么要使用RTTI呢?这听起来更适合使用泛型。 - Remy Lebeau
什么是“theEnumerationType”? - David Heffernan
@DavidHeffernan,theEnumerationType 可以是 CommandSetOne 或 CommandSetTwo 或任何我在 Trobot 中注册的枚举类型。 - Mengchao
@DavidHeffernan 这实际上是第一个问题,我想知道这里应该放什么类型。 - Mengchao
1
你可以查看这个链接,https://dev59.com/OmXWa4cB1Zd3GeqPKSwc - shyambabu
显示剩余7条评论
2个回答

2

每当你想把某个类型作为参数传递时,通用解决方案就是使用泛型。

我们将滥用枚举实际上是整数的事实。
假设您将实际命令编码在枚举标签的字符串表示中。
例如:

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;

如果这些检查为真,则编译器将删除所有此if代码,并仅在这些检查为假时生成代码,因为GetTypeKind是编译器内置例程这意味着执行这些检查不需要任何运行时
请注意,您可以使用if TypeInfo(E) = TypeInfo(TMyCommandSet)的编译器内部技巧来硬编码命令,以获得非常快的性能。

请注意,在早期版本的Delphi中,absolute指令会导致编译器内部错误(在Seattle中它完全正常工作)。在这种情况下,请像这样更改代码:

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

如果给定的TRobot后代只接受单一类型的命令,那么我会将通用类型移动到TRobot中,如下所示:
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;
....

编辑
不要在构造函数中进行检查,而是可以在一个类构造函数中进行。这样做的好处是,任何错误都会在应用程序启动时立即触发,而不是在某个随机时间触发,这可能永远不会在您的测试中发生。

删除构造函数并将其替换为类构造函数,如下所示:

//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;

有趣的方法,我非常欣赏TypeInfo方法。我将通过TypeInfo和Rtti的混合来进行一种方法。 - Mengchao

1
感谢@DavidHeffernan和@Johan的帮助,我采用了两种方法的混合体,现在它完美地工作了。遗憾的是我仍然无法对其进行编译时间检查。
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.

我仍然认为你完全错了。消费类根本不需要处理RTTI,这是糟糕的设计。 - David Heffernan
你可以按照Johan描述的方式使用泛型来完成。 - David Heffernan
@DavidHeffernan 这很奇怪,只发送一个命令来注册命令集。我认为这不太可能。 - Mengchao
你不觉得将类型信息转换为RTTI类型,然后再转回来很奇怪吗?只需使用通用函数并获取类型信息即可。但是在register函数中删除参数。 - David Heffernan
我在你的答案中编辑了我的意思。可能无法编译,但你能理解我的意思。 - David Heffernan
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接