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 type, 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_type = |
object |
method nom_methode : type |
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
print
:
unit
->
unit
method
distance
:
unit
->
float
end
;;
L'intérêt de cette déclaration est de pouvoir utiliser le type défini pour une coercion
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. Néanmoins, elles utilisent
aussi la relation d'héritage (entre interfaces).
# 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; print : unit -> unit;
rmoveto : int * int -> unit >
Only the first object type has a method to_string
En fait l'intérêt principal de le définition d'interfaces est
leur utilisation avec le mécanisme modules. Ainsi il sera possible
de construire la signature d'un module, utilisant des types objets,
uniquement en donnant la description des interfaces de ces 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. De plus, il est possible d'effectuer des
déclarations locales dans la classe. Ces valeurs peuvent être capturées dans le constructeur, permettant
de partager ces valeurs pour toutes les instances de la classe. Ces valeurs peuvent être considérée 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éral, il est préférable d'utiliser une valeur initiale comme
() pour conserver le caractère fonctionnel du constructeur, sinon il est évalué immédiatement.
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 de déclarations des classes
autorise les déclarations locales à cette expression fonctionnelle.
Variables de classes
On appelle variables de classe, des déclarations connues au niveau de la classe, et donc
partagées par toutes les instances de cette classe. Le plus souvent ces variables
de classe peuvent être utilisée en dehors de toute création d'instances. En Objective CAML,
on peut 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éfini 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
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.
La méthode destroy num enlève l'instance réceptrice de la liste l.
Les méthodes print affiche l'instance. La méthode print_all affiche toutes
les instances de la classe, contenues dans l.
# let
m1
=
new
lo
"debut"
;;
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,debut) - : unit = ()
# m2#all;;
- : ('a om as 'a) list = [<obj>; <obj>; <obj>]
# m2#destroy();;
- : unit = ()
# m1#print_all();;
(3,fin) (1,debut) - : unit = ()
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,
permettant d'effectuer un traitement sur toutes les instances créées de cette classe.
# m3#all;;
- : ('a om as 'a) list = [<obj>; <obj>]
# List.iter
(fun
x
->
x#to_string())
(m3#all);;
Characters 36-42:
This expression has type ('a om as 'a) list but is here used with type
< all : ('b om as 'b) list; destroy : unit -> unit;
finalize : unit -> unit; to_string : unit -> unit >
list
Type
'c om as 'c =
< all : ('d om as 'd) list; destroy : unit -> unit;
finalize : unit -> unit; to_string : unit -> string >
is not compatible with type
< all : ('e om as 'e) list; destroy : unit -> unit;
finalize : unit -> unit; to_string : unit -> unit >
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 particulirisant certaines sous-classes.