Autres traits objet
Référencements : self et super
Il est pratique, dans la définition d'une méthode d'une classe, de
pouvoir invoquer une autre méthode de cette même classe ou une
méthode de la classe ancêtre. Pour cela Objective CAML autorise à
nommer l'objet lui-même ou (les objets de) la classe ancêtre.
Dans le premier cas, on indique le nom choisi aprés le mot clef
object et dans le second cas, aprés la déclaration
d'héritage.
Par exemple, pour définir la méthode to_string des
points colorés, on peut invoquer la méthode to_string de
la classe ancêtre et utiliser la nouvelle méthode
get_color.
# class
colored_point
(x,
y)
c
=
object(self)
inherit
point
(x,
y)
as
super
val
c
=
c
method
get_color
=
c
method
to_string
()
=
super#to_string()
^
" de couleur "
^
self#get_color
end
;;
Il est possible de donner des noms quelconques pour la classe
ancêtre et sa sous-classe, mais autant utiliser la terminologie
objet (self ou this pour soi-même et super pour
l'ancêtre). Choisir d'autre noms s'avère cependant utile dans le
cas de l'héritage multiple pour différencier les ancêtres (voir
page X).
Warning
On ne peut pas référencer une variable d'instance ancêtre dans le cas où l'on déclare une nouvelle
variable d'instance du même nom qui masque la première.
Seules les méthodes peuvent l'être. |
Liaison retardée
On appelle liaison retardée (ou liaison dynamique) la
détermination à l'exécution de la méthode à utiliser lors de
l'envoi d'un message. La liaison retardée s'oppose à la liaison
statique qui est déterminée à la compilation.
Quand un programme s'exécute, il doit établir la valeur associée
à chaque identificateur rencontré. Cette liaison entre un
identificateur et sa valeur peut s'effectuer soit à la compilation
(liaison statique ou précoce), soit à l'exécution (liaison
dynamique ou retardée). Ce problème se posait avant la
programmation par objet. Par exemple la majorité des dialectes Lisp
interprétés possèdent une liaison dynamique, et les dialectes ML
compilés une liaison statique.
En Objective CAML la liaison des méthodes est retardée. C'est donc le receveur d'un message qui sait quel est le code à exécuter.
La déclaration précédente de la classe colored_point
redéfinit la méthode to_string. Cette nouvelle
définition utilise la méthode get_color de la classe.
Définissons à présent une nouvelle classe
colored_point_bis héritière de colored_point,
qui redéfinit la méthode get_color (en testant la
pertinence de la chaîne de caractères) mais qui ne redéfinit pas
to_string.
# class
colored_point_bis
coord
c
=
object
inherit
colored_point
coord
c
val
true_colors
=
[
"blanc"
;
"noir"
;
"rouge"
;
"vert"
;
"bleu"
;
"jaune"
]
method
get_color
=
if
List.mem
c
true_colors
then
c
else
"INCONNUE"
end
;;
La méthode to_string est la même pour les objets
appartenant aux deux classes de points colorés, cependant, elle
n'aura pas le même comportement.
# let
p1
=
new
colored_point
(1
,
1
)
"bleu comme une orange"
;;
val p1 : colored_point = <obj>
# p1#to_string();;
- : string = "( 1, 1) de couleur bleu comme une orange"
# let
p2
=
new
colored_point_bis
(1
,
1
)
"bleu comme une orange"
;;
val p2 : colored_point_bis = <obj>
# p2#to_string();;
- : string = "( 1, 1) de couleur INCONNUE"
La liaison de get_color dans le corps de to_string
n'est donc pas fixée lors de la compilation de la classe
colored_point. Le code à exécuter lors de l'invocation de
la méthode get_color est déterminée en fonction de
liste des méthodes associée aux instances des classes
colored_point et colored_point_bis. Ainsi, pour une
instance de colored_point, l'envoi du message
to_string provoque l'exécution de get_color définie dans la classe
colored_point. Alors que le même envoi de message à une
instance de colored_point_bis invoque bien la méthode de
la classe ancêtre, mais cette dernière active la méthode
get_color de la classe fille qui contrôle la pertinence de
la chaîne représentant la couleur.
Représentation mémoire et envoi de messages
Un objet est découpé en deux parties : une variable et une fixe. La partie variable
contient les variables d'instance, comme pour un enregistrement. La partie fixe correspond
à une table des méthodes. Chaque objet
d'une même classe, c'est-à-dire ayant été créé par la même fonction de construction,
possède la même table qui est partagée par toutes les instances de cette classe.
Chaque méthode, de la classe de construction de l'objet, possède un numéro dans cette
table. La table des méthodes est un tableau de fonctions. On suppose qu'il existe une instruction
machine GETMETHOD(o,numéro) qui prend un objet et un numéro en tant que paramètre
et retourne la fonction associée à ce numéro dans la table des méthodes de cette classe.
On note f_n la fonction de numéro n de l'appel à GETMETHOD(o,n).
La compilation d'un envoi de message, o#m, calcule le numéro n
de la méthode m,
et engendre le code suivant : GETMETHOD(o,n) o correspondant à l'application de la fonction
f_n sur l'objet receveur o.
La liaison retardée est implantée par l'appel à GETMETHOD lors de l'exécution.
L'envoi d'un message à self dans le corps d'une méthode est lui aussi compilé
en une recherche du numéro du message, suivi de l'application de la fonction trouvée sur l'objet
lui même.
Dans le cas d'un héritage, les méthodes redéfinies ou non conservent le même numéro. Seule la
table change pour les redéfinitions. Ainsi l'envoi du message to_string sur une instance
de la classe point appliquera la fonction de conversion d'un point
alors que l'envoi du même message sur une instance
de colored_point trouvera au même numéro la fonction correspondant à la méthode
redéfinie pour tenir compte du champ couleur.
C'est cette identification des numéros qui garantit que le sous-typage (voir page 2)
est cohérent du point
de vue de l'exécution. En effet si un point coloré est explicitement forcé en point, l'envoi
du message to_string calcule le numéro de méthode de la classe point
qui coïncide avec celui de la classe colored_point. La recherche de la méthode s'effectuera dans la table associée à l'instance réceptrice, c'est-à-dire la table des colored_point.
L'implantation réelle d'Objective CAML est légèrement plus complexe, mais le principe
de recherche dynamique de la méthode à employer reste le même.
Initialisation
Il est possible d'indiquer dans la définition de la classe des
traitements à effectuer lors de la construction de l'objet. Cette
initialisation peut faire tous les calculs et les accès aux champs ou
aux méthodes de l'instance qu'il est normalement autorisé dans une
méthode.
Syntaxe
Il est possible d'indiquer dans la définition de la classe, une
méthode initializer déclenchée immédiatement après
la construction de l'objet. Cet "initialisateur" peut faire n'importe
quel calcul et a accès aux champs et aux méthodes de l'instance (car celle-ci vient
d'être créée).
Reprenons l'exemple de la classe point pour définir un
point bavard qui annonce sa création.
# class
verbose_point
p
=
object(self)
inherit
point
p
initializer
let
xm
=
string_of_int
x
and
ym
=
string_of_int
y
in
Printf.printf
">> Création d'un point en (%s %s)\n"
xm
ym
;
Printf.printf
" a distance %f de l'origine\n"
(self#distance())
;
end
;;
# new
verbose_point
(1
,
1
);;
>> Création d'un point en (1 1)
a distance 1.414214 de l'origine
- : verbose_point = <obj>
Une utilisation amusante et instructive des initialisateurs est de
pouvoir suivre l'héritage des classes lors de la création
d'instances. En voici un exemple autant pédagogique que
dépouillé.
# class
c1
=
object
initializer
print_string
"Création d'une instance de c1\n"
end
;;
# class
c2
=
object
inherit
c1
initializer
print_string
"Création d'une instance de c2\n"
end
;;
# new
c1
;;
Création d'une instance de c1
- : c1 = <obj>
# new
c2
;;
Création d'une instance de c1
Création d'une instance de c2
- : c2 = <obj>
La construction d'une instance de c2 passe par la construction d'une
instance de la classe ancêtre.
Méthodes privées
Une méthode peut être déclarée privée par le mot clé
private. Elle n'apparaîtra pas dans l'interface de la
classe et donc dans le type de l'objet. Une méthode privée peut
être invoquée dans la définition des autres méthodes de la
classe mais ne peut pas l'être par une instance de cette classe.
Par contre les méthodes privées sont héritées
et pourront donc être utilisées dans les définitions de la
hiérarchie.
Syntaxe
method private nom_methode = expr |
Étendons la classe point en lui fournissant une méthode
undo lui permettant de revenir sur son dernier
mouvement. Pour réaliser cela, nous devons nous souvenir de la
position occupée avant d'effectuer un mouvement. Nous introduisons
donc deux nouveaux champs old_x et old_y avec leur
méthode de mise à jour mem_pos. Nous ne voulons pas que
l'utilisateur ait accès directement à cette méthode aussi la
déclarons nous privée. Il faut redéfinir les méthodes
moveto et rmoveto en mémorisant la position
courante avant d'appeler les méthodes initiales de mouvement.
# class
point_m1
(x0,
y0)
=
object(self)
inherit
point
(x0,
y0)
as
super
val
mutable
old_x
=
x0
val
mutable
old_y
=
y0
method
private
mem_pos
()
=
old_x
<-
x
;
old_y
<-
y
method
undo
()
=
x
<-
old_x;
y
<-
old_y
method
moveto
(x1,
y1)
=
self#mem_pos
()
;
super#moveto
(x1,
y1)
method
rmoveto
(dx,
dy)
=
self#mem_pos
()
;
super#rmoveto
(dx,
dy)
end
;;
class point_m1 :
int * int ->
object
val mutable old_x : int
val mutable old_y : int
val mutable x : int
val mutable y : int
method distance : unit -> float
method get_x : int
method get_y : int
method private mem_pos : unit -> unit
method moveto : int * int -> unit
method rmoveto : int * int -> unit
method to_string : unit -> string
method undo : unit -> unit
end
Remarquons que la méthode mem_pos apparaît dans le
type point_m1 précédée du mot clé private, elle ne pourra donc pas
être invoquée par une instance de point alors que la
méthode undo le pourra. C'est le même cas de figure que pour les variables d'instance.
Si les champs old_x et
old_y figurent dans l'affichage résultant de la
compilation ils ne peuvent pas pour autant être manipulés
directement (voir X).
# let
p
=
new
point_m1
(0
,
0
)
;;
val p : point_m1 = <obj>
# p#mem_pos()
;;
Characters 0-1:
This expression has type point_m1
It has no method mem_pos
# p#moveto(1
,
1
)
;
p#to_string()
;;
- : string = "( 1, 1)"
# p#undo()
;
p#to_string()
;;
- : string = "( 0, 0)"
Warning
Une contrainte de type peut rendre publique une méthode déclarée avec l'attribut private. |