Application au langage Eiffel - 3e partie
Nous allons, dans cette troisième partie de notre introduction à la programmation à objets, détailler deux de ses concepts les plus fondamentaux : le polymorphisme et la liaison dynamique. Nous allons ensuite finir de présenter la syntaxe Eiffel avec les structures de contrôle évoluées.
Rappelons que dans les deux précédents numéros de Linux Magazine, nous avions expliqué les concepts de base de la programmation à objets (classe, objet, héritage simple et multiple, généricité) ainsi que des notions de programmation par contrat (à l'aide d'assertions) et de gestion mémoire.
Nous continuerons à illustrer notre propos à l'aide d'exemples écrits en Eiffel, langage objet puissant et pourtant facile. Vous pourrez essayer ces exemples à l'aide de SmallEiffel, le compilateur GNU Eiffel, développé au laboratoire LORIA de Nancy. Il est accessible gratuitement à http://SmallEiffel.loria.fr, avec l'ensemble de son code source.
Type statique, type dynamique
Dans un langage de programmation «classique», comme C ou Pascal, une variable (ou, de façon plus générale, une expression quelconque) a un type et un seul, statique, qui ne variera pas au cours de l'exécution. Ce type est connu lors de la compilation, il s'agit de celui qui jouxte le nom de la variable là où elle est déclarée. Ainsi, par exemple, dans les déclarations C suivantes
int mon_entier;
struct client mon_client;
mon_entier est de type entier (int) alors que mon_client est une structure de type client. Ceci reste immuable et permet au compilateur de générer le code approprié au type de la variable considérée ainsi que d'effectuer des vérifications de type pour détecter les erreurs commises par le programmeur. En raison de contraintes de place, nous ne considérerons pas ici les «casts» C, qui permettent de forcer le type de la source d'une affectation et - bien que parfois nécessaires - occasionnent de nombreuses erreurs.
Dans un langage à objets, ce type, celui de la déclaration, qui apparaît dans le code source à côté du nom de la variable, est appelé «type statique». Il joue globalement le même rôle que pour un langage classique.
En revanche, dans les langages à objets, une variable (ou toute expression) possède également un «type dynamique». Comme son nom l'indique, il peut changer dynamiquement, lors de l'exécution. Le type dynamique d'une variable, à un instant précis de l'exécution, est en fait le type de l'objet attaché à cette variable à cet instant.
Polymorphisme
Le «polymorphisme» est justement le fait qu'une variable puisse avoir plusieurs types dynamiques à l'exécution (du grec «poly» plusieurs et «morphë», forme).
Comment cela est-il possible et à quoi cela sert-il ? En deux mots, et de manière un peu abstraite, ceci permet d'accéder aux primitives d'un (objet d'un) certain type de la même façon qu'à celles d'un (objet d'un) de ses sous-types. Ceci permet de substituer à un type l'un de ses sous-types, sans que ce changement n'apparaisse au niveau des clients.
Pour donner une explication plus concrète, nous allons reprendre un des exemples que nous avions utilisés dans le premier article de cette série :
deferred class POINT
feature
x: REAL is deferred end
y: REAL is deferred end
alpha: REAL is deferred end
d: REAL is deferred end
is_origin: BOOLEAN is
do
Result := (x = 0.0) and (y = 0.0)
end;
end;
class POINT_CARTESIEN
inherit POINT
creation
make
feature
make (abscisse, ordonnee: REAL) is
do
x := abscisse;
y := ordonne;
end
x: REAL
y: REAL
alpha: REAL is
do
Result := (y/x).atan.to_real
end
d: REAL is
do
Result := (x^2 + y^2).sqrt
end
end
class POINT_POLAIRE
inherit POINT
creation make
feature
make (angle, distance: REAL) is
do
alpha := angle;
d := distance;
end
x: REAL is
do
Result := d.cos(alpha)
end
y: REAL is
do
Result := d.sin(alpha)
end
alpha: REAL
d: REAL
end
Supposons que nous ayons une routine cliente comme suit :
do_something (p:POINT) is
do
if p.is_origin then
-- faire quelque chose
else
-- faire autre chose
end
end
La routine do_something prend un POINT en paramètre et lui applique la fonction is_origin. Ici, le type statique de p est POINT, mais son type dynamique peut être aussi bien POINT que l'un de ses descendants (POINT_CARTESIEN ou POINT_POLAIRE).
Ainsi, p est une variable polymorphe. Dans notre exemple, comme POINT est une classe abstraite (mot clé deferred), le lecteur attentif se rappellera qu'elle ne peut être instanciée, comme nous l'avons indiqué dans notre premier article. Il ne sera donc possible d'avoir que des objets de type POINT_CARTESIEN ou POINT_POLAIRE attachés à la variable polymorphique p. Comme is_origin existe au niveau de POINT, elle existe aussi dans ses descendants. Il n'est donc pas nécessaire de se soucier du type dynamique de p, car on peut substituer à POINT n'importe lequel de ses descendants et la routine is_origin adéquate, qui sera appelée. p.is_origin, est donc un appel statiquement valide.
Liaison dynamique
Le fait de sélectionner à l'exécution la routine adéquate s'appelle justement la «liaison dynamique».
Dans l'exemple ci-dessus, les trois types de points partagent tous la même routine is_origin (celle définie dans POINT). Il n'est donc pas nécessaire de faire appel à la liaison dynamique : l'appel à la bonne routine peut être codé correctement lors de la compilation. Supposons en revanche que l'on ait le client suivant :
do_something (p:POINT) is
do
if p.d < 1.0 then
-- faire quelque chose
else
-- faire autre chose
end
end
L'appel à p.d est statiquement valide puisque POINT, qui est le type statique de p, comprend bien une routine d. Cependant, celle-ci est codée différemment selon que l'on a affaire à un POINT_CARTESIEN ou à un POINT_POLAIRE. Il est donc nécessaire, à l'exécution, «d'aiguiller» l'appel p.d sur la routine correspondant au type dynamique de p. Si p est un POINT_POLAIRE, il s'agira d'un simple accès à un champ de l'objet p ; si p est un POINT_CARTESIEN, il s'agira d'un appel de fonction.
Du point de vue de l'implantation, il est donc nécessaire, afin de pouvoir effectuer l'opération de liaison dynamique, de disposer à tout moment du type des objets. Ceux-ci se voient donc attacher à leur création une information supplémentaire qui est leur identifiant de type. Cet identifiant peut consister en un entier codant le numéro du type ou un pointeur vers la description du type. Il implique un léger surcoût en termes de taille et de temps d'exécution, qui peut d'ailleurs être éliminé partiellement ou en totalité par des compilateurs Eiffel dotés d'importantes capacités d'optimisation de code, comme SmallEiffel.
En revanche, dans un langage classique, non objet, aucune trace explicite de ce type n'existe plus dans la représentation des données à l'exécution. Le code généré l'a été en connaissant le type des données qu'il manipule, mais rien ne permet plus, au simple vu des données, de déterminer à quel type elles correspondent.
On voit donc que les langages à objets offrent une bien plus grande flexibilité, une meilleure facilité d'utilisation et un pouvoir expressif bien supérieur aux langages classiques, au prix d'une légère baisse des performances (qui, rappelons-le, peut être gommée par de bons compilateurs). Il s'agit là d'une tendance générale en informatique, puisque l'on évolue sans cesse de langages de bas niveau vers des langages plus puissants, un peu plus gourmands en ressources machines mais permettant de faire des économies importantes en temps de développement.
Structures de contrôle en Eiffel :
Maintenant que nous avons vu les principaux concepts des langages à objets en général, et d'Eiffel en particulier, nous allons pouvoir nous consacrer à ce dernier plus précisément, en complétant les quelques bases déjà données de sa syntaxe. La spécification de l'ensemble de cette syntaxe est accessible (en anglais) depuis l'adresse suivante :
http://www.gobosoft.com/eiffel/syntax/index.html
Eiffel est un langage qui allie puissance, élégance et compacité du langage. Ce dernier point signifie que sa syntaxe est très lisible et facilement compréhensible pour le débutant. Un des moyens pour y arriver a été, par exemple, de n'avoir qu'une seule structure de contrôle pour tous les types d'itérations.
Une boucle «de base» en Eiffel se présente sous la forme suivante :
from
bloc_d_initialisation
until
condition_d'arret
loop
corps_de_la_boucle
end
Le bloc d'initialisation comprend les instructions d'initialisation de la boucle (en général, initialisation d'un indice, par exemple). Il peut être vide. Notons qu'il n'y a aucune différence à l'exécution entre une instruction écrite juste avant le from et une écrite dans le bloc d'initialisation ; il s'agit simplement d'une séparation destinée à faciliter la lecture.
La condition d'arrêt est une expression booléenne et est obligatoire.
Enfin, le corps contient les instructions à exécuter dans l'itération. C'est ici également que prennent place les instructions qui vont faire varier «l'indice» de boucle ou ce qui en tient lieu.
Avec cette structure, tous les types d'itération peuvent être exprimés. Ainsi, l'équivalent de l'instruction C :
for (i=0; i<n; i++) {...}
s'exprime en Eiffel :
from
i:=0
until
i>=n
loop
...
i := i + 1;
end
Notez que la condition de continuation de la structure C a été transformée (par simple négation) en la condition d'arrêt équivalente en Eiffel. Le while condition {...} et le
do {...} while condition du langage C se traduisent également aisément.
Contrairement à la plupart des autres langages, Eiffel offre la possibilité de «sécuriser» ses itérations à l'aide de deux types d'assertions : le variant de boucle et l'invariant de boucle. Voici la syntaxe qui s'y rapporte :
from
bloc_d_initialisation
invariant
assertion
variant
expression_entiere
until
condition_d_arret
loop
corps_de_la_boucle
end
L'invariant et le variant ne sont pas obligatoires. L'invariant sert à indiquer une condition qui doit être vraie tout au long de la boucle (par exemple, que l'indice de boucle ne doit pas déborder des bornes d'un tableau).
Le variant, lui, est une expression entière, dont la valeur doit décroître à chaque étape de l'itération, tout en restant supérieure à zéro. Il sert à garantir que l'itération progresse, mais qu'elle se finira un jour (le variant à zéro déclenchant une violation d'assertion avec un message d'erreur explicite). La terminaison de l'itération est ainsi assurée, évitant les boucles sans fin. Bien entendu, ces assertions de boucle, comme toutes les assertions Eiffel, peuvent être activées ou non lors de la compilation, selon que l'on est en train de tester son programme ou que l'on produise la version finale, optimisée, dont on est (à peu près) sûr qu'elle soit correcte.
Nous avons également déjà vu des instructions conditionnelles simples, comme le if...then...else, qu'il n'est pas besoin d'expliquer. Notons simplement qu'il existe une petite variante syntaxique bien pratique, qui permet d'éviter d'imbriquer des suites de conditionnelles :
if condition1 then
bloc_alors1
elseif condition2 then
bloc_alors2
elseif conditions3 then
bloc_alors3
...
else
bloc_sinon
end
Il existe bien entendu aussi en Eiffel une instruction de choix multiple (analogue au «case» ou au «switch» de nombreux langages). Il s'agit du inspect :
inspect
expression
when valeur1 then
bloc1
when valeur2 then
bloc2
when valeur3 then
bloc3
else
bloc_sinon
end
L'expression qui sert «d'aiguillage» doit être de type INTEGER, CHARACTER ou BOOLEAN. Selon la valeur qu'elle prend à l'exécution, un seul des choix sera valide. Les différents cas possibles (les différents when) doivent être disjoints et leur ordre n'a aucune importance (contrairement au if...then...else). Ils sont exprimés sous forme de valeur(s) ou d'intervalles de valeurs. Les blocs sont des suites d'instructions (éventuellement vides). Voici un exemple d'inspect complet sur une expression de type INTEGER :
inspect
mon_entier
when 0 then
bloc_zero
when 1,2,3 then
bloc_1_a_3
when 4..9 then
bloc_4_a_9
when 11, 13..17, 19 then
bloc_plus_complique
else
les_negatifs_10_18_et_plus_de_20
end
Il faut noter que si ce genre d'aiguillage est très souvent utilisé dans des langages comme le C, par exemple pour discriminer différents cas en testant le type des données, ceci est beaucoup moins vrai dans les langages à objets. En effet, ces derniers disposent du mécanisme de liaison dynamique qui, d'une certaine manière, joue ce rôle en appelant des routines adaptées au type dynamique des objets. Dans notre pratique d'Eiffel, nous avons constaté que les inspect sont assez souvent utilisés dans le cadre des entrées sorties et assez peu pour la «logique interne» des applications.
A suivre...
Nous avons maintenant fini de détailler les concepts objets et les mécanismes syntaxiques et sémantiques d'Eiffel. Nous aborderons dans le prochain article les bibliothèques Eiffel et certaines classes de base.
Olivier Zendra,
Dominique Colnet,
Philippe Coucaud