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.