![]() |
|
Marco Cantù
Traduit de l'anglais par Iannis Papageorgiadis [email protected] |
Chapitre 6
|
Un autre concept important mis en évidence par Pascal est celui de routine; il s'agit fondamentalement d'une série d'instructions sous un nom unique, que l'on peut activer plusieurs fois en utilisant leur nom. Cette façon de faire évite de répéter les mêmes instructions à plusieurs reprises, et, en disposant d'une seule version du code, on peut aisément le modifier partout dans le programme. De ce point de vue, on peut considérer les routines comme le mécanisme fondamental de l'encapsulation de code. Nous reviendrons sur ce sujet avec un exemple après une introduction à la syntaxe des routines Pascal.
En pratique, cependant, la différence entre
fonctions et procédures est très limitée
Voici les définitions d'une procédure et deux versions de la même fonction utilisant une syntaxe légèrement différente:
procedure Hello;
begin
ShowMessage ('Hello world!');
end; function Double (Value: Integer) : Integer;
begin
Double := Value * 2;
end; // ou, deuxième version
function Double2 (Value: Integer) : Integer;
begin
Result := Value * 2;
end;L'utilisation de Result plutôt que du nom de la fonction pour affecter la valeur de retour d'une fonction devient très populaire et tend, à notre avis, à rendre le code plus lisible.
Une fois ces routines définies, on peut les appeler une ou plusieurs fois. On appelle la procédure pour qu'elle effectue sa tâche, et on appelle une fonction pour calculer la valeur :
procedure TForm1.Button1Click (Sender: TObject);
begin
Hello;
end;
procedure TForm1.Button2Click (Sender: TObject);
var X, Y: Integer;
begin
X := Double (StrToInt (Edit1.Text));
Y := Double (X);
ShowMessage (IntToStr (Y));
end;
procedure Hello;
begin
MessageDlg ('Hello world!', mtInformation, [mbOK]);
end;
Passer un paramètre par référence signifie que
sa valeur n'est pas copiée sur la pile dans le paramètre
formel de la routine (éviter une copie signifie souvent que le programme
s'exécute plus rapidement). Par contre, le programme se réfère
à la valeur d'origine, même dans le code de la routine. Ceci
permet à la procédure ou à la fonction de modifier
la valeur du paramètre. Le paramètre passé par référence
est signalé par le mot réservé var.
procedure DoubleTheValue (var Value: Integer);
begin
Value := Value * 2;
end;Dans ce cas, le paramètre est utilisé aussi bien pour passer une valeur à la procédure que pour retourner une nouvelle valeur au code appelant. Quand vous écrivez
var
X: Integer;
begin
X := 10;
DoubleTheValue (X);la valeur de la variable X devient 20, parce que la fonction utilise une référence vers l'emplacement mémoire d'origine de X, en affectant sa valeur initiale.
Passer des paramètres par référence a du sens pour les types ordinaux, pour les anciennes chaînes et pour les grands enregistrements. Les objets Delphi, en fait, sont invariablement passés par valeur, parce qu'ils sont eux-mêmes des références. Pour cette raison, passer un objet par référence a peu de sens (mis à part des cas très spéciaux), parce que c'est comme si on passait "une référence à une référence".
Les chaînes longues Delphi ont un comportement légèrement
différent : elles se comportent comme des références,
mais si on modifie une des variables chaîne en faisant référence
à la même chaîne en mémoire, elle est copiée
avant sa mise à jour. Mais si on modifie la valeur de la chaîne,
la valeur d'origine n'est pas affectée. Par contre si on passe la
chaîne longue par référence, on peut modifier sa valeur
d'origine.
En fait, si vous essayez de compiler le code (idiot) suivant, Delphi détectera une erreur :
function DoubleTheValue (const Value: Integer): Integer;
begin
Value := Value * 2; // erreur de compilation
Result := Value;
end;
La définition de base du paramètre tableau ouvert est celui d'un tableau ouvert typé. Ceci veut dire que l'on indique le type de paramètre, mais on ne connaît pas le nombre d'éléments de ce type que comportera le tableau. Voici un exemple d'une telle définition:
function Sum (const A: array of Integer): Integer;
var
I: Integer;
begin
Result := 0;
for I := Low(A) to High(A) do
Result := Result + A[I];
end;En utilisant High(A) nous pouvons obtenir la taille du tableau. Notez aussi l'utilisation de la valeur retournée par la fonction, Result, pour stocker les valeurs intermédiaires. On peut appeler cette fonction en lui passant un tableau d'expressions de types Integer :
X := Sum ([10, Y, 27*I]);Si on a un tableau d'Integer, de n'importe quelle taille, on peut le passer directement à une routine possédant un paramètre tableau ouvert, ou plutôt, on peut appeler la fonction Slice pour passer uniquement une partie du tableau (comme indiqué par son deuxième paramètre). Voici un exemple où le tableau complet est passé par paramètre.
var
List: array [1..10] of Integer;
X, I: Integer;
begin
// initialiser le tableau
for I := Low (List) to High (List) do
List [I] := I * 2;
// appel
X := Sum (List);Si vous souhaitez passer uniquement une partie du tableau à la fonction Slice, appelez-la simplement de cette façon :
X := Sum (Slice (List, 5));On trouvera tous les fragments du code présenté dans cette section dans l'exemple OpenArr (pour la fiche, voir fig. 6.1, plus loin).
Figure 6.1 : L'exemple OpenArr lorsqu'on presse le bouton Partial Slice
En Delphi 4, les tableaux ouverts typés sont entièrement compatibles avec les tableaux dynamiques (introduits en Delphi 4 et traités dans le chapitre 8). Les tableaux dynamiques utilisent la même syntaxe que les tableaux ouverts avec cette différence qu'on peut utiliser une notation comme array of Integer pour déclarer une variable et non plus uniquement pour passer un paramètre.
Techniquement, la construction array of const permet, en une fois, de passer à une routine un tableau avec un nombre indéfini d'éléments de types différents. Voici par exemple la définition de la fonction Format (nous verrons, au chapitre 7 qui traite des chaînes de caractères, comment utiliser cette fonction) :
function Format (const Format: string;
const Args: array of const): string;Le second paramètre est un tableau ouvert, qui reçoit un nombre indéfini de valeurs. En fait, vous pouvez appeler cette fonction selon les façons suivantes
N := 20;
S := 'Total:';
Label1.Caption := Format ('Total: %d', [N]);
Label2.Caption := Format ('Int: %d, Float: %f', [N, 12.4]);
Label3.Caption := Format ('%s %d', [S, N * 2]);Remarquez qu'on peut passer un paramètre soit en tant que valeur constante, la valeur de la variable, soit en tant qu'expression. Déclarer une fonction de ce genre est simple, mais comment l'encoder? Comment connaître les types des paramètres? Les valeurs d'un paramètre tableau ouvert de type variant sont compatibles avec les éléments de type TVarRec.
type TVarRec = record case Byte of vtInteger: (VInteger: Integer; VType: Byte); vtBoolean: (VBoolean: Boolean); vtChar: (VChar: Char); vtExtended: (VExtended: PExtended); vtString: (VString: PShortString); vtPointer: (VPointer: Pointer); vtPChar: (VPChar: PChar); vtObject: (VObject: TObject); vtClass: (VClass: TClass); vtWideChar: (VWideChar: WideChar); vtPWideChar: (VPWideChar: PWideChar); vtAnsiString: (VAnsiString: Pointer); vtCurrency: (VCurrency: PCurrency); vtVariant: (VVariant: PVariant); vtInterface: (VInterface: Pointer); end;Chaque enregistrement possède le champ VType, bien qu'il ne soit pas facile à voir parce qu'il est déclaré une seule fois, ainsi que la vraie donnée taille de type Integer (généralement une référence ou un pointeur).
Grâce à cette information, nous pouvons en fait écrire
une fonction capable de fonctionner avec différents types de données.
Dans la fonction SumAll, nous souhaitons pouvoir additionner des
valeurs de types différents, transformer des chaînes en entiers,
des caractères en leur valeur d'ordre correspondant, et additionner
1 pour les valeurs booléennes vraies. Le code est basé sur
une instruction case et il est très simple, bien que nous
ayons dû déréférencer très souvent des
pointeurs
function SumAll (const Args: array of const): Extended; var I: Integer; begin Result := 0; for I := Low(Args) to High (Args) do case Args [I].VType of vtInteger: Result := Result + Args [I].VInteger; vtBoolean: if Args [I].VBoolean then Result := Result + 1; vtChar: Result := Result + Ord (Args [I].VChar); vtExtended: Result := Result + Args [I].VExtended^; vtString, vtAnsiString: Result := Result + StrToIntDef ((Args [I].VString^), 0); vtWideChar: Result := Result + Ord (Args [I].VWideChar); vtCurrency: Result := Result + Args [I].VCurrency^; end; // case end;Nous avons ajouté ce code dans l'exemple OpenArr qui appelle la fonction SumAll lorsqu'un bouton donné est pressé:
procedure TForm1.Button4Click(Sender: TObject); var X: Extended; Y: Integer; begin Y := 10; X := SumAll ([Y * Y, 'k', True, 10.34, '99999']); ShowMessage (Format ( 'SumAll ([Y*Y, ''k'', True, 10.34, ''99999'']) => %n', [X])); end;On peut voir le résultat de cet appel ainsi que la fiche de l'exemple OpenArr à la fig. 6.2.
FIGURE 6.2 : La fiche de l'exemple OpenArr avec la boîte de message ouverte lorsque le bouton Untyped a été pressé.
Le problème est qu'il s'agit de la convention par défaut et que les fonctions qui l'utilisent ne sont pas compatibles avec Windows : les fonctions API de Win32 doivent être déclarées en utilisant la convention d'appel stdcall, un mélange de la convention d'appel Pascal originale de l'API Win 16 et de la convention d'appel cdecl du langage C.
En général, il n'y a pas de raison de ne pas utiliser la nouvelle convention d'appel rapide, à moins qu'on n'effectue des appels externes Windows ou qu'on ne définisse des fonctions de rappel Windows. Nous verrons un exemple utilisant la convention stdcall avant la fin de ce chapitre. On trouvera un résumé des conventions d'appel de Delphi dans la rubrique Conventions d'appel de l'aide Delphi.
Nous avons déjà vu quelques méthodes dans les exemples de ce chapitre et du chapitre précédent. Voici une méthode vide ajoutée automatiquement au code source d'une fiche par Delphi :
procedure TForm1.Button1Click(Sender: TObject);
begin
{c'est ici que vous placerez votre code}
end;
Si on souhaite déclarer l'existence d'une procédure ou
d'une fonction avec un certain nom et des paramètres donnés,
sans fournir son code réel, on peut écrire la procédure
ou la fonction suivie du mot clé forward
procedure Hello; forward;Par la suite le code devrait fournir une définition complète de la procédure, mais celle-ci peut être appelée avant d'être complètement définie. Voici un exemple idiot, juste pour donner une idée
procedure DoubleHello; forward; procedure Hello; begin if MessageDlg ('Do you want a double message?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then DoubleHello else ShowMessage ('Hello'); end; procedure DoubleHello; begin Hello; Hello; end;Cette approche permet d'écrire de la récursivité indirecte : DoubleHello appelle Hello, mais Hello peut aussi appeler DoubleHello. Évidemment il doit y avoir une condition pour terminer la récursion, pour éviter le dépassement de la pile. On peut trouver ce code avec de légères modifications dans l'exemple DoubleH.
Bien qu'une déclaration de procédure forward ne soit pas très courante en Delphi, il existe un cas similaire beaucoup plus fréquent. La déclaration d'une procédure ou d'une fonction dans la partie interface d'une unité (on en dira plus sur les unités au chapitre suivant) est considérée comme une déclaration forward, même si le mot forward n'est pas présent. En fait, on ne peut pas écrire le corps d'une routine dans l'interface d'une unité. On doit fournir en même temps dans la même unité l'implémentation réelle de chaque routine que l'on a déclarée.
La même chose vaut pour la déclaration d'une méthode dans un type class générée automatiquement par Delphi (puisqu'on a ajouté un événement à la fiche ou à ses composants). Les gestionnaires d'événement déclarés dans une classe TForm sont des déclarations forward : le code sera fourni dans la partie implémentation de l'unité. Voici un extrait du code source d'un exemple précédent, avec la déclaration de la méthode Button1Click:
type TForm1 = class(TForm) ListBox1: TListBox; Button1: TButton; procedure Button1Click(Sender: TObject); end;
En Pascal existe le concept de type procédural (semblable au
concept de pointeur de fonction du C). La déclaration d'un type
procédural présente la liste de paramètres et éventuellement
le type retourné, dans le cas d'une fonction. Par exemple, vous
pouvez déclarer le type procédure avec un paramètre
Integer passé par référence comme suit
type
IntProc = procedure (var Num: Integer);Ce type procédural est compatible avec toute routine comportant exactement les mêmes paramètres (ou la même signature de fonction pour utiliser le jargon C). Voici un exemple d'une routine compatible
procedure DoubleTheValue (var Value: Integer);
begin
Value := Value * 2;
end;Remarque : Dans la version 16 bits de Delphi, les routines doivent être déclarées avec la directive far pour être utilisées en tant que vraies valeurs d'un type procédural.
Les types procéduraux peuvent être utilisés pour
deux raisons différentes : on peut déclarer des variables
d'un type procédural ou passer un type procédural - un pointeur
de fonction - en tant que paramètre à une autre routine.
Étant donné les déclarations de type et de procédure
précédentes, nous pouvons écrire ce code
var IP: IntProc; X: Integer; begin IP := DoubleTheValue; X := 5; IP (X); end;Ce code produit le même effet que la version plus courte ci-après:
var X: Integer; begin X := 5; DoubleTheValue (X); end;La première version est évidemment plus complexe; alors pourquoi l'utiliser? Dans certains cas, il peut être utile de pouvoir décider quelle fonction appeler et l'appeler réellement plus tard. On pourrait construire un exemple complexe pour illustrer cette approche. Cependant, nous préférons vous laisser en explorer un très simple, nommé ProcType. Pour rendre la situation un peu plus réaliste, cet exemple sera plus complexe que ceux que nous avons vus jusqu'ici.
Créez simplement un nouveau projet et placez sur la fiche deux boutons radio et un bouton de commande, comme l'indique la figure 6.3. Cet exemple est basé sur deux procédures. L'une est utilisée pour doubler la valeur du paramètre. Cette procédure est semblable à celle que nous avons déjà utilisée dans cette section. La seconde procédure est utilisée pour tripler la valeur du paramètre, elle s'appelle donc TripleTheValue :
FIGURE 6.3 : La fiche de l'exemple ProcType.
procedure TripleTheValue (var Value: Integer); begin Value := Value * 3; ShowMessage ('Value tripled: ' + IntToStr (Value)); end;Les deux procédures affichent ce qui se passe pour nous montrer qu'elles ont été appelées. On peut utiliser cette simple fonctionnalité de débogage pour tester si, ou quand, une portion de code est exécutée, au lieu d'y insérer des points d'arrêt.
A chaque pression sur le bouton Apply, une des deux procédures est exécutée en fonction de l'état des boutons radio. En fait, si on a deux boutons radio sur une fiche, on ne peut en sélectionner qu'un à la fois. Ce code pourrait être implémenté en testant la valeur des boutons radio à l'intérieur du code pour l'événement OnClick du bouton Apply. Pour montrer l'utilisation des types procéduraux, nous avons par contre utilisé une approche plus longue mais intéressante. A chaque clic sur l'un des boutons radio, une des procédures est stockée dans une variable :
procedure TForm1.DoubleRadioButtonClick(Sender: TObject);
begin
IP := DoubleTheValue;
end;Lorsque l'utilisateur clique sur le bouton de commande, la procédure que nous avons stockée est exécutée :
procedure TForm1.ApplyButtonClick(Sender: TObject);
begin
IP (X);
end;Pour permettre à trois fonctions différentes d'accéder aux variables IP et X, nous devons les rendre visibles dans toute la fiche; elles ne doivent pas être déclarées localement (à l'intérieur d'une des méthodes). Une solution est de placer ces variables dans la déclaration de la fiche:
type TForm1 = class(TForm) ... private { Private declarations } IP: IntProc; X: Integer; end;Nous verrons exactement ce que cela signifie dans le prochain chapitre. Pour le moment, nous devons modifier le code généré par Delphi pour le type class comme indiqué ci-dessus, et ajouter la définition du type procédural que nous avons montrée ci-dessus. Pour initialiser ces deux variables avec des valeurs convenables, nous pouvons gérer l'événement OnCreate de la fiche (sélectionnez cet événement dans l'inspecteur d'objets après avoir choisi la fiche, ou en double
Considérons cette série de fonctions extraites de l'unité Math de la VCL :
function Min (A,B: Integer): Integer; overload; function Min (A,B: Int64): Int64; overload; function Min (A,B: Single): Single; overload; function Min (A,B: Double): Double; overload; function Min (A,B: Extended): Extended; overload;Lorsque vous appelez Min(10, 20), le compilateur détermine que vous appelez la première fonction du groupe; la valeur retournée sera ainsi un Integer.
Il y a deux règles fondamentales:
Voici trois versions surchargées de la procédure ShowMsg, ajoutées à l'exemple Overdef (une application illustrant la surcharge et les paramètres par défaut) :Chaque version de la routine doit être suivie de la directive overload. Les différences doivent concerner le nombre ou le type des paramètres, ou les deux. Le type retourné, par contre, ne peut pas être utilisé pour distinguer les deux routines.
procedure ShowMsg (str: string); overload; begin MessageDlg (str, mtInformation, [mbOK], 0); end; procedure ShowMsg (FormatStr: string; Params: array of const); overload; begin MessageDlg (Format (FormatStr, Params), mtInformation, [mbOK], 0); end; procedure ShowMsg (I: Integer; Str: string); overload; begin ShowMsg (IntToStr (I) + ' ' + Str); end;Les trois fonctions affichent une boîte de message contenant une chaîne, en la formatant éventuellement de différentes manières. Voici les trois appels du programme :
ShowMsg ('Hello'); ShowMsg ('Total = %d.', [100]); ShowMsg (10, 'MBytes');Nous sommes agréablement surpris de constater que la technologie Paramètres du code fonctionne bien avec les procédures et fonctions surchargées. Dès que vous tapez la parenthèse ouverte après le nom de la routine, toutes les possibilités disponibles sont affichées. Dès que vous entrez les paramètres, Delphi utilise leur type pour déterminer quelle possibilité est encore disponible. La figure 6.4 montre que si vous commencez à taper une constante chaîne, Delphi affiche uniquement les versions compatibles (en omettant la version de la procédure ShowMsg qui comporte un premier paramètre de type Integer).
Par exemple, vous pouvez ajouter à une unité le code suivant :
procedure MessageDlg (str: string); overload; begin Dialogs.MessageDlg (str, mtInformation, [mbOK], 0); end;Ce code ne surcharge pas véritablement la routine initiale MesageDlg. En effet, si vous écrivez :
MessageDlg ('Hello');vous obtiendrez un petit message d'erreur signalant que quelques paramètres font défaut. La seule façon d'appeler la version locale au lieu de celle de la VCL est de faire explicitement référence à l'unité locale, ce qui contrecarre l'idée de surcharge :
OverDefF.MessageDlg ('Hello');
procedure MessBox (Msg: string; Caption: string = 'Warning'; Flags: LongInt = mb_OK or mb_IconHand); begin Application.MessageBox (PChar (Msg), PChar (Caption), Flags); end;Avec cette définition, nous pouvons appeler la procédure d'une des façons suivantes :
MessBox ('Quelque chose ne va pas ici!'); MessBox ('Quelque chose ne va pas ici!', 'Attention'); MessBox ('Hello', 'Message', mb_OK);La figure 6.5 montre que Paramètres du code de Delphi utilise à bon escient un style différent pour indiquer les paramètres qui ont une valeur par défaut, permettant ainsi de déterminer facilement quel paramètre a été omis.
Une restriction importante concernant l'utilisation des paramètres
par défaut est que l'on ne peut pas "sauter" des paramètres.
Par exemple, on ne peut pas passer le troisième paramètre
à la fonction après avoir omis le deuxième
MessBox ('Hello', mb_OK); // erreurLa règle principale pour les paramètres par défaut est la suivante : dans un appel, on ne peut omettre les paramètres qu'à partir du dernier. Autrement dit, si l'on omet un paramètre, on doit aussi omettre ceux qui le suivent.
Voici quelques autres règles concernant les paramètres par défaut :
procedure ShowMsg (Str: string; I: Integer = 0); overload; begin MessageDlg (Str + ': ' + IntToStr (I), mtInformation, [mbOK], 0); end;le compilateur ne protestera pas; la définition est légale. Cependant, l'appel :
ShowMsg ('Hello');est signalé par le compilateur comme appel surchargé Ambigu de ShowMsg. Notez que cette erreur survient dans une ligne de code correctement compilée avant la nouvelle définition de surcharge. En pratique, il n'est pas possible d'appeler la procédure ShowMsg avec un paramètre de type string, car le compilateur ne peut pas savoir si on veut appeler la version ayant uniquement le paramètre de type string ou celle ayant le paramètre de type string et le paramètre de type Integer avec une valeur par défaut. Devant un tel doute, le compilateur s'arrête et demande au programmeur de fixer ses intentions plus clairement.
Maintenant, au lieu d'aborder les caractéristiques de l'orienté objet, quelques chapitres seront consacrés à des détails sur d'autres éléments de programmation Pascal, à commencer par les chaînes.
Chapitre suivant: La gestion
des chaînes
© Copyright Marco Cantù, Wintech Italia Srl 1995-99 |