Précédent Index Suivant

Autres éléments de l'extension objet

On présente dans cette section la déclaration de types <<objet>> et surtout les déclarations locales dans les classes. Ces dernières peuvent construire des constructeurs qui possèdent un environnement local, considéré comme un ensemble de variables de classe.

Interface

Les interfaces de classes sont en général inférées par le système d'inférence de types, mais elles peuvent aussi être définies par une déclaration de type. Seules les méthodes publiques apparaissent dans ce type.

Syntaxe


class type nom =
  object
    :
    val nomi : typei
    :
    method nomj : typej
    :
  end

On peut ainsi définir l'interface de la classe point :

# class type interf_point =
object
method get_x : int
method get_y : int
method moveto : (int * int ) -> unit
method rmoveto : (int * int ) -> unit
method to_string : unit -> string
method distance : unit -> float
end ;;


L'intérêt de cette déclaration est de pouvoir utiliser le type défini pour une contrainte de type.

# let seg_length (p1:interf_point) (p2:interf_point) =
let x = float_of_int (p2#get_x - p1#get_x)
and y = float_of_int (p2#get_y - p1#get_y) in
sqrt ((x*.x) +. (y*.y)) ;;
val seg_length : interf_point -> interf_point -> float = <fun>


Les interfaces ne peuvent masquer que les champs de variable d'instance et les méthodes privées. Elles ne peuvent en aucun cas masquer des méthodes abstraites ou des méthodes publiques.

C'est une limitation de leur usage comme le montre l'exemple suivant :

# let p = ( new point_m1 (2,3) : interf_point);;
Characters 11-29:
This expression has type
point_m1 =
< distance : unit -> float; get_x : int; get_y : int;
moveto : int * int -> unit; rmoveto : int * int -> unit;
to_string : unit -> string; undo : unit -> unit >
but is here used with type
interf_point =
< distance : unit -> float; get_x : int; get_y : int;
moveto : int * int -> unit; rmoveto : int * int -> unit;
to_string : unit -> string >
Only the first object type has a method undo


Néanmoins, les interfaces autorisent la relation d'héritage entre interfaces. Un intérêt de la définition d'interfaces est leur utilisation conjointe avec le mécanisme de modules. Ainsi il est possible de construire la signature d'un module, utilisant des types objets, en donnant uniquement la description des interfaces des classes.

Déclarations locales dans les classes

Une déclaration de classe produit un type et un constructeur. Nous avons jusqu'à maintenant, pour la clarté de l'exposé, présenté ces constructeurs comme des fonctions sans environnement. En fait il est possible d'une part de définir des constructeurs qui n'ont pas besoin de valeurs initiales pour créer une instance. C'est-à-dire qui ne sont plus fonctionnels. D'autre part, il est possible d'effectuer des déclarations locales dans la classe. Ces valeurs peuvent être capturées dans le constructeur, permettant à ces valeurs d'être partagées par toutes les instances de la classe. Ces valeurs peuvent être considérées comme des variables de classe.

Constructeurs constants

Une déclaration de classe n'utilise pas forcément des valeurs initiales qui seront passées au constructeur. Par exemple la classe suivante :

# class exemple1 =
object
method print () = ()
end ;;
class exemple1 : object method print : unit -> unit end
# let p = new exemple1 ;;
val p : exemple1 = <obj>
Le constructeur d'instances est constant. L'allocation n'a pas besoin de valeurs initiales pour les variables d'instance. En règle générale, il est préférable d'utiliser une valeur initiale comme () pour conserver le caractère fonctionnel du constructeur.

Déclarations locales pour les constructeurs

Une déclaration de classe peut s'écrire en utilisant directement l'abstraction.

# class example2 =
fun a ->
object
val mutable r = a
method get_r = r
method plus x = r <- r + x
end;;
class example2 :
int ->
object val mutable r : int method get_r : int method plus : int -> unit end


On s'aperçoit mieux du caractère fonctionnel du constructeur. Le constructeur est donc une fermeture. Celle-ci peut posséder un environnement qui lie des variables libres à un environnement de déclarations. La syntaxe des déclarations de classes autorise les déclarations locales à cette expression fonctionnelle.

Variables de classes

On appelle variables de classe, les déclarations connues au niveau de la classe, donc partagées par toutes les instances de cette classe. Le plus souvent ces variables de classe peuvent être utilisées en dehors de toute création d'instances. En Objective CAML, on peut faire partager des valeurs, en particulier modifiables, par toutes les instances d'une classe grâce au caractère fonctionnel du constructeur qui possède alors un environnement non vide.

Nous illustrons cette possibilité par l'exemple suivant qui permet de tenir à jour le nombre d'instances d'une classe. Pour cela on définit une classe abstraite paramétrée 'a om.

# class virtual ['a] om =
object
method finalize () = ()
method virtual destroy : unit -> unit
method virtual to_string : unit -> string
method virtual all : 'a list
end;;


Puis on déclare la classe 'a lo dont la fonction de construction contient les déclarations locales de n pour associer un numéro unique à chaque instance et de l qui contient la liste des couples (numéro, instance) de chaque instance encore active.

# class ['a] lo =
let l = ref []
and n = ref 0 in
fun s ->
object(self:'b )
inherit ['a] om
val mutable num = 0
val nom = s
method to_string () = s
method print () = print_string s
method print_all () =
List.iter (function (a,b) ->
Printf.printf "(%d,%s) " a (b#to_string())) !l
method destroy () = self#finalize();
l:= List.filter (function (a,b) -> a <> num) !l; ()
method all = List.map snd !l
initializer incr n; num <- !n; l:= (num, (self :> 'a om) ) :: !l ; ()
end;;
class ['a] lo :
string ->
object
constraint 'a = 'a om
val nom : string
val mutable num : int
method all : 'a list
method destroy : unit -> unit
method finalize : unit -> unit
method print : unit -> unit
method print_all : unit -> unit
method to_string : unit -> string
end


À chaque création d'une instance de la classe lo, l'initialisateur incrémente la référence n et ajoute le couple (numéro, self) à la liste l. Les méthodes print et print_all affichent respectivement l'instance réceptrice et toutes les instances de la classe contenues dans l.

# let m1 = new lo "début";;
val m1 : ('a om as 'a) lo = <obj>
# let m2 = new lo "entre";;
val m2 : ('a om as 'a) lo = <obj>
# let m3 = new lo "fin";;
val m3 : ('a om as 'a) lo = <obj>
# m2#print_all();;
(3,fin) (2,entre) (1,début) - : unit = ()
# m2#all;;
- : ('a om as 'a) list = [<obj>; <obj>; <obj>]


La méthode destroy enlève une instance de la liste des instances créées et appelle la méthode finalize pour effectuer une dernière action sur cette instance avant sa disparition de la liste. La méthode all retourne toutes les instances d'une classe créées en utilisant new.

# m2#destroy();;
- : unit = ()
# m1#print_all();;
(3,fin) (1,début) - : unit = ()
# m3#all;;
- : ('a om as 'a) list = [<obj>; <obj>]


Il est à noter que les instances des sous-classes sont aussi conservées dans cette liste. Rien n'empêche d'utiliser la même technique en particularisant certaines de ces sous-classes. Par contre les instances obtenues par copie (Oo.copy ou {< >}) ne sont pas concernées.






Précédent Index Suivant