Search code examples
delphidelphi-10.2-tokyo

Visibility of enumeration types and helpers


I have a few base units, which I need to keep separate, but for ease of use I would like to group them into one unit. For now I have problem with enumeration types and helpers, not sure if anything else. Is it possible to make this work?

BaseUnit

type
  TCustomEnum = (ceValue1, ceValue2, ceValue3);

TCustomEnumHelper = record helper for TCustomEnum
  function AsString: string;
end;

GroupUnit

uses BaseUnit;

TCustomEnum = BaseUnit.TCustomEnum; 

Usage in other units.

uses GroupUnit;

procedure DoSomething;
var
  lValue : TCustomEnum;
begin
  lValue := ceValue1; // doesn't work
  lValue := TCustomEnum.ceValue1; // works 
  lValue.AsString; // doesn't work
end;

Solution

  • Your experimentation has yielded exactly what is to be expected.

    The only reason your third unit has access to TCustomEnum is because GroupUnit declared a type alias using the same identifier but only that identifier. It's important to note that this is an important way in which the Delphi compilation process differs from C++. Whereas C++ recursively pulls all includes into the compilation scope, Delphi only pulls in the interface section of directly included units.

    • lValue := ceValue1; doesn't work because the ceValue1 isn't defined in the compilation scope.
    • lValue := TCustomEnum.ceValue1; works because TCustomEnum is pulled in in its entirety. That's to say because TCustomEnum is equal to BaseUnit.TCustomEnum: it means TCustomEnum.ceValue1 must be valid.
      1. You should note similar with objects and records. You can always reference public members, even if you only pull in the class/record type.
      2. This is different from the previous case because you used ceValue1 without any qualification. And there is no unqualified definition for that identifier in scope.
    • lValue.AsString; this doesn't work because the helper that makes AsString available is not in scope.

    There are situations where declaring type aliases can be useful. But I must point out the the idea of general purpose grouping unit is deeply flawed.

    Yes, it does reduce the number of units you have to use in order to pull a large1 number of dependencies. I.e. You think you're saving time by replacing uses Unit1, Unit2, Unit3, Unit4, Unit5, Unit6; with the shorter uses GroupUnit;.

    But the large1 dependencies means you've also pulled in things you don't need. This leads to:

    • A poorly managed architecture where everything indirectly depends on almost everything else. (Otherwise known as a "Big Ball Of Mud" suggested research.)
    • Unnecessary levels of indirection; meaning if you want to find the definition of something you're using, you Find declaration... only to get to the alias, and have to Find declaration... again to find what you actually want.

    Yes, it seems like more work to list each and every thing you're using. But if you're using that many things that it's frustrating you should ask yourself: Why is my class so complicated? and then What's wrong with my design; how can I improve it?


    As a final note, you can alias more than just the enum; provided you do so explicitly.

    The following might suffice (but again, use it sparingly). Not least because it's so much work.

    unit GroupUnit;
    
    interface
    
    uses BaseUnit;
    
    type
      TCustomEnum = BaseUnit.TCustomEnum;
      TCustomEnumHelper = BaseUnit.TCustomEnumHelper;
    const
      ceValue1 = BaseUnit.ceValue1;
      ceValue2 = BaseUnit.ceValue2;
      ceValue3 = BaseUnit.ceValue3;