如何在Delphi中将常量字符串分组

11

我有一个应用程序,使用字符串表示物品在其生命周期中可能具有的不同状态。

例如:

打开,活跃,关闭,删除等。目前它们都硬编码在代码中,类似于这样。

MyVar := 'OPEN';

我正在尝试进行更改,因为这可能会成为一个维护问题,所以我想将它们全部更改为常量,我打算这样做

MyVar := STATUS_OPEN;

但我希望将它们组合成一个数据结构,如下所示。

MyVar := TStatus.Open;
什么是在Delphi 2007中实现这个的最佳方法?
我知道我可以为此创建一个记录,但如何填充它的值,以便所有对象都可以使用它而无需每次创建变量和填充值呢?
理想情况下,我想要一个数据结构和值的集中位置,并且可以轻松地访问它们(像TStatus.Open)而无需每次分配给一个变量或者创建一个对象。
我相信有一个简单的解决方案我只是没注意到。有什么想法吗?

非常好的问题。在我看来,这种思维方式使程序更加健壮,特别是当它们不断变化时。 - David Taylor
7个回答

13

就像Jim提到的那样,您可以使用类常量或枚举类型:

type
  TItemStatus = (isOpen, isActive, isClosed);
const
  ItemStatusStrings: array[TItemStatus] of string = ('Open', 'Active', 'Closed');

12

参见http://edn.embarcadero.com/article/34324(“自Delphi 7以来的新Delphi语言特性”)。

一个类常量可以很好地解决这个问题。根据上述链接:

type
    TClassWithConstant = class
      public 
        const SomeConst = 'This is a class constant';
    end;


 procedure TForm1.FormCreate(Sender: TObject);
 begin
   ShowMessage(TClassWithConstant.SomeConst);
 end;

我不明白这与普通的const有何不同。如果你要命名空间,可以使用单位名称加以限定。 - Marco van de Voort
1
+1 类常量是一种干净、面向对象的方式,可以将相关信息组合在一起。 - mjn
抱歉-可能忽略了这个答案。 这与我建议的记录方法非常相似(抱歉!) - David Taylor

8
我个人在多个大型关键数据处理平台上广泛使用TOndrej的方法。枚举的好处是它们可以在应用程序内轻松传递,非常紧凑(一种序数类型),在情况语句中完美工作,并且完全类型安全。后一个点对于维护很重要,因为删除或更改枚举值将导致编译错误(我认为这是一件好事)。
使用此方法时要注意以下问题:
- 更改枚举值的声明顺序将破坏枚举到字符串查找数组。 - 如果您在情况语句中使用枚举(一个不错的功能),请确保考虑到新值的添加。我通常在情况下添加一个else,并在未知值上抛出异常。比掉入情况好得多。 - 如果您非常关注第一个问题,可以在查找数组中使用记录,并在每个记录中包含枚举值并从单元初始化验证排序。这在维护频繁的关键系统中多次拯救了我。此方法还可用于向每个值添加其他元数据(一个非常方便的功能)。
祝你好运!
unit Unit1;

interface

type
  TItemStatusEnum = (isOpen, isActive, isClosed);

  TItemStatusConst = class
    class function EnumToString(EnumValue : TItemStatusEnum): string;
    property OPEN: string index isOpen read EnumToString;
    property CLOSED: string index isClosed read EnumToString;
    property ACTIVE: string index isActive read EnumToString;
  end;

var
  ItemStatusConst : TItemStatusConst;

implementation

uses
  SysUtils;

type
  TItemStatusRec = record
    Enum  : TItemStatusEnum;
    Value : string;
  end;

const
  ITEM_STATUS_LOOKUP : array[TItemStatusEnum] of TItemStatusRec =
    ((Enum: isOpen;   Value: 'OPEN'),
     (Enum: isActive; Value: 'ACTIVE'),
     (Enum: isClosed; Value: 'CLOSED'));

procedure ValidateStatusLookupOrder;
  var
    Status : TItemStatusEnum;
  begin
    for Status := low(Status) to high(Status) do
      if (ITEM_STATUS_LOOKUP[Status].Enum <> Status) then
        raise Exception.Create('ITEM_STATUS_LOOKUP values out of order!');
  end;

class function TItemStatusConst.EnumToString(EnumValue: TItemStatusEnum): string;
  begin
    Result := ITEM_STATUS_LOOKUP[EnumValue].Value;
  end;

initialization
  ValidateStatusLookupOrder;
end.

更新:

感谢批评 - 你是完全正确的,我解决的是我认为是问题的根本。如果您可以接受这个约束条件,下面是一个简单得多的方法,它只能在Delphi 2007或更高版本中使用(可能在D2006中也可以使用?)。很难摆脱那些令人烦恼的向后兼容性思维;)

type
  ItemStatusConst = record
    const OPEN   = 'OPEN';
    const ACTIVE = 'ACTIVE';
    const CLOSED = 'CLOSED';
  end;

这种方法简单易行,与Java或.Net应用程序的操作感觉相似。使用语义按预期工作,并且可以与代码补全配合使用。其中一个重要优点是常量范围限定在记录级别,因此不会像单位级别类型一样出现与其他定义冲突的风险。

Sample usage:

    ShowMessage(ItemStatusConst.ACTIVE);
    ShowMessage(ItemStatusConst.CLOSED);

仅出于娱乐目的,我还改进了我早期的方法,直接回答原始问题并获得类似的结果。这次我使用了“模拟类属性”(请参见此处)和属性索引器。这显然比记录方法更复杂,但保留了与枚举值、集合、字符串一起工作的能力,并且还可以扩展以实现其他元数据特性(如果需要)。我相信这适用于Delphi 5及其更高版本。

我曾经成功地使用这种技术创建了一个解析框架,用于处理Delphi中的完整的IBM AFPDS打印数据流语法。这种灵活性就是我喜欢在Delphi中工作的原因——它就像瑞士军刀一样;)


我非常同意你所写的一切,但我真的不明白这与问题有什么关系?你提出了一个很好的解决方案,但只有在使用枚举时,OP才会遇到这个问题。枚举很好,但对于用公共常量替换硬编码字符串来说,它们并不是必需的。 - mghie

4

或者你可以结合@Jim和@TOndrej

TGlobalConsts = class
type
  TItemStatus = (isOpen, isActive, isClosed);
const
  ItemStatusStrings: array[TItemStatus] of string = ('Open', 'Active', 'Closed');
end;

尽管您可以将其设置为记录。但如果它是一个,您可以像@mghie建议的那样添加类函数,并从const数组中获取值。就个人而言,我更喜欢在接口中将所有字符串放在const数组中,而不是在实现中的函数体中散布。
class function TGlobalConsts.Active: string;
begin
  Result := ItemStatusStrings[itsActive];
end;

class function TGlobalConsts.Open: string;
begin
  Result := ItemStatusStrings[itsOpen];
end;

有很多方法可以做到这一点,这是肯定的。


我不明白为什么要使用类函数TGlobalConsts.Active: string;而不是直接使用ItemStatusStrings[itsActive];。请帮忙解释一下。 - mca64
1
这只是语法糖。取决于您的偏好。 - Jim McKeeth

3

这是在所有版本的Delphi中都可以做到的:

type
  TStatus = class
    class function Active: string;
    class function Open: string;
    ...
  end;

  class function TStatus.Active: string;
  begin
    Result := 'ACTIVE';
  end;

  class function TStatus.Open: string;
  begin
    Result := 'OPEN';
  end;

你可以根据自己的需要使用它:

MyVar := TStatus.Open;

有且仅有一个地方可以更改字符串,只涉及代码,没有运行时实例化。最近Delphi版本的新功能并不总是必要的...


同意。仅为完整起见,在类常量方法中既没有代码也没有运行时实例化涉及。 - Jim
确实。我同意你的解决方案很好,它也允许使用"MyVar:=TStatus.Open;"这个赋值语句。我回答问题时提到的那部分是针对“必须将其分配给变量或创建对象”的说法而言的。从来没有必要这样做。 - mghie

2

除了TOndrej所说的内容:

还可以声明一个记录常量:

type
  TRecordName = record
    Field1: Integer;
    Field2: String
  end;

const
  itemstatus : TRecordName = (
    Field1: 0;
    Field2: 'valueoffield2'
  );

通过将上面的const记录语法和TOndrej显示的数组语法相结合,也可以创建记录数组。

然而,我通常更喜欢的方法需要初始化:

  • 通过RTTI使用枚举名称填充TStringDynArray
  • 如果后来发现名称应该是可配置的,则持久性(无论如何安排)可以简单地加载/存储动态数组。

使用RTTI将枚举值映射到名称是非常有效的,特别是如果您有一个标准的枚举值命名约定,例如可以在代码中剥离枚举前缀字符串。问题是 - 这种方法是否会在FastMM中导致内存泄漏报告?我没有检查过,但似乎可能会这样。 - David Taylor
我不知道有任何内存泄漏。这也都是正常的字符串操作,所以我不知道内存泄漏可能发生在哪里。 - Marco van de Voort
我所说的泄漏是FastMM检测到的感知泄漏,当它被配置为报告内存泄漏时(请参见FastMM4Options.ini中的注释)。问题在于,当FastMM在关闭时进行记时并绑定内存时,任何剩余的分配都会被报告为泄漏。在单元初始化块中分配的内存是主要候选对象,其他线程也是如此,除了主应用程序线程。Indy 10以这种类型的泄漏而闻名。如果您有兴趣,请查看FastMM RegisterExpectedMemoryLeak函数。 - David Taylor
是的,我知道那种泄漏。 FPC变体的内存检测列表甚至计数,并在确定性报告中进行减去。但这继承于在初始化中进行任何动态分配,我不认为这是代码的问题,而是内存管理器插入的方式造成的。代码没有泄漏,内存管理器只是不能确定它。 - Marco van de Voort

1
你可以拥有一个记录类型的全局变量。该记录类型必须具有每个状态的字段。
您可以在任何单元的初始化部分中填充记录,调用在该单元中声明的过程或函数。您可以拥有一个单独的单元来完成此工作,例如StatusTypes.pas。
在接口部分,您可以声明类似以下内容:
type 
  TStatus = record
    OPEN: string;
  end;

var
  Status: TStatus;

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