7

I have a Generic class which uses an Enum Generic Type. My problem how do I use GetEnumName on an instance of that type?

I've created a small demo class to illustrate the problem:

type
  TEnumSettings<TKey: record > = class
    private
      Key: TKey;
    public
      constructor Create(aKey: TKey);
      function ToString: string; override;
    end;

uses
  TypInfo;


{ TEnumSettings<TKey> }

constructor TEnumSettings<TKey>.Create(aKey: TKey);
begin
  if PTypeInfo(System.TypeInfo(TKey)).Kind <> tkEnumeration then
    Exception.Create(string(PTypeInfo(System.TypeInfo(TKey)).Name) + ' is not an Enumeration');
  Key := aKey;
end;

function TEnumSettings<TKey>.ToString: string;
begin
  Result := GetEnumName(System.TypeInfo(TKey), Integer(Key)) <== HERE I get a compile error: Invalid type cast
end;

I'm using Delphi XE. So can this be done? And if so how?

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Jens Borrisholt
  • 6,174
  • 1
  • 33
  • 67
  • Regarding your edit, I'd suggest that in production code it would be cleaner to add some static class methods like `ToOrdinal`, `FromOrdinal` from my answer. They will allow you to write your `ToString` method as `Result := GetEnumName(System.TypeInfo(TKey), ToOrdinal(Key));` and so avoid polluting this higher level method with local variables and gnarly calls to `Move`. – David Heffernan Jan 12 '15 at 12:42
  • 1
    I don't think that you *should use the Move command*. The intended way would be using `TValue` related code, like e.g. `TValue.From(Key).AsInteger`, I'd say. – TLama Jan 12 '15 at 12:44
  • 1
    @TLama That results in `EInvalidCast` – David Heffernan Jan 12 '15 at 12:45
  • @David, so is that broken, or did I get the `TValue` casting wrong ? – TLama Jan 12 '15 at 12:46
  • I think it is broken, because I get the same error here – Jens Borrisholt Jan 12 '15 at 12:46
  • @DavidHeffernan regarding your suggestions of static metods you have a point. but this small class where just create for this question. – Jens Borrisholt Jan 12 '15 at 12:47
  • 2
    @TLama No, you can do it with `TValue`. Use `AsOrdinal`. I added that to the answer. – David Heffernan Jan 12 '15 at 12:48
  • 2
    Actually simply `Result := TValue.From(Key).ToString;` works well. – TLama Jan 12 '15 at 12:56
  • 2
    Hi Jens instead of editing solutions into your question, you might make up an own answer. – bummi Jan 12 '15 at 13:22

2 Answers2

6

Personally, I would do this with a call to Move. I have the following type:

type
  TEnumeration<T: record> = class
  strict private
    class function TypeInfo: PTypeInfo; inline; static;
    class function TypeData: PTypeData; inline; static;
  public
    class function IsEnumeration: Boolean; static;
    class function ToOrdinal(Enum: T): Integer; inline; static;
    class function FromOrdinal(Value: Integer): T; inline; static;
    class function MinValue: Integer; inline; static;
    class function MaxValue: Integer; inline; static;
    class function InRange(Value: Integer): Boolean; inline; static;
    class function EnsureRange(Value: Integer): Integer; inline; static;
  end;

{ TEnumeration<T> }

class function TEnumeration<T>.TypeInfo: PTypeInfo;
begin
  Result := System.TypeInfo(T);
end;

class function TEnumeration<T>.TypeData: PTypeData;
begin
  Result := TypInfo.GetTypeData(TypeInfo);
end;

class function TEnumeration<T>.IsEnumeration: Boolean;
begin
  Result := TypeInfo.Kind=tkEnumeration;
end;

class function TEnumeration<T>.ToOrdinal(Enum: T): Integer;
begin
  Assert(IsEnumeration);
  Assert(SizeOf(Enum)<=SizeOf(Result));
  Result := 0; // needed when SizeOf(Enum) < SizeOf(Result)
  Move(Enum, Result, SizeOf(Enum));
  Assert(InRange(Result));
end;

class function TEnumeration<T>.FromOrdinal(Value: Integer): T;
begin
  Assert(IsEnumeration);
  Assert(InRange(Value));
  Assert(SizeOf(Result)<=SizeOf(Value));
  Move(Value, Result, SizeOf(Result));
end;

class function TEnumeration<T>.MinValue: Integer;
begin
  Assert(IsEnumeration);
  Result := TypeData.MinValue;
end;

class function TEnumeration<T>.MaxValue: Integer;
begin
  Assert(IsEnumeration);
  Result := TypeData.MaxValue;
end;

class function TEnumeration<T>.InRange(Value: Integer): Boolean;
var
  ptd: PTypeData;
begin
  Assert(IsEnumeration);
  ptd := TypeData;
  Result := Math.InRange(Value, ptd.MinValue, ptd.MaxValue);
end;

class function TEnumeration<T>.EnsureRange(Value: Integer): Integer;
var
  ptd: PTypeData;
begin
  Assert(IsEnumeration);
  ptd := TypeData;
  Result := Math.EnsureRange(Value, ptd.MinValue, ptd.MaxValue);
end;

The ToOrdinal method does what you need, and I'm sure you'll be able to adapt it to your class.

If you don't like using Move in this way, then you can use TValue.

TValue.From<TKey>(Key).AsOrdinal

And @TLama points out that you can avoid calling GetEnumName at all by using

TValue.From<TKey>(Key).ToString

On the face of it, using TValue seems to be more in keeping with the ethos of generics and RTTI. A call to Move relies on the specific implementation details of enumerated types. However, it's quite interesting to step through the debugger and observe quite how much code is involved in executing TValue.From<TKey>(Key).AsOrdinal. That alone is enough to make me hesitate to recommend using TValue.

Yet another way to achieve this is to use TRttiEnumerationType:

TRttiEnumerationType.GetName<TKey>(Key)

The implementation of this is much more efficient than using TValue.ToString, being little more than a call to GetEnumName.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
2

This is an updated version af my class, with the change suggested. Thanks to David and TLama

uses
  TypInfo, Rtti;

type
  TEnumSettings<TKey: record> = class
  private
    Key: TKey;
  public
    constructor Create(aKey: TKey);
    function ToString: string; override;
  end;


{ TEnumSettings<TKey> }

constructor TEnumSettings<TKey>.Create(aKey: TKey);
begin
  if PTypeInfo(System.TypeInfo(TKey)).Kind <> tkEnumeration then
    raise Exception.Create(string(PTypeInfo(System.TypeInfo(TKey)).Name) + ' is not an Enumeration');
  Key := aKey;
end;

function TEnumSettings<TKey>.ToString: string;
begin
  Result := TValue.From<TKey>(Key).ToString;
end;

And a little test example :

Copy the code into OnCreate of a From:

procedure TForm1.FormCreate(Sender: TObject);
begin
  with TEnumSettings<boolean> .Create(True) do
    try
      Caption := ToString;
    finally
      Free;
    end;
end;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Jens Borrisholt
  • 6,174
  • 1
  • 33
  • 67
  • 2
    You might turn into [`the similar fashion`](http://pastebin.com/SKqNgQG1) your constructur as well ;-) – TLama Jan 12 '15 at 14:56