IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

57. Les classes

Comme les structures, class est une fonctionnalité pour définir de nouveaux types. Cependant, les classes fournissent à D le paradigme de la programmation orientée objet (POO). Les principaux aspects de la POO sont les suivants :

  • l'encapsulation : le contrôle d'accès aux membres (l'encapsulation est également disponible pour les structs, mais n'a pas été encore mentionnée jusqu'ici) ;
  • l'héritage : l'acquisition de membres d'un autre type ;
  • le polymorphisme : la possibilité d'utiliser un type plus spécialisé à la place d'un type plus général.

L'encapsulation est obtenue par des attributs de protection, que nous verrons dans un chapitre ultérieur. L'héritage permet d'acquérir des implémentations d'autres types. Le polymorphisme permet de rendre abstraites des parties de programme l' une de l'autre et est obtenu au moyen des interfaces de classes.

Ce chapitre présente sommairement les classes, en soulignant le fait que ce sont des types références. Les classes seront expliquées plus en détail dans des chapitres ultérieurs.

57-1. Comparaison avec les structures

De manière générale, les classes sont très similaires aux structures. La plupart des fonctionnalités que nous avons vues dans les chapitres précédents s'appliquent également aux classes :

  • structures :
  • fonctions membres :
  • paramètres const ref et fonctions membres const :
  • constructeur et autres fonctions spéciales :
  • surcharge d'opérateurs.

Il y a néanmoins des différences importantes entre les classes et les structures.

57-1-1. Les classes sont des types références

La principale différence avec les structures est que ces dernières sont des types valeurs et que les classes sont des types références. Les autres différences résumées ci-après sont essentiellement dues à ce fait.

57-1-2. Les variables classes peuvent être nulles

Comme indiqué brièvement dans le chapitre sur la valeur null et l'opérateur is, les variables classes peuvent être null. En d'autres termes, les variables classes peuvent ne pas fournir d'accès à aucun objet. Les variables classes n'ont pas de valeur elles-mêmes ; les véritables objets classes doivent être construits via le mot-clé new. Comme vous vous en souvenez peut-être, comparer des références à null au moyen des opérateurs == ou != est une erreur. La comparaison doit plutôt être faite au moyen des opérateurs is ou !is, de cette manière :

 
Sélectionnez
MaClasse referenceUnObjet = new MaClasse;
assert(referenceUnObjet !is null);
 
MaClasse variable; // ne référence pas un objet
assert(variable is null);

La raison en est la suivante : l'opérateur == peut avoir besoin de consulter les valeurs des membres des objets et tenter d'accéder aux membres d'une variable null causerait une erreur d'accès mémoire. Pour cette raison, les variables classes doivent toujours être comparées au moyen des opérateurs is et !is.

57-1-3. Les variables classes et les objets classes

Les variables classes et les objets classes sont des concepts séparés.
Les objets classes sont construits via le mot-clé new ; ils n'ont pas de nom. Le véritable concept représenté par un type classe dans un programme est fourni par un objet classe. Par exemple, en supposant une classe Etudiant qui représente des étudiants avec leur nom et leurs notes, on pourrait enregistrer ces informations dans les membres des objets Etudiant. En partie parce qu'ils sont anonymes, il n'est pas possible d'accéder à des objets classes directement.
Une variable classe d'un autre côté est une fonctionnalité du langage qui permet d'accéder à des objets classes. Bien qu'il semble syntaxiquement que des opérations soient effectuées sur une variable classe, ces opérations sont en fait transmises à l'objet classe.
Considérons le code suivant que nous avions vu précédemment dans le chapitre sur les types valeurs et les types références :

 
Sélectionnez
auto variable1 = new MaClasse;
auto variable2 = variable1;

Le mot-clé new construit un objet classe anonyme. Les variables variable1 et variable2 ci-dessus fournissent juste un accès à cet objet anonyme :

Image non disponible

57-1-4. Copie

La copie affecte seulement les variables, pas l'objet.
Parce que les classes sont des types références, définir une nouvelle variable classe comme copie d'une autre fait que les deux variables fournissent un accès au même objet. L'objet réel n'est pas copié.
Puisqu'aucun objet n'est copié, la fonction postblit this(this) n'est pas disponible pour les classes.

 
Sélectionnez
auto variable2 = variable1;

Dans le code ce-dessus, variable2 est initialisée au moyen de variable1. Les deux variables fournissent un accès au même objet.
Lorsque l'objet réel doit être copié, la classe doit fournir une fonction membre à cet effet. Afin d'être compatible avec les tableaux, on peut nommer cette fonction dup(). Cette fonction doit créer et retourner un nouvel objet classe. Voyons cela sur une classe qui a plusieurs types de membres :

 
Sélectionnez
class Foo {
    S      o; // Suppose que S est un type structure
    char[] s;
    int    i;
 
// ...
 
    this(S o, const char[] s, int i) {
        this.o = o;
        this.s = s.dup;
        this.i = i;
    }
 
    Foo dup() const {
        return new Foo(o, s, i);
    }
}

La fonction membre dup() fabrique un nouvel objet via le constructeur de Foo et retourne ce nouvel objet. Notez que le constructeur copie le membre s explicitement via la propriété .dup des tableaux. Étant des types valeurs, o et i sont copiés automatiquement.
Le code suivant utilise dup() pour créer un nouvel objet :

 
Sélectionnez
auto var1 = new Foo(S(1.5), "bonjour", 42);
auto var2 = var1.dup();

En résultat, les objets qui sont associés à var1 et var2 sont distincts.
Pareillement, une copie immutable d'un objet peut être fournie par une fonction membre appropriée appelée idup() :

 
Sélectionnez
class Foo {
    // ...
    immutable(Foo) idup() const {
        return new immutable(Foo)(o, s, i);
    }
}
 
// ...
 
    immutable(Foo) imm = var1.idup();

57-1-5. Affectation

Tout comme la copie, l'affectation modifie seulement les variables.
Affecter à une variable classe dissocie cette variable de son objet courant et l'associe à un nouvel objet.
Si aucune autre variable classe ne fournit d'accès à l'objet qui vient d'être dissocié de la variable, alors cet objet sera détruit plus tard par le ramasse-miettes.

 
Sélectionnez
auto variable1 = new MaClasse();
    auto variable2 = new MaClasse();
    variable1 = variable2;

L'affectation ci-dessus oblige variable1 à laisser son objet et à fournir un accès à l'objet de variable2. Puisqu'il n'y a aucune autre variable pour l'objet original de variable1, cet objet sera détruit par le garbage collector.
Le comportement de l'affectation ne peut pas être changé pour les classes. En d'autres termes, opAssign ne peut pas être surchargé pour elles.

57-1-6. Définition

Les classes sont définies au moyen du mot-clé class en lieu et place du mot-clé struct :

 
Sélectionnez
class PieceEchec {
    // ...
}

57-1-7. Construction

Comme pour les structures, le nom du constructeur est this. Contrairement aux structures, les objets classes ne peuvent être construits avec la syntaxe { }.

 
Sélectionnez
class PieceEchec {
    dchar forme;
 
    this(dchar forme) {
        this.forme = forme;
    }
}

Contrairement aux structures, il n'y a pas de construction d'objet automatique lorsque les paramètres du constructeur sont affectés aux membres séquentiellement :

 
Sélectionnez
class PieceEchec {
    dchar forme;
    size_t valeur;
}
 
void main() {
    auto roi = new PieceEchec('♔', 100); // ← ERREUR de compilation
}

Sortie :

 
Sélectionnez
Erreur: pas de constructeur pour PieceEchec

Pour que cette syntaxe fonctionne, un constructeur doit être défini explicitement par le programmeur.

57-1-8. Destruction

Comme pour les structures, le nom du destructeur est this :

 
Sélectionnez
~this() {
    // ...
}

Néanmoins, à la différence des structures, les destructeurs des classes ne sont pas exécutés au moment où la durée de vie d'un objet classe se termine. Comme nous l'avons vu plus avant, le destructeur est appelé plus tard pendant un cycle du ramasse-miettes. (Par cette distinction, les destructeurs de classe auraient dû être appelés plus précisément finaliseurs).
Comme nous le verrons plus tard dans le chapitre sur la gestion de la mémoire, les destructeurs de classe doivent respecter les règles suivantes :

  • un destructeur de classe ne peut pas accéder à une membre qui est géré par le ramasse-miettes. La raison en est que les ramasse-miettes ne sont pas tenus de garantir l'ordre dans lequel l'objet et ses membres sont finalisés lorsque le ramasse-miettes s'exécute ;
  • un destructeur de classe ne doit pas allouer de mémoire qui soit gérée par le ramasse-miettes. En effet, les ramasse-miettes ne sont pas tenus de garantir qu'ils puissent allouer de nouveaux objets durant un cycle de ramasse-miettes.

Enfreindre ces règles est un comportement indéfini. Il est facile de voir un exemple de ce type de problème simplement en essayant d'allouer un objet dans un destructeur de classe :

 
Sélectionnez
class C {
    ~this() {
        auto c = new C(); // ← FAUX : allocation explicite dans un
                          // destructeur de classe
    }
}
 
void main() {
    auto c = new C();
}

Le programme est interrompu au moyen d'une exception :

 
Sélectionnez
core.exception.InvalidMemoryOperationError@(0)

Il est également faux de provoquer une allocation mémoire du ramasse-miettes indirectement dans un destructeur. Par exemple, la mémoire utilisée pour les éléments d'un tableau dynamique est aussi allouée au moyen du ramasse-miettes. Utiliser un tableau d'une manière qui requiert un nouveau bloc de mémoire pour les éléments est aussi un comportement indéfini :

 
Sélectionnez
~this() {
    auto tabl = [ 1 ]; // ← FAUX : Allocation indirecte dans un
                       // destructeur de classe
}

57-1-9. Accès aux membres

Comme avec les structures, on accède aux membres au moyen de l'opérateur point :

 
Sélectionnez
auto roi = new PieceEchec('♔');
writeln(roi.forme);

Même si la syntaxe peut faire croire que l'on accède à un membre de la variable, c'est en fait un membre de l'objet. Les variables classes n'ont pas de membres, ce sont les objets membres qui en ont. La variable roi n'a pas de membre forme, c'est l'objet anonyme qui en a.

Il n'est normalement pas correct d'accéder directement aux membres comme dans le code ci-avant. Quand cette même syntaxe est désirée, les propriétés devraient être préférées. Elles seront abordées dans un chapitre ultérieur.

57-1-10. Surcharge d'opérateurs

À part le fait qu'opAssign ne puisse pas être surchargé pour les classes, la surcharge d'opérateur est identique aux structures. Pour les classes, la signification de opAssign est toujours associée à une variable classe avec un objet classe.

57-1-11. Fonctions membres

Bien que les fonctions membres soient définies et utilisées de la même manière que les structures, il existe une différence importante : les fonctions membres des classes peuvent être spécialisées par défaut. Nous verrons ce concept plus tard dans le chapitre sur l'héritage.
Puisque les fonctions membres spécialisables ont un impact sur les performances à l'exécution, sans entrer plus dans les détails, je vous recommande de définir toutes les fonctions membres des classes qui n'ont pas besoin d'être spécialisables avec le mot-clé final. Vous pouvez appliquer cette consigne aveuglément sauf en cas d'erreurs de compilation :

 
Sélectionnez
class C {
    final int fonct() { // ← Recommandé
       // ...
    }
}

Une autre différence par rapport aux structures est que certaines fonctions membres sont automatiquement héritées de la classe Object. Nous verrons dans le chapitre suivant comment la définition de toString peut être changée par le mot-clé override.

57-1-12. Les opérateurs is et !is

Ces opérateurs s'appliquent aux variables classes.
L'opérateur is indique si deux variables classes fournissent un accès au même objet classe. Il retourne true s'il s'agit du même objet et false autrement. L'opérateur !is est l'opposé de is.

 
Sélectionnez
auto monRoi = new PieceEchec('♔');
auto tonRoi = new PieceEchec('♔');
assert(monRoi !is tonRoi);

Puisque les objets des variables monRoi et tonRoi sont différents, l'opérateur !is retourne true. Même si les deux objets sont construits avec le même caractère '♔' il sont néanmoins distincts.
Lorsque les variables fournissent un accès au même objet, is retourne true :

 
Sélectionnez
auto monRoi2 = monRoi;
assert(monRoi2 is monRoi);

Les deux variables ci-avant fournissent un accès au même objet.

57-2. En résumé :

  • Les classes et les structures partagent des fonctionnalités communes, mais ont de grandes différences.
  • Les classes sont des types référence. Le mot-clé new construit un objet classe anonyme et retourne une variable classe.
  • Les variables classes qui ne sont associées à aucun objet valent null. Vérifier la nullité d'une variable doit se faire au moyen de is ou !is, pas au moyen de == ou !=.
  • Le fait de copier associe une variable supplémentaire à un objet. Pour copier un objet, son type doit posséder une fonction spéciale préférablement nommée dup().
  • L'affectation associe une variable à un objet. Ce comportement ne peut pas être modifié.

précédentsommairesuivant