November 17, 2012

XE3: Ajouter des propriétés aux ensembles

[English]
Delphi XE3 dispose d'une nouvelle fonctionnalité particulièrement intéressante dès lors qu'on veut bien y regarder de plus près. Je veux parler de l'assistance d'enregistrements ("record helper" en anglais).

Malgré son nom qui évoque un type de donnée spécifique, les enregistrements, l'assistance d'enregistrements s'applique aussi à beaucoup de type de données intrinsèques de Delphi dont les ensembles (construction "set of ...").

L'assistance d'enregistrement va nous permettre d'ajouter à un ensemble une ou plusieurs propriétés ou méthode.

L'exemple simple que je vais développer dans cet article concerne l'ajout d'une propriété à un ensemble. Propriété qui va permettre d'initialiser une ensemble depuis un nombre entier, ou d'obtenir un nombre entier à partir d'un ensemble.

Vous me direz : "A quoi cela peut-il bien servir dans le monde réel ?".

C'est simple... J'ai été amené à écrire ce code parce que je travaillais sur un programme qui pilotais une carte d'entrée/sortie (E/S). Cette carte d'E/S dispose de registres dont chaque bit a une signification particulière. Représenter cela en Delphi est assez facile, c'est l'exemple même du cas d'application d'un ensemble : on donne un nom symbolique et parlant à chaque bit; un registre étant finalement un ensemble de bit, et bien nous avons une adéquation parfaite entre la construction "set of" de Delphi et le registre.

Je vais vous faire grâce des détails de ma carte d'E/S et vais basculer sur un exemple plus simple mais tout à fait équivalent. Je vais parler de fruits (les bits du registre) et d'un panier de fruits (Le registre lui-même). Panier spécial puisqu'il peut contenir plusieurs fruits, mais seuelemnt zéro ou un fruit d'un type déterminé.

En Delphi, cela donne:

type
  TFruit  = (frPomme, frPoire, frAbricot, frCerise);
  TFruits = set of TFruit;

Ayant ces deux déclarations, on peut créer des variables et manipuler l'ensemble TFruits:

var
  Panier : TFruits;
begin
  Panier := [frCerise, frPoire];
  Panier := Panier + [frPomme];
  if frPoire in Panier then
    Memo1.Lines.Add('Il y a une poire dans le panier');
end;


Tout cela, c'est très bien et classique, cela existe depuis toujours en Pascal et vous trouverez de nombreux articles qui expliquent en long et en large la manipulation des ensembles en Pascal.

Mais Delphi XE3 fait bien plus que cela ! Vous pouvez ajouter des methodes et propriétés aux ensembles. Voyons comment sur le cas concret que j'évoquais plus haut: transformer l'ensemble en un nombre entier et vice versa.

Disons-le d'emblée: il y a plusieurs manières de réaliser cette tâche de conversion. Je vais vous présenter celle qui est la plus respectueuse du langage et qui ne fait aucune supposition sur la manière dont les ensembles sont réprésentés en interne par le compilateur.

Ce que je veux pouvoir faire, c'est écire ce genre de code:

var
  Panier : TFruits;
  N      : Integer;
begin
  Panier := [frCerise, frPoire];
  // Conversion vers un entier
  N := Panier.Integer;
  // Conversion depuis un entier
  Panier.Integer := 5;
end;

Remarquez la notation: "Panier.Integer". C'est comme si la variable Panier était une classe et que cette classe avait une propriété "Integer". Mais Panier n'est pas une classe, c'est un ensemble ("Set of").

Voici comment il faut procéder:

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;


Et c'est tout...
Le code de conversion associe à chaque élément du type énuméré TFruit un bit dans l'entier. Pour cela, j'utilise
  1. Une boucle for F := Low(TFruit) to High(TFruit) do qui permet de parcourir la liste de l'énumération sans spécifier  aucun nom particulier;
  2. La fonction Ord() qui retourne l'ordre de l'élément dans la liste du type énuméré (frPomme vaut 0, frPoire vaut 1, et ainsi de suite);
  3. L'opérateur shl qui permet de décaler un nombre à gauche d'un certain nombre de positions. Le nombre que je décale est 1 et la position est l'ordre de l'élément. Il en résulte un bit à 1 à la position donnée par l'odre.
  4. L'opérateur in qui permet de savoir un des éléments de l'énumération est ou non présent dans l'ensemble.
  5. L'opérateur and qui me permet de masquer un bit dans le nombre entier pour savoir s'il est à zéro ou pas.
Et voilà...

Notes:
  1. Ce code ne fonctionne que si l'ensemble est suffisament petit pour tenir dans un nombre entier (32 bits). Il est facilement modifiable pour fonctionner avec un nombre de 64 bits. Mon objectif initial était de représenter des registres d'une carte d'entrées/sorties, donc aucun soucis car les registres sont généralement de 8, 16 ou 32 bits.
  2. En interne, le compilateur Delphi utilise déjà un bit pour chaque élément d'un ensemble. Sachant cela, il est possible de simplifier le code, mais à ce moment il devient dépendant de l'implémentation de Delphi, ce qui pourrait changer à l'avenir. Voici à quoi pourrait ressembler le code dans ce cas:
      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









 

No comments: