Question

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.

Était-ce utile?

La solution

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.

Autres conseils

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.

With the way Delphi handles basic types, I'm pretty this is a case of "you can't get there from here."

Your string type declarations are all going to satisfy Delphi's rules for type compatibility and assignment compatibility. What they will restrict is procedural declarations with var parameters. If you changed your function declarations to be var parameters instead of reference or value parameters, you would get a compile error in your final example.

All that said, it's all useless anyway. You still have no way to ensure that the input is only a filename, even with the TFilenameOnly type, and must test the contents in your procedures anyway.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top