Delphi doesn't directly support any range checking or out-of-range excption raising for typecasts of integers to an enumeration. See: How do I convert an integer to an enumerated type? I am trying to get around this by automatic range checking of an array indexed on the enumeration sub-range. In my test program I deliberately used a non-continuous enumeration, which disables RTTI and provides a test for valid sub-range values that don't have a named enumeration but can be accessed by typecasting, Inc, Dec etc.
The program handles the non-enumerated element of the sub-range (2) as expected, but does not generate an out-of-range exception for the enumerated array - but does for the equivalent integer index array. Is there any good reason for this? I can work around by using the integer indexed array, but the enumeration index would be a bit more robust.
type MyEnum = (zero,one,three=3);
var EnumCheck: array[MyEnum] of integer = (0,1,2,3);
var iEnum: integer;
var MyEnumVar: MyEnum;
var IntArray: array[0..3] of integer = (0,1,2,3);
procedure Test;
begin
{$R+}
MyEnumVar:= MyEnum (1); // One
iEnum := EnumCheck[MyEnumVar]; // OK - iEnum = 1
MyEnumVar:= MyEnum (2); // Out-of-bound
iEnum := EnumCheck[MyEnumVar]; // OK - iEnum = 2
MyEnumVar:= MyEnum (3); // Three
iEnum := EnumCheck[MyEnumVar]; // OK - iEnum = 3
MyEnumVar:= MyEnum (4); // Out-of-bound
iEnum := EnumCheck[MyEnumVar]; // no Exception; iEnum is set to random value
iEnum := 4;
iEnum := IntArray[iEnum]; // Exception thrown here
iEnum := IntArray[4]; // Compiler "subrange" error
end;
Using Delphi 10.4 update 2
EDIT: I found my work around. Define the check array as:
var iCheck: array[Ord(Low(MyEnum))..Ord(High(MyEnum))] of integer = (0,1,2,3);
and an out-of-range exception is thrown as expected.
I have implemented my work-around into a relatively neat solution using an enumeration record helper.
type MyEnum = (Zero, One, Two, Three);
type MyEnumHelper = record helper for MyEnum
function SetInRange(const Value: integer): boolean;
end;
function MyEnumHelper.SetInRange(const Value: integer): boolean;
var Check: array[Ord(Low(MyEnum))..Ord(High(MyEnum))] of integer;
begin
Result := true;
Self := MyEnum(Value);
{$UNDEF _RANGEOFF}
{$IFOPT R-} {$DEFINE _RANGEOFF} {$R+} {$ENDIF}
try
var iCheck := Check[Value];
except on ERangeError do
Exit(false);
end;
{$IFDEF _RANGEOFF} {$R-} {$ENDIF}
end;
// Usage
function SetEnums(const Val1, Val2, Val3: integer): boolean
var MyEnumVar1, MyEnumVar2, MyEnumVar3: MyEnum;
begin
if (not MyEnumVar1.SetInRange(Val1)) then Exit(false);
if (not MyEnumVar2.SetInRange(Val2)) then Exit(false);
if (not MyEnumVar3.SetInRange(Val3)) then Exit(false);
end;
NB. The fixed length array (Check) doesn't need to be manually initialized as all we are interested in in the range.
There is no totally reliable way of using this for non-continuous enumerations (as I used in my original example) as the "anonymous" enumeration is still in-range. And if you get to the point of using the actual eneration values in the helper to resolve this, it rather bypasses the point of using the helper in the first place.