How to store enums without losing your coding freedom

Enums are nice!

You have found out about enumeration types and you clearly see the advantages they hold over plain, tired, old integers. They are specific, self documenting, type safe – you can’t pass one type of enum when another is expected. Oh yes, you love enums. And enum sets. No more flags and bit operations for you.

But …

They are not supported directly by databases. Or INI files. Or Json. Or XML. Or any other type of storage for that matter.

Enums as integers

You know you can store their integer value equivalents. It isn’t exactly hard.

To get the integer equivalent of any enum type, simply use the Ord function:

type
  TMyEnum = (meOne, meTwo, meThree);
var
  MyEnum: TMyEnum;
  MyInteger: Integer;
begin
  MyEnum := meTwo;
  MyInteger := Ord(MyEnum);
end;

And the other way around is just as simple:

begin
  MyInteger := 2;
  MyEnum := TMyEnum(MyInteger);
end;

Easy enough, but storing enums using their integer equivalents has a couple of humongous drawbacks:

  • The values you store in your database lose all their meaning. Would you know, off hand, what `5` means for some `TAccountType` enum?
    While you might be able to memorize the integer equivalents of a couple of enums, doing so for all enums in your code… Eeks. Personally, I prefer to use my brain power for something more interesting.
  • You lose almost all freedom to change your enum declarations. Yes, you can change the names of your enum members and you can add new members at the end. But removing obsolete members, adding new members somewhere in the middle, reordering members so the order makes more functional sense… Big no-no’s! If you did anything like that you would effectively be changing the meaning of integer values that have been stored using the previous declaration.

Storing enums as their integer equivalents is not such a bright idea then. The walls of Jericho may not come tumbling down on you, but you could very well face support staffers beating down your door equipped with tar and feathers because they’d be dealing with disgruntled users after they upgraded to a new version because that changed the meaning of whatever they stored using the previous version of your software. Ouch…

So, what to do?

Enums with specific ordinal values

In all but the earliest couple of Delphi versions, you can give specific enum members specific ordinal values:

type
  TMyEnum2 = (meFive = 4, meSix, meSeven);

Seems nice?

Yeah. Until … you are some changes down the road and your enum declaration becomes a big mess, or … until you read the fine print that says that when you do so, you lose Run-time type identification on that enum.

Run-time type identification

What? Run-time type identification? What is that? Why do I care?

Run-time type identification, more commonly referred to as Run-time type information and RTTI for short, is what allows you to do stuff like:

Log(Format('AccountType has a value of [%s], which is invalid in this context.', 
  [GetEnumName(TypeInfo(TAccountType), Ord(AccountType))]));

and see a message like this in your logs:

AccountType has a value of [atBusinessOwner], which is invalid in this context.

instead of:

AccountType has a value of [7], which is invalid in this context.

Without it, all you can do is cast to an enum’s underlying basic type: byte, word, integer, …

Enums as strings

Storing enums as strings makes your data much more friendly for human eyes.

And … you regain your coding freedoms.

The freedom to order your enum members as you see fit, is pretty obvious. You are now storing names after all, and restoring values by reading names is not dependent on the position of that name in the declaration.

With that automatically comes the freedom to add new members anywhere in the declaration that makes the most sense functionally. New members can’t after all be stored yet, and reading existing names isn’t dependent on their position in the declaration.

The freedom to remove obsolete members is slightly trickier. You need a way to handle the fact that the now obsolete names still exist in stored data. For enum sets you could simply ignore them. For enums you need a sensible fallback value.

The freedom to rename members is the trickiest of the bunch, but is perfectly feasible. Any rename after all can be seen as a combination of deletion and addition. And thus can be dealt with by using the member with the new name as the fallback value for the old one.

Most significantly, inadvertently changing the meaning of stored data because someone rearranged an enum declaration that he shouldn’t have … well, it is no longer an issue. And that is a huge advantage as it makes your software a lot less brittle.

How to …

Best news is, all of this isn’t very hard to achieve.

The TypInfo unit contains the GetEnumName and GetEnumValue functions which do the work for “single” enums:

// ToString
AccountTypeAsString := GetEnumName(TypeInfo(TAccountType), Ord(AccountType));

// FromString
AccountTypeAsInteger := GetEnumValue(TypeInfo(TAccountType), AccountTypeAsString);
if AccountTypeAsInteger >= 0 then
    AccountType := TAccountType(AccountTypeAsInteger);

This still involves quite a bit of type casting. And you need the same kind of statements for every enum in your code, so you will find yourself creating copy-pasta just to avoid having to retype them again and again.

Type casting and creating copy-pasta are two things you should avoid. The combination should have you trembling at the knees.

Preserving type safety

Hiding the casting ugliness behind a couple of functions would be a lot better. A function like:

function AccountTypeToString(const aAccountType: TAccountType): string;
begin
  Result := GetEnumName(TypeInfo(TAccountType), Ord(aAccountType));
end;

function AccountTypeFromString(const aAccountTypeString: string; 
  const aDefault: TAccountType): TAccountType;
var
  AccountTypeInteger: Integer;
begin
  AccountTypeInteger := GetEnumValue(TypeInfo(TAccountType), aAccountTypeString);
  // Value < 0 indicates the string is not a known TAccountType member name.
  if AccountTypeInteger < 0 then
    Result := aDefault
  else
    Result := TAccountType(AccountTypeInteger);
  end;

allows the rest of your code to remain perfectly type specific and type safe:

begin
   MyLogString := AccountTypeToString(AccountType);
   AccountType := AccountTypeFromString(SomeString, atBasic);
end;

You would however need to create ...FromString and ...ToString functions for every enum and enum set in your code. (Unless you can use generics, see further down.)

No fun, I’ll readily admit. But something I have done a lot and think you should do a lot too, because it is a small sacrifice to gain the benefits of less copy-pasta and more expressive and readable code. After all:

AccountTypeToString(AccountType);

reads a lot better than

GetEnumName(TypeInfo(TAccountType), Ord(AccountType));

Generics with extended RTTI

If you are on a Delphi version that supports generics as well as the extended RTTI (D2010+), you are in luck. You can forget about type casting and forget about creating all manner of conversion function pairs. You get completely type safe conversions with just two methods in a “static” (*) class:

(*) If anybody knows the proper “Delphi” way to refer to a class that only contains class methods, I am interested.

type
  TEnum<T> = class(TObject)
  public
    // Use reintroduce to prevent compiler complaint about hiding virtual method from 
    // TObject. Breaking polymorphism like this is no problem here, because we don't 
    // need polymorphism for this class.
    class function ToString(const aEnumValue: T): string; reintroduce;
    class function FromString(const aEnumString: string; const aDefault: T): T;
  end;
  
  class function TEnum<T>.ToString(const aEnumValue: T): string;
begin
  Assert(PTypeInfo(TypeInfo(T)).Kind = tkEnumeration, 
    'Type parameter must be an enumeration');

  Result := GetEnumName(TypeInfo(T), TValue.From<T>(aEnumValue).AsOrdinal);
end;

class function TEnum<T>.FromString(const aEnumString: string; const aDefault: T): T;
var
  OrdValue: Integer;
begin
  Assert(PTypeInfo(TypeInfo(T)).Kind = tkEnumeration, 
    'Type parameter must be an enumeration');

  OrdValue := GetEnumValue(TypeInfo(T), aEnumString);
  if OrdValue < 0 then
    Result := aDefault
  else
    Result := TValue.FromOrdinal(TypeInfo(T), OrdValue).AsType<T>;
end;

You use it like:

type
  TAccountType = (atBasic, atDual, atBusiness);
  TMonsterType = (mtGravatar, mtWavatar, mtMysteryMan, mtRetro, mtIdenticon);

procedure RunExample;
var
  AccountType: TAccountType;
  MonsterType: TMonsterType;
  StringValue: string;
begin
    // ToString

    AccountType := atDual;
    WriteLn(Format('AccountType atDual to string: %s', 
      [TEnum<TAccountType>.ToString(AccountType)]));

    MonsterType := mtWavatar;
    WriteLn(Format('MonsterType mtWavatar to string: %s', 
      [TEnum<TMonsterType>.ToString(MonsterType)]));

    // FromString with a known value

    StringValue := 'atBusiness';
    AccountType := TEnum<TAccountType>.FromString(StringValue, atDual);
    WriteLn(Format(
      'AccountType from string "atBusiness" with atDual as default: %d (%s)', 
      [Ord(AccountType), TEnum<TAccountType>.ToString(AccountType)]));

    StringValue := 'mtMysteryMan';
    MonsterType := TEnum<TMonsterType>.FromString(StringValue, mtIdenticon);
    WriteLn(Format(
      'MonsterType from string "mtMysteryMan" with mtIdenticon as default: %d (%s)', 
      [Ord(MonsterType), TEnum<TMonsterType>.ToString(MonsterType)]));

    // FromString with an unknown value

    StringValue := 'Unknown';
    AccountType := TEnum<TAccountType>.FromString(StringValue, atDual);
    WriteLn(Format(
      'AccountType from string "Unknown" with atDual as default: %d (%s)', 
      [Ord(AccountType), TEnum<TAccountType>.ToString(AccountType)]));

    StringValue := 'Unknown';
    MonsterType := TEnum<TMonsterType>.FromString(StringValue, mtIdenticon);
    WriteLn(Format(
      'MonsterType from string "Unknown" with mtIdenticon as default: %d (%s)', 
      [Ord(MonsterType), TEnum<TMonsterType>.ToString(MonsterType)]));

Which produces the following output:

AccountType atDual to string: atDual
MonsterType mtWavatar to string: mtWavatar
AccountType from string "atBusiness" with atDual as default: 2 (atBusiness)
MonsterType from string "mtMysteryMan" with mtIdenticon as default: 2 (mtMysteryMan)
AccountType from string "Unknown" with atDual as default: 1 (atDual)
MonsterType from string "Unknown" with mtIdenticon as default: 4 (mtIdenticon)

As I said this is completely type safe code without any casting (apart from what TValue may do internally). The compiler will sling errors at you when you do something like this:

MonsterType := TEnum<TMonsterType>.FromString(StringValue, atDual);

or this:

AccountType := TEnum<TMonsterType>.FromString(StringValue, mtMysteryMan);

Because the compiler knows that atDual is not a member of TMonsterType and that FromString does not return a TAccountType.

Me like!

Posted in Software Development
Tags: , , , ,
4 comments on “How to store enums without losing your coding freedom
  1. David Heffernan says:

    I call those classes static classes. However, I also would declare the methods with the static keyword at the end of the declaration. This suppresses the passing of the class type in a hidden Self parameter. Which you neither need nor want.

  2. David Heffernan says:

    .net has static classes http://msdn.microsoft.com/en-us/library/79b3xss3.aspx and they are baked into the language. The class is marked as static. It can never be instantiated. There is never an instance. There can only be static methods since obviously instance methods could never be called. Delphi doesn’t allow you to enforce any of this, but a class full of static methods serves the same purpose.

  3. David Heffernan says:

    Sorry to bombard with multiple comments, but I don’t care for FromString error handling. An exception or a TryFromString would be better. No way for caller to distinguish between the string value being default or default being returned because string value was not recognised.

  4. Marjan Venema says:

    No worries mate, I don’t mind being bombarded šŸ™‚

    Yes, I don’t much like it that Delphi does not give you the means to enforce “static”-ness of a class.

    You are right that the caller can’t distinguish between the string value being default or default being returned because the string was not recognised. Indeed, I usually have a pair of functions: FromString and TryFromString where FromString calls TryFromString and checks what it returns picking the default if need be. Prefer to use the ‘Try’ variant only in deserialization code and other input checking code where you can’t trust the source. In all other code I use the ‘normal’ variant, making the default explicit and allowing for scenario dependent defaults. I also tend avoid having a mtUnknown enum member. Having a mtUnknown seems nice but you end up with all your code having to deal with a possibly “undealable” situation.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Show Buttons
Hide Buttons