Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a type that is string?

A blog entry from Raymond Chen today made me realize the elegant solution to a problem i'm having.

Various shell functions, rather than all taking ITEM­ID­LIST structure, can be made to only accept:

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

structures. The structures are all identical, but now you can enforce conceptual types at the compiler level.

Back to Delphi

i have a set of functions:

  • some only take a path: (e.g. C:\Users\Ian\Desktop\AllisonAngel.jpg)
  • some only take a filename: (e.g. AllisonAngel.jpg)
  • some only take a folder: (e.g. C:\Users\Ian\Desktop)

And right now they're all declared as string, e.g.:

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

and i have to trust that i'm passing the right stuff; and i'm trusting that developers (e.g. me), know the difference between:

  • a path
  • a filename
  • and a folder

i want to change the functions to use "typed" strings:

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

Where:

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

Except that there's no actual typing happening:

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

   GetFilenames(dropFolder); <-- no compile error

end;

What i want is a "distinct" type of string; that is string insofar that it is length prefixed, reference counted, null terminated.

like image 413
Ian Boyd Avatar asked Jan 24 '13 18:01

Ian Boyd


2 Answers

You can use advanced records to accomplish this. For instance, you could do

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;

and similarly for TPath and TFolder. Advantages:

  • A function expecting TPath will not accept a TFileName (or some other combination).
  • You can still assign a TPath to/from a regular string. If you cast from a string to a TPath, you will automatically check the string to see if it contains a valid path.
  • Optionally, you can specify how a TPath can be assigned to a TFileName (or some other combination), by writing more Implicit class operators.
like image 173
Andreas Rejbrand Avatar answered Sep 29 '22 05:09

Andreas Rejbrand


Create different record types for each string type. Distinct record types are not assignment-compatible, even though string types are.

type
  TFullPath = record value: string end;
  TFolderOnly = record value: string end;

Chen's article compares the new shell feature to the classic STRICT macro that makes distinct handle types, and as I recall, declaring distinct structs is exactly how the STRICT macro works.

like image 38
Rob Kennedy Avatar answered Sep 29 '22 04:09

Rob Kennedy