[delphi] déclarer et utiliser les types

[delphi] déclarer et utiliser les types

Introduction

Pourquoi ?
Tout simplement parce que Delphi hérite du Pascal qui est un langage extrêmement typé, c'est-à-dire qu'il faut déclarer scrupuleusement tout ce qu'on utilise. Non seulement cela structure sans faute les programmes, mais surtout cela apporte une logique et une rigueur qu'il faut comprendre.

Utiliser un type existant

C'est tout bête : on l'utilise dans la déclaration des variables. Exemple :

procedure MaProcedure;
var MaVariable : boolean;
    MaVariable2, MaVariable3 : integer;
    MaVariable4 : integer;
begin
end;

On vient de déclarer une variable booléenne (faux ou vrai) ainsi que trois entiers 32 bits. On remarque qu'on sépare les variables de même type par des virgules, et rien n'empêche d'avoir des lignes différentes déclarant des variables de même type.

Connaître les types de base

Plus les types sont étendus et plus ils consomment de la mémoire (tout est relatif). De ce fait, on choisira les types les moins étendus possible : c'est le fameux «immédiatement supérieur».

Les entiers

byte non signé 0 .. 255
shortint signé -128 .. 127
word non signé 0 .. 65 535
smallint signé -32 768 .. 32 767
cardinal non signé 0 .. 2 147 483 647
integer signé -2 147 483 648 .. 2 147 483 647
longint signé -2 147 483 648 .. 2 147 483 647
int64 signé integer version 64 bits
-9 223 372 036 854 775 808 ... 9 223 372 036 854 775 807

On remarque qu'il existe des couples de 8 bits, 16 bits et 32 bits avec l'un étant le demi-décalage de l'autre. Même si l'aide de Delphi énonce integer = -32 768 .. 32 767, sachez qu'il est toujours considéré comme un 32 bits. Au passage, cardinal doit être considéré comme un integer positif.

En Delphi, il n'y a pas de soucis pour indiquer si un entier est oui ou non signé (en résumé s'il peut être négatif), car on passe par des types différents. En C++, par exemple, il y a nuance entre signed int et unsigned int.

Note : les étendues de tous les types que je vous donne sont à connaître par coeur !

Les réels

Delphi est à la limite de l'arnaque. Voici les types, avec des arrondis assez ronds :

real ±10^38
single ±10^38
double ±10^308
extended ±10^4932

Généralement, pour les petits calculs de rien du tout, real suffit bien. C'est d'ailleurs le type le plus déclaré dans la VCL de Delphi. Sauf que, pour des calculs plus sympathiques, je préfère utiliser largement le type extended.

Vous allez me dire qu'il y a un problème d'étendue... Tout à fait, sauf que real est tellement étendu lui-aussi que les transcriptions de types se font sans dommage (dans la plupart des cas seulement, car il faut rester conscient du danger). Vous inquiétez pas, on va voir ça après.

Les booléens

Alors là, je ne comprend pas pourquoi ces zouaves n'apparaissent que maintenant, car c'est le type le plus simple qu'il soit. Enfin bref...

boolean = false ou true

Les types énumérés

C'est simple : on énumère un à un tout ce qu'il contient. Les éléments qui composent le type prennent le nom qu'ils veulent, car de toute façon, au sein de l'ordinateur, c'est leur position dans l'énumération qui les caractérisera.
Exemple :

type TCodesSources = (csDelphi, csPhp, csCpp, csAsm, csAsp);

Ainsi, que csDelphi s'appelle Delphi, LangageDelphi ou ModeDelphi, ça ne change rien.

Prenons nos habitudes. C'est Borland qui fixe les règles et c'est bien normal. L'idée est de précéder tous les éléments d'un couple de deux lettres en minuscules rappelant le nom du type dans lequel ils sont déclarés. Dans notre cas, le type TCodesSources est dominé par les lettres "C" et "S". De ce fait, ses éléments seront construits en csMachin, csBidule ou même csTruc. Si on avait eu TCodes, alors on aurait sûrement retenu coTartanpion. C'est uniquement un moyen mnémotechnique pour bien programmer.

Pour utiliser ces types, on fait ceci :

type TCodesSources = (csDelphi, csPhp, csCpp);
var Langage : TCodesSources;
begin
  Langage:=csPhp;
end;

C'est tout... L'intérêt est d'éviter de déclarer des entiers pour désigner tel ou tel langage. Là, tout est combiné : nom et valeur numérique. Ça permet également de simplifier les tests de conditions par le passage à une structure en CASE. Comme tout est question de facilité, on aurait alors :

begin
  case Language of
    csDelphi: ShowMessage('Delphi');
    csPhp: ShowMessage('PHP');
    else ShowMessage('C++');
  //si on met un ELSE c'est qu'on est sûr qu'il n'y a plus rien après csCpp
  end;
end;

Une fois le type énuméré, ses éléments doivent être considérés dans tout l'EDI de Delphi comme des constantes qui auraient été déclarées via le mot clé CONST. On peut donc faire des tests :

begin
  if csDelphi < csPhp then
    ShowMessage('Vous avez raison !');
end;

Je dois vous faire pointer le doigt sur un détail ultra important qui nous poursuivra jusqu'au bout de nos rêves, enfin... Le code suivant ne marche pas :

begin
  if csPhp=1 then
    ShowMessage('Vous avez structurellement tord !');
end;

... mais ce qui suit est parfait :

begin
  if Ord(csPhp)=1 then
    ShowMessage('Vous avez raison !');
end;

Tiens ? csDelphi est classé rang zéro ? Tout à fait et il ne faut pas l'oublier...

On vient d'introduire la fonction ORD de l'unité System (déclarée automatiquement par le compilateur, essayez de mettre son nom dans les USES et vous verrez). L'opération inverse n'existant pas, on doit procéder comme il suit. On va aller chercher explicitement la valeur, là où elle se trouve :

begin
  Langage:=TCodesSources(2);
  if Langage=csCpp then
    ShowMessage('Vous avez raison !');
end;

J'ai dit «l'opération inverse n'existant pas». En ce qui concerne les char (un caractère ASCII en bref), la fonction CHR permet de renvoyer le n_nième caractère de la table ASCII, sachant que l'on a :

type char = #0 .. #255;

Les types intervalle

Disons simplement que le type est défini par une étendue : ça va de tant à tant, les bornes étant des valeurs de base finies. On note les types énumérés par un double point "..".

Par exemple, nous avons :

type TShortIntPositif = 0..127 ;

L'habitude (et les bonnes manières !) veulent qu'on précède les noms de types par la lettre "T" en majuscule. Vous avez compris que cela désigne le mot Type. Je ne le dis que maintenant, mais c'était valable depuis le début.

Les types de base (cités dans le premier paragraphe) sont fixés et servent à catir d'autres types. Ainsi, integer ne pourra jamais être substitué à un type que vous aurez vous même construit par dérivation.

Je vous donne un petit morceau de code qui est incorrect en raison de l'existence d'une infinité de nombres réels entre -Pi et 2*Pi. On aurait alors déclaré une étendue infinie, ce qui est absurde... Au passage, il plante bien la version 3 de Delphi.

procedure TForm1.FormCreate(Sender: TObject);
type TFlt = -3.14 .. 6.28 ;
var Vra : TFlt;
begin
  Vra:=-3;
end

;

Donc ceci ne marche pas...

Vous vous souvenez que boolean = false..true ? Enfin, ça c'est la ruse du compilateur, car il faut déterminer false et true. C'est l'objet du point suivant.

type TCodesSources = (csDelphi, csPhp, csCpp) ;
     TMesLangagesFetiches = csDelphi .. csPhp ;
var LangagesAMoi : TMesLangagesFetiches;
begin
  LangagesAMoi:=csDelphi;
  LangagesAMoi:=csPhp;
  LangagesAMoi:=csCpp;
end;

On compile un beau message d'erreur : csCpp dépasse les limites de sous-entendues.

Ben, vous savez tout...

Déclarer succinctement des variables typées pas comme les autres

Un type sert pour déclarer des variables. Reprenons notre exemple du début :

type TCodesSources = (csDelphi, csPhp, csCpp) ;
var Langage, MonPoteFaitAussiDeLaProg : TCodesSources;
begin
  Langage:=csPhp;
end;

Vous ne voyez bien sûr aucune objection si j'écris ceci :

var Langage, MonPoteFaitAussiDeLaProg : (csDelphi, csPhp, csCpp);
begin
  Langage:=csPhp;
end;

Donc, vous avez tout compris...

Cette technique n'est intéressante que pour supprimer des lignes superflues concernant des types très peu utilisés.

Les ensembles

Je vous présente la propriété Style du persistent TFont :

type TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeOut);
     TFontStyles = set of TFontStyle;

Qu'est-ce qu'un persistent ?
C'est une classe TPersistent qui sert à faire des propriétés classées dans l'inspecteur d'objets de Delphi. Regardez, il y a un "+" devant la propriété Font. C'est lié au fait que :

type TFont = class(TPersistent)
       ...
     end;

Ça sert pour le développement de composants, qui ne nous intéressent pas ici.

Ainsi TFontStyles est un set of quelque chose, une combinaison d'éléments en fait. Vous remarquez que les habitudes préconisent de rajouter un "S" à TFontStyle pour créer un set, sachant que TFontStyle énumère toutes les entités pour l'ensemble (nécessairement fini).

On a alors les bouts de code suivants :

var FS : TFontStyles;
begin
  FS := [fsBold, fsStrikeOut];
  if fsBold in FS then
    ShowMessage('Vous avez raison');
  FS := FS - [fsBold];
  if FS = [fsStrikeOut] then
    ShowMessage('Vous avez encore raison');
  FS := FS + [fsItalic];
end;

On teste la présence d'un élément par le mot magique in. On ajoute et supprime avec les opérateurs + et -.
Et si j'ai envie de trouver les éléments communs, que dois-je faire ?
Eh bien, faites ceci :

var F1, F2, Intersec : TFontStyles;
begin
  F1 := [fsBold, fsStrikeOut];
  F2 := [fsItalic, fsUnderLine, fsStrikeOut];
  Intersec := F1 * F2;
  if Intersec = [fsStrikeOut] then
    ShowMessage('Vous avez raison');
end;

C'est tout...

Les enregistrements

Ça aussi, c'est un point super passionnant... Une fois de plus, il n'y a pas à discutailler 40 ans, mais ça permet de regrouper des variables désignant les propriétés d'un même objet. Voici un exemple :

type TVoiture = {packed} record
       Couleur : integer;
       Matiere : (maAcier, maPapier, maFerraille);
       Marque : string;
     end;
var MaVoiture : TVoiture;

Ravi de voir que vous vous intéressez à ma voiture... mais en papier, ça ne le fait pas trop ! Je passe chez mon garagiste et il me construit la voiture suivante :

begin
  MaVoiture.Couleur:=$000000; //toute noire
  MaVoiture.Matiere:=maAcier;
  MaVoiture.Marque:='Lada';
end;

Il est bien gentil ce garagiste, mais si je lui donne TaVoiture, il va devoir modifier les 3 lignes. Sachez qu'il peut n'en modifier qu'une s'il procède ainsi :

begin
  with MaVoiture do
    begin
      Couleur:=$000000;
      Matiere:=maAcier;
      Marque:='Lada';
    end;
end;

Vous voyez bien sûr le truc... mais le with réserve des surprises. Voici un code :

begin
  with MaVoiture, TaVoiture do
    Marque:='Lada';
end;

L'expérience montre que seule la dernière variable accolée dans le with est initialisée.
Alors, où est l'intérêt ?
Eh bien, comme MaVoiture et TaVoiture sont de même type, Delphi ne sait pas à laquelle des deux variables Marque se réfère. Si maintenant on mélange deux enregistrements n'ayant pas de "sous-clé" commune, par recherche inverse (droite vers la gauche dans le with), Delphi sait quoi associer à qui. Personnellement, je n'ai jamais utilisé une telle méthode... il y a trop de risques pour bugger un logiciel (ça suffit comme ça !).

Pour en revenir au début du paragraphe, pourquoi avoir mis en commentaire le mot magique packed ? Ben, si vous utilisez les fonctions BlockRead et BlockWrite, ça vous sera utile. Sinon, regardez le paragraphe sur les tableaux simples pour un autre exemple d'application. En fait, ça fait comme si le record était une seule variable, laquelle en se logeant en mémoire se partitionne automatiquement pour reformer les différentes parties de l'enregistrement.

Les tableaux simples

Vous aimez voyager à travers les dimensions parallèles ?
Eh bien, contentez vous de savoir compter jusqu'à 10, parce que pour une fois que Delphi nous laisse de la marge, ce n'est pas le moment de déraper.

Nous avons vu que les énumérations et compagnie commencent à partir de 0. Avec les tableaux, il est possible de les faire démarrer où on veut. Voici la syntaxe conseillée :

type TTableau = array [0..9] of byte;
var MonTableau : TTableau;

... et même (vous voyez que ça sert) :

var MonTableau : array [0..9] of byte;

Vous notez peut-être pas immédiatement que ce tableau comporte 10 cases et non 9 ! De même, le tableau suivant a aussi 10 cases (quoi que là c'est plus évident) :

var MonAutreTableau : array [1..10] of integer;

On vient de montrer au passage comment démarrer un tableau ailleurs. Pour illustrer le fonctionnement, voici des bouts de code :

var i : byte;
begin
  MonTableau[7]:=5;
  for i:=0 to 9 do
    MonTableau[i]:=i;
  i:=MonTableau[4];
end;

Ça me permet au passage de montrer que ma variable de boucle "i" est déclarée le plus petit possible. Rappel : byte = 0..255. Et comme j'ai envie de vous montrer autre chose, on va faire ceci :

var i : byte;
begin
  for i:=9 downto 0 do
    MonTableau[i]:=i;
end;

C'est le principe de la boucle inverse sans opérations de soustraction. Voilà la boucle for expliquée... Notons que Delphi ne donne pas la possibilité de faire des boucles à sauts d'indice. Soit on ruse avec l'ASM (pas très propre), soit on garde le Pascal et son opérateur modulo mod. Ainsi, le code suivant n'initialise que les indices pairs :

var i : byte;
begin
  for i:=9 downto 0 do
    if i mod 2 = 0 then
      MonTableau[i]:=i;
end;

Ou alors on fait des boucles repeat pour les sauts d'indice... Tout dépend de ce qu'on veut faire en fait.

Puisqu'on vient d'introduire la boucle for, pour remplir les deux premières cases du tableau, on peut aussi faire comme cela :

var b : boolean;
begin
  for b:=false to true do
    MonTableau[Ord(b)]:=Ord(b);
end;

J'avoue que c'était pas une bonne idée d'évoquer cette subtilité, mais cela montre bien les rages techniques des types intervalle.

Pompon final sur les tableaux (accrochez vous, ou passez au grand paragraphe suivant si vous êtes à la limite du décrochage [je suis sérieux]) : le mot packed. En fait, lorsque vous développer sous l'EDI de Delphi, les variables string sont limitées à 255 caractères. C'est un souci technique des linkers de traduire les programmes en instructions interprétables. A ce moment du logiciel, on a l'équivalence (pas l'égalité !) entre string et packed array [1..255] of char.

Cette astuce permet d'écrire MaString:='Salut mec' (car c'est un string), et on peut récupérer des caractères via MaString[IndexCase] car c'est un tableau (de 255 cases pour l'anecdote). Le comble est que ce tableau commence à 1 et pas à zéro. Ne m'attaquez pas ; je n'y suis pour rien... Mais attention, une fois que l'application est compilée, les string deviennent vastes et théoriquement infinis. Le linker a ajouté des modules afin que l'application sache manipuler seule les grandes données. On pourra noter des pertes de performances flagrantes quand une chaîne devient trop longue. Dans certains traitements, il faut programmer de manière à avoir les plus petits éléments possibles. Par exemple, en Turbo Pascal, les chaînes de caractères sont limitées à 255 caractères et ce, quel que soit l'état de vos développements.

En résumé, grâce à packed, au lieu de faire :

begin
  MaString[1]:='S';
  MaString[2]:='a';
  MaString[3]:='l';
  ....
end;

... eh bien vous faîtes ...

begin
  MaString:='Salut mec';
end;

... et c'est considéré comme [S][a][l][u][t][ ][m][e][c]. Le comble (oui encore un !), c'est qu'en faisant caractère par caractère, il peut y avoir une exception en mémoire (=erreur qui ne devrait jamais apparaître). Ben ouais, car une fois compilé, string est dynamique, donc il faut allouer, ce qui n'est pas possible via ce tâtonnement incrémental. Plus technique encore, quand vous faîtes MaString:='Salut mec', Delphi transforme votre syntaxe en utilisant des fonctions de l'unité System. C'est pour cela que cette unité est la plus mystérieuse de toute, car dans le fond, je suis persuadé qu'elle ne peut même pas se compiler...

Des fois, il vaudrait mieux ne pas se poser trop de questions.

Les tableaux multidimensions

Pour une matrice, il faut des tableaux de dimension 2 remplis de réels. On fait donc :

const MatSize = 5;
type TMatCol = array [0..MatSize] of real;
     TMatrice = array [0..MatSize] of TMatCol;
var Matrix : TMatrice;

On obtient ainsi une matrice 5x5. Ici TMatCol désigne virtuellement les colonnes de la matrice. Ça pourrait très bien considérer les lignes... En fait, tout dépend de la modélisation de la matrice dans la mémoire. La case [X,Y] est-elle (X,Y) ou (Y,X) sur le papier du mathématicien ? Bref, on s'en fiche du moment que les indices désignent ce qu'on veut qu'ils désignent.

Pour les multidimensions, le passage à une dimension supérieure requiert l'utilisation de types intermédiaires (pour une dimension 2, j'ai bien pris deux lignes de code), car sinon Delphi ne sait plus où donner de la tête.

On a alors :

var Reelle : real;
begin
  Matrix[4][3]:=3.14;
  Reelle:=Matrix[6][7];
end;

Attention : ce genre de tableau multiplie très rapidement le nombre de variables en mémoire !! Evitez d'être trop gourmand, surtout si vous succombez au charme de extended.

Pour plus d'informations sur les constantes, allez voir le lien suivant :
http://www.delphifr.com/code.aspx?ID=31036

Les tableaux à taille variable

Il faut utiliser la fonction SetLength.

Je ne fais aucun commentaire n'étant pas sujet à un tel problème. Allez voir le code spectral de Kenavo sur les entrées multimédia pour une mise en jambe.

Les classes

Ce paragraphe est la cerise sur le gâteau... sauf que c'est trop compliqué pour être appliqué à de simples variables de calcul.
Les classes sont la version moderne des enregistrements record. Elles sont un ensemble de variables, de types, de constantes, de procédures, de fonctions, de propriétés, d'interfaces, de constructeurs, de destructeurs... et vous concevez facilement que ça sert pour développer des composants dans vos applications.

type TMaClasse = class
     private
     public
     protected
     published
     end;

Imaginons que j'ai construit une classe TSupport. Je voudrais faire une classe TDeveloppement qui regroupe tous les éléments de TSupport mais en rajoutant certaines caractéristiques. Il est inutile de faire du copier-coller, on préfère dériver les classes. Exemple :

type
  TSupport = class
  end;
  TDeveloppement = class(TSupport)
  private
    FVariableNonInitialisee : boolean;
  published
    property Propriete:boolean read FVariableNonInitialisee write FVariableNonInitialisee;
  end;

Vous voyez l'apparition de class(TSupport) dans l'expression de TDeveloppement. Certes, TDeveloppement est définie à partir de TSupport, mais TDeveloppement ne peut plus accéder aux rubriques private et protected de sa classe parente. En effet, on est passé à une chose toute différente. C'est pour cela que les rubriques public et published doivent offrir à l'utilisateur le strict nécessaire sans qu'il soit possible de mettre le bazar dans la classe.

Si vous voyez ce qui suit, c'est pour indiquer à Delphi que la classe sera définie ultérieurement. L'analyse descendante de Delphi couplée avec les références circulaires rendent indispensables le code suivant :

type TSupport = class;

Si j'évoque cette dérivation, c'est pour parler de la transcription des types. On l'avait déjà évoqué plus haut, et c'est maintenant qu'on va expliquer.

Transcriptions des types

Il y a deux types de transcription : celle des types de base et celle des classes.

Pour les types de base, c'est la notion d'étendue qui est importante. Voyez cet exemple sur les entiers :

var b : byte;
    i : integer;
begin
  i:=257;
  b:=byte(i);
  ShowMessage(IntToStr(b));
end;

Il nous affiche "1" via la transcription de type sur byte.
Attention : byte(i) n'est pas une fonction, c'est une transcription, c'est-à-dire qu'on associe d'une certaine manière deux choses qui ne sont pas de même type.
Voyez l'exemple suivant :

var b : byte;
    i : integer;
begin
//d'abord...
  b:=57;
  i:=b;
  ShowMessage(IntToStr(i));
//...puis après
  i:=257;
  b:=i;
  ShowMessage(IntToStr(b));
end;

Les deux cas fonctionnent bien. Le premier ne pose pas de problème puisque l'étendue de byte est incluse dans celle de integer. En revanche, dans le deuxième cas, du travail est fait... En utilisant byte(i), ça permet de montrer à des collègues qu'il y a une possibilité de bug si on n'a pas vu le coincement entre byte et integer.

Ca marche pareillement avec les nombres flottants (=à virgule).

Pour convertir des flottants en entiers ou en chaînes de caractères (et vice versa), il faut utiliser les fonctions proposées par Delphi : int, round, trunc, abs, StrToFloat, FloatToStr, IntToStr, StrToInt... De l'assembleur est souvent caché dans ces fonctions.

Pour ce qui est des transcriptions de classe, c'est sympathique mais dangeureux. On a vu que par dérivation, il y a hiérarchisation des classes. On ne peut convertir des sous-classes en leur classes parentes que si vous n'utilisez pas les caractéristiques ajoutées aux sous-classes. Pour expliciter cela, le code suivant est bon si on n'exploite pas la propriété "Propriete" publiée par TDeveloppement. Il n'y aura alors pas de conflits possibles, même si le risque existe.

var Support : TSupport;
    Developpement : TDeveloppement;
begin
  Support:=TSupport(Developpement);
end;

J'ajouterai que cette traduction par pseudo-fonction est à exclure. Si on se le permet avec byte(...), c'est parce qu'il y a une notion d'étendue. Avec les classes, ce n'est vraiment pas ça. Mieux vaut utiliser le mot clé as. Ça a l'avantage de générer une exception en cas d'impossibilité de traduction : EInvalidCast. On aurait alors :

var Support : TSupport;
    Developpement : TDeveloppement;
begin
  Support:=(Developpement as TSupport);
end;

Si je garde les parenthèses, c'est qu'avec les items radio-checkés des TPopupMenu, on est souvent amené à coder des procédures du style :

procedure TForm1.Menu11Click(Sender: TObject);
begin
  (Sender as TMenuItem).Checked:=true;
end;

Delphi dit que TMenuItem dérive de TObject (même lointainement). Ici, nous avons eu affaire à une transcription descendante. En effet, on considère le parent comme un de ses enfant, et non l'enfant comme son parent. Ainsi, sachant que dans notre coding, Sender est nécessairement un TMenuItem, il n'y a aucun litige.

Conclusion

J'espère que ça vous aura intéressé et que vous aurez pris conscience de l'importance des types qui, en plus de vous aider, génèrent des bugs parfois très soutenus.

Tanguy ALTERT,
http://altert.family.free.fr/

A voir également
Ce document intitulé « [delphi] déclarer et utiliser les types » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous