Jump to content
chkaufmann

Enumeration Type as Parameter

Recommended Posts

I have a generic method that requires an enumeration type and a value in this enumeration. Right now my function definition looks like this:

procedure Foo(ASetType: PTypeInfo; ADefault: Integer);

This works fine. With GetTypeData() I can extract what I need (number of elements in enumeration). However, in the usage of this function I have to add TypeInfo() and Ord() each time:

type
  TSet1 = (a11, a12, a13);
  TSet2 = (a21, a22, a23)

then I can call:

  Foo(TypeInfo(TSet1), Ord(a12));
  Foo(TypeInfo(TSet2, Ord(a23));

My question is, how to define the two parameters in order to avoid the TypeInfo() and the Ord() each time?

 

Christian

Share this post


Link to post
32 minutes ago, chkaufmann said:

I have a generic method

I don't see any generics here.

 

Not sure I fully understand what you need. Anyway here is what I think is a solution:

 

type
    TSet1 = (a11, a12, a13);
    TSet2 = (a21, a22, a23, a33);
    TFoo<T: record> = class
    public
        class procedure Foo(ADefault : Integer);
    end;

implementation
      
class procedure TFoo<T>.Foo(ADefault: Integer);
var
    Info : PTypeInfo;
    NumberOfElement : Integer;
begin
    Info := TypeInfo(T);
    NumberOfElement := Info.TypeData.MaxValue - Info.TypeData.MinValue + 1;
    Form1.Memo1.Lines.Add(NumberOfElement.ToString);
end;

// Example use
procedure TForm1.Button1Click(Sender: TObject);
begin
    TFoo<TSet1>.Foo(123);
    TFoo<TSet2>.Foo(456);
end;

 

  • Like 1

Share this post


Link to post

Yes "generic" is maybe the wrong word because I don't mean a generic method. Maybe I should have said untyped or typeless. I would just like to call it like this:

Foo(TSet1, a13);
Foo(TSet2, a21);

Christian

 

Share this post


Link to post

I am still not sure what you want to achieve or what it is that is the actual problem?

Do you need to validate that the second parameter is valid for the first parameter?

Or is it converting to/from an integer?


if it compiles, aValue is a member of T

function TFoo.IsMember<T>(const aValue:T): Boolean;
begin
   Result := True;
end;

Convert it to an integer?

function TFoo.ToInteger<T>(const aValue:T): Integer
 var V: TValue;
begin
  V := TValue.From<T>(aValue);
  Result := V.AsOrdinal;
end;

 

  • Like 1

Share this post


Link to post

It's just a question of readability. My version works fine, but now I would like to avoid these TypeInfo() and Ord() when I call Foo().

 

And if I can validate the second parameter to be a valid member of the first (tkEnumeration) Parameter, this would be a nice additional check.

 

Christian

Share this post


Link to post

So you mean your remove type check at compilation time to do it at run time. Poor design.

It would probably be a good idea that you explain WHY you need such construct. Maybe there are other solution than the [flawed?] one you designed.

 

 

 

Share this post


Link to post

Ok, here is the long story. In my system I define catalogs that can be used as lookup for values. There are complex catalogs with dynamic content that can be edited by the users. And there are simple catalogs that are hard coded in my application. For these hard coded catalogs I have builder class:

  TBSCatalogBuilder = class abstract
  strict protected
    function AddItem(AItemId: Integer; const AName: String; AIsDefault: Boolean = False; const AParentItem: IBSCatalogItem = nil): IBSCatalogItem;
    procedure DoBuildItems; virtual; abstract;
  public
    constructor Create(ACatalogId: Integer; const AName: String; AOptions: TBSCatalogOptions = []);
  end;

So to define a catalog I create a subclass of TBSCatalogBuilder, overwrite the DoBuildItems method and call AddItem several times to define the catalog.

 

Now some hard coded catalogs just represent the items of an enumaration type and instead of creating a subclass I would like one class with a constructor that takes the type of the enumeration and a second parameter with the default value. Right now the definition looks like this:

  TBSCatalogBuilderSetType = class sealed(TBSCatalogBuilder)
  strict protected
    procedure DoBuildItems; override; final;
  public
    constructor Create(ACatalogId: Integer; const AName: String; ASetType: PTypeInfo; ADefault: Integer = 0);
  end;

 

and then I can use it like this:

type
  TBSColumnType = (ctUnknown, ctBoolean, ctBytes, ctDateTime, ctFloat, ctInteger, ctMemo, ctString, ctGuid);

begin
  myColumnTypeCatalog = TBSCatalogBuilderSetType.Create(1, 'Column Types', TypeInfo(TBSColumnType), Ord(ctString));
end;

This works and it does what I want, but the TypeInfo() and the Ord() just don't look nice and I was wondering if there is not a way to avoid these. And I didn't want to define a generic class because like this the code is duplicated for every catalog. But maybe this is the only way to do it:

  TBSCatalogBuilderSet<T> = class(TBSCatalogBuilder)
  public
    constructor Create(ACatalogId: Integer; const AName: String; ADefault: T);
  end;

Christian
 

Share this post


Link to post

A catalog looks like a StringList, isn't it?

Could you provide minimal but complete source code to build and use TWO catalogs?

Share this post


Link to post

This brings up my pet peeve with Generics - the lack of an Enumeration constraint for the generic type T.
If you put something else than an enumeration as T in the call, it will blow up, but putting <T> on the methods allows you to do what you want?

If you are running multiple sets, but the class basically does the same, can you use this?

  TBSCatalogBuilderSet = class(TBSCatalogBuilder)
  public
    // constructor Create; // Does what you need it do to, but stays away from the generics bits
    procedure Build<T>(ACatalogId: Integer; const AName: String; ADefault: T);
  end;
  
procedure TBSCatalogBuilderSet.Build<T>(ACatalogId: Integer; const AName: String; ADefault: T)
var 
  V: TValue;
  Ordinal: Integer;
begin
  V := TValue.From<T>(aDefault);
  Ordinal := V.AsOrdinal;
  // You already have the TypeInfo example above for extracting the range of T, or the name of the type as well
  // so the rest of your magic happens here
end;

 

Share this post


Link to post
4 hours ago, Lars Fosdal said:

This brings up my pet peeve with Generics - the lack of an Enumeration constraint for the generic type T.

A record constraint can be used so that only value types can be specified for T, and then RTTI used to verify that T is actually an enumeration type (that check can be done at compile-time in XE7+).

Edited by Remy Lebeau

Share this post


Link to post

This is what I did. The builder class itself has no RTTI. But I created a generic record.

 

IBSLookupElements is a simple collection of (Key, Caption, ParentKey) elements. Fmt() and Translate() are string helper methods for Format and language specific translations.
Then TBSCatalogBuilderSimple is a general subclass of my TBSCatalogBuilder class.

 

Here is my record definition:

type
  TBSCatalogBuilder<T> = record
  strict private
    FCatalogId : Integer;
    FDefault   : Integer;
    FName      : String;
    FTextIdent : String;
    FTypeData  : PTypeData;
  public
    constructor Create(ACatalogId: Integer; const AName: String);
    function Gen: TBSCatalogBuilder;
    function SetDefault(const AValue: T): TBSCatalogBuilder<T>;
    function SetTextIdent(const AValue: String): TBSCatalogBuilder<T>;
  end;

{ TBSCatalogBuilder<T> }

constructor TBSCatalogBuilder<T>.Create(ACatalogId: Integer; const AName: String);
var
  i : PTypeInfo;
begin
  i := TypeInfo(T);
  Assert(i.Kind = tkEnumeration, 'Passed type is not an enumeration.');
  FTypeData  := GetTypeData(i);
  FCatalogId := ACatalogId;
  FDefault   := 0;
  FName      := AName;
end;

function TBSCatalogBuilder<T>.Gen: TBSCatalogBuilder;
var
  tmp : IBSLookupElements;
begin
  tmp := NewBSLookupElements;
  for var ix := (FTypeData.MinValue + 1) to FTypeData.MaxValue
    do tmp.AddOrSet(FTextIdent.Fmt([ix]).Translate, ix);
  Result := TBSCatalogBuilderSimple.Create(FCatalogId, FName, tmp, FDefault);
end;

function TBSCatalogBuilder<T>.SetDefault(const AValue: T): TBSCatalogBuilder<T>;
begin
  FDefault := TValue.From<T>(AValue).AsOrdinal;
  Result   := Self;
end;

function TBSCatalogBuilder<T>.SetTextIdent(const AValue: String): TBSCatalogBuilder<T>;
begin
  FTextIdent := AValue + '%d';
  Result     := Self;
end;

 

Then the usage of the helper looks like this:

type
  TElmeSchlafmodus = (esmNone, esmKeinSchlafmodus, esmTeilzeit, esmVollzeit);

  ADef.RegisterCatalog(TBSCatalogBuilder<TElmeSchlafmodus>.Create(CAT_ELME_BETRIEB_SCHLAFMODUS, 'Schlafmodus')
                         .SetTextIdent('#ElmeBetrieb.Schlafmodus')
                         .SetDefault(esmVollzeit).Gen);

 

Thanks for all help.

 

Christian

Share this post


Link to post
2 hours ago, chkaufmann said:

i := TypeInfo(T);
Assert(i.Kind = tkEnumeration, 'Passed type is not an enumeration.');
FTypeData  := GetTypeData(i);
...

 

You should use the intrinsic GetTypeKind() function instead, eg:

constructor TBSCatalogBuilder<T>.Create(ACatalogId: Integer; const AName: String);
begin
  if GetTypeKind(T) <> tkEnumeration then begin
    raise Exception.Create('Passed type is not an enumeration.');
  end
  else begin
    FTypeData  := GetTypeData(TypeInfo(T));
    ...
  end;
end;

This way, if T is an enumeration type, the compiler will omit the raise statement from the final executable, and if T is not an enumeration then it will omit the rest of the constructor code leaving just the raise, eg:

constructor TBSCatalogBuilder<AnEnumType>.Create(ACatalogId: Integer; const AName: String);
begin
  FTypeData  := GetTypeData(TypeInfo(AnEnumType));
  ...
end;
  
constructor TBSCatalogBuilder<ANonEnumType>.Create(ACatalogId: Integer; const AName: String);
begin
  raise Exception.Create('Passed type is not an enumeration.');
end;

 

The way you wrote the code originally using Assert() does not allow the compiler to make that same optimization.

 

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
Guest
19 hours ago, chkaufmann said:

I have a generic method that requires an enumeration type and a value in this enumeration. Right now my function definition looks like this:


procedure Foo(ASetType: PTypeInfo; ADefault: Integer);

...

Christian

 

hi Christian,

 

you can creae a "HELPER" for your SET of values and get the values that you need:

and use it for pass to params for your funcions, of course, maybe it needs some adjusts!

 

Helper for your SETs values

unit uHelperToSets;

interface

type
  TSetValues = (svOne, svTwo, svThree, svFour, svFive); // 0..255 = 256 elements max

type
  THelperSetValues = record helper for TSetValues // or a Class if needs more complex tasks!
    function fncValueAsByte: Byte;                // 0..255 elements
    function fncValueInText: string;
  end;

implementation

uses
  System.TypInfo; // GetEnumNames()

{ THelperSetValues }

function THelperSetValues.fncValueAsByte: Byte;
begin
  result := Byte(Self); // Self is the TSetValues;
end;

function THelperSetValues.fncValueInText: string;
begin
  result := GetEnumName(TypeInfo(TSetValues), Ord(Self));
  Delete(result, 1, 2); // deleting "sv" prefix
end;

end.

 

FormMain

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  uHelperToSets;

procedure TForm1.Button1Click(Sender: TObject);
var
  lValues: TSetValues;
begin
  lValues := svFour; // starting from "0"...
  Caption := lValues.fncValueAsByte.ToString;
  Caption := Caption + ' - ' + lValues.fncValueInText;
end;

 

hug

 

Edited by Guest

Share this post


Link to post
On 12/3/2020 at 1:45 AM, Remy Lebeau said:

You should use the intrinsic GetTypeKind() function instead, eg:

Fantastic! This function is undocumented for some reason... I added it to my TEnum<T> implementation

Edited by Fr0sT.Brutal

Share this post


Link to post
10 minutes ago, Fr0sT.Brutal said:

Fantastic! This function is undocumented and couldn't be found in System for some reason... I added it to my TEnum<T> implementation

It's an intrinsic and has no implementation ... the function is evaluated on the fly (no code will be generated). BTW, Spring4D makes extensively use of it.

Share this post


Link to post
11 minutes ago, Mahdi Safsafi said:

It's an intrinsic and has no implementation ... the function is evaluated on the fly (no code will be generated). BTW, Spring4D makes extensively use of it.

Yep I already figured it out and modified the text. Anyway there's no any mention of this feature in docwiki

Share this post


Link to post
5 hours ago, Fr0sT.Brutal said:

Anyway there's no any mention of this feature in docwiki

There are quite a few intrinsic functions that have been around for awhile but are still not documented by Embarcadero, but they are by 3rd parties.  For instance, https://stackoverflow.com/questions/30417218/.  See https://www.google.com/search?q=delphi+undocumented+intrinsics

Edited by Remy Lebeau
  • Thanks 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×