Précédent Index Suivant

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.



Précédent Index Suivant