5

I'm trying to create a generic class to which I can use a set of enums to initiate the values inside. For example:

constructor TManager<TEnum>.Create;
var
  enum: TEnum;
  enumObj: TMyObject;
begin
  fMyObjectList:=  TObjectDictionary<TEnum,TMyObject>.Create([doOwnsValues],10);
  for enum:= Low(TEnum) to High(TEnum) do
  begin
    enumObj:= TMyObject.Create();
    fMyObjectList.Add(enum, enumObj);
  end;
end;

Additionally, later methods will fetch objects, via the enum value, for example:

function TManager<TEnum>.Fetch(enum: TEnum): TMyObject;
begin
  fMyObjectList.TryGetValue(enum, Result);
end;

However, passing as a generic parameter, delphi doesn't know that TEnum is going to be an enum. Can I enforce that in some way?

Eric G
  • 3,427
  • 5
  • 28
  • 52
  • Side note: I'm sure there is probably an actual map data structure in the Generics unit. Feel free to point it out if you know it off hand. – Eric G Sep 11 '12 at 22:54
  • 2
    how about TDictionary? –  Sep 11 '12 at 23:38
  • You can restict to the value types with the `record` constraint. Like so: `type TManager = class ...` The compiler will then prevent Interfaces and Classes being used for `TEnum` however you cannot currently (XE6 and before) restrict the value type further at compile time. – Johan Jul 24 '14 at 20:06

2 Answers2

7

As David mentioned the best you can do is at runtime with RTTI.

    type  
      TRttiHelp = record
        class procedure EnumIter<TEnum {:enum}>; static;
      end;

    class procedure TRttiHelp.EnumIter<TEnum {:enum}>;
    var
      typeInf: PTypeInfo;
      typeData: PTypeData;
      iterValue: Integer;
    begin
      typeInf := PTypeInfo(TypeInfo(TEnum));
      if typeInf^.Kind <> tkEnumeration then
        raise EInvalidCast.CreateRes(@SInvalidCast);

      typeData := GetTypeData(typeInf);
      for iterValue := typeData.MinValue to typeData.MaxValue do
        WhateverYouWish;
    end;  

Although I don't know how the code behaves when your enum has defined values such as

    (a=9, b=19, c=25)

Edit:

If you would like to return iterValue to the enum, you may use the following function, taken from a enum helper class by Jim Ferguson

class function TRttiHelp.EnumValue<TEnum {:enum}>(const aValue: Integer): TEnum;
var
  typeInf: PTypeInfo;
begin
  typeInf := PTypeInfo(TypeInfo(TEnum));
  if typeInf^.Kind <> tkEnumeration then
    raise EInvalidCast.CreateRes(@SInvalidCast);

  case GetTypeData(typeInf)^.OrdType of
    otUByte, otSByte:
      PByte(@Result)^ := aValue;
    otUWord, otSWord:
      PWord(@Result)^ := aValue;
    otULong, otSLong:
      PInteger(@Result)^ := aValue;
  else
    raise EInvalidCast.CreateRes(@SInvalidCast);
  end;
end;

You may then use the generically provided as the index to the dictionary in your constructor.

Tobias R
  • 928
  • 8
  • 22
  • Enums are contiguous so you'll get all ordinal values between 9 and 25. Not sure if it starts at 0. Think not. +1 – David Heffernan Sep 12 '12 at 07:28
  • 4
    As I remember enums with defined values like (a=9) do not have RTTI information at all – teran Sep 12 '12 at 08:46
  • 1
    checked. `type TTestEnum = (one, two, five = 5);` *->* `[DCC Error] Project4.dpr(14): E2134 Type 'TTestEnum' has no type info` Delphi 2010 – teran Sep 12 '12 at 08:50
  • I'm not familiar with RTTI and fairly new to Delphi in general. Couple questions: 1. I assume `{:enum}` is just a comment to clarify you should be sending an enum type? 2. What is the ^ operator? – Eric G Sep 12 '12 at 17:48
  • So also, later, in my functions like `getObject(enum:TEnum):TMyObject` I'm going to need to do this whole deal again to get the the enum integer? – Eric G Sep 12 '12 at 18:13
  • If you want to look up objects given a name, i.e. your getObject, then you actually want to use `TDictionary`. What's more your current code leaks. It creates objects but does not free them. So, use `TObjectDictionary` instead. Pass `doOwnsValues` to the constructor. See RRUZ's answer here for more details: http://stackoverflow.com/questions/8463968/example-for-using-generics-collections-tobjectdictionary and my updated answer. – David Heffernan Sep 12 '12 at 18:19
  • Yeah, I had updated my code based on the comment previously, but have made the change here as well. I was freeing everything manually in the destructor, but using the TObjectDictionary is much nicer. That doesn't seem to help with the issue of using the Generic TEnum as an enum. Do I really need the key value to be an Integer and then convert my enum value to an Integer at every use? Or is there a way to get the appropriate enum value from the `iterValue` found by this method? – Eric G Sep 12 '12 at 23:20
2

You cannot constrain a generic parameter such that low() and high() can be used in the generic class. The only constraints available are class or interface constraints.

To the very best of my knowledge, the language offers no generic way to enumerate over a generic enumerated type. Probably the best you can do is to use RTTI, sacrificing compile time type safety (as illustrated by Tobias).


Having read the comments to Tobias's answer, it seems likely that what you really want here is TObjectDictionary<TEnum,TMyObject>. That's because you want to be able to find a TMyObject instance given a TEnum key. And you want TObjectDictionary rather than TDictionary because the former takes over ownership of the TMyObject instances. You need somebody to free them all when you are done, and you may as well let TObjectDictionary do it for you.

For an example of the ownership side of this, see @RRUZ's answer here: Example for using Generics.Collections.TObjectDictionary

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