如何创建一个字符串类型?

10

今天Raymond Chen的博客让我意识到了一个优雅的解决办法,帮我解决了遇到的问题。

各种shell函数,不再使用 ITEM­ID­LIST 结构体,而是可以只接受以下类型:

  • ITEM­ID_CHILD
  • ID­LIST_RELATIVE
  • ID­LIST_ABSOLUTE
  • ITEM­ID_CHILD_ARRAY

结构。这些结构都相同,但现在你可以在编译器级别上强制执行 概念上 的类型。

回到Delphi

我有一组函数:

  • 有些接受路径:(例如C:\Users\Ian\Desktop\AllisonAngel.jpg
  • 有些接受文件名:(例如AllisonAngel.jpg
  • 有些接受文件夹:(例如C:\Users\Ian\Desktop

目前它们都被声明为 string,例如:

 function GetFilenames(Folder: string; ...): ...
 function IsValidBatchFilename(Path: string): ...
 function GetReportType(Filename: string): ...

我必须相信我正在传递正确的内容;而且我相信开发人员(例如)知道以下三者之间的区别:

  • 路径
  • 文件名
  • 文件夹

我想要改变函数以使用“类型化”字符串:

 function GetFilenames(Folder: TFolderOnly; ...): ...
 function IsValidBatchFilename(Path: TFullPath): ...
 function GetReportType(Filename: TFilenameOnly): ...

何时使用:

type
   TFullPath = type string;
   TFolderOnly = type string;
   TFilenameOnly = type string;

除了实际上没有打字发生:

var
   dropFolder: string;
begin
   dropFolder := GetDropFolderPath(LCT);

   GetFilenames(dropFolder); <-- no compile error

end;

我想要的是一种"独特的"字符串类型;它是一个带有长度前缀、引用计数和以空字符结尾的string


这就是 Delphi 相对于 C# 的弱点所在。据我所知,这是不可能的。 - TLama
3个回答

9
您可以使用高级记录来实现这一目标。例如,您可以执行以下操作:
type
  TFileName = record
    FFileName: string;
  public
    class function IsValidFileName(const S: string): boolean; static;
    class operator Implicit(const S: string): TFileName;
    class operator Implicit(const S: TFileName): string;
  end;

implementation

class function TFileName.IsValidFileName(const S: string): boolean;
begin
  result := true {TODO};
end;

class operator TFileName.Implicit(const S: string): TFileName;
begin
  if IsValidFileName(S) then
    result.FFileName := S
  else
    raise Exception.CreateFmt('Invalid file name: "%s"', [S]);
end;

class operator TFileName.Implicit(const S: TFileName): string;
begin
  result := S.FFileName;
end;

同样地,对于TPathTFolder也是一样的。优点:

  • 一个期望使用TPath的函数不会接受TFileName(或其他某些组合)。
  • 您仍然可以将TPath分配给/从普通字符串中分配。如果您将字符串转换为TPath,则将自动检查字符串以查看它是否包含有效路径。
  • 可选地,您可以通过编写更多的Implicit类操作符来指定如何将TPath分配给TFileName(或其他某些组合)。

3
虽然我不认为在 Delphi 5 中实现可能,但这是一个好主意!【+1】 - TLama
@TLama 我本来不想说什么,只是接受最受欢迎的答案。 - Ian Boyd

8
为每种字符串类型创建不同的记录类型。即使字符串类型相同,不同的记录类型也不能互相赋值。
type
  TFullPath = record value: string end;
  TFolderOnly = record value: string end;

陈先生的文章将新的shell功能与经典的STRICT宏进行了比较,据我所知,声明不同的结构体正是STRICT宏的工作原理。


1
如果我复制 TFullPaths 的实例(例如,FullPath2 := FullPath1;),那么引用计数会被保留吗? - afrazier
是的,记录内的字符串赋值方式与任何其他地方的字符串相同,@Afrazier。 - Rob Kennedy

2
以 Delphi 处理基本类型的方式来看,我认为这是一个“无法从此处开始”的情况。
你所有的字符串类型声明都将符合 Delphi 的类型兼容性和赋值兼容性规则。它们将限制具有 var 参数的过程声明。如果你将函数声明更改为 var 参数而不是引用或值参数,那么你会在最终示例中得到编译错误。
尽管如此,所有这一切都是无用的。即使使用 TFilenameOnly 类型,你仍然无法确保输入只是文件名,并且必须在你的过程中测试其内容。

如果调用者想要使用足够大的锤子来砸方钉进圆孔,那也不是问题。但这确实解决了将红色圆钉放入为绿色圆钉准备的孔中的问题。 - Ian Boyd
它对于色盲用户并没有解决任何问题。:-P 简单类型的别名存在的问题是你没有任何方法确保内容是正确的。这是在错误的抽象层次上攻击问题。如果你转换到类或高级记录,其中值可以在分配时在代码中进行验证,那么你就有了一些东西。最好的情况下,Delphi中的类型别名只不过是向其他程序员提供语义提示。 - afrazier
这就是为什么我不想要一个别名,而是一个独立的类型(这也是为什么我没有将它声明为TFolder = string,而是TFolder = type string)的原因。 - Ian Boyd
1
问题在于这些类型仍然高度兼容。Delphi的帮助明确指出: [... ]如果您定义新类型的目的是利用运行时类型信息,例如将属性编辑器与特定类型的属性关联 -“不同名称”和“不同类型”的区别变得重要。) 在声明后面,它们仍然只是字符串。 - afrazier
除非您声明 TGirth = type TSize;。尽管这些类型仍然高度兼容,并且在声明后它们对应于 TSize 的相同内存布局,但编译器可以赋予 TGirthTSize 更具语义意义 - 并使它们不同。如果您使用 type 定义记录,则在 Delphi 中可以正常工作,但如果您使用 type 定义字符串,则无法正常工作。 - Ian Boyd
2
虽然我无法回答为什么会出现这种情况,但这种不一致性已经有记录了:类型兼容性和标识。大多数情况都涉及简单类型、字符串和变量。 - afrazier

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