Delphi XE3 has a very interesting new feature: the record helpers.
Despite his name which reference a specific data type (record), a record helper can be applyed to most standard data types as well, include the sets.
Using a record helper, we will be able to add properties and methods to sets.
In this artcile, I will show you how it works using a simple case: adding a property to a set allowing the developer to convert the set to an integer or to assign a set with an integer.
You'll ask: Is it really useful in the real world ?
Yes it is! Here is the story which led me to this feature. I was working on a driver for a input/output card (I/O card). This card was driving an industrial machine. Most I/O card have "registers" which are used to program the hardware feature. Each bit in a register has a specific function such as turning on a relay or starting a moter, or reading a limit switch. Such a register can easily been represented by a Delphi set. Each bit correspond to a value in the enumeration which is the base of the set. When you read or write a register in the I/O card, you don't read or write a set but an integer. You have to add some code to transfrom the integer to/from the set so that inside the whole Delphi program, things are readable and easy to handle.
So this is the story. Of course I won't make things complex here and will translate this real world situation to something easier to grasp for anyone not accustomed to industrial control. I will replace the register by a basket with some fruits.
The basket is somewhat special: it can contain zero or more fruits, but at most one fruit of each kind taken from a fruit list.
Translated to Delphi, this gives:
type TFruit = (frApple, frPear, frApricot, frCherry); TFruits = set of TFruit;Having those declarations, we can create variables and handle the set of fruits:
var Basket : TFruits; begin Basket := [frCherry, frPear]; Basket := Panier + [frApple]; if frPear in Basket then Memo1.Lines.Add('Basket contain a pear'); end;This is actually standard Pascal as it has always existed. You'll find a lot of article on the net which will describe the set usage using Pascal.
But Delphi XE3 has much more features than standard Pascal! The one wi'll speak in a moment is the possibility to add methods and properties to a set. I will show you now how to apply this feature to convert our fruit basket into an integer and the reverse.
Let's be clear: there are a lot of ways doing this kind of conversion. I will use thoe one which is the most respectful of the language because it doesn't make any assumption about how the compiler internal represents a set.
The goal is to be able to write this kind of code:
var Basket : TFruits; N : Integer; begin Basket := [frCherry, frPear]; // Convert to an integer N := Basket.Integer; // Convert from an integer Basket.Integer := 5; end;
Look at the notation: "Basket.Integer". It is just like the Basket variable is a class and this class has a property named Integer. But Basket is not a class, it is a set.
Here is how to do that:
type TFruitsHelper = record helper for TFruits strict private function ToInteger : Integer; procedure FromInteger(Value : Integer); public property Integer : Integer read ToInteger write FromInteger; end; implementation function TFruitsHelper.ToInteger : Integer; var F : TFruit; begin Result := 0; for F := Low(TFruit) to High(TFruit) do begin if F in Self then Result := Result or (1 shl Ord(F)); end; end; procedure TFruitsHelper.FromInteger(Value: Integer); var F : TFruit; begin Self := []; for F := Low(TFruit) to High(TFruit) do begin if (Value and (1 shl Ord(F))) <> 0 then Self := Self + [F]; end; end;
That's it! The code assign a single bit into the integer to each of the enumerated type element. To do that, I have used:
- A loop for F := Low(TFruit) to High(TFruit) do used to scan the enumeration items without ever specfying any identifier;
- The intrinsic function Ord() which returns the index of the element in the enumerated type (frApple=0, frPear=1 and so on);
- The operator shl which shift a number to the left. The shithed number is 1 here. The result is a bit set to 1 at the position in the integer given by the index.
- The operator in which tells if a given enumeration element is or isn't present in the set.
- The operator and which I use to mask the interger bits to know if one specific bit is 1 or not.
Notes:
- This code assume the set is small enough to be contained in an integer (32 bit). You can easily change it to support 64 bit integer. My initial target was to represent an I/O card register and they almost always fit in an integer.
- Internally Delphi compiler is using a single bit to represent an enumeration item inside the set. Knowing that, it is possible to change the code to take advantage of it. It is faster but dependent on the internal implementation and could be broken in a future release. The code could look like this:
function TFruitsHelper.ToInteger: Integer; begin {$IF SizeOf(TFruits) = SizeOf(Byte)} Result := PByte(@Self)^; {$ELSEIF SizeOf(TFruits) = SizeOf(Word)} Result := PWord(@Self)^; {$ELSEIF SizeOf(TFruits) = SizeOf(Integer)} Result := PInteger(@Self)^; {$ELSE} {$MESSAGE FATAL 'TFruits cannot be represented as an integer'} {$IFEND} end;--
François Piette
Embarcadero MVP
http://www.overbyte.be