How to create a type that is string?
Asked Answered
N

3

10

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.

Nephelometer answered 24/1, 2013 at 18:8 Comment(1)
That's where Delphi is comparing e.g. to C# is weak. This is not possible as far as I know.Table
B
9

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.
Belemnite answered 24/1, 2013 at 18:24 Comment(2)
Although I don't think it's possible in Delphi 5, it's a good idea! [+1]Table
@Table i wasn't going to say anything; and just accept the most popular answer.Nephelometer
M
8

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.

Mocambique answered 24/1, 2013 at 18:19 Comment(2)
Will reference counting be maintained if I copy instances of TFullPaths? (e.g. FullPath2 := FullPath1;)Cording
Yes, strings inside records are assigned the same as strings anywhere else, @Afrazier.Mocambique
C
2

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.

Cording answered 24/1, 2013 at 18:19 Comment(6)
If the caller wants to use a big enough hammer to smash a square peg in a round hole, that's not a problem. But it does solve the problem of putting a red round peg in a hole meant for green round pegs.Nephelometer
It doesn't solve any problems for your color blind users. :-P The problem with aliases for simple types is that you don't have any way to ensure that the contents are correct. It's attacking the problem at the wrong level of abstraction. If you switched to classes or advanced records where the value can be validated in code at assignment, then you'd have something. At best, type alises in Delphi are nothing more than semantic hints to other programmers.Cording
That's why i didn't want an alias, but a distinct type (which was why i didn't declare it as TFolder = string, but TFolder = type string)Nephelometer
The thing is that the types are still highly compatible. Delphi's Help calls this out explicitly: [...]if your purpose in defining a new type is to utilize runtime type information, for example, to associate a property editor with properties of a particular type - the distinction between 'different name' and 'different type' becomes important.) Behind the declaration, they're still just strings.Cording
Except when you declare TGirth = type TSize;. Even though the types are still highly compatible, and behind the declaration they're identical memory layouts corresponding to TSize, the compiler can give semantic meaning to TGirth over TSize - and make them distinct. It works in Delphi if you type a record, but doesn't work if you type a string.Nephelometer
While I can't answer why this is the case, this incongruity is documented: Type Compatibility and Identity Most of the conditions revolve around simple types, strings, and variants.Cording

© 2022 - 2024 — McMap. All rights reserved.