38. Les paramètres de fonction▲
Ce chapitre couvre les différentes manières de définir des paramètres de fonction.
Certaines idées de ce chapitre sont déjà apparues dans les chapitres précédents. Par exemple, le mot-clé ref que nous avons vu dans le chapitre sur les boucles foreachBoucle foreach permettait d'accéder directement aux éléments eux-mêmes dans les boucles foreach au lieu d'accéder à des copies de ces éléments.
De plus, nous avons couvert les mots-clés const et immutable dans le chapitre précédent.
Nous avons écrit des fonctions qui produisaient des résultats en utilisant leurs paramètres. Par exemple, la fonction suivante utilise ses paramètres dans un calcul :
double
moyennePondérée
(
double
noteDuQuiz, double
noteFinale)
{
return
noteDuQuiz *
0
.4
+
noteFinale *
0
.6
;
}
Cette fonction calcule la note moyenne en prenant 40 % de la note du quiz et 60 % de la note finale. Voici comment elle peut être utilisée :
int
noteDuQuiz =
76
;
int
noteFinale =
80
;
writefln
(
"Moyenne pondérée : %2.0f"
,
moyennePondérée
(
noteDuQuiz, noteFinale));
38-1. La plupart des paramètres sont copiés▲
Dans le code qui précède, les deux variables sont passées en arguments à moyennePondérée() et la fonction utilise ses paramètres. Ceci peut donner la mauvaise impression que la fonction utilise directement les variables qui sont passées en argument. En réalité, la fonction utilise des copies de ces variables.
Cette distinction est importante parce que modifier un paramètre change uniquement la copie. On peut observer cela dans la fonction suivante qui essaie de modifier ses paramètres (c'est-à-dire avoir un effet de bord). Supposons que la fonction suivante soit écrite pour réduire l'énergie d'un personnage de jeu :
void
reduireEnergie
(
double
energie)
{
energie /=
4
;
}
Voici un programme qui teste reduireEnergie() :
import
std.stdio;
void
reduireEnergie
(
double
energie)
{
energie /=
4
;
}
void
main
(
)
{
double
energie =
100
;
reduireEnergie
(
energie);
writeln
(
"Nouvelle energie : "
, energie);
}
La sortie :
Nouvelle energie : 100
← non changée !
Même si reduireEnergie() divise la valeur de son paramètre par quatre, la variable energie dans main() ne change pas. La raison à cela est que la variable energie dans main() et le paramètre energie de reduireEnergie() sont distincts ; le paramètre est une copie de la variable de main().
Pour observer cela plus précisément, plaçons quelques writeln() :
import
std.stdio;
void
reduireEnergie
(
double
energie)
{
writeln
(
"En entrant dans la fonction : "
, energie);
energie /=
4
;
writeln
(
"En sortant de la fonction : "
, energie);
}
void
main
(
)
{
double
energie =
100
;
writeln
(
"En appelant la fonction : "
, energie);
reduireEnergie
(
energie);
writeln
(
"Après l'appel de la fonction : "
, energie);
}
La sortie :
En appelant la fonction : 100
En entrant dans la fonction : 100
En sortant de la fonction : 25
← le paramètre change,
Après l'appel de la fonction : 100 ← la variable reste la même
38-2. Les objets de type référence ne sont pas copiés▲
Les éléments des tranches et des tableaux associatifs, ainsi que les objets de classes, ne sont pas copiés quand ils sont passés en paramètres. De telles variables sont passées aux fonctions par références. En effet, le paramètre devient une référence à l'objet ; les modifications apportées à travers la référence affectent l'objet.
Étant des tranches, les chaînes sont également passées par référence :
import
std.stdio;
void
premiereLettreEnPoint
(
dchar
[] chaine)
{
chaine[0
] =
'.'
;
}
void
main
(
)
{
dchar
[] chaine =
"abc"
d.dup;
premiereLettreEnPoint
(
chaine);
writeln
(
chaine);
}
La modification apportée au premier élément du paramètre affecte l'élément dans main() :
.bc
38-3. Les qualificatifs de paramètre▲
Les paramètres sont passés aux fonctions selon les règles générales suivantes :
- les types valeur sont copiés ;
- les types référence sont passés par références.
Ce sont les règles par défaut qui sont appliquées quand les définitions de paramètres n'ont pas de qualificatifs. Les qualificatifs suivants changent la manière dont les paramètres sont passés et quelles opérations sur eux sont permises.
38-3-1. in ▲
Nous avons vu que les fonctions sont un service qui produit des valeurs et peut avoir des effets de bords. Le mot-clé in indique qu'un paramètre va être utilisé seulement comme une donnée d'entrée. De tels paramètres ne peuvent pas être modifiés par la fonction. in implique const :
import
std.stdio;
double
poidsTotal
(
in
double
totalActuel,
in
double
poids,
in
double
quantitéAjoutée)
{
return
totalActuel +
(
poids *
quantitéAjoutée);
}
void
main
(
)
{
writeln
(
poidsTotal
(
1
.23
, 4
.56
, 7
.89
));
}
Les paramètres in ne peuvent pas être modifiés :
void
foo
(
in
int
valeur)
{
valeur =
1
; // ← ERREUR de compilation
}
38-3-2. out ▲
Nous avons vu que les fonctions retournent les valeurs qu'elles produisent comme leur valeur de retour. Parfois, n'avoir qu'une seule valeur de retour est limitatif et certaines fonctions peuvent avoir besoin de produire plus d'une valeur (note : il est en fait possible de retourner plus d'un résultat en définissant le type de retour comme un n-uplet ou une structure. Nous verrons ces fonctionnalités dans des chapitres ultérieurs).
Le mot-clé out permet aux fonctions de retourner des résultats à travers leurs paramètres. Quand les paramètres out sont modifiés dans une fonction, ces modifications affectent la variable qui a été passée à la fonction.
Examinons la fonction qui divise deux nombres et produit le quotient et le reste. La valeur de retour peut être utilisée pour le quotient et le reste peut être retourné à travers un paramètre out :
import
std.stdio;
int
diviser
(
in
int
dividende, in
int
diviseur, out
int
reste)
{
reste =
dividende %
diviseur;
return
dividende /
diviseur;
}
void
main
(
)
{
int
reste;
int
resultat =
diviser
(
7
, 3
, reste);
writeln
(
"résultat : "
, resultat, ", reste : "
, reste);
}
Modifier le paramètre reste de la fonction modifie la variable reste dans main() (leurs noms n'ont pas besoin d'être les mêmes) :
résultat: 2
, reste : 1
Indépendamment de leurs valeurs au moment de l'appel, la valeur init du type des paramètres out leur est automatiquement affectée :
import
std.stdio;
void
foo
(
out
int
parametre)
{
writeln
(
"Après être entré dans la fonction : "
, parametre);
}
void
main
(
)
{
int
variable =
100
;
writeln
(
"Avant l'appel de la fonction : "
, variable);
foo
(
variable);
writeln
(
"Après le retour de la fonction : "
, variable);
}
Même s'il n'y a pas d'affectation explicite au paramètre dans la fonction, la valeur du paramètre devient automatiquement la valeur initiale de int, affectant la variable dans main() :
Avant l'appel de la fonction : 100
Après être entré dans la fonction : 0 ← la valeur de int.init
Après le retour de la fonction : 0
Cela montre que les paramètres out ne peuvent pas passer de valeur aux fonctions ; ils sont strictement là pour transmettre des valeurs hors de la fonction.
Nous verrons dans des chapitres ultérieurs que retourner des n-uplets ou structures peut être mieux qu'utiliser des paramètres out.
38-3-3. const ▲
Comme nous l'avons vu dans le chapitre précédent, const garantit que le paramètre ne sera pas modifié dans la fonction. Il est utile aux programmeurs de savoir que certaines variables ne seront pas modifiées par la fonction. const rend également les fonctions plus utiles en autorisant des variables const, immutable et non immutable à être passées en paramètres :
import std.stdio;
dchar derniereLettre
(
const dchar[] str)
{
return
str[$
- 1
];
}
void main
(
)
{
writeln
(
derniereLettre
(
"constante"
));
}
38-3-4. immutable ▲
Comme nous l'avons vu dans le chapitre précédent, immutable force certains arguments à être des éléments immutable. À cause d'un tel prérequis, la fonction suivante ne peut être appelée qu'avec des chaînes immuables (par ex. avec des littéraux de chaînes) :
import
std.stdio;
dchar
[] mix
(
immutable dchar
[] premier,
immutable dchar
[] second)
{
dchar
[] resultat;
int
i;
for
(
i =
0
; (
i <
premier.length) &&
(
i <
second.length); ++
i) {
resultat ~=
premier[i];
resultat ~=
second[i];
}
resultat ~=
premier[i..$];
resultat ~=
second[i..$];
return
resultat;
}
void
main
(
)
{
writeln
(
mix
(
"BONJOUR"
, "le monde"
));
}
Comme il demande un prérequis sur le paramètre, le qualificatif immutable ne devrait être utilisé que quand il est vraiment nécessaire. Étant plus accueillant, const est plus utile.
38-3-5. ref ▲
Ce mot-clé permet de passer un paramètre par référence même dans les cas où il serait normalement passé par valeur.
Pour que la fonction reduireEnergie() vue plus tôt modifie la variable qui lui est passée en argument, son paramètre doit être marqué avec ref :
import
std.stdio;
void
reduireEnergie
(
ref double
energie)
{
energie /=
4
;
}
void
main
(
)
{
double
energie =
100
;
reduireEnergie
(
energie);
writeln
(
"Nouvelle energie : "
, energie);
}
Cette fois, les modifications qui sont appliquées aux paramètres affectent la variable qui est passée à la fonction dans main() :
Nouvelle energie: 25
Comme on peut le remarquer, les paramètres ref peuvent être utilisés aussi bien comme entrée que comme sortie. Les paramètres ref peuvent aussi être vus comme des alias des variables passées en argument. Le paramètre de la fonction energie ci-dessus est un alias de la variable energie dans main().
Tout comme les paramètres out, les paramètres ref permettent aux fonctions d'avoir des effets de bords. En fait, reduireEnergie() ne retourne pas de valeur ; elle ne fait qu'avoir un effet de bord à travers son seul paramètre.
Le style de programmation dit fonctionnel favorise les valeurs de retour sur les effets de bords, à tel point que certains langages de programmation fonctionnels ne permettent pas du tout les effets de bords. Les fonctions qui produisent des résultats uniquement par leur valeur de retour sont en effet plus faciles à comprendre, à écrire correctement et à maintenir.
La même fonction peut être écrite en style fonctionnel en retournant le résultat, au lieu de créer un effet de bord.
import
std.stdio;
double
energieReduite
(
double
energie)
{
return
energie /
4
;
}
void
main
(
)
{
double
energie =
100
;
energie =
energieReduite
(
energie);
writeln
(
"Nouvelle energie: "
, energie);
}
Notez également le changement de nom de la fonction. Il ne s'agit plus d'un verbe, mais d'un groupe nominal.
38-3-6. auto ref ▲
Ce qualificateur ne peut être utilisé qu'avec les modèles (templates). Comme nous le verrons dans le chapitre suivant, un paramètre auto ref prend les valeurs de gauche par référence et les valeurs de droite par copie.
38-3-7. inout ▲
Malgré son nom composé de in et de out, ce mot-clé ne veut pas dire entrée et sortie ; nous avons déjà vu que le mot-clé qui fait cela est ref.
inout transmet la mutabilité du paramètre au type de retour. Si le paramètre est mutable, const ou immutable, alors la valeur de retour est respectivement mutable, const ou immutable.
Pour voir l'utilité d'inout, examinons une fonction qui retourne une tranche qui a un élément de moins au début et à la fin que la tranche d'origine :
import
std.stdio;
int
[] rognée
(
int
[] tranche)
{
if
(
tranche.length) {
--
tranche.length; // rogner depuis la fin
if
(
tranche.length) {
tranche =
tranche[1
.. $]; // rogner depuis le début
}
}
return
tranche;
}
void
main
(
)
{
int
[] nombres =
[ 5
, 6
, 7
, 8
, 9
];
writeln
(
rognée
(
nombres));
}
La sortie :
[6
, 7
, 8
]
Selon le chapitre précédent, pour que la fonction soit plus utile, son paramètre devrait être const(int)[] parce que le paramètre n'est pas modifié dans la fonction (notez qu'il n'est pas dangereux de modifier le paramètre tranche lui-même parce c'est une copie de l'argument originel).
Cependant, définir la fonction de la façon suivante entraînerait une erreur de compilation :
int
[] rognée
(
const
(
int
)[] tranche)
{
// ...
return
tranche; // ← ERREUR de compilation
}
L'erreur de compilation indique que la tranche de const(int) ne peut pas être retournée comme une tranche de mutable int :
Sortie :
Error: cannot implicitly convert expression (
tranche) of type
const
(
int)[] to int[]
On pourrait croire que spécifier le type de retour comme const(int)[] peut être la solution :
const
(
int
)[] rognée
(
const
(
int
)[] tranche)
{
// ...
return
tranche; // compile maintenant
}
Bien que le code puisse maintenant être compilé, il apporte une limitation : même si la fonction est appelée avec une tranche d'éléments mutable, la tranche retournée contient des éléments const. Pour voir à quel point ceci est limitant, regardons le code suivant, qui essaie de modifier les éléments d'une tranche autres que ceux qui sont au début ou à la fin :
int
[] nombres =
[ 5
, 6
, 7
, 8
, 9
];
int
[] milieu =
rognée
(
nombres); // ← ERREUR de compilation
milieu[] *=
10
;
Comme on peut s'y attendre, la tranche retournée de type const(int)[] ne peut pas être assignée à une tranche de type int[].
Error: cannot implicitly convert expression (
rognée
(
nombres))
of type const
(
int)[] to int[]
Cependant, comme la tranche de départ est constituée d'éléments mutable, cette limitation peut être vue comme artificielle et malheureuse. inout résout ce problème de mutabilité entre les paramètres et les valeurs de retour. Il est indiqué aussi bien sur le type du paramètre que sur le type de retour et transmet la mutabilité du premier au second :
inout
(
int
)[] rognée
(
inout
(
int
)[] tranche)
{
// ...
return
tranche;
}
Avec ce changement, la même fonction peut maintenant être appelée avec des tranches mutable, const et immutable :
{
int
[] nombres =
[ 5
, 6
, 7
, 8
, 9
];
// Le type de retour est une tranche d'éléments mutables
int
[] milieu =
rognée
(
nombres);
milieu[] *=
10
;
writeln
(
milieu);
}
{
immutable int
[] nombres =
[ 10
, 11
, 12
];
// Le type de retour est une tranche d'éléments immuables
immutable int
[] milieu =
rognée
(
nombres);
writeln
(
milieu);
}
{
const
int
[] nombres =
[ 13
, 14
, 15
, 16
];
// Le type de retour est une tranche d'éléments const
const
int
[] milieu =
rognée
(
nombres);
writeln
(
milieu);
}
38-3-8. Lazy (paresseux)▲
Il est naturel de s'attendre à ce que les arguments soient évalués avant d'entrer dans les fonctions qui utilisent ces arguments. Par exemple, la fonction ajouter() ci-dessous est appelée avec les valeurs de retours de deux autres fonctions :
resultat =
ajouter
(
uneQuantité
(
), uneAutreQuantité
(
));
Pour qu'ajouter() soit appelée, uneQuantité() et uneAutreQuantité() doivent être appelées avant. Autrement, les valeurs dont ajouter() a besoin ne seraient pas disponibles.
Évaluer les arguments avant d'appeler une fonction est non paresseux (eager).
Cependant, certains paramètres peuvent ne pas être utilisés du tout par une fonction selon certaines conditions. Dans de tels cas, les évaluations non paresseuses des arguments sont inutiles.
Regardons un programme qui utilise un de ses paramètres seulement quand il est nécessaire. La fonction suivante essaie de prendre le nombre requis d'œufs dans le réfrigérateur. Quand il y a un nombre suffisant d'œufs dans le réfrigérateur, elle n'a pas besoin de savoir combien d'œufs les voisins ont :
void
faireOmelette
(
in
int
œufsRequis,
in
int
œufsDansLeRefrigérateur,
in
int
œufsDesVoisins)
{
writefln
(
"Besoin de faire une omelette de %s œufs"
, œufsRequis);
if
(
œufsRequis <=
œufsDansLeRefrigérateur) {
writeln
(
"Prendre tous les œufs dans le réfrigérateur"
);
}
else
if
(
œufsRequis <=
(
œufsDansLeRefrigérateur +
œufsDesVoisins)) {
writefln
(
"Prendre %s œufs du réfrigérateur"
" et %s œufs chez les voisins"
,
œufsDansLeRefrigérateur, œufsRequis -
œufsDansLeRefrigérateur);
}
else
{
writefln
(
"Impossible de faire une omelette de % œufs"
, œufsRequis);
}
}
De plus, supposons qu'il y ait une fonction qui calcule et retourne le nombre total d'œufs du voisinage. Pour des raisons pédagogiques, la fonction affiche aussi quelques informations :
int
nombreŒufs
(
in
int
[string] œufsDisponibles)
{
int
resultat;
foreach
(
voisin, nombre; œufsDisponibles) {
writeln
(
voisin, " : "
, nombre, " œufs"
);
resultat +=
nombre;
}
writefln
(
"Un total de %s œufs disponibles chez les voisins"
,
resultat);
return
resultat;
}
La fonction itère sur les éléments d'un tableau associatif et somme le nombre d'œufs.
La fonction faireOmelette() peut être appelée avec la valeur de retour de nombreŒufs() comme dans le programme suivant :
import
std.stdio;
void
main
(
)
{
int
[string] chezLesVoisins =
[ "Jane"
:5
, "Jim"
:3
, "Bill"
:7
];
faireOmelette
(
2
, 5
, nombreŒufs
(
chezLesVoisins));
}
Comme on peut le constater dans la sortie du programme, la fonction nombreŒufs() est d'abord exécutée et faireOmelette() est ensuite appelée :
Jane : 5
œufs ⎫
Bill : 7
œufs ⎬ Comptage des œufs chez les voisins
Jim : 3
œufs ⎭
Un total de 15
œufs disponibles chez les voisins
Besoin de faire une omelette de 2
œufs
Prendre tous les œufs depuis le réfrigérateur
Bien qu'il soit possible de faire l'omelette de deux œufs avec les œufs du réfrigérateur uniquement, les œufs chez les voisins ont été comptés de façon non paresseuse.
Le mot-clé lazy (« paresseux ») indique qu'une expression qui a été passée à une fonction comme paramètre sera évaluée seulement si et quand elle est nécessaire :
void
faireOmelette
(
in
int
œufsRequis,
in
int
œufsDansLeRefrigérateur,
lazy int
œufsDesVoisins)
{
// …Le corps de la fonction est le même qu'avant ...
}
Comme vu dans la nouvelle sortie, quand le nombre d'œufs dans le réfrigérateur satisfait le nombre d'œufs requis, le comptage des œufs chez les voisins ne se fait plus :
Besoin de faire une omelette de 2
œufs
Prendre tous les œufs dans le réfrigérateur
Ce comptage sera toujours fait si nécessaire. Par exemple, considérons le cas où le nombre d'œufs requis est plus grand que le nombre d'œufs dans le réfrigérateur :
faireOmelette
(
9
, 5
, nombreŒufs
(
chezLesVoisins));
Cette fois, le nombre total d'œufs chez les voisins est vraiment nécessaire :
Besoin de faire une omelette de 9
œufs.
Jane : 5
œufs
Bill : 7
œufs
Jim : 3
œufs
Un total de 15
œufs disponibles chez les voisins
Prendre 5
œufs dans le réfrigérateur et 4
œufs chez les voisins
Les valeurs des paramètres lazy sont évalués à chaque fois qu'ils sont utilisés dans la fonction.
Par exemple, parce que le paramètre lazy de la fonction suivante est utilisé trois fois dans la fonction, l'expression qui donne sa valeur est évaluée trois fois :
import
std.stdio;
int
ValeurDeLArgument
(
)
{
writeln
(
"Calcul..."
);
return
1
;
}
void
FonctionAvecParametreLazy
(
lazy int
valeur)
{
int
resultat =
valeur +
valeur +
valeur;
writeln
(
resultat);
}
void
main
(
)
{
FonctionAvecParametreLazy
(
ValeurDeLArgument
(
));
}
La sortie :
Calcul...
Calcul...
Calcul...
3
38-3-9. scope ▲
Ce mot-clé indique qu'un paramètre ne sera pas utilisé au-delà de la portée de la fonction :
int
[] trancheGlobale;
int
[] foo
(
scope int
[] parametre)
{
trancheGlobale =
parametre; // ← ERREUR de compilation
return
parametre; // ← ERREUR de compilation
}
void
main
(
)
{
int
[] tranche =
[ 10
, 20
];
int
[] resultat =
foo
(
tranche);
}
La fonction casse la promesse de scope à deux endroits : elle assigne le paramètre à une variable globale et le retourne. Ces deux actions rendraient possible l'accès aux paramètres après que la fonction s'est terminée.
(Note : dmd 2.066.1, le compilateur qui a été utilisé pour compiler les exemples de ce chapitre, ne prend pas en charge le mot-clé scope. )
38-3-10. shared ▲
Ce mot-clé nécessite que le paramètre soit partageable entre les fils d'exécutions :
void
foo
(
shared int
[] i)
{
// ...
}
void
main
(
)
{
int
[] nombres =
[ 10
, 20
];
foo
(
nombres); // ← ERREUR de compilation
}
Le programme ci-devant ne peut pas être compilé parce que l'argument n'est pas partagé. Le programme peut être compilé avec les changements suivants :
shared int
[] nombres =
[ 10
, 20
];
foo
(
nombres); // maintenant, compile
Nous utiliserons le mot-clé shared dans le chapitre sur la concurrence des partages de donnéesConcurrence par messages.
38-4. Résumé▲
- Le paramètre est ce que la fonction prend depuis le code qui l'appelle pour réaliser une tâche.
- L'argument est une expression (par exemple une variable) qui est passée à une fonction en paramètre.
- Les arguments de type valeur sont copiés lors du passage, les arguments de type référence sont passés par référence (nous reverrons ce sujet dans des chapitres ultérieurs).
- in indique que le paramètre est seulement pour une entrée de données.
- out indique que le paramètre est seulement pour une sortie de données.
- ref indique que le paramètre est pour une entrée et une sortie de données.
- auto ref est seulement pour les modèles. Cela spécifie qu'un argument de type « valeur de gauche » argument est passé par référence et qu'un argument de type « valeur de droite » est passé par copie.
- const garantit que le paramètre n'est pas modifié à l'intérieur de la fonction.
- immutable nécessite que l'argument soit immuable.
- inout apparaît aussi bien pour le paramètre que pour le type de retour et transfère la mutabilité du paramètre au type de retour.
- lazy évalue le paramètre quand (et à chaque fois que) sa valeur est utilisée.
- scope garantit qu'aucune référence au paramètre ne sera « fuitée » par la fonction.
- shared nécessite que le paramètre soit partagé.
38-5. Exercice▲
Le programme suivant essaie d'échanger les valeurs de deux arguments :
import
std.stdio;
void
echanger
(
int
premier, int
second)
{
int
temp =
premier;
premier =
second;
second =
temp;
}
void
main
(
)
{
int
a =
1
;
int
b =
2
;
echanger
(
a, b);
writeln
(
a, ' '
, b);
}
Le programme n'a aucun effet sur a ni sur b :
1
2
← non échangés
Corrigez la fonction pour que les valeurs de a et b soient échangées.