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

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
void reduireEnergie(double energie)
{
    energie /= 4;
}

Voici un programme qui teste reduireEnergie() :

 
Sélectionnez
import std.stdio;
 
void reduireEnergie(double energie)
{
    energie /= 4;
}
 
void main()
{
    double energie = 100;
 
    reduireEnergie(energie);
    writeln("Nouvelle energie : ", energie);
}

La sortie :

 
Sélectionnez
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() :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
.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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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) :

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
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 :

 
Sélectionnez
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) :

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
[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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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[].

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
{
    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);
}
 
Sélectionnez
{
    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);
}
 
Sélectionnez
{
    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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
faireOmelette(9, 5, nombreŒufs(chezLesVoisins));

Cette fois, le nombre total d'œufs chez les voisins est vraiment nécessaire :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1 2          ← non échangés

Corrigez la fonction pour que les valeurs de a et b soient échangées.

La solutionParamètre des fonctions - Correction.


précédentsommairesuivant