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 clé
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, il est préférable d'invoquer la méthode to_string de
la classe ancêtre et d'étendre son comportement en utilisant 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()
^
" ["
^
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'autres noms s'avère cependant utile dans le
cas de l'héritage multiple pour différencier les ancêtres (voir
page ??).
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.
Liaison retardée
On appelle liaison retardée 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.
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 dans les deux classes de
points colorés, cependant, deux objets de chacune de ces classes
n'auront pas le même comportement.
# let
p1
=
new
colored_point
(1
,
1
)
"bleue comme une orange"
;;
val p1 : colored_point = <obj>
# p1#to_string();;
- : string = "( 1, 1) [bleue comme une orange] "
# let
p2
=
new
colored_point_bis
(1
,
1
)
"bleue comme une orange"
;;
val p2 : colored_point_bis = <obj>
# p2#to_string();;
- : string = "( 1, 1) [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é en fonction
des méthodes associées 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 la méthode de
la classe ancêtre, 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 : l'une variable et l'autre fixe. La
partie variable contient les variables d'instance, comme pour un
enregistrement. La partie fixe correspond à une table des méthodes qui
est partagée par toutes les instances de cette classe.
La table des méthodes est un tableau de fonctions et chaque méthode
possède un numéro qui est son indice dans ce tableau. On suppose
qu'il existe une instruction machine GETMETHOD(o,n) qui prend
en paramètre un objet o et un numéro n. Elle
retourne la fonction associée à ce numéro dans la table des méthodes.
On note f_n le résultat de l'appel à
GETMETHOD(o,n). La compilation de l'envoi de message
o#m calcule le numéro n de la méthode m
et engendre le code de l'application GETMETHOD(o,n)
à l'objet o. Il correspond
à l'application de la fonction f_n à 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 dans 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 préservation des numéros qui garantit que le sous-typage
(voir page ??) est cohérent du point de vue de
l'exécution. En effet si un point coloré est explicitement contraint 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 par le mot clé initializer
dans la définition de la classe, des
traitements à effectuer lors de la construction de l'objet. Cette
initialisation peut effectuer tous les calculs et les accès aux champs ou
aux méthodes de l'instance qui sont normalement autorisés dans une
méthode.
Syntaxe
initializer expr
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 apparaîtra dans l'interface de la
classe mais pas dans le type des instances de cette classe. Une méthode privée peut
être invoquée dans la définition des autres méthodes de la
classe mais ne peut pas être envoyée à 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érarchie3.
Syntaxe
method private nom = 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 ??).
# 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.