Cours complet pour apprendre à programmer en D


précédentsommairesuivant

54. Fonctions membres

Bien que ce chapitre se concentre principalement sur les structures, une grande partie des informations détaillées ci-après peuvent être également appliquées aux classes également.

Au cours de ce chapitre, nous couvrirons les méthodes des structures, notamment la méthode spéciale toString() qui est utilisée afin de représenter les objets sous forme d'une chaîne de caractères, ou string.

Lorsqu'une structure ou une classe est définie, on définit aussi souvent un certain nombre de fonctions avec elle. Nous avions déjà vu des exemples dans les chapitres précédents : ajouterDuree() et la surcharge de info() ont été écrites spécialement pour être utilisées avec le type TempsDuJour. En quelque sorte, ces deux fonctions forment l'interface de TempsDuJour.

ajouterDuree() et info() avaient pris pour premier paramètre un objet TempsDuJour sur lequel elles allaient opérer. En plus de cela, comme c'est le cas pour toutes les fonctions jusque-là étudiées, nos deux fonctions avaient été définies au niveau du module (module level), en dehors de toute autre portée.

Le fait qu'un ensemble de fonctions définissent l'interface d'une structure est un concept très répandu. Pour cette raison, les fonctions ayant une relation proche d'un certain type peuvent être définies au sein de celui-ci.

54-1. Définir des méthodes

Les fonctions définies à l'intérieur des accolades d'une struct sont dites des méthodes :

 
Sélectionnez
struct UneStructure
{
    void methode(/* les paramètres de la fonction */)
    {
        // ... la définition de la fonction ...
    }
 
    // ... les autres membres de la structure ...
}

Les méthodes peuvent être accédées de la même façon que les champs, en les séparant du nom de l'objet par un point :

 
Sélectionnez
[i objet.methode(arguments));

Nous avions déjà utilisé les méthodes quand on avait pris soin de spécifier stdin et stdout lors des opérations d'entrée et de sortie :

 
Sélectionnez
stdin.readf(" %s", &nombre);
stdout.writeln(nombre);

Dans ce code, nous avons effectué des appels aux méthodes readf() et writeln() qui opèrent respectivement sur les objets stdin et stdout.

Définissions maintenant info() en tant que méthode. Auparavant, nous l'avions définie ainsi :

 
Sélectionnez
void info(in TempsDuJour temps)
{
    writef("%02s:%02s", temps.heure, temps.minute);
}

Afin de transformer info() en une méthode, il faudra non seulement déplacer sa définition à l'intérieur de la structure, mais aussi lui faire subir deux modifications :

 
Sélectionnez
struct TempsDuJour
{
    int heure;
    int minute;
 
    void info()    // (1)
    {
        writef("%02s:%02s", heure, minute);    // (2)
    }
}
  1. La méthode ne prend plus l'objet comme paramètre.
  2. Suite à ceci, elle accède aux champs directement par heure et minute.

La raison pour cela est que les méthodes sont toujours appelées sur un objet existant. Ce dernier est implicitement disponible pour la méthode :

 
Sélectionnez
auto temps = TempsDuJour(10, 30);
temps.info();

La méthode info() est appelée sur l'objet temps ci-avant. Les variables heure et minute qui sont indiquées à l'intérieur de la définition de la fonction correspondent aux champs de l'objet temps, plus précisément, temps.heure et temps.minute.

Ici, l'appel à la méthode est presque l'équivalent de l'appel à une fonction normale :

 
Sélectionnez
temps.info();    // méthode
info(temps);     // fonction habituelle (la définition précédente)

Dès qu'une méthode est appelée sur un objet, les champs de celui-ci sont implicitement accessibles pour la fonction :

 
Sélectionnez
auto matin = TempsDuJour(10, 0);
auto apresmidi = TempsDuJour(22, 0);
 
matin.info();
write('-');
apresmidi.info();
writeln();

Appelées sur matin, les variables heure et minute utilisées à l'intérieur de la méthode ne sont autres que matin.heure et matin.minute. De même, en appelant apresmidi.info(), elles font référence à apresmidi.heure et apresmidi.minute :

 
Sélectionnez
10:00-22:00

54-1-1. toString() pour les représentations sous forme de chaînes de caractères

Dans le chapitre précédent, nous venons de parler des limitations de la fonction info(). Mais malgré tout ce que nous venons de voir, la fonction garde au moins un autre inconvénient : bien qu'elle affiche le temps dans un format lisible, le programmeur devra toujours prendre soin d'expliciter le caractère '-' ainsi que le caractère de fin de ligne.

Il serait plus pratique si les objets TempsDuJour pouvaient être utilisés comme des types fondamentaux dans le code ci-après :

 
Sélectionnez
writefln("%s-%s", matin, apresmidi);

En plus d'avoir réduit quatre lignes de code en seulement une, cela aurait aussi pour effet de pouvoir écrire l'objet à n'importe quel flux :

 
Sélectionnez
auto fichier = File("temps_information", "w");
fichier.writefln("%s-%s", matin, apresmidi);

La méthode toString() des types définis par l'utilisateur est un peu spéciale : elle est automatiquement appelée pour produire la représentation string des objets. toString() doit impérativement retourner la représentation string de l'objet en question.

Avant de plonger dans les détails, étudions d'abord la définition de la fonction toString() :

 
Sélectionnez
import std.stdio;
 
struct TempsDuJour
{
    int heure;
    int minute;
 
    string toString()
    {
        return "a_faire";
    }
}
 
void main()
{
    auto matin = TempsDuJour(10, 0);
    auto apresmidi = TempsDuJour(22, 0);
 
    writefln("%s-%s", matin, apresmidi);
}

Pour le moment, toString() ne produit pas grand-chose, mais l'exécution du code ci-avant montre qu'elle a effectivement été appelée à deux reprises par writefln() :

 
Sélectionnez
a_faire-a_faire

Remarquez aussi que info() est désormais redondante vu que toString() offre la même fonctionnalité.

La façon la plus simple d'implémenter toString() serait d'appeler la fonction format(), celle-ci est disponible dans le module std.string. Le fonctionnement de format() est similaire à celui des fonctions d'affichage formaté comme writef(). La seule différence réside dans le fait qu'au lieu d'afficher directement les variables, format() retourne le résultat sous la forme d'une string.

toString() peut donc directement retourner le résultat de format() :

 
Sélectionnez
import std.string;
// ...
struct TempsDuJour
{
// ...
    string toString()
    {
        return format("%02s:%02s", heure, minute);
    }
}

Remarquez que toString() retourne uniquement la représentation textuelle de l'objet this. writefln() se charge d'afficher le reste : elle appelle la méthode toString() pour chacun des deux objets tout en prenant soin d'afficher le caractère '-' entre les deux appels, puis termine la ligne :

 
Sélectionnez
10:00-22:00

La définition de toString() qui a été expliquée ci-avant ne prend aucun paramètre, elle produit simplement une string et la retourne. Cependant, il existe une autre façon de définir toString(), celle-ci prend en paramètre un delegate. Nous étudierons cette définition plus tard dans le chapitre sur les pointeurs de fonctions, les delegates et les lambdas.

54-1-2. Exemple : la méthode increment())

Définissons une méthode qui ajoute une certaine durée aux objets TempsDuJour.

Avant d'aller plus loin, corrigeons d'abord un défaut de conception qui était là tout le temps. Dans le chapitre sur des structures, nous avions vu que l'addition de deux objets TempsDuJour dans ajouterDuree n'avait pas de sens :

 
Sélectionnez
TempsDuJour ajouterDuree(in TempsDuJour start,
                    in TempsDuJour duree)    // absurde
{
    // ...
}

Il serait plus naturel d'ajouter une durée à un point dans le temps. En ajoutant par exemple la durée d'un voyage à son temps de départ, on obtiendrait le temps d'arrivée.

D'un autre côté, soustraire deux points dans le temps est une opération naturelle, et dans ce cas le résultat serait une durée.

Le programme ci-après définit une structure Duree d'une précision allant à la minute, ainsi qu'une fonction ajouterDuree() qui l'utilise :

 
Sélectionnez
struct Duree
{
    int minute;
}
 
TempsDuJour ajouterDuree(in TempsDuJour debut,
                    in Duree duree)
{
    // Commençons par faire une copie de start
    TempsDuJour resultat = debut;
 
    // Ajoutons-y la durée
    resultat.minute += duree.minute;
 
    // Puis prenons soin de régler les débordements
    resultat.heure += resultat.minute / 60;
    resultat.minute %= 60;
    resultat.heure %= 24;
 
    return resultat;
}
 
unittest
{
    // Un test banal
    assert(ajouterDuree(TempsDuJour(10, 30), Duree(10))
        == TempsDuJour(10, 40));
 
    // Un temps à minuit
    assert(ajouterDuree(TempsDuJour(23, 9), Duree(51))
        == TempsDuJour(0, 0));
 
    // Un temps dans le jour suivant
    assert(ajouterDuree(TempsDuJour(17, 45), Duree(8 * 60))
        == TempsDuJour(1, 45));
}

Maintenant, redéfinissons cette fonction sous la forme d'une méthode. Jusqu'à présent, ajouterDuree() retournait un nouvel objet à chaque appel. Définissons donc une méthode incrementer() qui modifiera directement l'objet this :

 
Sélectionnez
struct Duree
{
    int minute;
}
 
struct TempsDuJour
{
    int heure;
    int minute;
 
    string toString()
    {
        return format("%02s:%02s", heure, minute);
    }
 
    void $(HILITE incrementer)(in Duree duree)
    {
        minute += duree.minute;
 
        heure += minute / 60;
        minute %= 60;
        heure %= 24;
    }
 
    unittest
    {
        auto temps = TempsDuJour(10, 30);
 
        // Un test banal
        temps$(HILITE .incrementer)(Duree(10));
        assert(temps == TempsDuJour(10, 40));
 
        // 15 heures plus tard devra être dans le jour suivant
        temps$(HILITE .incrementer)(Duree(15 * 60));
        assert(temps == TempsDuJour(1, 40));
 
        // 22 heures et 20 minutes devra être à minuit
        temps$(HILITE .incrementer)(Duree(22 * 60 + 20));
        assert(temps == TempsDuJour(0, 0));
    }
}

incrementer() incrémente la valeur de l'objet dépendamment de la durée qu'on lui passe. Plus tard, nous étudierons comment la surcharge d'opérateur (operator overloading) va nous permettre d'ajouter une durée à l'aide de l'opérateur += :

 
Sélectionnez
temps += Duree(10);    // sera expliqué dans un chapitre ultérieur

Notez également que les blocs de tests unitaires unittest peuvent être écrits à l'intérieur des définitions des struct aussi, c'est surtout le cas lorsque l'on souhaite tester ses méthodes. Mais il reste tout de même possible de déplacer de tels blocs unittest en dehors du corps de la structure :

 
Sélectionnez
struct TempsDuJour
{
    // ... définition de la structure ...
}
 
unittest
{
    // ... tests de la structure ...
}

54-2. Exercices

  1. Ajoutez une méthode decrementer() à TempsDuJour qui en retrancherait la durée de temps spécifiée. Tout comme pour incrementer(), elle devrait repasser au jour précédent s'il n'y a pas assez de temps dans le jour actuel. Par exemple, soustraire 10 minutes de 00:05 devra donner pour résultat 23:55.
    En d'autres termes, implémentez decrementer() de façon à ne pas échouer les tests unitaires suivants :

     
    Sélectionnez
    struct TempsDuJour
    {
        // ...
     
        void decrementer(in Duree duree)
        {
            // ... veuillez implémenter cette fonction ...
        }
     
        unittest
        {
            auto temps = TempsDuJour(10, 30);
     
            // Un test banal
            temps.decrementer(Duree(12));
            assert(temps == TempsDuJour(10, 18));
     
            // 3 jours and 11 heures plus tôt
            temps.decrementer(Duree(3 * 24 * 60 + 11 * 60));
            assert(temps == TempsDuJour(23, 18));
     
            // 23 heures and 18 minutes plus tôt devra être à minuit
            temps.decrementer(Duree(23 * 60 + 18));
            assert(temps == TempsDuJour(0, 0));
     
            // 1 minute plus tôt
            temps.decrementer(Duree(1));
            assert(temps == TempsDuJour(23, 59));
        }
    }
  2. Convertissez également les surcharges de infos() en méthodes toString() des structures Reunion, Repas et MomentDeLaJournee.

Vous remarquerez qu'en plus de rendre leur structure respective plus pratique, les implémentations des méthodes toString() ne prendront chacune qu'une ligne de code.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+