【问题标题】:Delphi - access class string property by its valueDelphi - 通过值访问类字符串属性
【发布时间】:2014-07-29 14:07:58
【问题描述】:

我定义了一个只包含字符串作为属性的类,我需要根据其值获取属性名称,如下例所示。在示例中只有 3 个属性,在现实生活中的类中几乎有 1000 个。问题是这个类被大量使用,我想知道是否可以更快地通过其值获取属性名称。

unit Unit5;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,RTTI, StdCtrls, Diagnostics;

type
  TConstDBElem = class
  public
    CCFN_1 : String;
    CCFN_2 : String;
    CCFN_3 : String;
    constructor Create;
  end;

  TForm5 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  Form5: TForm5;
  Obj: TConstDBElem;

implementation


{$R *.dfm}

procedure TForm5.Button1Click(Sender: TObject);
var iPos:Integer;
    timer:TStopwatch;
    Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
  var
    vrttiContext: TRttiContext;
    vrttiField : TRttiField;
    vType : TRttiType;
  begin
      vType := vrttiContext.GetType(TConstDBElem);

      for vrttiField in vType.GetFields do
        if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
        begin
           result := vrttiField.Name;
         end;
  end;

begin
  timer := TStopwatch.Create;
  timer.Start;
  Memo1.Lines.Clear;
  for iPos := 0 to 100000 do
    GetName(Obj,'TEST3');
  timer.Stop;
  Memo1.Lines.Add(FloatToStr(timer.Elapsed.TotalSeconds));
end;

constructor TConstDBElem.Create;
begin
  CCFN_1 := 'TEST1';
  CCFN_2 := 'TEST2';
  CCFN_3 := 'TEST3' ;
end;

initialization
  Obj := TConstDBElem.Create;

finalization
  Obj.Free;


end.

是的,我知道这是一个非常糟糕的设计,不应该这样做。是否有任何选项可以加快搜索速度?

【问题讨论】:

  • 字段的值在运行时会改变吗?如果是这样,你怎么能比遍历所有字段寻找具有所需名称的字段做得更好?我会建议一个不同的设计,但看起来课程已经设计好了,为时已晚。
  • 是什么让您决定在这三个参考资料上致电Free?在许多层面上这样做是错误的。 vrttiContext 不需要。也不是对TRttiContext.Create 的调用。 vTypevrttiField 完全错误。不要破坏它们。 vrttiField 是一个循环变量。您在循环结束时调用Free 的任何值。对于 for in 循环,这可能已经很好地定义了。我什至不确定。不管怎样都是假的。最后,尝试是在错误的地方。您可以轻松地在未初始化的变量上调用 Free
  • 不,这些值在运行时不会更改。是的,改设计不合适……
  • 顺便说一句,一个拥有近 1000 个属性/字段的类一开始看起来并不是一个好主意。
  • 如果所有属性都是字符串,那么您可以使用 TStringList 及其名称/值对。那么你只需要一件事,而不是成千上万。这就是 TStringListValues 属性的设计目的。顺便说一句,看看你的代码,你没有任何“属性”,只有“字段”。

标签: delphi delphi-xe rtti


【解决方案1】:

GetName() 找到匹配项时,它并没有停止循环,因此它会继续搜索更多匹配项。分配函数的 Result 不会退出函数,就像您显然认为的那样。因此,GetName() 最终返回 last 匹配,而不是 first 匹配。当找到第一个匹配项时,循环应该调用Exit

Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
  vrttiContext: TRttiContext;
  vrttiField : TRttiField;
  vType : TRttiType;
begin
  vType := vrttiContext.GetType(TConstDBElem);

  for vrttiField in vType.GetFields do
    if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
    begin
      result := vrttiField.Name;
      Exit; // <-- add this
    end;
end;

或者,使用带有参数的Exit() 版本:

Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
  vrttiContext: TRttiContext;
  vrttiField : TRttiField;
  vType : TRttiType;
begin
  vType := vrttiContext.GetType(TConstDBElem);

  for vrttiField in vType.GetFields do
    if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
    begin
      Exit(vrttiField.Name); // <-- assigns Result and exits at the same time
    end;
end;

在您的简单示例中,搜索 3 个字段所浪费的时间几乎不会引起注意,但在搜索 1000 个字段时,它会有所不同。

【讨论】:

  • +1 为Exit(vrttiField.Name); // &lt;-- assigns Result and exits at the same time,我什至不知道你能做到这一点!
  • 根据System.Exit documentation:“从Delphi 2009 开始,Exit 可以采用指定结果的参数。该参数必须与函数结果的类型相同。”
  • @Blobby 这并不像您想象的那么好。现在有多种返回值的方法。设置Result 或将参数传递给Exit()。这是不一致的。我多么渴望一个正确的return 语句,甚至更好的是通过值返回而不是使用var 参数。
  • @blobby 如果你知道要返回的值,设置它并退出。仅当您有更多工作要做时才使用 break。
  • @RemyLebeau Exit(...) 不是一个声明,而是一个过程。 Result 变量在函数的整个生命周期中都可用。 Delphi 返回值确实有时被实现为var 参数,有时通过值传递优化到寄存器中。在后一种情况下,声明了一个新的局部变量来保存Result。当它是var 参数时,编译器必须进行一些调整以确保实现值语义。这有点乱。它不遵循平台 ABI。尽管这几乎没有很好的定义。我喜欢 C、C++、C# return.
【解决方案2】:

您在评论中声明这些值在运行时永远不会改变。在这种情况下,您可以在启动时简单地构建一个字典,其中属性值作为字典键,属性名称作为字典值。

我假设类的所有实例都具有相同的属性值。如果不是这种情况,那么每个实例都需要一本字典。

【讨论】:

    【解决方案3】:

    这是一个“糟糕的设计”,因为有人编写了一个他们将其视为 C 风格结构的类。正如已经说过的,类中没有定义任何属性,只有一堆公共数据成员,也就是“字段”。

    没有封装,因此您为更改结构所做的任何事情都可能对使用它的任何单元产生深远的影响。我同意用 TStringList 或 TDictionary 替换 IMPLEMENTATION 会很聪明,但是......没有要遵守的接口!您有 1000 多个对公共数据成员的硬连线引用。

    (上次我看到这样的东西是一群 VB 程序员编写的一些代码,他们编写的类好像是包含公共数据成员的 C 风格结构,然后他们编写外部函数来访问数据,只是就像你在 C 中所做的那样。只有它们也将业务逻辑隐藏在访问器方法中,这会导致随机的代码片段直接引用类的数据成员。)

    副手,我会说你完全误用了 RTTI 代码。当然,上面的优化会提高性能,但那又如何呢?这是错误的解决方案!

    如果你真的想重构这个(你当然应该!),首先我会通过将公共更改为私有来看看这个可怜的类的使用有多广泛,看看你得到了多少错误。

    然后我会从 TStringList 派生它,删除所有本地字段,并将 GetName 函数移动到类中:

    type
      TConstDBElem = class( TStringList )
      public
        constructor Create;
        function GetName( aName : string ) : string;
      end;
    

    现在,如果我正确解释了您的示例,您希望这样做来初始化对象:

    constructor TConstDBElem.Create;
    begin
      Add( 'TEST1=CCFN_1' );
      Add( 'TEST2=CCFN_2' );
      Add( 'TEST3=CCFN_3' );
    end;
    

    然后调用 obj.GetName() 替换其他单元中的所有引用:

    function TConstDBElem.GetName( aName : string ) : string;
    begin
      Result := Values[aName];
    end;
    

    您正在用obj.GetName('TEST1') 替换对obj.CCFN_1 (?) 或GetName(obj,'TEST1') 的引用。

    (也许我在这一点上完全偏离了基础。对不起,但我只是不明白你是如何从这个例子中使用这个类的,而且它对我来说没有多大意义.如果你说你在什么之间映射会更有意义。我的意思是......谁需要从与其关联的值中查找本地字段名称?一旦找到它你会怎么处理它? 写这篇文章的人必须经历一些令人难以置信的扭曲才能使代码正常工作,因为他/她在设计时肯定不了解 OOP!)

    此时,您将成功地将此类(其他单元)的客户端与其内部实现分离,并将这些引用替换为对类中定义的接口(方法)的调用。

    然后你可以做一些测试来看看如果你改变实现会发生什么,比如从 TStringList 到 TDictionary。但是无论您如何切片,我都无法想象 TStringList 或 TDictionary 会比您在示例中滥用 RTTI 系统的方式慢。 :)

    【讨论】:

    • 虽然我同意当前的设计很差,但我绝对认为从TStringList 派生也很差。您现在有了一个继承各种不需要的功能的类。它有 SortInsertObjectSaveToFile 这样的方法。 CommaTextValuesFromIndex[]StrictDelimiterWriteBOM 等属性。继承在这里不是一个好的选择。封装是需要的。既然你想要一本字典,为什么要TStringList。当然TDictionary&lt;K,V&gt; 是需要的。
    • 你说得很好!我的答复的要点不是建议最好的解决方案,而只是说明如何达到可以更改实施而不影响客户使用它的程度。无论如何,我确信如果我发布一个使用容器作为成员的示例,肯定有人会建议我直接从对象继承。这无关紧要。他需要首先打破对直接访问数据成员的依赖。剩下的只是实现细节。 Add() 和 GetName() 方法几乎都适用于继承或组合。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-31
    • 2016-07-30
    • 1970-01-01
    相关资源
    最近更新 更多