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 :
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 :
[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 :
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 :
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 :
struct
TempsDuJour
{
int
heure;
int
minute;
void
info
(
) // (1)
{
writef
(
"%02s:%02s"
, heure, minute); // (2)
}
}
- La méthode ne prend plus l'objet comme paramètre.
- 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 :
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 :
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 :
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 :
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 :
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 :
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() :
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() :
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() :
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 :
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 :
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 :
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 :
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 += :
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 :
struct
TempsDuJour
{
// ... définition de la structure ...
}
unittest
{
// ... tests de la structure ...
}
54-2. Exercices▲
-
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électionnezstruct
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
));}
}
- 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.