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
Me like!
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.
.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.
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.
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
andTryFromString
whereFromString
callsTryFromString
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 amtUnknown
enum member. Having amtUnknown
seems nice but you end up with all your code having to deal with a possibly “undealable” situation.