proofread by Stéphane

This commit is contained in:
Raphaël JAKSE 2015-08-30 21:03:45 +02:00
parent 0b2ee8f3ee
commit 07ff8ea8b8
18 changed files with 2092 additions and 1015 deletions

View File

@ -2,13 +2,14 @@
title = "[c assert] et [c enforce]"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Dans les deux chapitres, nous avons vu comment les exceptions et les instructions [c scope] sont utilisés pour améliorer la correction des programmes. [c assert] est aussi un outil puissant dans le même esprit pour s'assurer que certaines suppositions sur lesquelles le programme est basé sont vaildes.
Dans les deux chapitres précédents, nous avons vu comment les exceptions et les instructions [c scope] sont utilisées pour améliorer la justesse des programmes. [c assert] est aussi un outil puissant qui poursuit le même but, en s'assurant que certaines suppositions sur lesquelles le programme est basé sont valides.
Il peut parfois être difficile de décider entre lever une exception ou utiliser [c assert]. On utilisera [c assert] dans tous les exemples suivant sans grande justification. On verra les différences plus tard dans le chapitre.
Il peut parfois être difficile de choisir entre lever une exception et utiliser [c assert]. On utilisera [c assert] dans tous les exemples suivants sans grande justification. On verra les différences plus tard dans le chapitre.
Même si ce n'est pas toujours évident, les programmes sont pleins de suppositions. Par exemple, la fonction suivante est écrite sous l'hypothèse que les deux paramètres ages sont supérieurs ou égaux à zéro :
Même si ce n'est pas toujours évident, les programmes sont pleins de suppositions. Par exemple, la fonction suivante est écrite en faisant l'hypothèse que les deux paramètres d'âges sont supérieurs ou égaux à zéro~ :
[code=d <<<
@ -18,9 +19,9 @@ Même si ce n'est pas toujours évident, les programmes sont pleins de suppositi
}
>>>]
Même s'il peut être incorrect pour le programme de rencontrer un age qui est négatif, la fonction produirait quand même une moyenne, qui peut être utilisée dans le programme sans que personne ne le remarque, ce qui fait que le programme continue avec des données incorrectes.
Même s'il peut être incorrect pour le programme de rencontrer un âge qui est négatif, la fonction produirait quand même une moyenne, qui peut être utilisée dans le programme sans que personne ne le remarque, ce qui fait que le programme continue avec des données incorrectes.
Un autre exemple est la fonction suivante qui suppose qu'elle sera toujours appelée avec deux commandes : "chanter" ou "dancer" :
Un autre exemple est la fonction suivante qui suppose qu'elle sera toujours appelée avec une des deux commandes~ «~ chanter~ » ou «~ dancer~ »~ :
[code=d <<<
void executerCommande(string commande)
@ -34,65 +35,65 @@ Un autre exemple est la fonction suivante qui suppose qu'elle sera toujours appe
}
>>>]
À cause de cette supposition, la fonction [c robotDance] serait appelée pour toute commande autre que "chanter", correcte ou incorrecte.
À cause de cette supposition, la fonction [c robotDance] sera appelée pour toute commande autre que «~ chanter~ », correcte ou incorrecte.
Quand de telles suppositions n'existent seulement dans la tête du programmeur, le programme peut finir par fonctionner de façon incorrect. [c assert] vérifie les suppositions et termine le programme immédiatement quand elles sont incorrectes.
Quand de telles suppositions n'existent que dans la tête du programmeur, le programme peut finir par fonctionner de façon incorrecte. [c assert] vérifie les suppositions et termine le programme immédiatement quand elles sont incorrectes.
[ = Syntaxe
[c assert] peut être utilisé de deux manières :
[c assert] peut être utilisé de deux manières~ :
[code=d <<<
assert(expression_logique);
assert(expression_logique, message);
>>>]
L'expression logique représente une supposition sur le programme. [c assert] évalue cette expression pour valider cette supposition. Si la valeur de l'expression logique est vraie, alors la supposition est considérée comme valide. Sinon, la supposition est invalide et une [c AssertError] est levée.
L'expression logique représente une supposition sur le programme. [c assert] évalue cette expression pour valider cette supposition. Si la valeur de l'expression logique est vraie, alors la supposition est considérée comme valide. Sinon, la supposition est invalide et une exception [c AssertError] est levée.
Comme son nom le suggère, cette exception hérite d'[c Error], et comme nous l'avons vu dans le [[part:exceptions | chapitre sur les Exceptions]], les exceptions qui héritent de [c Error] ne doivent pas être attrapées. Il est important que le programme se termine au lieu de continuer avec des hypothèses invalides.
Les deux suppositions implicites de [c averageAge()] ci-avant peuvent être écrites avec deux appels à [c assert] :
Les deux suppositions implicites de la fonction [c ageMoyen()] que l'on vient de voir peuvent être écrites avec deux appels à [c assert]~ :
[code=d <<<
double averageAge(double first, double second)
double ageMoyen(double premier, double second)
{
assert(first >= 0);
assert(premier >= 0);
assert(second >= 0);
return (first + second) / 2;
return (premier + second) / 2;
}
void main()
{
auto result = averageAge(-1, 10);
auto resultat = ageMoyen(-1, 10);
}
>>>]
Ces vérifications avec [c assert] portent le sens de « On suppose que les deux âges sont supérieurs ou égaux à zéro. ». On peut aussi le voir comme « Cette fonction ne peut marcher correctement que si les deux âges sont supérieurs ou égaux à zéro ».
Ces vérifications avec [c assert] portent le sens de «~ on suppose que les deux âges sont supérieurs ou égaux à zéro.~ ». On peut aussi le voir comme «~ cette fonction ne peut marcher correctement que si les deux âges sont supérieurs ou égaux à zéro~ ».
[c assert] vérifie ces suppositions et termine le programme avec une [c AssertError] quand elles ne sont pas valides :
[c assert] vérifie ces suppositions et termine le programme avec une [c AssertError] quand elles ne sont pas valides~ :
[output <<<
core.exception.AssertError@deneme(3): Assertion failure
core.exception.AssertError@essai(3): Assertion failure
>>>]
La partie après l'arobase dans le message indique le fichier source et le numéro de la ligne de l'assertion qui a échoué. Selon la sortie ci-avant, l'assertion qui a échoué est à la ligne 3 du fichier [c deneme.d].
La partie après l'arobase dans le message indique le fichier source et le numéro de la ligne de l'assertion qui a échoué. Selon la sortie ci-dessus, l'assertion qui a échoué est à la ligne 3 du fichier [c essai.d].
L'autre syntaxe de [c assert] permet d'afficher un message personnalisé quand l'assertion échoue :
L'autre syntaxe de [c assert] permet d'afficher un message personnalisé quand l'assertion échoue~ :
[code=d <<<
assert(first >= 0, "L'âge ne peut pas être négatif.");
assert(premier >= 0, "L'âge ne peut pas être négatif.");
>>>]
La sortie :
La sortie~ :
[output <<<
core.exception.AssertError@deneme.d(3): L'âge ne peut pas être négatif.
core.exception.AssertError@essai.d(3): L'âge ne peut pas être négatif.
>>>]
Parfois, on pense qu'il est impossible pour un programme de rentrer dans un bloc de code donné. Dans de tels cas, il est courant d'utiliser le littéral [c false] comme expression logique pour faire échouer une assertion. Par exemple, pour indiquer qu'on ne s'attend pas à ce que la fonction [c executerCommande()] soit appelée avec autre chose que"chanter" et "danser", et pour se protéger d'une telle possibilité, [c assert(false)] peut être inséré dans la branche [* impossible] :
Parfois, on pense qu'il est impossible pour un programme de rentrer dans un bloc de code donné. Dans de tels cas, il est courant d'utiliser le littéral [c false] comme expression logique pour faire échouer une assertion. Par exemple, pour indiquer qu'on ne s'attend pas à ce que la fonction [c executerCommande()] soit appelée avec autre chose que «~ chanter~ » ou «~ danser~ », et pour se protéger d'une telle possibilité, [c assert(false)] peut être inséré dans la branche [* impossible]~ :
[code=d <<<
@ -110,15 +111,15 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
}
>>>]
On garantie que la fonction ne fonctionne qu'avec les deux commandes qu'elle connaît. Note : une autre possibilité aurait été d'utiliser un [c final switch].
On garantit que la fonction ne fonctionne qu'avec les deux commandes qu'elle connaît. Note~ : une autre possibilité aurait été d'utiliser un [c final switch].
]
[ = [c static assert]
Comme les assertions sont là pour vérifier l'exécution correcte du programme, elle sont testées quand le programme est en fonctionnement. D'autres vérifications s'appliquent à la structure du programme et peuvent être vérifié lors de sa compilation elle-même.
Comme les assertions sont là pour vérifier l'exécution correcte du programme, elle sont testées quand le programme est en fonctionnement. D'autres vérifications s'appliquent à la structure du programme et peuvent être vérifiées lors de sa compilation elle-même.
[c static assert] est l'équivalent d'[c assert] qui est appliqué lors de la compilation. L'avantage est que la compilation d'un programme qui aurait autrement eu un comportement incorrect à l'exécution ne peut pas compiler. Un prérequis naturel est qu'il doit être possible d'évaluer l'expression logique lors de la compilation.
[c static assert] est l'équivalent d'[c assert], mais appliqué lors de la compilation. L'avantage est qu'un programme qui aurait autrement eu un comportement incorrect à l'exécution ne peut ainsi même pas compiler. Un prérequis naturel est qu'il doit être possible d'évaluer l'expression logique lors de la compilation.
Par exemple, en supposant que le titre d'un menu sera affiché sur un périphérique de sortie qui a une largeur limitée, le [c static assert] suivant s'assure qu'il ne sera jamais plus large que cette limite :
Par exemple, en supposant que le titre d'un menu soit affiché sur un périphérique de sortie qui a une largeur limitée, le [c static assert] suivant s'assure qu'il ne sera jamais plus large que cette limite~ :
[code=d <<<
@ -128,29 +129,30 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
Notez que la chaîne est définie comme [c enum] pour que sa taille puisse être évaluée lors de la compilation.
Supposons qu'un programmeur change ce titre pour le rendre plus descriptif :
Supposons qu'un programmeur change ce titre pour le rendre plus descriptif~ :
[code=d <<<
enum dstring titreMenu = "Menu des Commandes Directionnelles";
static assert(titreMenu.length <= 16);
>>>]
L'assertion statique empêche la compilation du programme :
L'assertion statique empêche la compilation du programme~ :
[output <<<
Error: static assert (25u <= 16u) is false
Error: static assert (34u <= 16u) is false
>>>]
Ceci rappellerait au programmeur la limitation du périphérique de sortie.
Ceci rappelle au programmeur la limitation du périphérique de sortie.
[c static assert] est encore plus utile lorsqu'on l'utilise avec les modèles (''templates''). Nous verrons les modèles dans des chapitres ultérieurs.
]
[ = Utilisez les assertions même si c'est absolument vrai
J'insiste sur le "absolument vrai" parce qu'on ne s'attend jamais à ce que les suppositions sur le programmes soient fausses dans tous les cas. Une grande partie des erreurs de programmations viennent de suppositions qu'on pensait absolument vraies.
J'insiste sur le «~ absolument vrai~ » parce que, de toutes façons, on ne s'attend jamais à ce que les suppositions sur le programmes soient fausses. Une grande partie des erreurs de programmations viennent de suppositions qu'on pensait absolument vraies.
Pour cette raison, utilisez des assertions même si cela ne semble pas nécessaire. Regardons La fonction suivante qui retourne les jours des mois d'une année donnée :
Pour cette raison, utilisez des assertions même si cela ne semble pas nécessaire. Regardons la fonction suivante qui retourne les jours des mois d'une année donnée~ :
[code=d <<<
int[] joursMois(in int annee)
@ -169,42 +171,42 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
Cette assertion peut paraître inutile parce que la fonction retourne naturellement soit 365, soit 366. Cependant, ces vérifications protègent également contre les erreurs potentielles dans la fonction [c joursFevrier]. Par exemple, le programme se terminerait si [c joursFevrier] retournait [c 30].
Une autre vérification paraissant inutile peut vérifier que la taille de la tranche est toujours 12 :
Une autre vérification paraissant inutile peut vérifier que la taille de la tranche est toujours 12~ :
[code=d <<<
assert(jours.length == 12);
>>>]
De cette manière, si on supprime ou on ajouter des éléments dans la tranche involontairement, cela sera détecté. De telles vérifications sont des outils importants vis à vis de la correction des programmes.
De cette manière, si on supprime ou on ajoute des éléments dans la tranche involontairement, cela sera détecté. De telles vérifications sont des outils importants vis-à-vis de la correction des programmes.
[c assert] est aussi l'outil de base qui est utilisé dans les [* tests unitaires] et la [* programmation par contrat], que nous verrons dans des chapitres ultérieurs.
]
[ = Pas de valeur ni d'effet de bord
Nous avons vu que les expressions produisent des valeurs ou ont des effets de bords. les assertions n'ont pas de valeur et ne [* devraient] par avoir d'effet de bord.
Nous avons vu que les expressions produisent des valeurs ou ont des effets de bords. Les assertions n'ont pas de valeur et ne [* devraient] pas avoir d'effet de bord.
Le langage D requiert que l'évaluation de l'expression logique ne doit pas avoir d'effet secondaire. [c assert] doit rester un observateur passif de l'état du programme.
Le langage D requiert que l'évaluation de l'expression logique ne doit pas avoir d'effet de bord. [c assert] doit rester un observateur passif de l'état du programme.
]
[ = Désactiver les assertions
Comme les assertions concernent la correction des programmes, elle peuvent sembler inutiles dès lors que le programme a été testé suffisamment. De plus, comme les assertions ne produisent pas de valeur et qu'elles n'ont pas d'effet de bord, les supprimer du programme ne devrait pas changer le comportement du programme.
L'option [c -release] de [c dmd] permet d'ignorer les assertions, comme si elles n'avaient jamais été écrites dans le programme :
L'option [c -release] de [c dmd] permet d'ignorer les assertions, comme si elles n'avaient jamais été écrites dans le programme~ :
[code=bash <<<
dmd deneme.d -release
dmd essai.d -release
>>>]
[p ndt : | Pour [c gdc], vous pouvez utiliser l'option [c -frelease] ou [c -fno-assert].]
[p NdT~ : | pour [c gdc], vous pouvez utiliser l'option [c -frelease] ou [c -fno-assert].]
Cela devrait permettre aux programme de s'exécuter plus vite en évitant d'évaluer des expressions logiques potentiellement lentes dans les assertions.
En revanche, les assertions qui ont le littéral [c false] ou [c 0] comme expression logique ne sont pas désactivées même quand le programme est compilé avec ces options. En effet, [c assert(false)] est là pour s'assurer qu'un bloc de code n'est jamais atteint, et cela devrait être fait même pour une version publiée.
En revanche, les assertions qui ont le littéral [c false] ou [c 0] comme expression logique ne sont pas désactivées même quand le programme est compilé avec ces options. En effet, [c assert(false)] est là pour s'assurer qu'un bloc de code n'est jamais atteint, et cela doit être le cas même pour une version en mode ''release''.
]
[ = [c enforce] pour lever des exceptions
Toutes les situations inattendues ne signifient pas une erreur de programmation. Les programmes peuvent aussi rencontrer des entrées ou des états de l'environnement inattendus. Par exemple, une donnée qui est entrée par l'utilisateur ne devrait pas être validée par une assertion parce qu'une donnée invalide n'a rien avoir avec la correction du programme lui-même. Dans de tels cas, lever une exception comme nous l'avons fait dans les programmes précédents est plus approprié.
Toutes les situations inattendues ne signifient pas une erreur de programmation. Les programmes peuvent aussi rencontrer des entrées ou des états de l'environnement inattendus. Par exemple, une donnée qui est entrée par l'utilisateur ne devrait pas être vérifée en utilisant une assertion, parce qu'une donnée invalide n'a rien avoir avec la correction du programme lui-même. Dans de tels cas, lever une exception comme nous l'avons fait dans les programmes précédents est plus approprié.
[c std.exception.enforce] est une manière commode de lever des exceptions. Par exemple, supposons qu'une exception doive être levé quand une condition donnée n'est pas vérifiée :
[c std.exception.enforce] est une manière commode de lever des exceptions. Par exemple, supposons qu'une exception doive être levée quand une condition donnée n'est pas remplie~ :
[code=d <<<
if (nombre < 3) {
@ -212,7 +214,7 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
}
>>>]
[c enforce] fait une vérification et lève une exception si elle n'est pas vérifiée. Le code suivant est l'équivalent du code précédent :
[c enforce] fait une vérification et lève une exception si elle n'est pas vérifiée. Le code suivant est l'équivalent du code précédent~ :
[code=d <<<
import std.exception;
@ -220,38 +222,38 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
enforce(nombre >= 3, "Doit être supérieur ou égal à 3.");
>>>]
Notez que l'expression logique est inversée par rapport à l'instruction [c if], puisqu'elle traduit ce qui est [c forcé].
Notez que l'expression logique est inversée par rapport à l'instruction [c if], puisqu'elle traduit ce qui est [* forcé].
]
[ = Utilisation
[c assert] est là pour détecter des erreurs du programmeur. Les conditions que vérifie [c assert] dans la fonction [c joursMois] sur la variable [c titreMenu] ci-avant concernent les erreurs du programmeur.
[c assert] est là pour détecter des erreurs du programmeur. Les conditions que vérifie [c assert] dans la fonction [c joursMois], et celles sur la variable [c titreMenu], vues plus tôt dans ce chapitre, concernent les erreurs du programmeur.
Parfois, il est difficile de choisir entre utiliser [c assert] ou lever une exception. La décision devrait être basée sur si la situation inattendue vient d'un problème dans le code du programme.
Parfois, il est difficile de choisir entre utiliser [c assert] ou lever une exception. La décision devrait être basée sur le fait que la situation inattendue vienne d'un problème dans le code du programme, ou non.
Sinon, le programme doit lever une exception quand il n'est pas possible d'accomplir une tâche. [c enforce] est expressif et commode pour lever des exceptions.
Si ce n'est pas le cas, le programme doit lever une exception quand il n'est pas possible d'accomplir une tâche. [c enforce] est expressif et commode pour lever des exceptions.
Un autre point à considérer est la possibilité de remédier à la situation d'une quelconque manière. Si le programme peut faire quelque chose, même simplement afficher un message d'erreur sur le problème avec des données entrées, il est approprié de lever une exception. De cette manière, les appelants du code qui a levé l'exception peut l'attraper et gérer l'erreur.
Un autre point à considérer est la possibilité de remédier à la situation d'une quelconque manière. Si le programme peut faire quelque chose, même simplement afficher un message d'erreur sur le problème avec des données entrées, il est approprié de lever une exception. De cette manière, les appelants du code qui a levé l'exception peuvent l'attraper et gérer l'erreur.
]
[ = Exercices
- [
# [
Le programme suivant inclut plusieurs assertions. Compilez et exécutez le programme pour découvrir les bogues qui sont révélés par ces assertions.
Le programme suivant inclue un nombre d'assertion. Compilez et exécuter le programme pour découvrir les bogues qui sont révélés par ces assertions.
Le programme demande un moment de départ et une durée à l'utilisateur et calcule le moment de fin en ajoutant la durée au moment de départ :
Le programme demande un moment de départ et une durée à l'utilisateur et calcule le moment de fin en ajoutant la durée au moment de départ~ :
[output <<<
10 heures et 8 minutess après 06:09 donne 16:17.
10 heures et 8 minutes après 06:09 donne 16:17.
>>>]
Notez que ce problème peut être écrit d'une manière beaucoup plus propre en définissant des structures. Nous reprendrons ce programme dans des chapitres ultérieurs.
Notez que ce problème pourrait être écrit d'une manière beaucoup plus propre en définissant des structures. Nous reprendrons ce programme dans des chapitres ultérieurs.
[code=d <<<
import std.stdio;
import std.string;
import std.exception;
/* Récupère le moment comme des heures et des minutess après avoir affiché
/* Récupère le moment en tant qu'heures et minutes après avoir affiché
* un message. */
void lireMoment(in string message, out int heures, out int minutes)
{
@ -274,7 +276,7 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
}
/* Ajoute la durée au moment de départ et retourne le résultat dans
* la troisième paire de caractères */
* la troisième paire de paramètres */
void ajouterDuree(in int heuresDepart, in int minutesDepart,
in int dureeHeures, in int dureeMinutes,
out int heuresResultat, out int minutesResultat)
@ -303,7 +305,7 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
dureeHeures, dureeMinutes,
heuresFin, minutesFin);
writefln("%s heuress and %s minutess after %s is %s.",
writefln("%s heures et %s minutes après %s donne %s.",
dureeHeures, dureeMinutes,
momentVersChaine(heuresDepart, minutesDepart),
momentVersChaine(heuresFin, minutesFin));
@ -312,15 +314,17 @@ Quand de telles suppositions n'existent seulement dans la tête du programmeur,
Lancez le programme et entrez [c 06:09] comme moment de départ et [c 1:2] comme durée. Observez que le programme termine normalement.
[p Note : | Vous pouvez remarquer un problème avec la sortie. Ignorez ce problème, vous le découvrirez avec l'aide des assertions très bientôt.]
[p Note~ : | il se peut que vous remarquiez un problème avec la sortie. Ignorez ce problème, vous le découvrirez avec l'aide des assertions très bientôt.]
]
- [
Cette fois, entrez [c 06:09] et [c 15:2]. Observez que le proramme se terminez par une [c AssertError]. Allez à la ligne du programme qui est indiquée dans le message de l'assertion et voyez qu'une des assertions a échoué. Découvrir la cause de cet échec particulier peut prendre du temps.
# [
Cette fois, entrez [c 06:09] et [c 15:2]. Observez que le programme se termine par une [c AssertError]. Allez à la ligne du programme qui est indiquée dans le message de l'assertion et voyez qu'une des assertions a échoué. Découvrir la cause de cet échec particulier peut prendre du temps.
]
- [
Entrez [c 06:09] et [c 20:0] et observez que la même assertion échoue encore et corrigez aussi ce bogue.
# [
Entrez [c 06:09] et [c 20:0]~ ; observez que la même assertion échoue encore et corrigez aussi ce bogue.
]
- [
Modifiez le programme pour afficher les moments au format 12 heures avec des indicateurs [c am] (avant midi) et [c pm] (après midi).
# [
Modifiez le programme pour afficher les temps au format 12 heures avec des indicateurs «~ am~ » (avant midi) et «~ pm~ » (après midi).
]
[[part:corrections/assert | Les solutions]]
]

View File

@ -4,46 +4,46 @@
translator = "Raphaël Jakse"
]
Nous avons vu que les variables représentent des idées dans les programmes. Les interactions de ces idées sont matérialisées par les expressions qui changent les valeurs de ces variables :
Nous avons vu que les variables représentent des idées dans les programmes. Les interactions de ces idées sont matérialisées par des expressions qui changent les valeurs de ces variables~ :
[code=d <<<
// Régler la note
prixTotal = calculerTotal(prixDesElements);
MonnaieDansLePortefeuille -= prixTotal;
monnaieDuMarchand += prixTotal;
argentDansLePortefeuille -= prixTotal;
argentDuMarchand += prixTotal;
>>>]
Quand on modifie une variable, on dit qu'on la [* mute]. La mutabilité est essentielle pour la plupart des tâches. Cependant, dans certains cas, la mutabilité n'est pas adaptée :
Quand on modifie une variable, on dit qu'on la [* mute]. La mutabilité est essentielle pour la plupart des tâches. Cependant, dans certains cas, la mutabilité n'est pas adaptée~ :
- [
Certaines idées sont immuables par définition. Par exemple, il y a toujours sept jours par semaine, la constante pi (π) est constante, un programme peut ne prendre en charge qu'un nombre limité de langages (par ex. seulement français et turc), etc.
]
- [
Si toute variable était modifiable comme nous l'avons vu jusqu'à maintenant, alors toute pièce de code qui utiliserait pourrait potentiellement la modifier. Même s'il n'y a pas de raison de modifier une variable lors d'une opération quelconque, il n'y a également aucune garantie que ce n'est pas le cas. Les programmes sont difficiles à lires et à modifier quand il n'y a pas de garantie d'immuabilité.
Si toute variable était modifiable comme nous l'avons vu jusqu'à maintenant, alors tout morceau de code qui l'utiliserait pourrait potentiellement la modifier. Même quand il n'y a pas de raison de modifier une variable lors d'une opération quelconque, il n'y a également aucune garantie que ce n'est pas le cas. Les programmes sont difficiles à lire et à modifier quand il n'y a pas de garantie d'immuabilité.
Par exemple, il peut être clair que l'appel de fonction [c retirer(bureau, employé)] retirerait un employé d'un bureau. Si toutes les variables étaient mutables, on ne saurait déterminer avec certitude laquelle des deux variables serait modifiée par l'appel de fonction. On peut s'attendre à ce que le nombre d'employés actifs soit décrémenté, mais est-ce que l'appel de fonction modifie également [c employé] d'une certaine manière ?
Par exemple, il peut être clair que l'appel de fonction [c retirer(bureau, employé)] retire un employé d'un bureau. Si toutes les variables étaient mutables, on ne saurait déterminer avec certitude laquelle des deux variables serait modifiée par l'appel de fonction. On peut s'attendre à ce que le nombre d'employés de [c bureau] actifs soit décrémenté, mais est-ce que l'appel de fonction modifie également [c employé] d'une certaine manière~ ?
]
L'idée d'immuabilité aide à comprendre des bouts de programmes en garantissant que certaines opérations ne modifient pas certaines variables. Elle permet aussi de réduire le risque de faire certaines erreurs de programmations.
L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immutable]. Même si les deux mots sont proches dans leur signification, leurs rôles sont différents et parfois incompatibles.
L'[* immuabilité] en D est représentée par les mots-clés [c const] et [c immutable]. Même si les deux mots sont proches dans leur signification, leurs rôles sont différents et parfois incompatibles.
[ = Variables immuables
Les expressions "variables immuables" et "variables constantes" sont toutes deux insensées quand le mot "variable" est pris au sens littéral pour désigner [* quelque chose qui change]. Le mot "variable" désigne n'importe quel élément d'un prorgamme qui peut être mutable ou immuable.
Les expressions «~ variables immuables~ » ou «~ variables constantes~ » sont contradictoires quand le mot «~ variable~ » est pris au sens littéral pour désigner [* quelque chose qui change]. Le mot «~ variable~ » désigne n'importe quel élément d'un programme qui peut être mutable ou immuable. [comment <<< Immuable ? Immutable ? >>>]
Il y a trois manières de définir des variables qui ne peuvent jamais être mutées :
Il y a trois manières de définir des variables qui ne peuvent jamais être mutées~ :
[ = Les constantes [c enum]
[ = Constantes [c enum]
Nous avons vu plus tôt dans le [[part:enum | chapitre sur les énumérations]] qu'[c enum] définie des valeurs constantes nommées :
Nous avons vu plus tôt dans le [[part:enum | chapitre sur les énumérations]] qu'[c enum] définie des valeurs constantes nommées~ :
[code=d <<<
enum nomFichier = "liste.txt";
>>>]
Tant que leurs valeurs peuvent être déterminées lors de la compilation, les variables [c enum] peuvent aussi être initialisées par des valeurs de retour de fonctions :
Tant que leurs valeurs peuvent être déterminées lors de la compilation, les variables [c enum] peuvent aussi être initialisées par des valeurs de retour de fonctions~ :
[code=d <<<
int nbLignesTotal()
@ -68,7 +68,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
}
>>>]
Comme on peut s'y attendre, les valeurs des constantes [c enum] ne peuvent pas être modifiées :
Comme on peut s'y attendre, les valeurs des constantes [c enum] ne peuvent pas être modifiées~ :
[code=d <<<
++nbCarrésTotal; // ← ERREUR de compilation
@ -76,7 +76,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
Même si c'est une manière très efficace de représenter des valeurs immuables, [c enum] ne peut être utilisé que pour des valeurs connues lors de la compilation.
Une constante [c enum] est une [* constante manifeste], ce qui signifie que le programme est compilé comme si la constante avait été remplacé par sa valeur partout dans le code. Par exemple, considérons la définition énumérée suivante et les deux expressions qui l'utilisent :
Une constante [c enum] est une [* constante manifeste], ce qui signifie que le programme est compilé comme si la constante avait été remplacée par sa valeur partout dans le code. Par exemple, considérons la définition énumérée suivante et les deux expressions qui l'utilisent~ :
[code=d <<<
enum i = 42;
@ -84,14 +84,14 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
foo(i);
>>>]
Le code ci-avant serait le même en remplaçant [c i] par sa valeur 42 :
Ce code serait le même en remplaçant [c i] par sa valeur 42~ :
[code=d <<<
writeln(42);
foo(42);
>>>]
Même si ce remplacement fait sens pour des types simples comme [c int] et ne fait aucune différence dans le programme, les constantes [c enum] apportent un coût caché quand elles sont utilisées pour des tableaux ou des tableaux associatifs :
Même si ce remplacement fait sens pour des types simples comme [c int] et ne fait aucune différence dans le programme, les constantes [c enum] apportent un coût caché quand elles sont utilisées pour des tableaux ou des tableaux associatifs~ :
[code=d <<<
enum a = [ 42, 100 ];
@ -99,23 +99,23 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
foo(a);
>>>]
En remplaçant [c a] par sa valeur, le code équivalent que le compilateur compilerait est le suivant :
En remplaçant [c a] par sa valeur, le code équivalent que le compilateur compilerait est le suivant~ :
[code=d <<<
writeln([ 42, 100 ]); // Un tableau est créé lors de l'exécution
foo([ 42, 100 ]); // Un autre tableau est créé lors de l'exécution
>>>]
Le coût caché ici est qu'il y aurait deux tableaux créé pour les deux expressions ci-dessus. Pour cette raison, il est plus sensé de définir les tableaux et les tableaux associatifs comme des variables immuables s'ils sont utilisés plusieurs fois dans le programme.
Le coût caché ici est qu'il y aurait deux tableaux créés pour les deux expressions ci-dessus. Pour cette raison, il vaut mieux définir les tableaux et les tableaux associatifs en tant que variables immuables s'ils sont utilisés plusieurs fois dans le programme.
]
[ = variables [c immutable]
[ = Variables [c immutable]
Comme [c enum], ce mot-clé indique que la valeur d'une variable ne changera jamais. Sa différence par rapport à [c enum] est que les valeurs des variables [c immutable] peuvent être calculées lors de l'exécution du programme.
Le programme compare les utilisations de [c enum] et de [c immutable]. Le programme attend que l'utilisateur devine un nombre qui a été choisi au hasard. Comme le nombre aléatoire ne peut pas être déterminé lors de la compilation, il ne peut pas être défini comme un [c enum]. Cependant, comme la valeur choisie ne doit jamais être changée après avoir été choisie, il est correct de marquer cette variable comme [c immutable].
Le programme suivant compare les utilisations de [c enum] et de [c immutable]. Le programme attend que l'utilisateur devine un nombre qui a été choisi au hasard. Comme le nombre aléatoire ne peut pas être déterminé lors de la compilation, il ne peut pas être défini comme un [c enum]. Cependant, comme la valeur choisie ne doit jamais être changée après avoir été choisie, il est correct de marquer cette variable comme [c immutable].
Le programme utilise la fonction [c lire_entier()] définie dans le chapitre précédent :
Le programme utilise la fonction [c lire_entier()] définie dans le chapitre précédent~ :
[code=d <<<
import std.stdio;
@ -149,13 +149,13 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
}
>>>]
Observations :
Observations~ :
[c min] et [c max] font partie intégrante du comportement du programme et leurs valeurs sont connues lors de la compilation. Pour cette raison, elles sont définie comme des constantes [c enum]. [c nombre] est défini comme [c immutable] parce qu'il ne serait pas approprié de modifier cette variable lors de l'exécution du programme. Idem pour chaque tentative de l'utilisateur : une fois qu'elle est lue, la tentative ne devrait pas être modifiée.
[c min] et [c max] font partie intégrante du comportement du programme et leurs valeurs sont connues lors de la compilation. Pour cette raison, elles sont définie comme des constantes [c enum]. [c nombre] est défini comme [c immutable] parce qu'il ne serait pas approprié de modifier cette variable lors de l'exécution du programme. Idem pour chaque tentative de l'utilisateur~ : une fois qu'elle est lue, la tentative ne devrait pas être modifiée.
Notez que les types de ces variables ne sont pas explicitement spécifiés. Comme avec [c auto], les types des variables [c enum] et [c immutable] peuvent être inférés depuis l'expression à droite de l'opérateur d'affectation.
Même s'il n'est pas nécessaire d'écrire le type complet comme dans par ex. [c immutable(int)], [c immutable] prend normalement le type entre parenthèse. La sortie du programme suivant montre que le nom du type des trois variables est en fait le même :
Même s'il n'est pas nécessaire d'écrire le type complet comme dans par ex. [c immutable(int)], [c immutable] prend normalement le type entre parenthèse. La sortie du programme suivant montre que le nom du type des trois variables est en fait le même~ :
[code=d <<<
import std.stdio;
@ -163,16 +163,16 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
void main()
{
immutable typeInféré = 0;
immutable int TypeExplicite = 1;
immutable int typeExplicite = 1;
immutable(int) typeComplet = 2;
writeln(typeof(typeInféré).stringof);
writeln(typeof(TypeExplicite).stringof);
writeln(typeof(typeExplicite).stringof);
writeln(typeof(typeComplet).stringof);
}
>>>]
Le nom du type inclue [c immutable] :
Le nom du type inclut [c immutable]~ :
[code=d <<<
immutable(int)
@ -180,18 +180,18 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
immutable(int)
>>>]
Le type qui est spécifié entre parenthèses a une signification. Nous verrons cela plus tard quand nous aborderons l'immuabilité de la tranche entière vs. de ses éléments.
Le type qui est spécifié entre parenthèses a une signification. Nous verrons cela plus tard quand nous aborderons l'immuabilité d'une tranche entière comparativement à celle de ses éléments.
]
[ = Variables [c const]
Ce mot-clé a le même effet qu'[c immutable] sur les variable. Les variables [c const] ne peuvent pas être modifiées :
Ce mot-clé a le même effet qu'[c immutable] sur les variable. Les variables [c const] ne peuvent pas être modifiées~ :
[code=d <<<
const moitié = total / 2;
moitié = 10; // ← ERREUR de compilation
>>>]
Je (NdT: l'auteur) vous suggère d'utiliser [c immutable] plutôt que [c const] pour les variables. La raison à cela est que les variables [c immutable] peuvent être passées aux fonctions en paramètre [c immutable]. Nous allons voir cela tout de suite.
Je vous suggère d'utiliser [c immutable] plutôt que [c const] pour les variables. La raison à cela est que les variables [c immutable] peuvent être passées aux fonctions en paramètre [c immutable]. Nous allons voir cela tout de suite.
]
]
@ -201,9 +201,9 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
D'après ce que nous avons vu dans le [[part:tranches | chapitre sur les tranches et autres fonctionnalités des tableaux], les tranches ne stockent pas leurs éléments mais y donnent accès. Il peut y avoir plus d'une tranche à un moment donné qui donne accès aux mêmes éléments.
Même si les exemples de cette section ne se concentre que sur les tranches, cela s'applique aussi aux tableaux associatifs et aux classes parce que ce sont également des [* types référence].
Même si les exemples de cette section ne se concentrent que sur les tranches, cela s'applique aussi aux tableaux associatifs et aux classes parce que ce sont également des [* types référence].
Une tranche qui est passé en argument à une fonction n'est pas la tranche avec laquelle la fonction est appelée. L'argument est une copie de la tranche :
Une tranche qui est passée en argument à une fonction n'est pas la tranche avec laquelle la fonction est appelée. L'argument est une copie de la tranche~ :
[code=d <<<
import std.stdio;
@ -215,7 +215,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
writeln(tranche);
}
void moitié(int[] nombres) // 2
void moitié(int[] nombres) // 2
{
foreach (ref nombre; nombres) {
nombre /= 2;
@ -223,128 +223,128 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
}
>>>]
Quand l'exécution du programme entre dans la fonction [c moitié()], il y a deux tranches qui donnent accès aux mêmes éléments :
- La tranche nommée [c tranche] qui est définie dans [c main()], qui est passée en argument à [c moitié()]
- La tranche nommée [c nombres] que [c moitié()] reçoit en argument, qui donne accès aux mêmes éléments que [c tranche].
Quand l'exécution du programme entre dans la fonction [c moitié()], il y a deux tranches qui donnent accès aux mêmes éléments~ :
- la tranche nommée [c tranche] qui est définie dans [c main()], qui est passée en argument à [c moitié()]~ ;
- la tranche nommée [c nombres] que [c moitié()] reçoit en argument, qui donne accès aux mêmes éléments que [c tranche].
Aussi, à cause du mot-clé [c ref] dans la boucle [c foreach], les valeurs des éléments d'origine (et seulement eux) sont divisées par deux :
Aussi, à cause du mot-clé [c ref] dans la boucle [c foreach], les valeurs des éléments d'origine (et seulement eux) sont divisées par deux~ :
[output <<<
[5, 10, 15, 20]
>>>]
Il est utile pour les fonctions d'avoir la possibilité de modifier les éléments des tranches qui sont passées en arguments. Certaines fonction n'existe que pour cela, comme dans cet exemple.
Il est utile pour les fonctions d'avoir la possibilité de modifier les éléments des tranches qui sont passées en arguments. Certaines fonctions n'existent que pour cela, comme dans cet exemple.
Le compilateur n'autorise pas à passer des variables immuables comme arguments à de telles fonctions, parce qu'il est impossible de modifier une variable immuable :
Le compilateur n'autorise pas à passer des variables immuables comme arguments à de telles fonctions, parce qu'il est impossible de modifier une variable immuable~ :
[code=d <<<
immutable int[] tranche = [ 10, 20, 30, 40 ];
moitié(tranche); // ← ERREUR de compilation
>>>]
L'erreur de compilation indique qu'une variable de type [c <<<immutable(int[])>>>] ne peut pas être utilisée comme un argument de type [c <<< int[]>>>] :
L'erreur de compilation indique qu'une variable de type [c <<<immutable(int[])>>>] ne peut pas être utilisée comme un argument de type [c <<< int[]>>>]~ :
[output <<<
Error: function deneme.moitié (int[] nombres) is not callable
Error: function essai.moitié (int[] nombres) is not callable
using argument types (immutable(int[]))
>>>]
]
[ = Paramètres [c const]
[ = Paramètres [c const]
Il est important et naturel que passer des variables [c immutable] à des fonctions comme [c moitié()], qui modifient leurs arguments, soit interdit. Cependant, ne pas pouvoir les passer à des fonctions qui ne modifient pas leurs arguments serait une limitation :
Il est important et naturel que le passage de variables [c immutable] à des fonctions comme [c moitié()], qui modifient leurs arguments, soit interdit. Cependant, ne pas pouvoir les passer à des fonctions qui ne modifient pas leurs arguments serait une limitation~ :
[code=d <<<
import std.stdio;
[code=d <<<
import std.stdio;
void main()
{
immutable int[] tranche = [ 10, 20, 30, 40 ];
afficher(tranche); // ← ERREUR de compilation
}
void main()
{
immutable int[] tranche = [ 10, 20, 30, 40 ];
afficher(tranche); // ← ERREUR de compilation
}
void afficher(int[] tranche)
{
writefln("%s éléments: ", tranche.length);
void afficher(int[] tranche)
{
writefln("%s éléments: ", tranche.length);
foreach (i, element; tranche) {
writefln("%s: %s", i, element);
}
}
>>>]
foreach (i, element; tranche) {
writefln("%s: %s", i, element);
}
}
>>>]
Interdire à [c tranche] d'être imprimée uniquement parce qu'elle est immuable n'a pas de sens. La bonne manière de gérer cette situation est d'utiliser les paramètres [c const].
Interdire à [c tranche] d'être imprimée uniquement parce qu'elle est immuable n'a pas de sens. La bonne manière de gérer cette situation est d'utiliser les paramètres [c const].
Le mot-clé [c const] indique qu'une variable n'est pas modifiée à travers [* cette référence particulière] (par ex. une tranche) de la variable. Indiquer un paramètre comme [c const] garantie que les éléments de la tranche ne seront pas modifiés à l'intérieur de la fonction. Une fois que [c afficher()] donne cette garantie, le programme peut être compilé :
Le mot-clé [c const] indique qu'une variable n'est pas modifiée à travers [* cette référence particulière] (par ex. une tranche) de la variable. Indiquer un paramètre comme [c const] garantie que les éléments de la tranche ne seront pas modifiés à l'intérieur de la fonction. Une fois que [c afficher()] donne cette garantie, le programme peut être compilé~ :
[code=d <<<
afficher(tranche); // maintenant, compile
// ...
void afficher(const int[] tranche)
>>>]
[code=d <<<
afficher(tranche); // maintenant, compile
// ...
void afficher(const int[] tranche)
>>>]
Cette garantie permet de passer des variables mutables comme immuables en arguments :
Cette garantie permet de passer aussi bien des variables mutables que des variables immuables en arguments~ :
[code=d <<<
immutable int[] tranche = [ 10, 20, 30, 40 ];
afficher(tranche); // compile
[code=d <<<
immutable int[] tranche = [ 10, 20, 30, 40 ];
afficher(tranche); // compile
int[] trancheMutable = [ 7, 8 ];
afficher(trancheMutable); // compile
>>>]
int[] trancheMutable = [ 7, 8 ];
afficher(trancheMutable); // compile
>>>]
Un paramètre qui n'est pas modifié dans une fonction mais qui n'est pas indiqué comme [c const] limite l'utilisabilité de cette fonction. De plus, les paramètres [c const] donne des informations utiles au programmeur. Sachant qu'une variable ne sera pas modifié lorsqu'elle est passée à une fonction rend le code plus facile à comprendre. Cela évite également de faire des erreurs puisque le compilateur interdit les modifications sur les paramètres [c consts] :
Un paramètre qui n'est pas modifié dans une fonction mais qui n'est pas indiqué comme [c const] limite l'utilisabilité de cette fonction. De plus, les paramètres [c const] donnent des informations utiles au programmeur. Savoir qu'une variable ne sera pas modifiée lorsqu'elle est passée à une fonction rend le code plus facile à comprendre. Cela évite également de faire des erreurs puisque le compilateur interdit les modifications sur les paramètres [c const]~ :
[code=d <<<
void afficher(const int[] tranche)
{
tranche[0] = 42; // ← ERREUR de compilation
// ...
}
>>>]
[code=d <<<
void afficher(const int[] tranche)
{
tranche[0] = 42; // ← ERREUR de compilation
// ...
}
>>>]
Le programmeur prend alors conscience de l'erreur dans la fonction ou repense la conception et supprime éventuellement l'indicateur [c const].
Le programmeur prend alors conscience de l'erreur dans la fonction ou repense la conception et supprime éventuellement l'indicateur [c const].
Le fait que les paramètres [c const] acceptent et les variables mutables et les variables immuables a une conséquence intéressante. Ceci est expliqué plus loin dans la section "Un paramètre devrait-il être [c const] ou [c immutable] ?".
Le fait que les paramètres [c const] acceptent et les variables mutables et les variables immuables a une conséquence intéressante. Ceci est expliqué plus loin dans la section «~ Un paramètre devrait-il être [c const] ou [c immutable]~ ?~ ».
]
[ = Paramètres [c immutable]
]
[ = Paramètres [c immutable]
Comme nous l'avons vu ci-avant, les variables immuables ou non peuvent toutes être passées aux fonctions en paramètre [c const]. Dans un sens, les paramètres [c const] sont accueillants.
Comme nous l'avons vu à la section précédente, les variables immuables ou non peuvent toutes être passées aux fonctions en paramètre [c const]. Dans un sens, les paramètres [c const] sont accueillants.
En revanche, les paramètres [c immutable] apportent un prérequis important : seules les variables [c immutable] peuvent être passées aux fonctions en paramètres [c immutable] :
En revanche, les paramètres [c immutable] apportent un prérequis important~ : seules les variables [c immutable] peuvent être passées aux fonctions en paramètres [c immutable]~ :
[code=d <<<
void func(immutable int[] tranche)
{
/* ... */
}
[code=d <<<
void func(immutable int[] tranche)
{
/* ... */
}
void main()
{
immutable int[] immTranche = [ 1, 2 ];
int[] tranche = [ 8, 9 ];
void main()
{
immutable int[] immTranche = [ 1, 2 ];
int[] tranche = [ 8, 9 ];
func(immTranche); // compile
func(tranche); // ← ERREUR de compilation
}
>>>]
func(immTranche); // compile
func(tranche); // ← ERREUR de compilation
}
>>>]
Pour cette raison, l'indicateur [c immutable] devrait être utilisé uniquement quand ce prérequis est nécessaire. Nous avons en effet utilisé l'indicateur [c immutable] indirectement à travers certains types de chaînes de caractères. Ceci sera couvert ci-après.
Pour cette raison, l'indicateur [c immutable] devrait être utilisé uniquement quand ce prérequis est nécessaire. Nous avons en fait déjà utilisé l'indicateur [c immutable] indirectement à travers certains types de chaînes de caractères. Ceci sera couvert plus loin.
Nous avons vu que les paramètres qui sont indiqués comme [c const] ou [c immutable] promettent de ne pas modifier la variable qui est passée en argument. Ceci n'est pertinent que pour les types référence, le problème ne se pose pas pour les types valeurs.
Nous avons vu que les paramètres qui sont indiqués comme [c const] ou [c immutable] promettent de ne pas modifier la variable qui est passée en argument. Ceci n'est pertinent que pour les types référence, le problème ne se pose pas pour les types valeurs.
Les [* types référence] et les [* types valeur] seront couverts dans des chapitres ultérieurs. Parmi les types que nous avons vus jusqu'à maintenant, seuls les tranches et les tableaux associatifs sont des types référence ; les autres sont des types valeur.
Les [* types référence] et les [* types valeur] seront couverts dans des chapitres ultérieurs. Parmi les types que nous avons vus jusqu'à maintenant, seuls les tranches et les tableaux associatifs sont des types référence~ ; les autres sont des types valeur.
]
]
[ = Un paramètre devrait-il être [c const] ou [c immutable] ?
Les deux sections ci-avant peuvent donner l'impression que comme ils sont plus flexibles, les paramètres [c const] devraient être préférés aux paramètres [c immutable]. Ceci n'est pas toujours vrai.
Les deux sections précédentes peuvent donner l'impression que comme ils sont plus flexibles, les paramètres [c const] devraient être préférés aux paramètres [c immutable]. Ceci n'est pas toujours vrai.
[c const] efface l'information de l'immuabilité de la variable d'origine. Cette information est cachée même au compilateur.
Une conséquence ce ceci est que les paramètres [c const] ne peuvent pas être passés en arguments à des fonctions qui prennent des paramètres [c immutable]. Par exemple, la fonction [c foo()] ci-après ne peut pas passer son paramètre [c const] à [c bar()] :
Une conséquence de ceci est que les paramètres [c const] ne peuvent pas être passés en arguments à des fonctions qui prennent des paramètres [c immutable]. Par exemple, la fonction [c foo()] qui suit ne peut pas passer son paramètre [c const] à [c bar()]~ :
[code=d <<<
void main()
@ -371,9 +371,9 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
[c bar()] nécessite un paramètre [c immutable]. Cependant, on ne sait pas si la variable originale que le paramètre [c const] de [c foo()] référence est immuable ou non.
[p Note : | Il est clair dans le code ci-avant que la variable d'origine dans [c main()] est immuable. Cependant, le compilateur compile les individuellement sans regarder à tous les endroits depuis lesquels la fonction est appelée. Pour le compilateur, le paramètre [c tranche()] peut se référer à une variable mutable ou à une variable immuable.]
[p Note~ : | il est clair dans le code qui précède que la variable d'origine dans [c main()] est immuable. Cependant, le compilateur compile les fonctions individuellement sans regarder à tous les endroits depuis lesquels la fonction est appelée. Pour le compilateur, le paramètre [c tranche()] peut se référer à une variable mutable ou à une variable immuable.]
Une solution serait d'appeler [c bar()] avec une copie immuable du paramètre :
Une solution serait d'appeler [c bar()] avec une copie immuable du paramètre~ :
[code=d <<<
void foo(const int[] tranche)
@ -384,7 +384,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
Même s'il s'agit d'une solution raisonnable, elle a le coût d'une copie, qui serait un gâchis dans le cas où la variable d'origine était déjà [c immutable].
Après cette analyse, il devrait être clair qu'utiliser systématiquement des paramètres [c const] ne semble pas la meilleure approche dans toutes les situations. Après tout, si le paramètre [c foo()] a été défini comme [c immutable], il ne devrait pas être nécessaire de le copier avant d'appeler [c bar()] :
Après cette analyse, il devrait être clair qu'utiliser systématiquement des paramètres [c const] ne semble pas la meilleure approche dans toutes les situations. Après tout, si le paramètre [c foo()] a été défini comme [c immutable], il ne devrait pas être nécessaire de le copier avant d'appeler [c bar()]~ :
[code=d <<<
void foo(immutable int[] tranche) // Cette fois-ci, immutable
@ -393,20 +393,20 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
}
>>>]
Même si le code compile, définir le paramètre en tant qu'[c immutable] a un coût similaire : cette fois, une copie immuable de la variable originale est nécessaire lors de l'appel à [c foo()] si cette variable n'était pas [c immutable] à l'origine :
Même si le code compile, définir le paramètre en tant qu'[c immutable] a un coût similaire~ : cette fois, une copie immuable de la variable originale est nécessaire lors de l'appel à [c foo()] si cette variable n'était pas [c immutable] à l'origine~ :
[code=d <<<
foo(trancheMutable.idup);
>>>]
Les modèles peuvent aider. (Nous verrons les modèles dans des chapitres ultérieurs). Même si je ne m'attends pas à ce que vous compreniez totalement la fonction suivante à cet endroit du livre, je vous la présente comme une solution à ce problème. La fonction modèle [c foo()] suivante peut être appelée aussi bien avec des arguments mutables qu'immuables. Le paramètre ne sera copié que si que si la variable d'origine était mutable ; aucune copie ne sera faite si elle était immuable :
Les modèles peuvent aider. (Nous verrons les modèles dans des chapitres ultérieurs). Même si je ne m'attends pas à ce que vous compreniez totalement la fonction suivante à cet endroit du livre, je vous la présente comme une solution à ce problème. La fonction modèle [c foo()] suivante peut être appelée aussi bien avec des arguments mutables qu'immuables. Le paramètre ne sera copié que si que si la variable d'origine était mutable~ ; aucune copie ne sera faite si elle était immuable~ :
[code=d <<<
import std.conv;
// ...
/* Parce qu'elle est un modèle, foo() peut être appelée avec
* des variables mutable et immuable. */
* des variables mutable et immuable. */
void foo(T)(T[] tranche)
{
/* 'to()' ne fait pas de copie si la variable d'origine est
@ -415,9 +415,9 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
}
>>>]
]
[ = Immuabilité de la tranche ''versus'' des éléments
[ = Immuabilité de la tranche ''versus'' immuabilité des éléments
Nous avons vu ci-avant que le type d'une tranche immuable été affiché comme [c immutable(int[])]. Comme les parenthèses après [c immutable] l'indique, c'est la tranche entière qui est immuable. Une telle tranche ne peut être modifié d'aucune manière : les éléments ne peuvent pas être ajoutés ou supprimés, leurs valeurs ne peut pas être modifiée et la tranche ne peut pas se mettre à donner accès à un autre ensemble d'éléments :
Nous avons vu plus haut le type d'une tranche immuable affiché comme [c immutable(int[])]. Comme les parenthèses après [c immutable] l'indiquent, c'est la tranche entière qui est immuable. Une telle tranche ne peut être modifié d'aucune manière~ : les éléments ne peuvent pas être ajoutés ou supprimés, leurs valeurs ne peuvent pas être modifiées et la tranche ne peut pas se mettre à donner accès à un autre ensemble d'éléments~ :
[code=d <<<
immutable int[] immTranche = [ 1, 2 ];
@ -431,7 +431,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
Pousser cette immuabilité à un tel extrême peut ne pas être adapté dans tous les cas. Dans la plupart des cas, ce qui est important est seulement l'immuabilité des éléments. Comme une tranche est seulement un outil d'accès aux éléments, effectuer des changements sur la tranche elle-même ne devrait pas poser de problème tant que les éléments ne sont pas modifiés.
Pour indiquer que seuls les éléments sont immuables, seul le type des éléments est écrit entre parenthèses. Quand le code est modifié de cette manière, seuls les éléments sont immuables, pas la tranche elle-même :
Pour indiquer que seuls les éléments sont immuables, le type des éléments (seul) est écrit entre parenthèses. Quand le code est modifié de cette manière, seuls les éléments sont immuables, pas la tranche elle-même~ :
[code=d <<<
immutable(int)[] immTranche = [ 1, 2 ];
@ -443,41 +443,41 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
immTranche = immAutreTranche; // peut donner accès à d'autres éléments
>>>]
Même si les deux syntaxes sont très similaires, elles ont des significations différentes. En résumé :
Même si les deux syntaxes sont très similaires, elles ont des significations différentes. En résumé~ :
[code=d <<<
immutable int[] a = [1]; /* Ni les éléments, ni la
* tranche ne peuvent être modifiés */
* tranche ne peuvent être modifiés */
immutable(int[]) b = [1]; /* identique à la précédente */
immutable(int)[] c = [1]; /* Les éléments ne peuvent pas être
* modifiés mais la tranche peut l'être */
* modifiés mais la tranche peut l'être */
>>>]
Cette distinction a pris effet dans certain programmes que nous avons rencontrés. Comme vous devez vous en souvenir, les trois alias de chaînes de caractères impliquent l'immuabilité :
Cette distinction se retrouve dans certain programmes que nous avons rencontrés. Comme vous devez vous en souvenir, les trois alias de chaînes de caractères impliquent l'immuabilité~ :
- [c string] est un alias de [c immutable(char)~[~]]
- [c wstring] est un alias de [c immutable(wchar)~[~]]
- [c dstring] est un alias de [c immutable(dchar)~[~]]
- [c string] est un alias de [c immutable(char)~[~]]~ ;
- [c wstring] est un alias de [c immutable(wchar)~[~]]~ ;
- [c dstring] est un alias de [c immutable(dchar)~[~]].
De même, les littéraux de chaînes sont également immuables~ :
De même, les littéraux de chaînes sont également immuables :
- Le type du littéral [c "hello"c] est [c string]
- Le type du littéral [c "hello"w] est [c wstring]
- Le type du littéral [c "hello"d] est [c dstring]
- Le type du littéral [c "hello"c] est [c string]~ ;
- Le type du littéral [c "hello"w] est [c wstring]~ ;
- Le type du littéral [c "hello"d] est [c dstring].
Selon ces définitions, les chaînes D sont normalement des tableaux de caractères immuables.
]
[ = [c .dup] et [c .idup]
Il peut y avoir des problèmes d'immuabilité quand les chaînes sont passés aux fonctions en paramètres. Les propriétés [c .dup] et [c .idup] font des copies des tableaux avec l'immuabilité désirée :
Il peut y avoir des problèmes d'immuabilité quand les chaînes sont passés aux fonctions en paramètres. Les propriétés [c .dup] et [c .idup] font des copies des tableaux avec l'immuabilité désirée~ :
- [c .dup] fait une copie mutable du tableau ; son nom vient de [c "dupliquer"].
- [c .dup] fait une copie mutable du tableau (son nom vient de [c "dupliquer"])~ ;
- [c .idup] fait une copie [c immuable] du tableau.
Par exemple, une fonction qui insiste sur l'immuabilité d'un paramètre peut devoir être appelée avec une copie immuable d'une chaîne mutable :
Par exemple, une fonction qui insiste sur l'immuabilité d'un paramètre peut devoir être appelée avec une copie immuable d'une chaîne mutable~ :
[code=d <<<
void foo(string s)
@ -496,26 +496,28 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
]
[ = Utilisation
- En règle générale, préférez les variables immuables aux variables mutables.
- [
Définissez les valeurs constantes comme [c enum] si elles peuvent être calculées lors de la compilation. Par exemple, La valeur constante de secondes par minute peut être [c enum] :
En règle générale, préférez les variables immuables aux variables mutables.
]
- [
Définissez les valeurs constantes comme [c enum] si elles peuvent être calculées lors de la compilation. Par exemple, la valeur constante représentant le nombre de secondes par minute peut être une [c enum]~ :
[code=d <<<
enum int secondesParMinute = 60;
>>>]
Il n'y a pas besoin de spécifier le type de façon explicite s'il peut être inféré depuis le côté droit de l'affectation :
Il n'y a pas besoin de spécifier le type de façon explicite s'il peut être inféré depuis le côté droit de l'affectation~ :
[code=d <<<
enum secondesParMinute = 60;
>>>]
]
- Considérez le coût de copie des tableaux (associatifs) [c enum]. Définissez-les commes des variables [c immutable] si les tableaux sont larges et qu'ils sont utilisés plus d'une fois dans le programme.
- [
Indiquez les variables comme [c immutable] si leurs valeurs ne changeront pas mais qu'elles ne peuvent pas être connues lors de la compilation. Pareil, le type peut être inféré :
Considérez le coût de copie des tableaux (associatifs) [c enum]. Définissez-les commes des variables [c immutable] si les tableaux sont larges et qu'ils sont utilisés plus d'une fois dans le programme.
]
- [
Indiquez les variables comme [c immutable] si leurs valeurs ne changeront pas mais qu'elles ne peuvent pas être connues lors de la compilation. De même, le type peut être inféré~ :
[code=d <<<
immutable tentative = lire_entier("Que proposez-vous");
@ -523,7 +525,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
]
- [
Si une fonction ne modifie pas un paramètre, indiquez ce paramètre comme [c const]. Ceci permettra de passer en arguments aussi bien des variables mutables qu'immuable :
Si une fonction ne modifie pas un paramètre, indiquez ce paramètre comme [c const]. Ceci permettra de passer en argument aussi bien des variables mutables qu'immuables~ :
[code=d <<<
void foo(const char[] s)
@ -542,10 +544,10 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
>>>]
]
- Pour compléter la règle précédente, considérez que les paramètres [c const] ne peuvent pas être passés aux fonctions qui prennent des [c immutable]. Référez-vous à la section "Un paramètre devrait-il être [c const] ou [c immuable] ?" ci-avant.
- Pour compléter la règle précédente, considérez que les paramètres [c const] ne peuvent pas être passés aux fonctions qui prennent des [c immutable]. Référez-vous à la section «~ Un paramètre devrait-il être [c const] ou [c immuable]~ ?~ ».
- [
Si la fonction modifie un paramètre, laissez le paramètre comme mutable ([c const] or [c immutable] ne permettrait de toute façon pas les modifications) :
Si la fonction modifie un paramètre, laissez le paramètre comme mutable ([c const] or [c immutable] ne permettrait de toute façon pas les modifications)~ :
[code=d <<<
import std.stdio;
@ -567,7 +569,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
}
>>>]
La sortie :
La sortie~ :
[output <<<
tulas
@ -578,7 +580,7 @@ L'[* immuabilité] en D est représenté par les mots-clés [c const] et [c immu
- Les variables [c enum] représentent des idées immuables connues lors de la compilation.
- Les variables [c immutable] representent des idées immuables qui doivent être calculés lors de l'exécution.
- Les paramètres [c const] sont ceux que les fonctions ne modifient pas. Les variables mutable et immuables peuvent toutes être passées comme arguments de paramètres [c const].
- Les paramètres [c immutable] sont ceux que les fonctions nécessitent explicitement de l'être. Seules les variables [c immutable] peuvent être passées comme arguments de paramètres [c immutable].
- Un paramètre ne doit être déclaré [c immutable] que si cela répond à un impératif spécifique à l'intérieur de la fonction.. Seules les variables [c immutable] peuvent être passées comme arguments de paramètres [c immutable].
- [c <<<immutable(int[])>>>] indique que ni la tranche ni ses éléments ne peuvent être modifiés.
- [c <<<immutable(int)[]>>>] indique que seuls les éléments ne peuvent être modifiés.
- L'immuabilité est un outil très puissant en programmation. Il est utile de savoir que les variables et les paramètres ne sont pas modifiés dans des contextes spécifiques ou lors d'opérations spécifiques.

View File

@ -1,27 +1,27 @@
[set
title = "Programmation par contrats"
title = "Programmation par contrat"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
]
La programmation par contrat est une approche en conception logicielle qui traîte les parties d'un logiciel comme des entités individuelles qui se rfinent mutuellement des services. Cette approche considère que le programme peut fonctionner en accord avec sa spécification tant que le fournisseur et le consommateur du service obéissent tous deux à un [* contrat].
La programmation par contrat est une approche en conception logicielle qui traite les parties d'un logiciel comme des entités individuelles qui se rendent mutuellement des services. Cette approche considère que le programme peut fonctionner en accord avec sa spécification tant que le fournisseur et le consommateur du service obéissent tous deux à un [* contrat].
Les fonctionnalités du D en matière de programmation par contrat voient les fonctions comme les unités de services du logiciel. Comme les tests unitaires, la programmation par contrat est basée sur les assertions.
Les fonctionnalités du D en matière de programmation par contrat considèrent les fonctions comme les unités de services du logiciel. Comme les tests unitaires, la programmation par contrat est basée sur les assertions.
La programmation par contrat en D est implémentée avec trois types de blocs de code :
La programmation par contrat en D est implémentée avec trois types de blocs de code~ :
- les blocs [c in] des fonctions~ ;
- les blocs [c out] des fonctions~ ;
- les blocs [c invariant] des structures et des classes.
- Les blocs [c in] des fonctions
- Les blocs [c out] des fonctions
- Les blocs [c invariant] des structures et des classes.
Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapitre ultérieur, après avoir couvert les structures et les classes.
Nous verrons les blocs [c invariant] et l'[* héritage par contrat] dans un chapitre ultérieur, après avoir couvert les structures et les classes.
[ = Les blocs [c in] pour les conditions d'entrée
L'exécution correcte des fonctions dépfinent habituellement de la correction des valeurs de leurs paramètres. Par exemple, une fonction racine carrée peut avoir besoin que sont paramètre ne soit pas négatif. Une fonction qui traite des dates peut avoir besoin que le nombre de mois soit compris entre 1 et 12.
L'exécution correcte des fonctions dépend habituellement de la correction des valeurs de leurs paramètres. Par exemple, une fonction racine carrée peut avoir besoin que son paramètre ne soit pas négatif. Une fonction qui traite des dates peut avoir besoin que le numéro de mois soit compris entre 1 et 12.
Nous avons déjà vu de telles vérifications dans le [[part:assert | chapitre sur [c assert] et [c enforce]]]. Les conditions sur les valeurs des paramètres peuvent être forcées par des assertions à l'intérieur des définitions de fonctions :
Nous avons déjà vu de telles vérifications dans le [[part:assert | chapitre sur [c assert] et [c enforce]]]. Les conditions sur les valeurs des paramètres peuvent être forcées par des assertions à l'intérieur des définitions de fonctions~ :
[code=d <<<
@ -34,7 +34,7 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
}
>>>]
En programmation par contrat, les mêmes vérifications sont écrits dans les blocs [c in] des fonctions. Quand un bloc [c in] ou [c out] est utilisé, le contenu actuel de la fonction doit être placé dans un bloc [c body] :
En programmation par contrat, les mêmes vérifications sont écrits dans les blocs [c in] des fonctions. Quand un bloc [c in] ou [c out] est utilisé, le contenu réel de la fonction doit être placé dans un bloc [c body]~ :
[code=d <<<
import std.stdio;
@ -66,13 +66,13 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
]
[ = Les blocs [c out] pour les postconditions
L'autre côté du contrat implique les garanties que la fonction donne. Un exemple de fonction avec une postcondition serait une fonction qui retourne le nombre de jour en Février ; la valeur retournée sera toujours 28 ou 29.
L'autre côté du contrat implique les garanties que la fonction donne. Un exemple de fonction avec une postcondition serait une fonction qui retourne le nombre de jour en février~ ; la valeur retournée sera toujours 28 ou 29.
Les postconditions sont vérifiées dans les blocs [c out] des fonctions.
La valeur qu'une fonction retourne avec l'instruction [c return] n'a pas besoin d'être définie comme une variable à l'intérieur la fonction, il n'y a habituellement pas de nom faisant référence à cette valeur de retour. Ceci peut être problématique parce que les assertions dans le bloc [c out] ne peuvent pas utiliser la valeur de retour par son nom.
D propose une solution à ce problème en donnant un moyen de nommer la valeur de retour juste après le mot-clé [c out]. Ce nom représente précisément la valeur que la fonction est en train de retourner :
D propose une solution à ce problème en donnant un moyen de nommer la valeur de retour juste après le mot-clé [c out]. Ce nom représente précisément la valeur que la fonction est en train de retourner~ :
[code=d <<<
@ -98,18 +98,18 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
}
>>>]
De manière similaire aux blocs [c in], les blocs [c out] sont exécutés automatiquement après l'exécution du le corps de la fonction.
De manière similaire aux blocs [c in], les blocs [c out] sont exécutés automatiquement après l'exécution du corps de la fonction.
Une assertion qui échoue dans un bloc [c out] indique que le contrat a été violé par la fonction.
Évidemment, les blocs [c in] et [c out] sont optionnels. En incluant les blocs [c unittest], qui sont également optionnels, les fonctions D peuvent être constitués d'un nombre de blocs pouvant aller jusqu'à 4 :
Évidemment, les blocs [c in] et [c out] sont optionnels. En incluant les blocs [c unittest], qui sont également optionnels, les fonctions D peuvent être constituées d'un nombre de blocs pouvant aller jusqu'à 4~ :
- [ c in]: optionnel
- [ c out]: optionnel)
- [ c body]: obligatoire mais le mot-clé [c body] peut être omis s'il n'y a pas de bloc [c in] ni de bloc [c out].
- [ c unittest]: optionnel et ne fait techniquement pas partie de la définition de la fonction mais est couramment défini juste après la fonction.
- [c in]~ : optionnel~ ;
- [c out]~ : optionnel~ ;
- [c body]~ : obligatoire mais le mot-clé [c body] peut être omis s'il n'y a pas de bloc [c in] ni de bloc [c out]~ ;
- [c unittest]~ : optionnel et ne fait techniquement pas partie de la définition de la fonction mais est couramment défini juste après la fonction.
Voici un exemple qui utilise tous ces blocs :
Voici un exemple qui utilise tous ces blocs~ :
[code=d <<<
import std.stdio;
@ -118,7 +118,7 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
* Distribue la somme entre deux variables.
*
* Distribue d'abord à la première variable, mais ne lui donne
* jamais plus de 7. Le reste de la somme est distribuée
* jamais plus de 7. Le reste de la somme est distribué
* à la seconde variable.
*/
void distribuer(in int somme, out int premiere, out int seconde)
@ -175,49 +175,49 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
int seconde;
distribuer(123, premiere, seconde);
writeln("premiere: ", premiere, " seconde: ", seconde);
writeln("premiere : ", premiere, ", seconde : ", seconde);
}
>>>]
Le programme peut être compilé et exécuté dans la console avec la commande suivante :
[code <<<
$ dmd deneme.d -w -unittest // ou : gdc -Wall -funittest deneme.d -o deneme
$ ./deneme
premiere: 7 seconde: 116
$ dmd essai.d -w -unittest // ou : gdc -Wall -funittest essai.d -o essai
$ ./essai
premiere : 7, seconde : 116
>>>]
Même si le travail de la fonction lui-même consiste en seulement deux lignes, il y a un total de 19 lignes non triviales qui prennent en charge sa fonctionnalité. On peut discuter du fait qu'autant de code en plus est superflu pour une si petite fonction. Cependant, les bogues ne sont jamais intentionnels. Le programme écrit toujours du code qui [* devrait] fonctionner correctement, qui fini souvent par contenir divers types de bogues.
Même si le travail de la fonction lui-même consiste en seulement deux lignes, il y a un total de 19 lignes non triviales qui prennent en charge sa fonctionnalité. On peut discuter du fait qu'autant de code en plus est superflu pour une si petite fonction. Cependant, les bogues ne sont jamais intentionnels. Le programmeur écrit toujours du code qui [* devrait] fonctionner correctement, et qui finit souvent par contenir divers types de bogues.
Quant les attentes sont explicitement spécifiées dans les tests unitaires et dans les contrats, les fonction qui sont initialement correctes ont plus de chance de rester correctes. Je vous recommande de tirer parti de toutes les fonctionnalités qui améliorent la correction des programmes. Les tests unitaires comme les contrats sont des outils efficaces pour cela. Ils aident à réduire le temps passé à déboguer au profit du temps passé à écrire le code.
Quant les attentes sont explicitement spécifiées dans les tests unitaires et dans les contrats, les fonctions qui sont initialement correctes ont plus de chance de rester correctes. Je vous recommande de tirer parti de toutes les fonctionnalités qui améliorent la correction des programmes. Les tests unitaires comme les contrats sont des outils efficaces pour cela. Ils aident à réduire le temps passé à déboguer (au dépens du temps passé à écrire le code).
]
[ = Désactiver la programmation par contrats
Contrairement au test unitaire, la programmation par contrat est activée par défaut. L'option [c -release] de [c dmd] et les options [c -fno-in] [c -fno-out] [c -fno-invariants] ou [c -frelease] de [c gdc] désactivent la programmation par contrat :
Contrairement au test unitaire, la programmation par contrat est activée par défaut. L'option [c -release] de [c dmd] et les options [c -fno-in] [c -fno-out] [c -fno-invariants] ou [c -frelease] de [c gdc] désactivent la programmation par contrats~ :
[code=bash <<<
dmd deneme.d -w -release // ou gdc deneme.d -Wall -frelease deneme
dmd essai.d -w -release // ou gdc essai.d -Wall -frelease essai
>>>]
Quand la programmation par contrat est désactivée, le contenu des blocs [c in], [c out] et [c invariant] est ignoré.
]
[ = Blocs [c in] versus [c enforce]
[ = Blocs [c in] [* versus] [c enforce]
Nous avons vu dans le [[part:assert | chapitre sur [c assert] et [c enforce]]] qu'il est parfois difficile de choisir entre [c assert] ou [c enforce] pour faire des vérifications. De manière similaire, il est parfois difficile de choisir entre les assertions dans des blocs [c in] et des vérifications avec [c enforce] à l'intérieur du corps des fonctions.
La possibilité de désactiver la programmation par contrat est une indication que celle-ci est là pour protégere contre les erreurs de programmation. Pour cette raison, la décition devrait ici être basée sur les mêmes règles que ce que nous avons vu dans le [[part:assert | chapitre sur [c assert] et [c enforce]] :
La possibilité de désactiver la programmation par contrat est une indication que celle-ci est là pour protéger contre les erreurs de programmation. Pour cette raison, la décision devrait ici être basée sur les mêmes règles que ce que nous avons vues dans le [[part:assert | chapitre sur [c assert] et [c enforce]]~ :
- Si une vérification est là pour prévenir une erreur de programmation, alors elle devrait être dans le bloc [c in]. Par exemple, si la fonction est appelée seulement depuis d'autres finroits du programme pour construire une fonctionnalité, les valeurs des paramètres sont entièrement sous la responsabilité du programmeur. Pour cette raison, les préconditions d'une telle fonction devrait être vérifiées dans son bloc [c in].
- Si une vérification est là pour prévenir une erreur de programmation, alors elle devrait être dans le bloc [c in]. Par exemple, si la fonction est appelée seulement depuis d'autres endroits du programme pour construire une fonctionnalité, les valeurs des paramètres sont entièrement sous la responsabilité du programmeur. Pour cette raison, les préconditions d'une telle fonction devraient être vérifiées dans son bloc [c in].
- [
Si la fonction ne peut pas jouer son rôle pour n'importe quelle autre raison, des valeurs de paramètres incorrects inclus, alors elle doit lever une exception et [c enforce] est un moyen pratique de le faire.
Pour voir un exemple de cela, définissons une fonction qui retourne une tranche du milieu d'une autre tranche. Supposons que cette fonction est là pour être utilisée par les utilisateurs du module, et non une fonction interne utilisée uniquement par le module lui-même. Comme les utilisateurs de ce module peuvent appeler cette fonction avec divers paramètres potentiellement incorrects, il peut être approprié de vérifier les valeurs des paramètres à chaque fois que la fonction est appelée. Il serait insuffisant de ne les vérifier que pfinant le développement, après lequel les contrats peuvent être désactivés.
Pour voir un exemple de cela, définissons une fonction qui retourne une tranche du milieu d'une autre tranche. Supposons que cette fonction est là pour être utilisée par les utilisateurs du module, et non une fonction interne utilisée uniquement par le module lui-même. Comme les utilisateurs de ce module peuvent appeler cette fonction avec divers paramètres potentiellement incorrects, il peut être approprié de vérifier les valeurs des paramètres à chaque fois que la fonction est appelée. Il serait insuffisant de ne les vérifier que pendant le développement, après lequel les contrats peuvent être désactivés.
Pour cette raison, la fonction suivante vérifie ses paramètres en appelant [c enforce] dans le corps de la fonction au lieu d'une assertion dans le bloc [c in] :
Pour cette raison, la fonction suivante vérifie ses paramètres en appelant [c enforce] dans le corps de la fonction au lieu d'une assertion dans le bloc [c in]~ :
[code=d <<<
import std.exception;
@ -251,19 +251,19 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
>>>]
Le problème se pose moins pour les blocs [c out]. Comme la valeur de retour de toute fonction relève de la responsabilité du programmeur, les postconditions doivent toujours être vérifiées dans le bloc [c out]. La fonction ci-avant suit cette règle.
Le problème se pose moins pour les blocs [c out]. Comme la valeur de retour de toute fonction relève de la responsabilité du programmeur, les postconditions doivent toujours être vérifiées dans le bloc [c out]. La fonction que l'on vient de voir suit cette règle.
]
- Un autre critère à consider lors du choix entre les blocs [c in] et [c enforce] est de se demander si la situation est rattrapable. Si elle est rattrapable par les couches de code de plus haut niveau, alors il peut être plus approprié de lever une exception (ce qu'[c enforce] rend pratique).
- Un autre critère à considérer lors du choix entre les blocs [c in] et [c enforce] est de se demander si la situation est rattrapable. Si elle est rattrapable par les couches de code de plus haut niveau, alors il peut être plus approprié de lever une exception (ce qu'[c enforce] rend pratique).
]
[ = Exercice
Écrivez un programme qui augmente le nombre total de points de deux équipes de foot selon le résultat d'un match.
Les deux premiers paramètres de cette fonction sont les buts que les deux équipes ont marqués. Les deux autres paramètres sont les points de chaque équipe avant le match. Cette fonction devrait ajuster les points des équipes selon les buts qu'ils ont marqués. Pour rappelle, l'équipe gagnante prend 3 points et l'équipe perdante ne prend pas de point. Dans le cas d'un match nul, les deux équipes prennent un point chacune.
Les deux premiers paramètres de cette fonction sont les buts que les deux équipes ont marqués. Les deux autres paramètres sont les points de chaque équipe avant le match. Cette fonction doit ajuster les points des équipes selon les buts qu'elles ont marqués. Pour rappel, l'équipe gagnante prend 3 points et l'équipe perdante ne prend pas de point. Dans le cas d'un match nul, les deux équipes prennent un point chacune.
De plus, la fonction devrait indiquer l'équipe gagnante : 1 si la première équipe a gagné, 2 si la seconde équipe a gagné, 0 si le match a finit par un match nul.
De plus, la fonction devrait indiquer l'équipe gagnante~ : 1 si la première équipe a gagné, 2 si la seconde équipe a gagné, 0 si le match a fini par un match nul.
Partez du programme suivant et complétez les quatre blocs de la fonction de façon appropriée. Ne supprimez pas les assertions dans la fonction [c main] ; elles montrent comment cette fonction doit se comporter.
Partez du programme suivant et complétez les quatre blocs de la fonction de façon appropriée. Ne supprimez pas les assertions dans la fonction [c main]~ ; elles montrent comment cette fonction doit se comporter.
[code=d <<<
int ajouterPoints(in int buts1,
@ -310,7 +310,7 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
}
>>>]
[p Note : | il peut être plus judicieux de retourner une valeur énumérée de cette fonction :]
[p Note~ : | il peut être plus judicieux de retourner une valeur énumérée depuis cette fonction~ :]
[code=d <<<
enum ResultatJeu
@ -318,7 +318,7 @@ Nous verront les blocs [c invariant] et l'[*héritage par contrat] dans un chapi
premiereGagne, secondeGagne, nul
}
ResultatJeu) ajouterPoints(in int buts1,
ResultatJeu ajouterPoints(in int buts1,
in int buts2,
ref int points1,
ref int points2)

106
corrections/assert.whata Normal file
View File

@ -0,0 +1,106 @@
[set
title = "assert et enforce"
partAs = correction
translator = "Raphaël Jakse"
]
# [
Vous remarquerez que le programme termine normalement quand on saisit [c 06:09] et [c 1:2]. Cepandant, vous pourrez remarquer que le temps de départ ne correspond pas à ce qui a été saisi par l'utilisateur~ :
[output <<<
1 heure et 2 minutes après 09:06 donne 10:08.
>>>]
Comme vous pouvez le voir, même si le moment qui a été saisi est [c 06:09], la sortie contient [c 09:06]. Cette erreur sera attrapée à l'aide d'une assertion dans le problème suivant.
]
# [
L'erreur d'assertion après la saisie de [c 06:09] et [c 15:2] nous amène à la ligne suivante~ :
[code=d <<<
string momentVersChaine(in int heure, in int minute)
{
assert((heure >= 0) && (heure <= 23));
// ...
}
>>>]
Pour que cette assertion échoue, cette fonction doit être appelée avec une heure incorrecte.
Les deux seuls appels à [c momentVersChaine()] dans le programme ne semble pas avoir de problème~ :
[code=d <<<
writefln("%s heures et %s minutes après %s donne %s.",
DuréeHeures, duréeMinutes,
momentVersChaine(heuresDepart, minutesDepart),
momentVersChaine(endHour, endMinute));
>>>]
Une enquête un peu plus poussée devrait révéler la vraie cause du bogue~ : les variables [c heure] et [c minute] sont échangées lors de la lecture du moment de départ~ :
[code=d <<<
lireMoment("Moment de départ", minutesDepart, heuresDepart);
>>>]
That programming error causes the time to be interpreted as 09:06 and incrementing it by duration 15:2 causes an invalid heure value.
An obvious correction is to pass the heure and minute variables in the right order:
[code=d <<<
lireMoment("Start time", heuresDepart, minutesDepart);
>>>]
La sortie~ :
[output <<<
Moment de départ ? (HH:MM) 06:09
Durée ? (HH:MM) 15:2
15 heures et 2 minutes après 06:09 donne 21:11.
>>>]
]
# [
Il s'agit de la même assertion~ :
[code=d <<<
assert((heure >= 0) && (heure <= 23));
>>>]
La raison est que [c ajouterMoment()] peut produire des heures qui sont plus grandes que [c 23]. Ajouter un modulo à la fin garantirait une des sorties de la fonction~ :
[code=d <<<
void ajouterDurée(in int heuresDepart, in int minutesDepart,
in int DuréeHeures, in int duréeMinutes,
out int heuresResultat, out int minutesResultat)
{
heuresResultat = heuresDepart + DuréeHeures;
minutesResultat = minutesDepart + duréeMinutes;
if (minutesResultat > 59) {
++heuresResultat;
}
heuresResultat %= 24;
}
>>>]
Notez que la fonction a d'autres problèmes. Par exemple, [c minutesResultat] peut être supérieure à [c 59]. La fonction suivante calcule la valeur des minutes correctement et s'assure que la sortie de la fonction vérifie les spécifications~ :
[code=d <<<
void ajouterDurée(in int heuresDepart, in int minutesDepart,
in int DuréeHeures, in int duréeMinutes,
out int heuresResultat, out int minutesResultat)
{
heuresResultat = heuresDepart + DuréeHeures;
minutesResultat = minutesDepart + duréeMinutes;
heuresResultat += minutesResultat / 60;
heuresResultat %= 24;
minutesResultat %= 60;
assert((heuresResultat >= 0) && (heuresResultat <= 23));
assert((minutesResultat >= 0) && (minutesResultat <= 59));
}
>>>]
]
# [
Bonne chance.
]

View File

@ -1,20 +1,21 @@
[set
title = "Programmation par contrats"
title = "Programmation par contrat"
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
partAs = correction
]
Le bloc [c unittest] peut être implémenté simplement en copiant les vérifications qui ont déjà été écrites dans la fonction [c main]. Le seul ajout ci-dessous est le test pour le cas dans lequel la seconde équipe gagne :
Le bloc [c unittest] peut être implémenté simplement en copiant les vérifications qui ont déjà été écrites dans la fonction [c main]. Le seul ajout ci-dessous est le test pour le cas dans lequel la seconde équipe gagne~ :
[code=d <<<
int ajouterPoints(in int goals1,
in int goals2,
int ajouterPoints(in int buts1,
in int buts2,
ref int points1,
ref int points2)
in
{
assert(goals1 >= 0);
assert(goals2 >= 0);
assert(buts1 >= 0);
assert(buts2 >= 0);
assert(points1 >= 0);
assert(points2 >= 0);
}
@ -26,11 +27,11 @@ Le bloc [c unittest] peut être implémenté simplement en copiant les vérifica
{
int gagnante;
if (goals1 > goals2) {
if (buts1 > buts2) {
points1 += 3;
gagnante = 1;
} else if (goals1 < goals2) {
} else if (buts1 < buts2) {
points2 += 3;
gagnante = 2;

View File

@ -22,10 +22,10 @@
}
>>>]
# [
Voici quelques idées :
Voici quelques idées~ :
- Écrire une fonction nommée [c dessinerLigneHorizontale()] qui dessine des lignes horizontales.
- Écrire une fonction nommée [c dessinerCarré()] qui dessine des carrés. Cette fonction pourrait utiliser [dessinerLigneVerticale()] et [c dessinerLigneHorizontale()] pour dessiner le carré.
- Améliorer les fonctions pour qu'elles prennent le caractère utilisé pour dessiner en paramètre. Ceci permettrait de dessiner chaque forme avec un caractère différent :
- Écrire une fonction nommée [c dessinerCarré()] qui dessine des carrés. Cette fonction pourrait utiliser [c dessinerLigneVerticale()] et [c dessinerLigneHorizontale()] pour dessiner le carré.
- Améliorer les fonctions pour qu'elles prennent en paramètre le caractère utilisé pour dessiner. Ceci permettrait de dessiner chaque forme avec un caractère différent~ :
[code=d <<<
void placerPoint(Toile toile, int ligne, int colonne, dchar point)

View File

@ -1,6 +1,7 @@
[set
title = "Environnement du Programme"
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
partAs = correction
]
@ -55,10 +56,10 @@
void main()
{
write("Veuillez entrer la ligne de commande à exécuter : ");
write("Veuillez saisir la ligne de commande à exécuter : ");
string ligneDeCommande = readln();
writeln("Valeur de retour : ", system(ligneDeCommande));
writeln("La sortie : ", executeShell(ligneDeCommande));
}
>>>]
]

View File

@ -2,25 +2,26 @@
title = "Paramètre des fonctions"
translator = "Raphaël Jakse"
partAs = correction
proofreader = "Stéphane Gouget"
]
# Parce que les paramètres de cette fonction sont du type de ceux qui sont copiés depuis les arguments, ce qui est échangé dans la fonction sont ces copies.
Pour faire que la fonction échange les arguments, les deux paramètres doivent être passés par référence :
Parce que les paramètres de cette fonction sont du genre de ceux qui sont copiés depuis les arguments (types valeur), ce sont ces copies qui sont échangées à l'intérieur de la fonction.
Pour faire en sorte que la fonction échange les arguments, les deux paramètres doivent être passés par référence~ :
[code=d <<<
void swap($(HILITE ref) int first, $(HILITE ref) int second)
void echanger(ref int premier, ref int second)
{
immutable int temp = first;
first = second;
immutable int temp = premier;
premier = second;
second = temp;
}
>>>]
Avec ce changement, les variables dans [c main()] sont échangés:
Avec ce changement, les variables dans [c main()] sont échangées~ :
[output <<<
2 1
>>>]
Même si ce n'est pas relatif au problème original, notez également que [c temp] est spécifié comme [c immutable] comme il ne devrait pas être changé dans la fonction après son initialisation.
Même si ce n'est pas lié au problème original, notez également que [c temp] est spécifié comme [c immutable] car il n'a pas à être modifié dans la fonction après son initialisation.

View File

@ -70,7 +70,7 @@
Nous verrons comment ceci peut être utilisé dans la section suivante.
[c NomDuType] définit la signification commune des valeurs. Toutes les valeurs d'un type énuméré sont listées entres accolades. Voici quelques exemples~ :
[c NomDuType] définit la signification commune des valeurs. Toutes les valeurs d'un type énuméré sont listées entre accolades. Voici quelques exemples~ :
[code=d <<<
enum PileOuFace { pile, face }

View File

@ -2,13 +2,14 @@
title = "Exceptions"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Les situations non attendues font partie des programmes : erreurs de l'utilisateur, erreurs de programmation, changements dans l'environnement de programmation, etc. Les programmes doivent être écrits d'une manière qui évite la prodution de résultats incorrects quand on se trouve devant de telles conditions [i exceptionnelles].
Les situations non attendues font partie des programmes~ : erreurs de l'utilisateur, erreurs de programmation, changements dans l'environnement de programmation, etc. Les programmes doivent être écrits d'une manière qui évite la production de résultats incorrects quand on se trouve face à de telles conditions [i exceptionnelles].
Certaines de ces conditions peuvent être suffisamment graves pour stopper l'exécution du programme. Par exemple, une information nécessaire peut manquer ou être invalide, ou un périphérique peut ne pas fonctionner correctement. Le mécanisme de gestion des exceptions du D aide à stopper l'exécution du programme lorsque c'est nécessaire et de récupérer d'une situation inattendue quand c'est possible.
Certaines de ces conditions peuvent être suffisamment graves pour stopper l'exécution du programme. Par exemple, une information nécessaire peut manquer ou être invalide, ou un périphérique peut ne pas fonctionner correctement. Le mécanisme de gestion des exceptions du D aide à stopper l'exécution du programme lorsque c'est nécessaire et à récupérer d'une situation inattendue quand c'est possible.
Comme exemple de situation inattendue, on peut penser au cas où l'on passe un opérateur inconnu à une fonction qui ne connaît que les 4 opérateurs arithmétiques, comme nous l'avons vu dans les exercices d'un chapitre précédent :
Comme exemple de situation inattendue, on peut penser au cas où l'on passe un opérateur inconnu à une fonction qui ne connaît que les 4 opérateurs arithmétiques, comme nous l'avons vu dans les exercices d'un chapitre précédent~ :
[code=d <<<
@ -35,9 +36,9 @@ Comme exemple de situation inattendue, on peut penser au cas où l'on passe un o
}
>>>]
L'instruction [c switch] ci-dessus ne sait pas quoi faire avec les opérateurs qui ne sont pas listés dans les instructions case, elle lève donc une exception.
L'instruction [c switch] ci-dessus ne sait pas quoi faire avec les opérateurs qui ne sont pas listés dans les instructions [c case], elle lève donc une exception.
Il y a plein d'exemples d'expressions levées dans Phobos. Par exemple, [c to!int], qui peut être utilisé pour convertir une représentation textuelle d'un entier à une valeur [c int] lève une exception quand cette représentation n'est pas valide :
Il y a plein d'exemples d'expressions levées dans Phobos. Par exemple, [c to!int], qui peut être utilisé pour convertir une représentation textuelle d'un entier à une valeur [c int] lève une exception quand cette représentation n'est pas valide~ :
[code=d <<<
import std.conv;
@ -48,28 +49,28 @@ Il y a plein d'exemples d'expressions levées dans Phobos. Par exemple, [c to!in
}
>>>]
Le programme termine avec une exception qui est levé par [c to!int] :
Le programme termine avec une exception levée par [c to!int]~ :
[code <<<
std.conv.ConvException@/usr/include/d/4.8/std/conv.d(1826): Unexpected 's' when converting from type string to type int
>>>]
[c std.conv.ConvException] au début du message est le type de l'objet exception levé. On peut dire que le nom de ce type est [c ConvException] qui est défini dans le module [c std.conv].
[c std.conv.ConvException] au début du message est le type de l'objet exception levé. D'après ce nom, on peut dire que le type est [c ConvException], qui est défini dans le module [c std.conv].
[ = L'instruction [c throw] pour lever des exceptions
Nous avons vu l'instruction [c throw] aussi bien dans les exemples ci-avant que dans des chapitres précédents.
Nous avons vu l'instruction [c throw] aussi bien dans les exemples ci-dessus que dans des chapitres précédents.
[c throw] lève un [c objet exception] et cela termine l'opération courante du programme. Les expressions et les instructions qui sont écrites après l'instruction [c throw] ne sont pas exécutées. Ce comportement correspond à la nature des exceptions : elle doivent être levées quand le programme ne peut pas continuer sa tâche courante.
[c throw] lève un [* objet exception] et cela termine l'opération courante du programme. Les expressions et les instructions qui sont écrites après l'instruction [c throw] ne sont pas exécutées. Ce comportement correspond à la nature des exceptions~ : elle doivent être levées quand le programme ne peut pas continuer sa tâche courante.
À l'inverse, si le programme pouvait continuer, alors la situation ne justifierait pas l'utilisatioin d'une exception. Dans de tels cas, la fonction trouverait une solution et continuerait.
À l'inverse, si le programme pouvait continuer, alors la situation ne justifierait pas l'utilisation d'une exception. Dans de tels cas, la fonction trouverait une solution et continuerait.
[ = Les types d'exceptions [c Exception] et [c Error]
Seuls les types qui sont hérités de la classe [c Throwable] peuvent être levés. [c Throwable] n'est quasiment jamais directement utilisée dans les programmes. En pratique, les types qui sont levés sont des types qui héritent d'[c Exception] ou d'[c Error], qui sont eux-mêmes des types qui héritent de [c Throwable]. Par exemple, toutes les exceptions que Phobos lève sont héritées de [c Exception] ou [c Error].
[c Error] représente des situations irrécupérables qu'il n'est pas recommandé d'attraper. Pour cette raison, la plupart des exceptions qu'un programme lève sont d'un type hérité de [c Exception]. L'héritage est un concept relatif aux classes. Nous verrons les classes dans un chapitre ultérieur.
Les objets de type [c Exception] sont construits à partir d'une valeur [c string] qui représente un message d'erreur. Vous pouvez utiliser la fonction [c format] du module [c std.string] pour créer ce message :
Les objets de type [c Exception] sont construits à partir d'une valeur [c string] qui représente un message d'erreur. Vous pouvez éventuellement utiliser la fonction [c format] du module [c std.string] pour créer ce message~ :
[code=d <<<
import std.stdio;
@ -101,12 +102,21 @@ Le programme termine avec une exception qui est levé par [c to!int] :
[output <<<
object.Exception...: Nombre de dés invalide : -5
>>>]
Dans la plupart des cas, au lieu de créer un object [c Exception] explicitement avec [c new] et lever l'exception avec [c throw], la fonction [c enforce()] est appelée. Par exemple, l'équivalent de la vérification d'erreur précédente est l'appel suivant à [c enforce()]~ :
[code=d <<<
enforce(nombre >= 0, format("Nombre de dés invalide : %s", nombre));
>>>]
Nous verrons les différences entre [c enforce()] et [c assert()] dans un chapitre ultérieur.
]
[ = Les expressions levées mettent fin à toutes les portées
Nous avons vu que l'exécution du programme commence par la fonction [c main] et entre dans les autres fonctions à partir de là. Cette exécution par étapes entrant en progressivement en profondeur dans les fonctions et sortant d'elles peut être vue comme les branches d'un arbre.
Nous avons vu que l'exécution du programme commence par la fonction [c main] et entre dans les autres fonctions à partir de là. Cette exécution par étapes, entrant progressivement en profondeur dans les fonctions et en ressortant, peut être vue comme les branches d'un arbre.
Par exemple, [c main] peut appeler une fonction nommée [c faireOmelette], qui peut à son tour appeler une autre fonction nommée [c toutPreparer], qui peut à son tour appeler une autre fonction nommée [c preparerOeufs], etc. En supposant que les flèches indiquent des appels, l'arborescence d'un tel programme peut être vu comme dans l'arbre des appels de fonctions suivant :
Par exemple, [c main()] peut appeler une fonction nommée [c faireOmelette()], qui peut à son tour appeler une autre fonction nommée [c toutPreparer()], qui peut à son tour appeler une autre fonction nommée [c preparerOeufs()], etc. En supposant que les flèches indiquent des appels, l'arborescence d'un tel programme peut être représentée comme dans cet arbre d'appels de fonctions~ :
[code <<<
main
@ -116,8 +126,8 @@ Le programme termine avec une exception qui est levé par [c to!int] :
| +--▶ toutPreparer
| | |
| | +-▶ preparerOeufs
| | +-▶ PreparerBeurre
| | +-▶ préparerPoelle
| | +-▶ preparerBeurre
| | +-▶ preparerPoelle
| |
| +--▶ cuisinerOeufs
| +--▶ toutNettoyer
@ -125,90 +135,90 @@ Le programme termine avec une exception qui est levé par [c to!int] :
+--▶ mangerOmelette
>>>]
Le programme suivant montre l'arborescence suivante en utilisant différents niveaux d'indentation dans sa sortie. Le programme ne fait rien d'utile autre que produire une sortie qui correspond à nos besoins :
Le programme suivant montre son arborescence en utilisant différents niveaux d'indentation dans sa sortie. Le programme n'a d'autre utilité que de produire une sortie qui correspond à ce que nous voulons illustrer~ :
[code=d <<<
import std.stdio;
void indenter(in int niveau)
{
foreach (i; 0 .. niveau * 2) {
write(' ');
}
foreach (i; 0 .. niveau * 2) {
write(' ');
}
}
void entrant(in char[] nomFonction, in int niveau)
{
indenter(niveau);
writeln("▶ Première ligne de ", nomFonction);
indenter(niveau);
writeln("▶ Première ligne de ", nomFonction);
}
void sortant(in char[] nomFonction, in int niveau)
{
indenter(niveau);
writeln("◁ Dernière ligne de ", nomFonctionx);
indenter(niveau);
writeln("◁ Dernière ligne de ", nomFonctionx);
}
void main()
{
entrant("main", 0);
faireOmelette();
mangerOmelette();
sortant("main", 0);
entrant("main", 0);
faireOmelette();
mangerOmelette();
sortant("main", 0);
}
void faireOmelette()
{
entrant("faireOmelette", 1);
toutPreparer();
cuisinerOeufs();
toutNettoyer();
sortant("faireOmelette", 1);
entrant("faireOmelette", 1);
toutPreparer();
cuisinerOeufs();
toutNettoyer();
sortant("faireOmelette", 1);
}
void mangerOmelette()
{
entrant("mangerOmelette", 1);
sortant("mangerOmelette", 1);
entrant("mangerOmelette", 1);
sortant("mangerOmelette", 1);
}
void toutPreparer()
{
entrant("toutPreparer", 2);
preparerOeufs();
PreparerBeurre();
préparerPoelle();
sortant("toutPreparer", 2);
entrant("toutPreparer", 2);
preparerOeufs();
preparerBeurre();
preparerPoelle();
sortant("toutPreparer", 2);
}
void cuisinerOeufs()
{
entrant("cuisinerOeufs", 2);
sortant("cuisinerOeufs", 2);
entrant("cuisinerOeufs", 2);
sortant("cuisinerOeufs", 2);
}
void toutNettoyer()
{
entrant("toutNettoyer", 2);
sortant("toutNettoyer", 2);
entrant("toutNettoyer", 2);
sortant("toutNettoyer", 2);
}
void preparerOeufs()
{
entrant("preparerOeufs", 3);
sortant("preparerOeufs", 3);
entrant("preparerOeufs", 3);
sortant("preparerOeufs", 3);
}
void PreparerBeurre()
void preparerBeurre()
{
entrant("PreparerBeurre", 3);
sortant("PreparerBeurre", 3);
entrant("preparerBeurre", 3);
sortant("preparerBeurre", 3);
}
void préparerPoelle()
void preparerPoelle()
{
entrant("préparerPoelle", 3);
sortant("préparerPoelle", 3);
entrant("preparerPoelle", 3);
sortant("preparerPoelle", 3);
}
>>>]
@ -219,10 +229,10 @@ Le programme termine avec une exception qui est levé par [c to!int] :
▶ Première ligne de toutPreparer
▶ Première ligne de preparerOeufs
◁ Dernière ligne de preparerOeufs
▶ Première ligne de PreparerBeurre
◁ Dernière ligne de PreparerBeurre
▶ Première ligne de préparerPoelle
◁ Dernière ligne de préparerPoelle
▶ Première ligne de preparerBeurre
◁ Dernière ligne de preparerBeurre
▶ Première ligne de preparerPoelle
◁ Dernière ligne de preparerPoelle
◁ Dernière ligne de toutPreparer
▶ Première ligne de cuisinerOeufs
◁ Dernière ligne de cuisinerOeufs
@ -234,9 +244,9 @@ Le programme termine avec une exception qui est levé par [c to!int] :
◁ Dernière ligne de main
>>>]
Les fonctions [c entrant] et [c sortant] sont utilisées pour indiquer les première et dernière lignes des fonctions avec l'aide des caractères [c ▶] et [c ◁]. Le programme commence par la première ligne de [c main], entre dans les autres fonctions et finit par la dernière ligne de [c main].
Les fonctions [c entrant] et [c sortant] sont utilisées pour indiquer les première et dernière lignes des fonctions avec l'aide des caractères [c ▶] et [c ◁]. Le programme commence par la première ligne de [c main()], entre dans les autres fonctions et finit par la dernière ligne de [c main()].
Modifions la fonction [c preparerOeufs] pour prendre le nombre d'œufs en paramètres. Comme certaines valeurs de ce paramètre seraient des erreurs, faisons en sorte que cette fonction lève une exception quand le nombre d'œufs est inférieur à 1 :
Modifions la fonction [c preparerOeufs()] pour prendre le nombre d'œufs en paramètre. Comme certaines valeurs de ce paramètre seraient des erreurs, faisons en sorte que cette fonction lève une exception quand le nombre d'œufs est inférieur à 1~ :
[code=d <<<
void preparerOeufs(int nombre)
@ -245,196 +255,196 @@ Le programme termine avec une exception qui est levé par [c to!int] :
if (nombre < 1) {
throw new Exception(
format("Impossible de prendre %s eggs du réfrigérateur", nombre));
format("Impossible de prendre %s oeufs du réfrigérateur", nombre));
}
sortant("preparerOeufs", 3);
}
>>>]
Afin de pouvoir compiler le programme, nous devons modifier les autres lignes du programme pour être compatible avec cette modification. Le nombre d'œufs à prendre du réfrigérateur peut être passé de fonction en fonction, en commençant par [c main]. Les parties du programme qui doivent changer sont les suivantes. La valeur invalide de [c -8] est voulue, elle est là pour montrer ce qui change dans la sortie du programme par rapport à la sortie précédente, quand une exception est levée :
Afin de pouvoir compiler le programme, nous devons modifier d'autres lignes du programme pour les rendre compatibles avec cette modification. Le nombre d'œufs à prendre du réfrigérateur peut être passé de fonction en fonction, en commençant par [c main()]. Les parties du programme qui doivent changer sont les suivantes. La valeur invalide de [c -8] est voulue, elle est là pour montrer ce qui change dans la sortie du programme par rapport à la sortie précédente, quand une exception est levée~ :
[code=d <<<
// ...
void main()
{
entrant("main", 0);
faireOmelette($(HILITE -8));
mangerOmelette();
sortant("main", 0);
entrant("main", 0);
faireOmelette(-8);
mangerOmelette();
sortant("main", 0);
}
void faireOmelette($(HILITE int eggCount))
void faireOmelette(int nombreOeufs)
{
entrant("faireOmelette", 1);
toutPreparer($(HILITE eggCount));
cuisinerOeufs();
toutNettoyer();
sortant("faireOmelette", 1);
entrant("faireOmelette", 1);
toutPreparer(nombreOeufs);
cuisinerOeufs();
toutNettoyer();
sortant("faireOmelette", 1);
}
// ...
void toutPreparer($(HILITE int eggCount))
void toutPreparer(int nombreOeufs)
{
entrant("toutPreparer", 2);
preparerOeufs($(HILITE eggCount));
PreparerBeurre();
préparerPoelle();
sortant("toutPreparer", 2);
entrant("toutPreparer", 2);
preparerOeufs(nombreOeufs);
preparerBeurre();
preparerPoelle();
sortant("toutPreparer", 2);
}
// ...
>>>]
Maintenant, quand on démarre le programme, on voit qu'il manque les lignes qui étaient affichées après le point où l'exception est levée :
Maintenant, quand on démarre le programme, on voit qu'il manque les lignes qui étaient affichées après le point où l'exception est levée~ :
[output <<<
main, première ligne
faireOmelette, première ligne
toutPreparer, première ligne
▶ preparerOeufs, première ligne
object.Exception: Cannot take -8 eggs from the fridge
Première ligne de main
Première ligne de faireOmelette
▶ Première ligne de toutPreparer
Première ligne de preparerOeufs
object.Exception: Impossible de prendre -8 oeufs du réfrigérateur
>>>]
Quand l'exception est levée, l'exécution du programme sort des fonctions [c preparerOeufs], [c toutPreparer], [c faireOmelette] et [c main] dans cet ordre, du niveau le plus profond au niveau le moins profond. Aucune étape additionnelle n'est exécutée quand le programme sort de ces fonctions.
Quand l'exception est levée, l'exécution du programme sort des fonctions [c preparerOeufs()], [c toutPreparer()], [c faireOmelette()] et [c main()] dans cet ordre, du niveau le plus profond au niveau le moins profond. Aucune étape additionnelle n'est exécutée quand le programme sort de ces fonctions.
Un arrêt si brutal est justifié par le fait qu'on devrait considérer qu'un échec dans une fonction de plus bas niveau implique que les fonctions de plus hauts niveaux qui en ont eu besoin ont également échoué.
Un arrêt si brutal est justifié par le fait qu'on devrait considérer qu'un échec dans une fonction de plus bas niveau implique que les fonctions de plus hauts niveaux, nécessitant un succès de celle-ci, ont également échoué.
L'objet exception qui est levé d'un plus bas niveau de fonction est transféré aux fonctions de plus hauts niveaux niveau par niveau pour finalement entraîner la sortie du programme de la fonction [c main]. Le chemin que l'exception prend peut être vu comme le chemin rouge dans l'arbre suivant :
L'objet exception qui est levé depuis un niveau de fonction plus bas est transféré aux fonctions de niveaux supérieurs, niveau par niveau, pour finalement entraîner la sortie du programme de la fonction [c main()]. Le chemin que l'exception prend peut être vu comme le chemin rouge dans l'arbre suivant :
[img=exception_rouge.svg | chemin de l'exception dans l'arbre d'exécution]
Le but du mécanisme des exceptions est précisément d'avoir ce comportement : sortir de tous les appels de fonctions directment. Parfois, ''attraper'' l'exception levée pour trouver une manière de continuer l'exécution du programme fait sens. Nous allons introduire le mot-clé [c catch] ci-après.
Le but du mécanisme des exceptions est précisément d'avoir ce comportement~ : sortir de tous les appels de fonctions directment. Parfois, ''attraper'' l'exception levée pour trouver une manière de continuer l'exécution du programme fait sens. Nous allons bientôt introduire le mot-clé [c catch].
[ = Quand utiliser [c throw]
Utilisez [c throw] dans les situations où il n'est pas possible de continuer. Par exemple, une fonction qui lit un nombre d'étudiants depuis un fichier peut lever une exception si l'information n'est pas disposible ou si elle est incorrecte.
D'un autre côté, si le problème est causé par une action quelconque de l'utilisateur telle que la sasie d'une valeur invalide, il peut être plus judicieux de valider cette donnée au lieu de lever une exception. Afficher un message d'erreur et demander à l'utilisateur de saisir une nouvelle fois la donnée est plus appropriée dans la plupart des cas.
Utilisez [c throw] dans les situations où il n'est pas possible de continuer. Par exemple, une fonction qui lit un nombre d'étudiants depuis un fichier peut lever une exception si l'information n'est pas disponible ou si elle est incorrecte.
D'un autre côté, si le problème est causé par une action quelconque de l'utilisateur telle que la saisie d'une valeur invalide, il peut être plus judicieux de valider cette donnée au lieu de lever une exception. Afficher un message d'erreur et demander à l'utilisateur de saisir une nouvelle fois la donnée est plus approprié dans ce genre de cas.
]
[ = L'instruction [c try-catch] pour attraper une exception
]
Comme nous l'avons vu précédement, une exception levée entraîne la sortie par l'exécution du programme de toutes les fonctions et arrête le programme entier.
[ = L'instruction [c try-catch] pour attraper une exception
L'objet exception peut être attrapé avec une instruction [c try-catch] à n'importe quel endroit du chemin qu'elle emprunte pour sortir des fonctions. L'instruction [c try-catch] modélise la phrase « [* essaie] de faire quelque chose et [* attrape] les exceptions qui peuvent être levées. ». Voici la syntaxe de [c try-catch] :
Comme nous l'avons vu précédement, une exception levée entraîne la sortie de toutes les fonctions et, au final, l'arrêt du programme entier.
[code=d <<<
try {
// Le bloc de code qui est exécuté, où une
// exception peut être levée
L'objet exception peut être attrapé avec une instruction [c try-catch] à n'importe quel endroit du chemin qu'elle emprunte pour sortir des fonctions. L'instruction [c try-catch] modélise la phrase «~ [* essaie] de faire quelque chose et [* attrape] les exceptions qui peuvent être levées.~ ». Voici la syntaxe de [c try-catch]~ :
} catch (un_type_d_exception)) {
// Expressions à exécuter si une exception de ce
// type est attrapée
[code=d <<<
try {
// Le bloc de code qui est exécuté, où une
// exception peut être levée
} catch (un_autre_type_d_exception)) {
// Expressions à exécuter si une exception de cet
// autre type est attrapée
} catch (un_type_d_exception)) {
// Expressions à exécuter si une exception de ce
// type est attrapée
// ... d'autres blocs catch peuvent être placés ici ...
} catch (un_autre_type_d_exception)) {
// Expressions à exécuter si une exception de cet
// autre type est attrapée
} finally {
// Expressions à exécuter indépendament du type
// d'exception qui est levé
}
>>>]
// ... d'autres blocs catch peuvent être placés ici ...
Commençons par le programme suivant qui n'utilise pas d'instruction [c try-catch]. Le programme lit une valeur d'un dé depuis un fichier et l'affiche
dans l'entrée standard :
[code=d <<<
import std.stdio;
int lireDeDepuisFichier()
{
auto fichier = File("le_fichier_qui_contient_la_valeur", "r");
int de;
fichier.readf(" %s", &de);
return de;
}
void main()
{
const int de = lireDeDepuisFichier();
writeln("Valeur du dé : ", de);
}
>>>]
Notez que la fonction [c lireDeDepuisFichier] est écrite de manière à ignorer les conditions d'erreurs, s'attendant à ce que le fichier et la valeur qu'il contient sont là. En d'autres termes, la fonction fait ce qu'elle doit faire sans s'occuper des conditions d'erreurs. C'est un des avantages des exceptions : beaucoup de fonctions peuvent être écrites en se concentrant sur leur tâche, plutôt que de se concentrer sur les conditions d'erreurs.
Lançons le programme alors que le fichier [c le_fichier_qui_contient_la_valeur] manque :
[output <<<
std.exception.ErrnoException@std/stdio.d(286): Cannot open
fichier `le_fichier_qui_contient_la_valeur' in mode `r' (No such
fichier or directory)
} finally {
// Expressions à exécuter indépendamment du fait
// qu'une exception soit levée ou pas
}
>>>]
Une exception du type [c ErrnoException] est levée et le programme se termine sans afficher « Valeur du  : ».
Ajoutons une fonction intermédiaire au programme qui appelle [c lireDeDepuisFichier] depuis un bloc [c try] et [c main] appelera cette nouvelle fonction :
Commençons par le programme suivant qui n'utilise pas d'instruction [c try-catch]. Le programme lit une valeur d'un dé depuis un fichier et l'affiche sur la sortie standard~ :
[code=d <<<
import std.stdio;
int lireDeDepuisFichier()
{
auto fichier = File("le_fichier_qui_contient_la_valeur", "r");
auto fichier = File("le_fichier_qui_contient_la_valeur", "r");
int de;
fichier.readf(" %s", &de);
int de;
fichier.readf(" %s", &de);
return de;
}
int essayerDeLireDepuisLeFichier()
{
int de;
try {
de = lireDeDepuisFichier();
} catch (std.exception.ErrnoException exc) {
writeln("Impossible de lire le fichier; on suppose 1");
de = 1;
}
return de;
return de;
}
void main()
{
const int de = essayerDeLireDepuisLeFichier();
const int de = lireDeDepuisFichier();
writeln("Valeur du dé : ", de);
writeln("Valeur du dé : ", de);
}
>>>]
Quand on lance le programme alors que [c le_fichier_qui_contient_la_valeur] n'existe pas, le programme ne termine cette fois pas par une exception :
Notez que la fonction [c lireDeDepuisFichier()] est écrite de manière à ignorer les conditions d'erreurs, s'attendant à ce que le fichier et la valeur qu'il contient soient là. En d'autres termes, la fonction fait ce qu'elle doit faire sans s'occuper des conditions d'erreurs. C'est un des avantages des exceptions~ : beaucoup de fonctions peuvent être écrites en se concentrant sur leur tâche, plutôt qu'en se concentrant sur les conditions d'erreurs.
Lançons le programme alors que le fichier [c le_fichier_qui_contient_la_valeur] manque~ :
[output <<<
std.exception.ErrnoException@std/stdio.d(286): Cannot open
file `le_fichier_qui_contient_la_valeur' in mode `r' (No such
file or directory)
>>>]
Une exception du type [c ErrnoException] est levée et le programme se termine sans afficher «~ Valeur du  :~ ».
Ajoutons une fonction intermédiaire au programme qui appelle [c lireDeDepuisFichier()] depuis un bloc [c try] et [c main()] appelera cette nouvelle fonction~ :
[code=d <<<
import std.stdio;
int lireDeDepuisFichier()
{
auto fichier = File("le_fichier_qui_contient_la_valeur", "r");
int de;
fichier.readf(" %s", &de);
return de;
}
int essayerDeLireDepuisLeFichier()
{
int de;
try {
de = lireDeDepuisFichier();
} catch (std.exception.ErrnoException exc) {
writeln("Impossible de lire le fichier ; on suppose 1");
de = 1;
}
return de;
}
void main()
{
const int de = essayerDeLireDepuisLeFichier();
writeln("Valeur du dé : ", de);
}
>>>]
Quand on lance le programme alors que [c le_fichier_qui_contient_la_valeur] n'existe pas, le programme ne termine cette fois pas par une exception~ :
[output <<<
Impossible de lire depuis le fichier ; on suppose 1
Valeur du dé : 1
>>>]
Le nouveau programme essaie d'exécuter [c lireDeDepuisFichier] dans un bloc [c try]. Si ce bloc s'exécute correctement, la fonction se termine normalement avec l'instruction [c return de;]. Si l'exécution du bloc [c try] finit avec l'exception [c std.exception.ErrnoException], alors le programme entre dans le bloc [c catch].
Le nouveau programme essaie d'exécuter [c lireDeDepuisFichier()] dans un bloc [c try]. Si ce bloc s'exécute correctement, la fonction se termine normalement avec l'instruction [c return de;]. Si l'exécution du bloc [c try] finit avec l'exception [c std.exception.ErrnoException], alors le programme entre dans le bloc [c catch].
Voici un récapitulatif de ce qu'il se passe lorsque le programme est démarré alors que le fichier n'existe pas :
Voici un récapitulatif de ce qu'il se passe lorsque le programme est démarré alors que le fichier n'existe pas~ :
- Comme dans le programme précédent, une exception [c std.exception.ErrnoException] est levée (par [c File()], pas par notre code),
- Cette exception est attrapée par [c catch],
- La valeur de [c 1] est supposée pendant l'exécution normale du bloc [c catch],
- et le programme continue normalement
- comme dans le programme précédent, une exception [c std.exception.ErrnoException] est levée (par [c File()], pas par notre code)~ ;
- cette exception est attrapée par [c catch]~ ;
- la valeur de [c 1] est supposée pendant l'exécution normale du bloc [c catch]~ ;
- et le programme continue normalement.
[c catch] est là pour attraper les exceptions levées afin de tenter de trouver comment continuer l'exécution du programme.
[c catch] est là pour attraper les exceptions levées afin de tenter de continuer l'exécution du programme.
En guise d'exemple supplémentaire, revenons au programme des omelettes et ajoutons une instruction [c try-catch] à sa fonction [c main] :
En guise d'exemple supplémentaire, revenons au programme des omelettes et ajoutons une instruction [c try-catch] à sa fonction [c main()]~ :
[code=d <<<
void main()
@ -448,14 +458,14 @@ Le programme termine avec une exception qui est levé par [c to!int] :
} catch (Exception exc) {
write("Impossible de manger l'omelette : ");
writeln('"', exc.msg, '"');
writeln("' Devriez manger chez le voisin...");
writeln("Je mangerai chez le voisin...");
}
sortant("main", 0);
}
>>>]
Ce bloc [c try] contient deux lignes de codes. Toute exception levée depuis une de ces deux lignes serait attrapée par le bloc [c catch].
Ce bloc [c try] contient deux lignes de codes. Toute exception levée depuis une de ces deux lignes sera attrapée par le bloc [c catch].
[output <<<
▶ main, première ligne
@ -463,21 +473,22 @@ Le programme termine avec une exception qui est levé par [c to!int] :
▶ toutPreparer, première ligne
▶ preparerOeufs, première ligne
Impossible de manger l'omelette : "Impossible de prendre -8 oeufs du réfrigérateur"
'Devriez manger chez le voisin...
Je mangerai chez le voisin...
◁ main, dernière ligne
>>>]
Comme nous pouvons le voir dans la sortie, le programme ne termine plus à cause de l'exception. Il rattrape la condition d'erreur et continue à s'exécuter normalement jusqu'à la fin de la fonction [c main].
Comme nous pouvons le voir dans la sortie, le programme ne termine plus à cause de l'exception. Il se remet de son état d'erreur et continue à s'exécuter normalement jusqu'à la fin de la fonction [c main()].
]
[ = Les blocs [c catch] sont parcourus séquentiellement
Le type [c Exception], que nous avons utilisé jusqu'à maintenant dans les exemples, est un type générique d'exception. Ce type indique simplement qu'une erreur s'est produite dans le programme. Il contient aussi un message qui peut expliquer l'erreur plus en détail, mais il ne contient pas d'information sur le [* type] de l'erreur.
[c ConvException] et [c ErrnoException], que nous avons rencontrés plutôt dans le chapitre, sont des types d'exceptions plus spécifiques : celui-là concerne une erreur de conversion et celui-ci une erreur système. Comme beaucoup d'autres types d'exceptions dans Phobos et comme leurs noms respectifs le suggèrent, [c ConvException] et [c ErrnoException] héritent tous deux de la classe [c Exception].
[c ConvException] et [c ErrnoException], que nous avons rencontrés plutôt dans le chapitre, sont des types d'exceptions plus spécifiques~ : le premier concerne une erreur de conversion et le second une erreur système. Comme beaucoup d'autres types d'exceptions dans Phobos et comme leurs noms respectifs le suggèrent, [c ConvException] et [c ErrnoException] héritent tous deux de la classe [c Exception].
[c Exception] et sa sœur [c Error] sont eux-même hérités de [c Throwable], le type d'exception le plus général.
Même si c'est possible, il n'est pas recommandé d'attraper des objets de type [c Error] et des types qui en héritent. Comme [c Throwable] est plus général qu'[Error], il n'est pas non plus recommandé d'attraper [c Throwable] non plus. Les seuls objets qui devraient normalement être attrapés sont ceux qui font partie de la hiérarchie [c Exception].
Même si c'est possible, il n'est pas recommandé d'attraper des objets de type [c Error] ou de types qui en héritent. Comme [c Throwable] est plus général qu'[Error], il n'est pas non plus recommandé d'attraper [c Throwable] non plus. Les seuls objets qui devraient normalement être attrapés sont ceux qui font partie de la hiérarchie [c Exception].
[pre <<<
Throwable (attraper n'est pas recommandé)
@ -487,17 +498,17 @@ Le programme termine avec une exception qui est levé par [c to!int] :
... ... ... ...
>>>]
[p Note : | on verra la représentation hiérarchique plus tard dans le [[part:heritage | chapitre sur l'héritage]. L'arbre ci-avant indiquent que [c Throwable] est la plus générale et qu'[c Exception] et [c Error] sont les spécifiques.
[p Note~ : | on verra la représentation hiérarchique plus tard dans le [[part:heritage | chapitre sur l'héritage]]. L'arbre précédent indique que [c Throwable] est la plus générale et qu'[c Exception] et [c Error] sont plus spécifiques.]
Il est possible d'attraper les objets d'un type particulier. Par exemple, il est possible d'attraper de manière spécifique un objet [c ErrnoException] pour gérer une erreur système.
Les exceptions sont attrapées seulement si elle correspondent au type qui sont spécifiés dans un bloc [c catch]. Par exemple, un bloc [c catch] qui essaie d'attraper une exception [c SpecialExceptionType] n'attraperait pas un [c ErrnoException].
Les exceptions sont attrapées seulement si elle correspondent au type qui est spécifié dans un bloc [c catch]. Par exemple, un bloc [c catch] qui essaie d'attraper une exception [c SpecialExceptionType] n'attrapera pas un [c ErrnoException].
Le type de l'objet exception qui est levée pendant l'exécution d'un bloc [c try] est comparé aux types qui sont spécifiés par les blocs [c catch], dans l'ordre dans lequel les blocs [c caught] sont écrits. Si le type de l'objet correspond au type d'un bloc [c catch], alors l'exception est considérée comme attrapée par ce bloc [c catch] et le code qui est à l'intérieur de ce bloc est exécuté. Une fois qu'une correspondance a été trouvé, les blocs [c catch] restant sont ignorés.
Le type de l'objet exception qui est levée pendant l'exécution d'un bloc [c try] est comparé aux types qui sont spécifiés par les blocs [c catch], dans l'ordre dans lequel les blocs [c catch] sont écrits. Si le type de l'objet correspond au type d'un bloc [c catch], alors l'exception est considérée comme attrapée par ce bloc [c catch] et le code qui est à l'intérieur de ce bloc est exécuté. Une fois qu'une correspondance a été trouvée, les blocs [c catch] restant sont ignorés.
Du fait que les blocs [c catch] sont testés dans l'ordre, les blocs [c catch] doivent être ordonnées de l'exception du type le plus spécifique au plus général. De même, si nécessaire, le type [c Exception] doit être indiqué au dernier bloc [c catch].
Par exemple, une instruction [c try-catch] qui essaie d'attraper plusieurs types spécifiques d'exceptions à propos d'une liste d'étudiants peut ordonner les blocs [c catch] du plus spécifique au plus général comme dans le code qui suit :
Par exemple, une instruction [c try-catch] qui essaie d'attraper plusieurs types spécifiques d'exceptions à propos d'une liste d'étudiants peut ordonner les blocs [c catch] du plus spécifique au plus général comme dans le code qui suit~ :
[code=d <<<
try {
@ -526,10 +537,11 @@ Le programme termine avec une exception qui est levé par [c to!int] :
}
>>>]
]
[ = Le bloc [c finally]
[c finally] est un bloc optionnel de l'instruction [c try-catch]. Il contient des expressions qui doivent être exécutées, qu'une exception ait été levée ou pas.
Pour voir comment [c finally] fonctionne, voyons ce qu'un programme qui lève une exception 50% du temps :
Pour voir comment [c finally] fonctionne, examinons un programme qui lève une exception 50% du temps~ :
[code=d <<<
import std.stdio;
@ -537,27 +549,27 @@ Le programme termine avec une exception qui est levé par [c to!int] :
void leverExceptionLaMoitieDuTemps()
{
if (uniform(0, 2) == 1) {
throw new Exception("le message d'erreur");
}
if (uniform(0, 2) == 1) {
throw new Exception("le message d'erreur");
}
}
void foo()
{
writeln("La première ligne de foo()");
writeln("La première ligne de foo()");
try {
writeln("La première ligne du bloc try");
leverExceptionLaMoitieDuTemps();
writeln("La dernière ligne du bloc try");
try {
writeln("La première ligne du bloc try");
leverExceptionLaMoitieDuTemps();
writeln("La dernière ligne du bloc try");
// ... il peut y avoir un ou des blocs catch ici ...
} finally {
writeln("Le corps du bloc finally");
}
} finally {
writeln("Le corps du bloc finally");
}
writeln("la dernière ligne de foo()");
writeln("La dernière ligne de foo()");
}
void main()
@ -566,27 +578,28 @@ Le programme termine avec une exception qui est levé par [c to!int] :
}
>>>]
La sortie du programme est la suivante quand la fonction ne lève pas une exception :
La sortie du programme est la suivante quand la fonction ne lève pas une exception~ :
[output <<<
La première ligne de foo()
La première ligne du bloc try
La dernière ligne du bloc try
Le corps du bloc finally
la dernière ligne de foo()
La dernière ligne de foo()
>>>]
La sortie du programme est la suivante quand la fonction lève une exception :
La sortie du programme est la suivante quand la fonction lève une exception~ :
[output <<<
La première ligne de foo()
La première ligne du bloc try
Le corps du bloc finally
object.Exception@deneme.d: le message d'erreur
object.Exception@essai.d: le message d'erreur
>>>]
Comme on peut le constater, même si "La dernière ligne du bloc try" et "la dernière ligne de foo()" ne sont pas affichées, le contenu du bloc [c finally] est toujours exécuté quand une exception est levée.
Comme on peut le constater, quand une exception est levée, bien que «~ La dernière ligne du bloc try~ » et «~ La dernière ligne de foo()~ » ne soient pas affichées, le contenu du bloc [c finally] est quant à lui toujours exécuté.
]
[ = Quand utiliser l'instruction [c try-catch]
L'instruction [c try-catch] est utile pour attraper des exceptions pour faire quelque chose de spécial avec.
@ -594,22 +607,102 @@ Le programme termine avec une exception qui est levé par [c to!int] :
Pour cette raison, l'instruction [c try-catch] ne devrait être utilisée que quand il y a quelque chose de spécial à faire. Sinon, n'attrapez pas d'exceptions et laissez-les aux fonctions des niveaux supérieurs qui pourraient vouloir les attraper.
]
]
[ = Propriétés des exceptions
L'information qui est automatiquement affichée dans la sortie quand le programme se termine à cause d'une exception est aussi accessible par les propriétés des objets [c Exception]. Ces propriétés sont fournies par l'interface [c Throwable]~ :
- [c .file] : Le fichier source à partir duquel l'exception a été levée
- [c .line] : Le numéro de la ligne à partir de laquelle l'exception a été levée
- [c .msg] : Le message d'erreur
- [c .info] : L'état de la pile du programme quand l'exception a été levée
- [c .next] : L'exception collatérale suivante
Nous avons vu que les blocs [c finally] sont exécutés quand on sort des blocs de code, y compris à cause des exceptions (comme nous le verrons dans des chapitres ultérieurs, cela est aussi vrai pour les instructions [c scope] et les destructeurs).
Naturellement, de tels blocs de code peuvent aussi lever des exceptions. Les exceptions qui sont levées quand on quitte des blocs de code à cause d'une exception sont appelées [* exceptions collatérales]. L'exception principale et les exceptions collatérales sont des éléments d'une structure de type liste chaînée, dans laquelle toutes les exceptions sont accessibles à travers la propriété [c .next] de l'exception précédente. La valeur de la propriété [c .next] de la dernière exception est [c null] (nous verrons [c null] dans un chapitre ultérieur).
Il y a trois exceptions qui sont levées dans l'exemple suivant~ : L'exception principale qui est levée dans [c foo()] et les deux exceptions collatérales qui sont levées dans les blocs [c finally] de [c foo()] et [c bar()]. Le programme accède aux exceptions collatérales à travers les propriétés~ [c .next].
Certaines concepts qui sont utilisées dans ce programme seront expliqués dans des chapitres ultérieurs. Par exemple, la condition de continuation de la boucle [c for] , [c exc], signifie que [c exc] n'est pas [c null].
[code=d <<<
import std.stdio;
void foo()
{
try {
throw new Exception("Exception levée dans foo");
} finally {
throw new Exception(
"Exception levée dans le bloc finally de foo");
}
}
void bar()
{
try {
foo();
} finally {
throw new Exception(
"Exception levée dans le block finally de bar");
}
}
void main()
{
try {
bar();
} catch (Exception exceptionLevee) {
for (Throwable exc = exceptionLevee;
exc; // ← ce qui veut dire: dans que exc n'est pas 'null'
exc = exc.next) {
writefln("message d'erreur : %s", exc.msg);
writefln("fichier source : %s", exc.file);
writefln("ligne source : %s", exc.line);
writeln();
}
}
}
>>>]
La sortie~ :
[output <<<
message d'erreur : Exception levée in foo
fichier source : deneme.d
ligne source : 6
message d'erreur : Exception levée dans le bloc finally de foo
fichier source : deneme.d
ligne source : 9
message d'erreur : Exception levée dans le bloc finally de bar
fichier source : deneme.d
ligne source : 20
>>>]
[ = Types d'erreurs
Nous avons vu comment le mécanisme des exceptions peut être utile. Il permet aux opérations de bas niveaux comme aux opérations de hauts niveaux d'être interrompues directement, au lieu de laisser le programme continuer avec des données incorrectes ou manquantes ou d'agir de n'importe quelle autre mauvaise manière.
Nous avons vu comment le mécanisme des exceptions peut être utile. Il permet aux opérations de bas niveaux comme aux opérations de hauts niveaux d'être interrompues directement, au lieu de laisser le programme continuer avec des données incorrectes ou manquantes ou agir de n'importe quelle autre mauvaise manière.
Ceci ne veut pas dire que chaque condition d'erreur justifie une levée d'exception. On peut parfois faire mieux selon les types d'erreurs.
Ceci ne veut pas dire que chaque état erroné justifie une levée d'exception. On peut parfois faire mieux selon les types d'erreurs.
[ = Erreurs de l'utilisateur
Certaines erreurs sont causées par l'utilisateur. Comme nous l'avons vu ci-avant, l'utilisateur peut avoir saisi une chaîne comme "hello" alors que le programme attendait un nombre. Il peut être plus approprié pour afficher un message d'erreur et demander à l'utilisateur de saisir la donnée une nouvelle fois.
Certaines erreurs sont causées par l'utilisateur. Comme nous l'avons vu précédemment, l'utilisateur peut avoir saisi une chaîne comme «~ bonjour~ » alors que le programme attendait un nombre. Il peut être plus approprié d'afficher un message d'erreur et demander à l'utilisateur de saisir la donnée une nouvelle fois.
Cela dit, il peut être correct d'accepter et d'utiliser l'information directement sans la valider à l'avance, tant que le code qui utilise la donnée lèverait l'exception quand même. L'important est de pouvoir prévenir l'utilisateur que la donnée n'est pas appropriée.
Cela dit, il peut être correct d'accepter et d'utiliser l'information directement sans la valider à l'avance, tant que le code qui utilise la donnée lève l'exception quand même. L'important est de pouvoir prévenir l'utilisateur que la donnée n'est pas appropriée.
Par exemple, considérons un programme qui demande un nom de fichier à l'utilisateur. Il y a au moins deux manières de gérer des noms de fichiers potentiellement invalides :
Par exemple, considérons un programme qui demande un nom de fichier à l'utilisateur. Il y a au moins deux manières de gérer des noms de fichiers potentiellement invalides~ :
- [
Valider l'information avant l'utilisation : on peut déterminer si le fichier avec le nom donné existe en appelant la fonction [c exists] du module [c std.file] :
Valider l'information avant l'utilisation~ : on peut déterminer si le fichier avec le nom donné existe en appelant la fonction [c exists()] du module [c std.file]~ :
[code=d <<<
if (exists(nomFichier)) {
@ -620,52 +713,52 @@ Le programme termine avec une exception qui est levé par [c to!int] :
}
>>>]
Cela donne la possibilité de n'ouvrir le fichier que s'il existe. Malheureusement, il est encore possible que le fichier ne puisse pas être ouvert même si [c exists()] retourne [c true], si par exemple un autre processus du système supprime ou renomme le fichier avant que notre programme l'ouvre.
Cela donne la possibilité de n'ouvrir le fichier que s'il existe. Malheureusement, il est encore possible que le fichier ne puisse pas être ouvert même si [c exists()] retourne [c true], si par exemple un autre processus du système supprime ou renomme le fichier avant que notre programme l'ouvre.
Pour cette raison, la méthode suivante peut être plus utile.
]
- [
Utiliser les données avant de les valider : on peut supposer les données valides et les utiliser directement, parce que [c File] lèvera de toute façon une exception si le fichier ne peut pas être ouvert.
Utiliser les données avant de les valider~ : on peut supposer les données valides et les utiliser directement, parce que [c File] lèvera de toute façon une exception si le fichier ne peut pas être ouvert.
[code=d <<<
import std.stdio;
import std.string;
void useTheFile(string nomFichier)
void utiliserLeFichier(string nomFichier)
{
auto fichier = File(nomFichier, "r");
// ...
}
string read_string(in char[] prompt)
string lireChaîne(in char[] invite)
{
write(prompt, ": ");
write(invite, ": ");
return chomp(readln());
}
void main()
{
bool is_fichierUsed = false;
bool fichierUtilisé = false;
while (!is_fichierUsed) {
while (!fichierUtilisé) {
try {
useTheFile(
read_string("Please enter a fichier name"));
utiliserLeFichier(
lireChaîne("Entrez un nom de fichier"));
/*
* If we are at this line, it means that
* useTheFile() function has been completed
* successfully. This indicates that the fichier
* name has been valid.
* Si noux arrivons ici, c'est que la fonction
* utiliserLeFichier() s'est déroulée avec succès,
* ce qui indique que le nom de fichier était
* valide.
*
* We can now set the value of the loop flag to
* terminate the while loop.
* Nous pouvons maintenant positionner le drapeau
* de boucle afin de quitter la boucle while.
*/
is_fichierUsed = true;
writeln("The fichier has been used successfully");
fichierUtilisé = true;
writeln("Le fichier a bien été utilisé");
} catch (std.exception.ErrnoException exc) {
stderr.writeln("This fichier could not be opened");
stderr.writeln("Ce fichier n'a pas pu être ouvert");
}
}
}
@ -674,11 +767,11 @@ Le programme termine avec une exception qui est levé par [c to!int] :
[ = Les erreurs du programmeur
Certaines erreurs sont causées par des erreurs du programmeur. Par exemple, le programmeur peut penser qu'une fonctino qui vient d'être écrite sera toujours appelée avec une valeur plus supérieure ou égale à zéro, et cela peut être vrai selon la conception du programme. Appeler la fonction avec une valeur inférieure ou égale à zéro serait une erreur dans la conception du programme ou dans l'implémentation de cette conception. Ces deux cas peuvent être considérés comme des erreurs de programmations.
Certaines erreurs sont causées par des erreurs du programmeur. Par exemple, le programmeur peut penser qu'une fonction qui vient d'être écrite sera toujours appelée avec une valeur supérieure ou égale à zéro, et cela peut être vrai d'après la conception du programme. Appeler la fonction avec une valeur inférieure ou égale à zéro serait une erreur dans la conception du programme ou dans l'implémentation de cette conception. Ces deux cas peuvent être considérés comme des erreurs de programmation.
Il est plus judicieux d'utiliser [c assert] au lieu du mécanisme des exceptions pour des erreurs qui sont causées par des erreurs du programeur.
[p Note : | Nous verrons [c assert] dans un chapitre ultérieur.]
[p Note~ : | nous verrons [c assert] dans un chapitre ultérieur.]
[code=d <<<
void gérerSelectionMenu(int selection)
@ -696,32 +789,32 @@ Le programme termine avec une exception qui est levé par [c to!int] :
Le programme se termine par une erreur d'assertion :
[output <<<
core.exception.AssertError@deneme.d(3): Assertion failure
core.exception.AssertError@essai.d(3): Assertion failure
>>>]
[c assert] vérifie l'état du programme et affiche le nom du fichier et le numéro de la ligne de la vérification si elle échoue. Le message ci-avant indique que l'assertion à la ligne 3 de [c deneme.d] a échoué.
[c assert] vérifie l'état du programme et affiche le nom du fichier et le numéro de la ligne de la vérification si elle échoue. Le message précédent indique que l'assertion à la ligne 3 de [c essai.d] a échoué.
]
[ = Situations inattendues
Pour les situations inattendue qui sont hors des deux cas généraux ci-avant, il est quand même approprié de lever des exceptions. Si le programme ne peut pas continuer son exécution, il n'y a rien d'autre à faire que lever une exception.
Pour les situations inattendues qui sont hors des deux cas généraux que l'on vient de voir, il est quand même justifié de lever des exceptions. Si le programme ne peut pas continuer son exécution, il n'y a rien d'autre à faire que lever une exception.
Il appartient aux fonctions de plus hauts niveaux qui appelle cette fonction de décider quoi faire avec les exceptions levées. Elle peuvent attraper les exceptions que nous levons pour rectifier le tir.
Il appartient aux fonctions de plus hauts niveaux qui appellent cette fonction de décider que faire avec les exceptions levées. Elle peuvent attraper les exceptions que nous levons pour rectifier le tir.
]
]
[ = Résumé
- Lorsque que vous êtes confronté-e à une erreur de l'utilisateur, avertissez l'utilisateur directement ou assurez-vous qu'une exception est levée ; l'exception peut être levée par une autre fonction lors de l'utilisation d'une donnée incorrecte, ou directement par vous.
- Lorsque que vous êtes confronté à une erreur de l'utilisateur, avertissez l'utilisateur directement ou assurez-vous qu'une exception soit levée~ ; l'exception peut être levée par une autre fonction lors de l'utilisation d'une donnée incorrecte, ou directement par vous.
- Utilisez [c assert] pour vérifier la logique ou l'implémentation du programme (nous verrons [c assert] dans un chapitre ultérieur).
[comment Use $(C enforce) to validate programmers' use of your functions.]
- Utilisez [c enforce] pour valider la bonne utilisation de vos fonctions par les programmeurs.
- En cas de doute, levez une exception.
- Attrapez les exceptions si et seulement si vous pouvez faire quelque chose d'utile avec ces exceptions. Sinon, n'encapsulez pas de code avec une instruction [c try-catch]. Laissez plutôt la gestion de ces exceptions aux couches de code de plus hauts niveaux qui peuvent en faire quelque chose.
- Attrapez les exceptions si et seulement si vous pouvez faire quelque chose d'utile avec ces exceptions. Sinon, n'encapsulez pas de code avec une instruction [c try-catch]. Laissez plutôt la gestion de ces exceptions aux couches de code de plus haut niveau qui peuvent en faire quelque chose.
- Ordonnez les blocs [c catch] du plus spécifique au plus général.

View File

@ -1,7 +1,6 @@
[set
title = "Nombre variable de paramètres"
partAs = chapitre
translator = "Raphaël Jakse"
translator = "Olivier Pisano"
]

View File

@ -4,38 +4,38 @@
translator = "Raphaël Jakse"
]
Alors que les types fondamentaux sont les briques élémentaires des données d'un programme, les fonctions sont les briques élémentairess du comportement de ce programme.
Alors que les types fondamentaux sont les briques élémentaires des données d'un programme, les fonctions sont les briques élémentaires du comportement de ce programme.
Les fonctions sont également très lié à l'expérience du programmeur. Les fonctions qui sont écrites par des programmeurs expérimentés sont succinctes, simples et claires. Cela est vrai dans les deux sens : le simple fait d'essayer d'identifier et d'écrire des briques élémentairess plus petites d'un programme rend un programmeur meilleur.
Les fonctions sont également très liées au savoir-faire du programmeur. Les fonctions qui sont écrites par des programmeurs expérimentés sont succinctes, simples et claires. Cela est vrai dans les deux sens~ : le simple fait d'essayer d'identifier et d'écrire des briques élémentaires plus petites rend un programmeur meilleur.
Nous avons vu des expressions et instructions basiques dans les chapitres précédents. Même s'il y en aura encore beaucoup dans les chapitres ultérieurs, ce que nous avons vu jusqu'à maintenant sont des fonctionnalités couramment utilisées du D. Cependant, elle ne sont pas suffisantes d'elles-même pour écrire des grands programmes. Les programmes que nous avons écrits jusqu'à maintenant sont très petits, chacun montrant une seule fonctionnalité simple du langage. Essayer d'écrire un programme à n'importe quel niveau de complexité sans fonction serait très difficile et serait exposé aux bogues.
Nous avons vu des expressions et instructions basiques dans les chapitres précédents. Même s'il y en aura encore beaucoup dans les chapitres à venir, celles que nous avons vues jusqu'à maintenant sont des fonctionnalités couramment utilisées du D. Cependant, à elles seules, elles ne suffisent pas à écrire de grands programmes. Les programmes que nous avons écrits jusqu'à maintenant sont très petits, chacun montrant une seule fonctionnalité simple du langage. Essayer d'écrire un programme un tant soit peu complexe sans fonction serait très difficile et propice aux bogues.
Les fonctions regroupent des instructions et des expressions pour former des unités d'exécution de programme. On donne un nom à ce regroupement d'instructions et d'expressions qui décrit la tâche que celles-ci accomplissent. Elle peuvent être [* appelées] (ou [* exécutées]) par ce nom.
L'idée de donner des noms à un groupe d'étapes est courant dans la vie de tous les jours. Par exemple, l'action de cuisiner une omelette peut être décrite plus en détail par les étapes suivantes :
L'idée de donner des noms à un groupe d'étapes est courant dans la vie de tous les jours. Par exemple, l'action de cuisiner une omelette peut être décrite plus en détail par les étapes suivantes~ :
- prendre du pain
- prendre du beurre
- prendre un œuf
- allumer la cuisinière
- mettre le pain sur le feu
- mettre le beurre dans le pain quand celui-ci est chaud
- mettre l'œuf dans le beurre quand celui-ci est fondu
- enlever le pain du feu quand l'œuf est cuit
- éteindre la cuisinière
- prendre une poêle~ ;
- prendre du beurre~ ;
- prendre un œuf~ ;
- allumer la cuisinière~ ;
- mettre la poêle sur le feu~ ;
- mettre le beurre dans la poêle quand celui-ci est chaud~ ;
- mettre l'œuf dans le beurre quand celui-ci est fondu~ ;
- enlever la poêle du feu quand l'œuf est cuit~ ;
- éteindre la cuisinière.
Comme une telle précision est évidemment excessive, les étapes qui sont liées entre elles peuvent être regroupées sous un nom unique :
Comme une telle précision est évidemment excessive, les étapes qui sont liées entre elles peuvent être regroupées sous un nom unique~ :
- faire les préparations (prendre le pain, le beurre et l'œuf)
- allumer la cuisinière
- cuire l'œuf (mettre le pain sur le feu, etc.)
- éteindre la cuisinière
- faire les préparations (prendre la poêle, le beurre et l'œuf)~ ;
- allumer la cuisinière~ ;
- cuire l'œuf (mettre la poêle sur le feu, etc.)~ ;
- éteindre la cuisinière.
En allant plus loin, il peut y avoir un seul nom pour toutes les étapes :
En allant plus loin, il peut y avoir un seul nom pour toutes les étapes~ :
- faire une omelette avec un œuf (toutes les étapes)
- faire une omelette avec un œuf (toutes les étapes).
Les fonctions sont basées sur la même idée : les étapes qui peuvent être regroupées sous un même nom sont mises ensemble pour former une fonction. Par exemple, commençons avec les lignes de code suivantes qui affichent un menu :
Les fonctions sont basées sur la même idée~ : les étapes qui peuvent être regroupées sous un même nom sont mises ensemble pour former une fonction. Par exemple, commençons avec les lignes de code suivantes qui affichent un menu~ :
[code=d <<<
writeln(" 0 Quitter");
@ -45,7 +45,7 @@ Les fonctions sont basées sur la même idée : les étapes qui peuvent être r
writeln(" 4 Diviser");
>>>]
Comme rassembler ces lignes sous le nom d'[c afficherMenu] aurait un sens, on peut les mettre ensemble pour former une fonction avec la syntaxe suivante :
Comme rassembler ces lignes sous le nom d'[c afficherMenu] aurait un sens, on peut les mettre ensemble pour former une fonction avec la syntaxe suivante~ :
[code=d <<<
void afficherMenu()
@ -75,24 +75,24 @@ On peut noter, en observant les similarités entre les définitions de [c affich
Une des forces des fonctions vient du fait que l'on peut ajuster leur comportement à travers des paramètres.
Continuons avec l'exemple de l'omelette en le modifiant pour faire une omelette de cinq œufs au lieu d'un. Les étapes sont exactement les mêmes, la seule différence étant le nombre d'œufs à utiliser. On peut changer la description ci-avant en conséquence :
Continuons avec l'exemple de l'omelette en le modifiant pour faire une omelette de cinq œufs au lieu d'un. Les étapes sont exactement les mêmes, la seule différence étant le nombre d'œufs à utiliser. On peut changer la description précédente en conséquence~ :
- faire les préparations (prendre le pain, le beurre et l'œuf)
- allumer la cuisinière
- cuire les œufs (mettre le pain sur le feu, etc.)
- éteindre la cuisinière
- faire les préparations (prendre la poêle, le beurre et cinq œufs)~ ;
- allumer la cuisinière~ ;
- cuire les œufs (mettre la poêle sur le feu, etc.)~ ;
- éteindre la cuisinière.
De même, l'étape la plus générale deviendrait :
De même, l'étape la plus générale deviendrait~ :
- préparer une omelette avec cinq œufs (toutes les étapes)
- préparer une omelette avec cinq œufs (toutes les étapes).
Cette fois, il y a une information de plus qui concerne certaines étapes : "prendre cinq œufs", "cuisiner les œufs" et "préparer une omelette avec cinq œufs".
Cette fois, il y a une information de plus qui concerne certaines étapes~ : «~ prendre cinq œufs~ », «~ cuisiner les œufs~ » et «~ préparer une omelette avec cinq œufs~ ».
Le comportement des fonctions peut être ajusté de la même manière que dans l'exemple de l'omelette. Les données que les fonctions utilisent pour ajuster leur comportement sont appelés [* paramètres]. Les paramètres sont indiqués dans la liste des paramètres de la fonction, séparés par des virgules. La liste des paramètres est écrite entre parenthèses après le nom de la fonction?
Le comportement des fonctions peut être ajusté de la même manière que dans l'exemple de l'omelette. Les données que les fonctions utilisent pour ajuster leur comportement sont appelées [* paramètres]. Les paramètres sont indiqués dans la liste des paramètres de la fonction, séparés par des virgules. La liste des paramètres est écrite entre parenthèses après le nom de la fonction.
La fonction [c afficherMenu()] ci-dessus a été définie par une liste de paramètre vide parce que cette fonction a toujours afficher le même menu. Supposons que parfois, le menu aura besoin d'être affiché différemment dans différents contextes. Par exemple, il pourrait être plus sensé d'afficher la première entrée comme "Revenir" plutôt que "Quitter", selon l'endroit du programme dans lequel on se trouve.
La fonction [c afficherMenu()] ci-dessus était définie par une liste de paramètre vide parce que cette fonction affichait toujours le même menu. Supposons que, parfois, on veuille que le menu soit affiché différemment dans différents contextes. Par exemple, il pourrait être préférable d'afficher la première entrée comme «~ Revenir~ » plutôt que «~ Quitter~ », selon l'endroit du programme dans lequel on se trouve.
Dans une telle situation, la première entrée du menu peut être paramétrée en étant définie dans la liste des paramètres. La fonction utilise alors la valeur de ce paramètre au lieu du littéral "Quitter" :
Dans une telle situation, la première entrée du menu peut être paramétrée en étant définie dans la liste des paramètres. La fonction utilise alors la valeur de ce paramètre au lieu du littéral [c "Quitter"]~ :
[code=d <<<
void afficherMenu(string premiereEntree)
@ -105,7 +105,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
}
>>>]
Notez que comme l'information que [c premiereEntree] contient est un bout de texte, son type a été indiqué comme [c string] dans la liste des paramètre. Cette fonction peut maintenant être appelée avec différentes valeurs de paramètre pour afficher des menus qui ont une première entrée différente. Tout ce qu'il est nécessaire de faire est d'utiliser des valeurs de chaînes appropriées selon l'endroit où la fonction est appelée :
Notez que comme l'information que [c premiereEntree] contient est un bout de texte, son type a été indiqué comme [c string] dans la liste des paramètre. Cette fonction peut maintenant être appelée avec différentes valeurs de paramètre pour afficher des menus qui ont une première entrée différente. Il suffit pour cela d'utiliser des valeurs de chaînes appropriées selon l'endroit où la fonction est appelée~ :
[code=d <<<
// À un endroit du programme :
@ -115,7 +115,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
afficherMenu("Revenir");
>>>]
[p Note : | Quand vous écrivez vos propres fonctions, vous pouvez rencontrer des erreurs de compilation avec des paramètres de type [c string]. La fonction [c afficherMenu()] ci-dessus a un problème de ce genre. Telle qu'elle est écrite, elle ne peut pas être appelée avec des paramètres de type [c char~[~]]. Par exemple, le code suivant donnerait une erreur de compilation :]
[p Note~ : | Quand vous écrivez vos propres fonctions, vous pouvez rencontrer des erreurs de compilation avec des paramètres de type [c string]. La fonction [c afficherMenu()] ci-dessus a un problème de ce genre. Telle qu'elle est écrite, elle ne peut pas être appelée avec des paramètres de type [c char~[~]]. Par exemple, le code suivant donnerait une erreur de compilation~ :]
[code=d <<<
char[] uneEntrée;
@ -125,20 +125,20 @@ On peut noter, en observant les similarités entre les définitions de [c affich
En revanche, si [c afficherMenu()] avait été définie pour prendre un paramètre [c char~[~]], alors elle n'aurait pas pu être appelée avec des chaînes du type [c "Quitter"]. Ceci est lié à l'idée d'immuabilité et du mot-clé [c immutable], que nous verrons tous deux dans le chapitre suivant.
Continuons avec la fonction du menu et supposons qu'il n'est pas approprié de toujours commencer la numérotation des entrées par zéro. Dans ce cas, le numéro de débt peut également être passé à la fonction en deuxième paramètre. Les paramètres de la fonction doivent être séparés par des virgules :
Continuons avec la fonction du menu et supposons qu'il n'est pas approprié de toujours commencer la numérotation des entrées par zéro. Dans ce cas, le numéro de début peut également être passé à la fonction en deuxième paramètre. Les paramètres de la fonction doivent être séparés par des virgules~ :
[code=d <<<
void afficherMenu(string premiereEntree, int premierNombre)
void afficherMenu(string premiereEntree, int premierNumero)
{
writeln(' ', premierNombre, ' ', premiereEntree);
writeln(' ', premierNombre + 1, " Ajouter");
writeln(' ', premierNombre + 2, " Soustraire");
writeln(' ', premierNombre + 3, " Multiplier");
writeln(' ', premierNombre + 4, " Diviser");
writeln(' ', premierNumero, ' ', premiereEntree);
writeln(' ', premierNumero + 1, " Ajouter");
writeln(' ', premierNumero + 2, " Soustraire");
writeln(' ', premierNumero + 3, " Multiplier");
writeln(' ', premierNumero + 4, " Diviser");
}
>>>]
Il est maintenant possible de dire à la fonction quel est le nombre de début :
Il est maintenant possible de dire à la fonction à partir de quel numéro débuter~ :
[code=d <<<
afficherMenu("Revenir", 1);
@ -147,13 +147,13 @@ On peut noter, en observant les similarités entre les définitions de [c affich
]
[ = Appeler une fonction
Commencer une fonction pour qu'elle effectue sa tâche est appelé [* appeler une fonction]. La syntaxe d'appel d'une fonction est la suivante :
Démarrer une fonction pour qu'elle effectue sa tâche est appelé [* appeler une fonction]. La syntaxe d'appel d'une fonction est la suivante~ :
[code=d <<<
nom_de_la_fonction(valeurs_des_parametres)
>>>]
Les valeurs réelles des paramètres qui sont passées aux fonctions sont appelées [* arguments de la fonction]. Même si les termes [* paramètre] et [* argument] sont parfois interchangés dans la littérature, ils désignent des concepts différents.
Les valeurs réelles des paramètres qui sont passées aux fonctions sont appelées [* arguments de la fonction]. Même si les termes [* paramètre] et [* argument] sont parfois interchangés dans la littérature, ils désignent des concepts différents.
Les arguments sont mis en correspondance avec les paramètres un à un dans l'ordre de définition des paramètres. Par exemple, le dernier appel à [c afficherMenu()] ci-dessus utilise les arguments [c "Revenir"] et [c 1], qui correspondent aux paramètres [c premiereEntree] et [c premierNombre], respectivement.
@ -163,27 +163,26 @@ On peut noter, en observant les similarités entre les définitions de [c affich
[ = Faire le travail
Dans les chapitres précédents, nous avons défini les expressions comme des entités qui effectuent une tâche. Les appels de fonctions sont aussi des expressions : elle effectue une certaine tâche. Effectuer une tâche peut signifier une combinaison de deux choses :
Dans les chapitres précédents, nous avons défini les expressions comme des entités qui effectuent une tâche. Les appels de fonctions sont aussi des expressions~ : elles effectuent une certaine tâche. Effectuer une tâche peut signifier une combinaison de deux choses~ :
- [
[p Avoir des effets de bord. | Les effets de bord sont n'importe quel changement d'état du programme ou de son environnement. Seules certaines opérations ont des effets de bord. Un exemple est la modification de stdout quand [c afficherMenu()] affiche le menu. Un autre exemple d'effet de bord serait une fonction qui ajouterais un objet [c Etudiant] à un conteneur étudiant : cela entraînerait l'agrandissement du conteneur.]
[p Avoir des effets de bord~ : | les effets de bord sont n'importe quel changement d'état du programme ou de son environnement. Seules certaines opérations ont des effets de bord. Un exemple est la modification de [c stdout] quand [c afficherMenu()] affiche le menu. Un autre exemple d'effet de bord serait une fonction qui ajouterais un objet [c Etudiant] à un conteneur étudiant~ : cela entraînerait l'agrandissement du conteneur.]
En résumé, les opérations qui modifient l'état du programme ont des effets de bord.
]
- [p Produire une valeur : | Certaines opérations ne font que produire une valeur. Par exemple, une fonction qui ajoute des nombres produirait le résultat de cette opération. Un autre exemple serait une fonction qui créerait un objet [c Etudiant] à partir du nom et de l'adresse d'un étudiant produirait un objet [c Etudiant].]
- [p Produire une valeur~ : | certaines opérations ne font que produire une valeur. Par exemple, une fonction qui ajoute des nombres produirait le résultat de cette opération. Un autre exemple serait une fonction qui créerait un objet [c Etudiant] à partir du nom et de l'adresse d'un étudiant produirait un objet [c Etudiant].]
- [p Avoir des effets de bords et produire une valeur : | Certaines opérations font les deux. Par exemple, une fonction qui lit deux valeurs depuis stdin et calcule leur somme a des effets de bords parce qu'elle modifie l'état de [c stdin] et produit également la somme des deux valeurs.]
- [p Avoir des effets de bords et produire une valeur~ : | certaines opérations font les deux. Par exemple, une fonction qui lit deux valeurs depuis [c stdin] et calcule leur somme a des effets de bords parce qu'elle modifie l'état de [c stdin] et produit également la somme des deux valeurs.]
- [p Aucune opération : |
Même si toute fonction est d'une des trois catégories, il arrive que certaine fonctions ne fassent rien, dans certaines conditions lors de la compilation ou de l'exécution.
- [p Aucune opération~ : | même si toute fonction fait partie d'une des trois catégories ci-dessus, il arrive que certaine fonctions ne fassent rien, dans certaines conditions lors de la compilation ou de l'exécution.
]
]
[ = La valeur de retour
La résultat qu'une fonction produit est appelée sa [* valeur de retour]. Ce terme vient de l'observation qu'une fois que l'exécution du programme arrive dans une fonction, elle finit par [* revenir] là où elle a été appelée. Les fonctions sont appelées et elles retournent des valeurs.
La résultat qu'une fonction produit est appelé sa [* valeur de retour]. Ce terme vient de l'observation qu'une fois que l'exécution du programme passe dans une fonction, elle finit par [* revenir] là où elle a été appelée. Les fonctions sont [* appelées] et elles [* retournent] des valeurs.
Comme n'importe quelle autre valeur, les valeurs de retour ont un type. Le type de la valeur de retour est indiqué juste avant le nom de la fonction à l'endroit où la fonction est définie. Par exemple, une fonction qui ajoute deux valeurs [c int] et qui retourne leur somme tant qu'[c int] serait définie de cette manière :
Comme n'importe quelle autre valeur, les valeurs de retour ont un type. Le type de la valeur de retour est indiqué juste avant le nom de la fonction à l'endroit où la fonction est définie. Par exemple, une fonction qui ajoute deux valeurs [c int] et qui retourne leur somme tant qu'[c int] serait définie de cette manière~ :
[code=d <<<
int ajouter(int premier, int second)
@ -192,7 +191,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
}
>>>]
La valeur que la fonction retourne est donnée par l'appel de la fonction lui-même. Par exemple, en supposant que l'appel de fonction [c ajouter(5, 7)] produise la valeur 12, les deux lignes suivantes sont équivalentes :
La valeur que la fonction retourne vient en place de l'appel de la fonction lui-même. Par exemple, en supposant que l'appel de fonction [c ajouter(5, 7)] produise la valeur 12, les deux lignes suivantes sont équivalentes~ :
[code=d <<<
writeln("Résultat: ", ajouter(5, 7));
@ -201,18 +200,18 @@ On peut noter, en observant les similarités entre les définitions de [c affich
À la première ligne, la fonction [c ajouter()] est appelée avec les arguments 5 et 7 avant que [c writeln()] soit appelée. La valeur 12 que la fonction retourne est à son tour passée à [c writeln()] en deuxième argument.
Ceci permet de passer les valeurs de retour des fonctions aux autres fonctions pour former des expressions complexes :
Ceci permet de passer les valeurs de retour des fonctions aux autres fonctions pour former des expressions complexes~ :
[code=d <<<
writeln("Résultat: ", ajouter(5, diviser(100, nombreEtudiants())));
writeln("Résultat : ", ajouter(5, diviser(100, nombreEtudiants())));
>>>]
Dans la ligne ci-avant, la valeur de retour de [c nombreEtudiants()] est passée à [c diviser()] en second argument, la valeur de retour de [c diviser()] est passée à [c ajouter()] en second argument, et finalement la valeur de retour de [c ajouter()] est passé à writeln() en second argument.
Dans cette ligne, la valeur de retour de [c nombreEtudiants()] est passée à [c diviser()] en second argument, la valeur de retour de [c diviser()] est passée à [c ajouter()] en second argument, et finalement la valeur de retour de [c ajouter()] est passée à [c writeln()] en second argument.
]
[ = L'instruction [c return]
La valeur de retour d'une fonction est indiquée par le mot-clé [c return] :
La valeur de retour d'une fonction est indiquée par le mot-clé [c return]~ :
[code=d <<<
int ajouter(int premier, int second)
@ -224,7 +223,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
Une fonction produit sa valeur de retour en utilisant des instructions, des expressions et éventuellement en appelant d'autres fonctions. La fonction retourne ensuite cette valeur avec le mot-clé [c return], point auquel l'exécution de la fonction se finit.
Il est possible d'avoir plus d'une instruction de retour dans une fonction. La valeur de la première instruction de retour qui ext exécutée détermine la valeur de retour de la fonction pour un appel particulier :
Il est possible d'avoir plus d'une instruction de retour dans une fonction. La valeur de la première instruction de retour qui est exécutée détermine la valeur de retour de la fonction pour un appel particulier~ :
[code=d <<<
int calculComplexe(int unParametre, int unAutreParametre)
@ -242,18 +241,18 @@ On peut noter, en observant les similarités entre les définitions de [c affich
]
[ = Les fonctions [c void]
Le type de retour des fonctions qui ne produisent pas de valeur est conventionnellement [c void]. Nous l'avons vu de nombreuses fois avec la fonction [c main()], de même qu'avec la fonction [c afficherMenu] ci-avant. Comme elles ne retournent pas de valeur aux fonctions qui les appellent, leurs valeurs de retour a été défini comme [c void]. (Note : [c main()] peut également être définie comme retournant [c int]. Nous le verrons dans un chapitre ultérieur.)
Le type de retour des fonctions qui ne produisent pas de valeur est conventionnellement [c void]. Nous l'avons vu de nombreuses fois avec la fonction [c main()], de même qu'avec la fonction [c afficherMenu] définie plus haut. Comme elles ne retournent pas de valeur aux fonctions qui les appellent, leurs valeurs de retour a été défini comme [c void]. (Note~ : [c main()] peut également être définie comme retournant [c int]. Nous le verrons dans un chapitre ultérieur.)
]
[ = Le nom de la fonction
Le nom d'une fonction doit être choisi pour donner clairement son rôle. Par exemple, les noms [c ajouter] et [c afficherMenu] étaient appropriés parce que leur rôle était respectivement d'ajouter deux valeurs et d'afficher un menu.
Le nom d'une fonction doit être choisi de manière à indiquer clairement son rôle. Par exemple, les noms [c ajouter] et [c afficherMenu] étaient appropriés parce que leurs rôles étaient respectivement d'ajouter deux valeurs et d'afficher un menu.
Une règle courante pour les noms de fonctions est qu'ils contiennent un verbe comme [c ajouter] ou [c afficher]. Selon cette règle, des noms comme [c ajouterition()] et [c menu()] seraient moins bons.
Une règle courante pour les noms de fonctions est de contenir un verbe comme [c ajouter] ou [c afficher]. Selon cette règle, des noms comme [c addition()] et [c menu()] seraient moins bons.
Cependant, il est acceptable de simplement nommer les fonctions avec un substantif si ces fonctions n'ont pas d'effet secondaire. Par exemple, une fonction qui retourne la température actuelle peut être nommée [c temperatureActuelle()] au lieu de [c obtenirTemperatureActuelle()].
Avec des noms clairs, concis et consistants relève un peu de l'art de la programmation.
Trouver des noms clairs, concis et cohérents relève un peu de l'art de la programmation.
]
@ -261,256 +260,255 @@ On peut noter, en observant les similarités entre les définitions de [c affich
Les fonctions peuvent améliorer la qualité du code. Des fonctions plus petites avec des responsabilités plus légères permettent d'écrire des programmes qui sont plus faciles à maintenir.
]
[ = La duplication de code est nuisible
[ = La duplication de code est nuisible
Une pratique hautement néfaste à la qualité d'un programme est la duplication de code. La duplication de code est ce qui se produit quand il y a plus d'un bout de code dans le programme qui réalise la même tâche.
Une des pratiques hautement néfaste à la qualité d'un programme est la duplication de code. La duplication de code est quand il y a plus d'un bout de code dans le programme qui réalise la même tâche.
Même si cela arrive parfois en copiant des lignes de code à un endroit différent, cela peut également arriver en écrivant des lignes de codes similaires sans le vouloir.
Même si cela arrive parfois en copiant des lignes de code à un endroit différent, cela peut également arriver en écrivant des lignes de codes similaires sans le vouloir.
Un des problèmes avec la duplication de code est qu'un éventuel bogue serait présent dans chaque copie, nécessitant d'effectuer sa correction à plusieurs endroits. Inversement, quand le code apparaît une seule fois dans le programme, le corriger à un seul endroit permet de se débarrasser du bogue une fois pour toute.
Un des problèmes avec la duplication de code est que des bogues potentiels seraient présent dans toutes les copies, nécessitant la correction à plusieurs endroits. Inversement, quand le code apparaît une seule fois dans le programme, le corriger à un seul endroit permet de s'en débarrasser une fois pour toute.
Comme je l'ai mentionné précédemment, les fonctions sont très liées au savoir-faire du programmeur. Les programmeurs expérimentés sont toujours sur leurs gardes à propos de la duplication de code. Ils essayent toujours d'identifier les similarités dans le code et déplacent les bouts de codes communs dans des fonctions à part (ou dans des structures, classes, modèles… communs, comme nous le verrons dans des chapitres ultérieurs).
Comme je l'ai mentionné précédemment, les fonctions sont très liés à l'expérience du programmeur. Les programmeurs expérimentés sont toujours sur leurs gardes à propos de la duplication de code. Ils essayent toujours d'identifier les similarités dans le code et déplacent les bouts de codes communs dans des fonctions à part (ou dans des structures, classes, ..., modèles communs comme nous le verrons dans des chapitres ultérieurs).
Commençons par un programme qui contient du code dupliqué. Voyons comment cette duplication peut être éradiquée en déplaçant du code dans des fonctions (c.-à-d. en refactorisant le code). Le programme suivant lit des nombres depuis l'entrée et les affiche d'abord dans l'ordre dans lequel ils sont arrivés et ensuite dans l'ordre croissant~ :
Commençons par un programme qui contient du code dupliqué. Voyons comment cette duplication peut être éradiquée en déplaçant du code dans des fonctions (c.-à-d. en refactorisant le code). Le programme suivant lit des nombres depuis l'entrée et les affiche d'abord dans l'ordre dans lequel ils sont arrivés et dans l'ordre croissant :
[code=d <<<
import std.stdio;
[code=d <<<
import std.stdio;
void main()
{
int[] nombres;
void main()
{
int[] nombres;
int nombre;
write("Combien de nombre allez-vous saisir ? ");
readf(" %s", &nombre);
int compte;
write("Combien de nombres allez-vous saisir ? ");
readf(" %s", &compte);
// Lecture des nombres
foreach (i; 0 .. nombre) {
int nombre;
write("Number ", i, "? ");
readf(" %s", &nombre);
// Lecture des nombres
foreach (i; 0 .. compte) {
int nombre;
write("Nombres ", i, "? ");
readf(" %s", &nombre);
nombres ~= nombre;
nombres ~= nombre;
}
// affichage des nombres
writeln("Avant de trier :");
foreach (i, nombre; nombres) {
writefln("%3d:%5d", i, nombre);
}
nombres.sort;
// affichage des nombres
writeln("Après avoir trié :");
foreach (i, nombre; nombres) {
writefln("%3d:%5d", i, nombre);
}
}
>>>]
Certaines lignes dupliquées sont évidentes dans ce code. Les deux dernières boucles qui sont utilisées pour afficher les nombres sont exactement les mêmes. Définir une fonction qui peut être judicieusement nommée [c afficher()] éviterait cette duplication. La fonction pourrait prendre une tranche en paramètre et l'afficher~ :
[code=d <<<
void afficher(int[] tranche)
{
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
>>>]
Notez que le nom du paramètre est défini de façon plus générale comme [c tranche] plutôt que [c nombres]. La raison est que la fonction n'a pas spécialement à savoir ce que les éléments de la tranche représentent. Cela n'est connu qu'à l'endroit où la fonction est appelée. Les éléments peuvent être des numéros d'étudiants, des bouts d'un mot de passe, etc. Comme cela peut ne pas être connu par la fonction [c afficher()], des noms généraux comme [c tranche] et [c element] sont utilisés dans son implémentation.
La nouvelle fonction peut être appelée depuis les deux endroits où la tranche doit être affichée~ :
[code=d <<<
import std.stdio;
void afficher(int[] tranche)
{
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
// affichage des nombres
writeln("Avant de trier :");
foreach (i, nombre; nombres) {
writefln("%3d:%5d", i, nombre);
void main()
{
int[] nombres;
int compte;
write("Combien de nombres allez-vous saisir ? ");
readf(" %s", &compte);
// Lecture des nombres
foreach (i; 0 .. compte) {
int nombre;
write("Nombre ", i, "? ");
readf(" %s", &nombre);
nombres ~= nombre;
}
// Affichage des nombres
writeln("Avant le tri :");
afficher(nombres);
nombres.sort;
// Affichage des nombres
writeln("Après le tri :");
afficher(nombres);
}
>>>]
On peut faire encore mieux. Notez qu'il y a toujours une ligne de titre affichée juste avant l'affichage des éléments de la tranche. Même si le titre est différent, la tâche est la même. Si afficher le titre peut être vu comme faisant partie de l'affichage de la tranche, le titre peut également être passé en paramètre. Voici les nouvelles modifications~ :
[code=d <<<
void afficher(string titre, int[] tranche)
{
writeln(titre, " :");
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
nombres.sort;
// ...
// affichage des nombres
writeln("Après avoir trié:");
foreach (i, nombre; nombres) {
writefln("%3d:%5d", i, nombre);
}
}
>>>]
// Afficher les nombres
afficher("Avant le tri", nombres);
Certaines lignes dupliquées sont évidentes dans ce code. Les deux dernières boucles qui sont utilisées pour afficher les nombres sont exactement les mêmes. Définir une fonction qui peut être judicieusement nommée [c afficher()] éviterait cette duplication. La fonction pourrait prendre une tranche en paramètre et l'afficher :
// ...
[code=d <<<
void afficher(int[] tranche)
{
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
>>>]
// Afficher les nombres
afficher("Après le tri", nombres);
>>>]
Notez que le nom du paramètre est défini plus largement comme [c tranche] (tranche) plutôt que [c nombres]. La raison est que la fonction ne sait pas spécialement ce que les éléments de la tranche représentent. Cela n'est connu qu'à l'endroit où la fonction est appelée. Les éléments peuvent être des numéros d'étudiants, des bouts d'un mot de passe, etc. Comme cela ne peut pas être connu par la fonction [c afficher()], des noms généraux comme [c tranche] et [c element] sont utilisés dans son implémentation.
Cette étape a l'avantage de rendre évidents les commentaires qui apparaissent juste avant les deux appels d'[c afficher()]. Comme le nom de la fonction communique déjà clairement ce qu'elle fait, ces commentaires ne sont plus nécessaires~ :
La nouvelle fonction peut être appelée depuis les deux endrois où la tranche doit être affichée :
[code=d <<<
afficher("Avant le tri", nombres);
nombres.sort;
afficher("Apres le tri", nombres);
>>>]
[code=d <<<
import std.stdio;
Même si c'est subtil, il y a encore de la duplication de code dans ce programme~ : les valeurs de [c compte] et [c nombre] sont lues exactement de la même manière. Les seules différences sont le message qui est affiché à l'utilisateur et le nom de la variable~ :
void afficher(int[] tranche)
{
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
void main()
{
int[] nombres;
int nombre;
[code=d <<<
int compte;
write("Combien de nombres allez-vous saisir ? ");
readf(" %s", &nombre);
readf(" %s", &compte);
// Lecture des nombres
foreach (i; 0 .. nombre) {
int nombre;
write("Nombre ", i, "? ");
readf(" %s", &nombre);
nombres ~= nombre;
}
// Affichage des nombres
writeln("Avant le tri :");
afficher(nombres);
nombres.sort;
// Affichage des nombres
writeln("Après le tri :");
afficher(nombres);
}
>>>]
On peut faire encore mieux. Notez qu'il y a toujours une ligne de titre affichée juste avant l'affichage des éléments de la tranche. Même si le titre est différent, la tâche est la même. Si afficher le titre peut être vu comme faisant partie de l'affichage de la tranche, le titre peut également être passé en paramètre. Voici les nouvelles modifications :
[code=d <<<
void afficher(string titre, int[] tranche)
{
writeln(titre, ":");
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
// ...
// Afficher les nombres
afficher("Avant le tri", nombres);
// ...
// Afficher les nombres
afficher("Après le tri", nombres);
>>>]
Cette étape a l'avantage de rendre évidents les commentaires qui apparaissent juste avant les deux appels d'[c afficher()]. Comme le nom de la fonction communique déjà clairement ce qu'elle fait, ces commentaires ne sont plus nécessaires :
[code=d <<<
afficher("Avant le tri", nombres);
nombres.sort;
afficher("Apres le tri", nombres);
>>>]
Même si c'est subtil, il y a encore de la duplication de code dans ce programme : les valeurs de [c nombre] et [c nombre] sont lus exactement de la même manière. La seule différence est le message qui est affiché à l'utilisateur et le nom de la variable :
[code=d <<<
int nombre;
write("Combien de nombres allez-vous saisir ? ");
readf(" %s", &nombre);
// ...
// ...
int nombre;
write("Nombre ", i, "? ");
readf(" %s", &nombre);
>>>]
>>>]
Le code serait encore mieux s'il utilisait une nouvelle fonction nommée de façon appropriée [c lire_entier()]. La nouvelle fonction peut prendre le message en paramètre, afficher ce message, lire un entier depuis l'entrée et le retourner :
Le code serait encore mieux s'il utilisait une nouvelle fonction nommée de façon appropriée [c lire_entier()]. La nouvelle fonction peut prendre le message en paramètre, afficher ce message, lire un entier depuis l'entrée et le retourner~ :
[code=d <<<
int lire_entier(string message)
{
int resultat;
write(message, " ? ");
readf(" %s", &resultat);
return resultat;
}
>>>]
[c nombre] peut maintenant être initialisé directement avec la valeur de retour d'un appel à cette nouvelle fonction :
[code=d <<<
int nombre = lire_entier("Combien de nombres allez-vous saisir");
>>>]
[c nombre] ne peut pas être initialisé directement parce qu'il se trouve que le compteur de boucle [c i] est un message qui est affiché quand on lit le nombre.
[code=d <<<
import std.string;
// ...
int nombre = lire_entier(format("Nombre %s", i));
>>>]
De plus, comme [c nombre] n'est utilisé qu'à un endroit dans la boucle [c foreach], sa définition peut être complètement supprimé et la valeur de retour de [c lire_entier()] peut directement être utilisé à sa place :
[code=d <<<
foreach (i; 0 .. nombre) {
nombres ~= lire_entier(format("Nombre %s", i));
}
>>>]
Apportons une dernière modification à ce programme en déplaçaant les lignes qui lisent les nombres dans une fonction à part. Ceci éliminerait la nécessité du commentaire "Lecture des nombres" parce que le nom de la nouvelle fonction porterait déjà cette information.
La nouvelle fonction [c lireLesNombres()] n'a besoin d'aucun paramètre pour effectuer sa tâche. Elle lit quelques nombres et les retourne dans une tranche. Voici la version finale du programme :
[code=d <<<
import std.stdio;
import std.string;
void afficher(string titre, int[] tranche)
{
writeln(titre, ":");
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
[code=d <<<
int lire_entier(string message)
{
int resultat;
write(message, " ? ");
readf(" %s", &resultat);
return resultat;
}
}
>>>]
int lire_entier(string message)
{
int resultat;
write(message, " ? ");
readf(" %s", &resultat);
return resultat;
}
[c compte] peut maintenant être initialisé directement avec la valeur de retour d'un appel à cette nouvelle fonction~ :
int[] lireLesNombres()
{
int[] resultat;
[code=d <<<
int compte = lire_entier("Combien de nombres allez-vous saisir");
>>>]
int nombre = lire_entier("Combien de nombres allez-vous saisir");
[c nombre] ne peut pas être initialisé aussi simplement parce qu'il se trouve que le compteur de boucle [c i] fait partie du message à afficher quand on lit le nombre.
foreach (i; 0 .. nombre) {
resultat ~= lire_entier(format("Nombre %s", i));
[code=d <<<
import std.string;
// ...
int nombre = lire_entier(format("Nombre %s", i));
>>>]
De plus, comme [c nombre] n'est utilisé qu'à un endroit dans la boucle [c foreach], sa définition peut être complètement supprimée et la valeur de retour de [c lire_entier()] peut directement être utilisée à sa place~ :
[code=d <<<
foreach (i; 0 .. compte) {
nombres ~= lire_entier(format("Nombre %s", i));
}
>>>]
Apportons une dernière modification à ce programme en déplaçant les lignes qui lisent les nombres dans une fonction à part. Ceci éliminera la nécessité du commentaire «~ Lecture des nombres~ » parce que le nom de la nouvelle fonction portera déjà cette information.
La nouvelle fonction [c lireLesNombres()] n'a besoin d'aucun paramètre pour effectuer sa tâche. Elle lit quelques nombres et les retourne dans une tranche. Voici la version finale du programme~ :
[code=d <<<
import std.stdio;
import std.string;
void afficher(string titre, int[] tranche)
{
writeln(titre, " :");
foreach (i, element; tranche) {
writefln("%3s:%5s", i, element);
}
}
return resultat;
}
int lire_entier(string message)
{
int resultat;
write(message, " ? ");
readf(" %s", &resultat);
return resultat;
}
void main()
{
int[] nombres = lireLesNombres();
afficher("Avant le tri", nombres);
nombres.sort;
afficher("Après le tri", nombres);
}
>>>]
int[] lireLesNombres()
{
int[] resultat;
Comparez cette version du programme à la première. Les étapes principales du programme sont très claires dans la fonction [c main()] du nouveau programme alors que la fonction [c main()] de la première version du programme était à examiner pour comprendre le bur de ce programme.
int compte = lire_entier("Combien de nombres allez-vous saisir");
Même si ici le nombre total de lignes non triviales des deux versions du programme reste égal, les fonctions rendent les programmes généralement plus petits. Ceci n'est pas apparant dans ce programme simple. Par exemple, avant que la fonction [c lire_entier()] n'ait été définie, lire un entier depuis l'entrée demandait trois lignes de code. Après la définition de cette fonction, le même but est atteint avec une seule ligne de code. De plus, la définition de [c lire_entier()] a permis de se débarrasser complètement de la définition de la variable [c nombre].
foreach (i; 0 .. compte) {
resultat ~= lire_entier(format("Nombre %s", i));
}
]
return resultat;
}
[ = Lignes de codes commentées en tant que fonctions
void main()
{
int[] nombres = lireLesNombres();
afficher("Avant le tri", nombres);
nombres.sort;
afficher("Après le tri", nombres);
}
>>>]
Parfois, le besoin d'écrire un commentaire pour décrire le rôle d'un groupe de ligne de code est un indice sur le fait que ces lignes pourraient être mieux dans une nouvelle fonction. Si le nom de la fonction est assez explicite, il n'y aurait même pas besoin de commentaire.
Comparez cette version du programme à la première. Les étapes principales du programme sont très claires dans la fonction [c main()] du nouveau programme alors qu'il fallait examiner avec attention la fonction [c main()] de la première version du programme pour en comprendre le but.
Les trois groupes de lignes commentés dans la première version du programme ont été utilisés pour définir de nouvelles fonctions qui effectuent la même tâche.
Même si ici le nombre total de lignes non triviales des deux versions du programme reste similaire, les fonctions rendent généralement les programmes plus courts. Ceci n'est pas apparant dans ce programme simple. Par exemple, avant que la fonction [c lire_entier()] n'ait été définie, lire un entier depuis l'entrée demandait trois lignes de code. Après la définition de cette fonction, le même but est atteint avec une seule ligne de code. De plus, la définition de [c lire_entier()] a permis de se débarrasser complètement de la définition de la variable [c nombre].
Un autre gros avantage à supprimer des lignes de commentaires est que les commentaires tendent à devenir obsolètes par rapport au code qui reçoit des modifications. La mise à jour des commentaires lors de la modification du code est parfois oubliée, ce qui rend ces commentaires inutiles voire trompeurs. Pour cette raison, écrire des programmes qui n'ont pas besoin d'être beaucoup commentés est bénéfique.
]
]
[ = Lignes de codes commentées en tant que fonctions
Parfois, le besoin d'écrire un commentaire pour décrire le rôle d'un groupe de lignes de code est un indice sur le fait que ces lignes pourraient être mieux dans une nouvelle fonction. Si le nom de la fonction est assez explicite, il n'y aurait même pas besoin de commentaire.
Les trois groupes de lignes commentés dans la première version du programme ont été utilisés pour définir de nouvelles fonctions qui effectuent la même tâche.
Un autre gros avantage à supprimer des lignes de commentaires est que les commentaires tendent à devenir obsolètes par rapport au code qui reçoit des modifications. La mise à jour des commentaires lors de la modification du code est parfois oubliée, ce qui rend ces commentaires inutiles voire trompeurs. Pour cette raison, écrire des programmes qui n'ont pas besoin d'être beaucoup commentés est bénéfique.
]
[ = Exercices
# [
Modifiez la fonction [c afficherMenu()] pour qu'elle prenne l'ensemble des éléments du menu dans un paramètre. Par exemple, les éléments du menu peuvent être passés à la fonction comme dans le code suivant :
Modifiez la fonction [c afficherMenu()] pour qu'elle prenne l'ensemble des éléments du menu dans un paramètre. Par exemple, les éléments du menu peuvent être passés à la fonction comme dans le code suivant~ :
[code=d <<<
string[] items =
@ -518,7 +516,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
afficherMenu(items, 1);
>>>]
Le programme devra produire la sortie suivante :
Le programme devra produire la sortie suivante~ :
[output <<<
1 Noir
2 Rouge
@ -529,7 +527,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
]
# [
Le programme suivant utilise un tableau bidimensionnel pour représenter une toile. Partez de ce programme et améliorez-le en lui ajoutant des nouvelles fonctionnalités :
Le programme suivant utilise un tableau bidimensionnel pour représenter une toile. Partez de ce programme et améliorez-le en lui ajoutant des fonctionnalités nouvelles~ :
[code=d <<<
import std.stdio;
@ -539,9 +537,9 @@ On peut noter, en observant les similarités entre les définitions de [c affich
/*
* L´'alias' dans la ligne suivante fait de 'Ligne' un alias de
* dchar[nbColonnesTotal]. Chaque 'Ligne' qui est utilisé dans le
* reste du programme voudra dire dchar[nbColonnesTotal] depuis
* cet endroit.
* dchar[nbColonnesTotal]. À partir de cet endroit, chaque
* 'Ligne' qui est utilisé dans le reste du programme voudra
* dire dchar[nbColonnesTotal] .
*
* Notez également que 'Ligne' est un tableau à taille fixe.
*/
@ -553,7 +551,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
alias Ligne[] Toile;
/*
* Affiche le toile ligne par ligne.
* Affiche la toile ligne par ligne.
*/
void afficher(Toile toile)
{
@ -572,7 +570,7 @@ On peut noter, en observant les similarités entre les définitions de [c affich
}
/*
* Dessine une ligne verticale de taille indiqué et à
* Dessine une ligne verticale de la taille indiquée et à
* l'endroit indiqué.
*/
void dessinerLigneVerticale(Toile toile,

879
fonctions_speciales.whata Normal file
View File

@ -0,0 +1,879 @@
[set
title = "Le constructeur et autres fonctions spéciales"
partAs = chapitre
translator = "Olivier Pisano"
]
Bien que ce chapitre se focalise sur les structures, les sujets abordés s'appliquent pour la plupart également aux classes. Les différences seront abordées dans des chapitres ultérieurs.
Quatre fonctions membres des structures sont spéciales, car elles définissent les opérations fondamentales d'un type :
- Le constructeur : [c this()]
- Le destructeur : [c ~~this()]
- La postcopie : [c this(this)]
- L'opérateur d'affectation : [c opAssign()]
Bien que ces opérations fondamentales soient prises en charge automatiquement pour les structures, et donc n'aient pas besoin d'être définies par le programmeur, elles peuvent être surchargées pour spécialiser le comportement d'une structure.
[ = Le constructeur
Le constructeur a la responsabilité de préparer un objet à être utilisé en assignant des valeurs à ses membres.
Nous avons déjà utilisé des constructeurs dans les chapitres précédents. Quand un nom de type est utilisé comme une fonction, c'est en fait le constructeur de ce type qui est appelé. Nous pouvons le voir sur la partie droite de la ligne suivante :
[code=d <<<
auto arriveeDuBus = MomentDeLaJournee(8, 30);
>>>]
De la même manière, un objet class est construit dans la partie droite de la ligne suivante :
[code=d <<<
auto variable = new UneClasse();
>>>]
Les arguments qui sont spécifiés entre les parenthèses correspondent aux paramètres du constructeur. Par exemple, les valeurs 8 et 30 ci-avant sont passées au constructeur de MomentDeLaJournee comme paramètres.
[ = Syntaxe d'un constructeur
Contrairement aux autres fonctions, les constructeurs n'ont pas de valeur de retour. Le nom d'un constructeur est toujours « this » :
[code=d <<<
struct UneStructure
{
// ...
this(/* Paramètres du constructeur */)
{
// Opérations préparant l'objet à être utilisé
}
}
>>>]
Les paramètres du constructeur incluent des informations qui sont nécessaires à créer un objet utile et cohérent.
]
[ = Constructeur automatique généré par le compilateur
Toutes les structures que nous avons vues jusqu'à présent ont tiré avantage d'un constructeur qui avait été généré automatiquement par le compilateur. Le constructeur automatique assigne les valeurs de ses paramètres à ses membres dans l'ordre dans lequel ils sont spécifiés.
Comme nous l'avons vu au chapitre sur les structures, toutes les valeurs initiales des membres n'ont pas besoin d'être spécifiées. Les membres non spécifiés sont initialisés à la valeur [c .init] de leurs types respectifs. La valeur .init d'un membre peut être spécifiée lors de la déclaration de celui-ci après l'opérateur d'affectation :
[code=d <<<
struct Test
{
int membre = 42;
}
>>>]
Si l'on considère les paramètres par défaut vus dans le chapitre sur les nombres de paramètres variables, on pourrait imaginer que le constructeur automatique de la structure suivante pourrait ressembler à cette fonction this() :
[code=d <<<
struct Test
{
char c;
int i;
double d;
/* L'équivalent du constructeur automatique généré par le compilateur
* (note : Ce code est à but illustratif, le constructeur suivant ne
* sera pas réellement appelé lors de la construction par défaut de
* l'objet via Test()). */
this(in char parametre_c = char.init,
in int parametre_i = int.init,
in double parametre_d = double.init)
{
c = parametre_c;
i = parametre_i;
d = parametre_d;
}
}
>>>]
Pour la plupart des structures, le constructeur automatique est suffisant. Généralement, fournir les valeurs appropriées pour chaque membre est tout ce dont on a besoin pour construire des objets.
]
[ = Accéder aux membres avec this
Dans le code précédent, pour éviter de confondre les paramètres du constructeur et les membres de l'objet, j'ai préfixé tous les noms de paramètres par « parametre ». Sans cela, il y aurait des erreurs de compilation :
[code=d <<<
struct Test
{
char c;
int i;
double d;
this(in char c = char.init,
in int i = int.init,
in double d = double.init)
{
// Une tentative d'assigner un paramètre in à lui-même !
c = c; // Erreur de compilation
i = i;
d = d;
}
}
>>>]
La raison en est la suivante : « c » tout seul désigne le paramètre, pas le membre, et les paramètres ci-dessus sont marqués comme in et ne peuvent être modifiés.
[output <<<
Erreur: Variable Test.this.c impossible de modifier const
>>>]
Une solution est d'ajouter [c this]. avant les noms des membres. Dans les fonctions membres, [c this] veut dire « cet objet », et donc [c this.c] désigne la variable [c c] membre de l'objet courant.
[code=d <<<
this(in char c = char.init,
in int i = int.init,
in double d = double.init)
{
// Une tentative d'assigner un paramètre in à lui-même !
this.c = c; // Erreur de compilation
this.i = i;
this.d = d;
}
>>>]
Désormais, [c content] tout seul signifie le paramètre [c c] et this.c signifie le membre, et le code se compile et [c content] comme on pourrait s'y attendre. Le membre c est initialisé avec la valeur du paramètre [c c].
]
[ = Constructeurs définis par l'utilisateur
Je viens de décrire le comportement du constructeur généré par le compilateur. Puisque ce constructeur est approprié à la plupart des cas, il n'est pas nécessaire de définir manuellement un constructeur.
Néanmoins, il arrive que la construction d'un objet nécessite des opérations plus complexes que l'assignation ordonnée de valeurs à des membres. Par exemple, considérons la structure Duree des chapitres précédents :
[code=d <<<
struct Duree
{
int minute;
}
>>>]
Le constructeur généré par le compilateur est suffisant pour cette structure à un membre :
[code=d <<<
moment.decrementer(Duree(12));
>>>]
Puisque le constructeur prend un nombre de minutes en paramètre, les programmeurs auront parfois besoin de faire de calculs préalables avant de l'utiliser :
[code=d <<<
// 23 heures et 18 minutes plus tôt
moment.decrementer(Duree(23 * 60 + 18));
// 22 heures et 20 minutes plus tard
moment.incrementer(Duree(22 * 60 + 20));
>>>]
Pour éliminer le besoin de faire ces calculs, nous pouvons concevoir un constructeur de Duree qui prend deux paramètres et fait automatiquement ce calcul :
[code=d <<<
struct Duree
{
int minute;
this(int heure, int minute)
{
this.minute = heure * 60 + minute;
}
}
>>>]
Puisquheure et minute sont maintenant des paramètres séparés, les utilisateurs fournissent désormais leurs valeurs sans faire eux-mêmes les calculs :
[code=d <<<
// 23 heures et 18 minutes plus tôt
moment.decrementer(Duree(23, 18));
// 22 heures et 20 minutes plus tard
moment.incrementer(Duree(22, 20));
>>>]
]
[ = Les constructeurs définis par l'utilisateur désactivent le constructeur généré par le compilateur
Un constructeur qui est défini par le programmeur invalide certaines utilisations du constructeur généré par le compilateur. Les objets ne peuvent plus être construits par des valeurs de paramètres par défaut. Par exemple, essayer de construire un objet Duree avec un seul paramètre produit une erreur de compilation :
[code=d <<<
moment.decrementer(Duree(12)); // Erreur de compilation
>>>]
Appeler le constructeur avec un seul paramètre ne correspond pas au constructeur défini par le programmeur et le constructeur généré par le compilateur est désactivé.
Une solution est de surcharger le constructeur en définissant un autre constructeur qui prend un seul paramètre :
[code=d <<<
struct Duree
{
int minute;
this(int heure, int minute)
{
this.minute = heure * 60 + minute;
}
this(int minute)
{
this.minute = minute;
}
}
>>>]
Un constructeur défini par l'utilisateur désactive également la possibilité de construire des objets avec la syntaxe [c { }] :
[code=d <<<
Duree duree = { 5 }; // Erreur de compilation
>>>]
Une initialisation sans paramètre est toujours autorisée :
[code=d <<<
auto d = Duree(); // compile
>>>]
Parce qu'en D, la valeur [c .init] de chaque type doit être connu à la compilation. La valeur de [c d] ci-dessus est égale à la valeur initiale de [c Duree] :
[code=d <<<
assert(d == Duree.init);
>>>]
]
[ = [c static opCall] en place de constructeur par défaut
Puisqu'il faut que la valeur initiale de chaque type soit connue dès la compilation, il est impossible de définir explicitement un constructeur par défaut.
Considérons le constructeur suivant qui essaie d'afficher des informations à chaque fois qu'un objet de ce type est construit :
[code=d <<<
struct Test
{
this() // ← Erreur de compilation
{
writeln("Un objet Test est construit");
}
}
>>>]
La sortie du compilateur :
[output <<<
Erreur: constructeur Test.this le constructeur par défaut pour les structures n'est autorisé qu'avec @disabled et sans corps.
>>>]
[p Note : |
Nous verrons dans des chapitres ultérieurs qu'il est possible de définir le constructeur par défaut pour les classes.
]
Pour pallier ce problème, une fonction statique [c opCall] peut être utilisée pour construire des objets sans fournir de paramètre. Notez que cela n'a aucun effet sur la valeur [c .init] du type.
Pour que cela fonctionne, la fonction statique [c opCall] doit construire et retourner un objet du type de la structure :
[code=d <<<
import std.stdio;
struct Test
{
static Test opCall()
{
writeln("Un objet Test est construit");
Test test;
return test;
}
}
void main()
{
auto test = Test();
}
>>>]
L'appel à [c Test()] dans la fonction [c main()] appelle la fonction statique [c opCall] :
[output <<<
Un objet [c Test] est construit.
>>>]
Notez qu'il n'est possible d'appeler [c Test()] à l'intérieur de la fonction statique [c opCall()]. La syntaxe exécuterait aussi la fonction statique [c opCall()] et causerait une récursion infinie :
[code=d <<<
static Test opCall()
{
writeln("Un objet Test est construit.");
return Test(); // Rappelle static OpCall()
}
>>>]
Le résultat :
[output <<<
Un objet Test est construit.
Un objet Test est construit.
Un objet Test est construit.
... ← Répète le même message
>>>]
]
[ = Appeler un autre constructeur
Les constructeurs peuvent appeler d'autres constructeurs pour éviter de dupliquer du code. Bien que [c Duree] soit trop simple pour montrer l'utilité de cette fonctionnalité, le constructeur à un paramètre qui suit utilise le constructeur à deux paramètres :
[code=d <<<
this(int heure, int minute)
{
this.minute = heure * 60 + minute;
}
this(int minute)
{
this(0, minute); // appelle l'autre constructeur
}
>>>]
Le constructeur qui prend un seul paramètre appelle l'autre constructeur en lui passant [c 0] comme valeur d'heure.
[p Attention : |
il y a une erreur de conception dans les constructeurs ci-avant parce que l'intention n'est pas claire lorsque les objets sont construits avec un seul paramètre.
]
[code=d <<<
// 10 heures ou 10 minutes
auto dureeVoyage = Duree(10);
>>>]
Bien qu'il soit possible de déterminer à la lecture de la documentation ou le code de la structure que le paramètre veut en fait dire « 10 minutes », c'est une incohérence vis-à-vis du premier paramètre du constructeur à deux paramètres qui est le nombre d'heures.
Ce genre d'erreur de conception est source de bugs et doit être évité.
]
[ = Immutabilité des paramètres du constructeur
Dans le chapitre sur l'immutabilité, nous avons vu qu'il n'est pas évident de décider si les paramètres des types référence devraient être déclarés [c const] ou [c immutable]. Bien que les mêmes considérations s'appliquent aux paramètres de constructeurs, [c immutable] est souvent un bien meilleur choix pour les paramètres de constructeur.
La raison en est qu'il est courant d'assigner aux membres des paramètres qui seront utilisés plus tard. Quand un paramètre n'est pas [c immutable], il n'y a aucune garantie que la variable originale n'ait pas été modifiée entre temps.
Considérons un constructeur qui prend un nom de fichier en paramètre. Le nom de fichier sera utilisé ultérieurement lorsqu'on écrira des notes d'élèves. Selon les lignes directrices du chapitre sur l'immutabilité, pour être plus utile, supposons que le paramètre du constructeur soit déclaré comme [c const char~[~]] :
[code=d <<<
import std.stdio;
struct Eleve
{
const char[] nomFichier;
int[] notes;
this(const char[] nomFichier)
{
this.nomFichier = nomFichier;
}
void enregistrer()
{
auto fichier = File(nomFichier.idup, "w");
fichier.writeln("Les notes de l'élève :");
fichier.writeln(notes);
}
// ...
}
void main()
{
char[] nomFichier;
nomFichier ~= "notes_eleve";
auto eleve = Eleve(nomFichier);
/* supposons que le nom de fichier soit modifié plus tard
* peut-être par inadvertance (tous les caractères sont transformés
* en 'A' ici) : */
nomFichier[] = 'A';
// ...
/* les notes sont enregistrées dans le mauvais fichier */
eleve.enregistrer();
}
>>>]
Le programme ci-avant enregistre les notes de l'élève dans un fichier dont le nom est constitué de caractères [c A], pas dans « notes_eleve ». C'est pour cette raison qu'il est parfois plus judicieux de déclarer les paramètres d'un constructeur et les variables membres de types références comme immutable. Nous savons que cela est facile pour les chaînes de caractères en utilisant des alias comme [c string]. Le code suivant montre les parties de la structure qui devraient être modifiées :
[code=d <<<
struct Eleve
{
string nomFichier;
// ...
this(string nomFichier
{
// ...
}
// ...
}
>>>]
Maintenant les utilisateurs de la structure doivent fournir des chaînes immuables, en conséquence de quoi la confusion sur le nom de fichier pourra être évitée.
]
[ = Conversion de types via des constructeurs à un paramètre
Les constructeurs à un seul paramètre peuvent être vus comme des sortes de fonctions de conversion. Ces constructeurs produisent un objet d'un type structure particulier depuis un paramètre de constructeur. Par exemple, le constructeur suivant produit un objet [c Etudiant] depuis une chaîne de caractères :
[code=d <<<
struct Etudiant
{
string nom;
this(string nom)
{
this.nom = nom;
}
}
>>>]
La fonction [c to()] et l'opérateur cast se comportent de la même manière. Pour en voir un exemple, considérons la fonction saluer() suivante. Passer en paramètre une chaîne de caractère alors qu'elle attend un [c Etudiant] causerait bien évidemment une erreur de compilation :
[code=d <<<
void saluer(Etudiant etudiant)
{
writeln("Bonjour ", etudiant.nom);
}
// ...
saluer("Jane"); // Erreur de compilation !
>>>]
D'un autre côté, chacune des lignes suivantes s'assure qu'un objet Etudiant est construit avant d'appeler la fonction :
[code=d <<<
import std.conv;
// ...
saluer(Etudiant("Jane"));
saluer(to!Etudiant("Jean"));
saluer(cast(Etudiant)"Jim");
>>>]
[c to()] et [c cast] utilisent le constructeur à un paramètre pour construire un objet [c Etudiant] temporaire et appeler [c saluer()] avec cet objet.
]
[ = Désactiver le constructeur par défaut
Les fonctions qui sont déclarées avec l'attribut [c @disable] ne peuvent pas être appelées.
Parfois, il n'y a pas de valeurs par défaut pertinentes pour les membres d'un type. Par exemple, il pourrait être illégal pour le type suivant d'avoir un nom de fichier vide :
[code=d <<<
struct Archive
{
string nomFichier;
}
>>>]
Malheureusement, le constructeur par défaut généré par le compilateur l'initialiserait par une chaîne vide :
[code=d <<<
auto archive = Archive(); // nomFichier est vide
>>>]
Le constructeur par défaut peut être explicitement désactivé en le déclarant comme @disable, pour que ces objets doivent être construits par un des autres constructeurs. Il n'y a pas besoin de fournir un corps à une fonction @disabled :
[code=d <<<
struct Archive
{
string nomFichier;
@disable this(); // ne peut être appelé
this(string nomFichier)
{
//...
}
}
// ...
auto archive = Archive(); // Erreur de compilation
>>>]
Cette fois, le compilateur n'autorise pas l'appel à this() :
[output <<<
Erreur: le constructeur Archive.this n'est pas appelable, car il est annoté avec @disable
>>>]
Les objets Archive doivent être construits avec un autre constructeur :
[code=d <<<
auto archive = Archive("enregistrements"); // compile
>>>]
]
]
[ = Le destructeur
Le destructeur contient des opérations qui doivent être exécutées à la fin de fin d'un objet.
Le destructeur généré par le compilateur exécute les destructeurs de tous les membres dans l'ordre. De cette façon, comme pour le constructeur par défaut, il n'est souvent pas nécessaire de définir de destructeur pour la plupart des structures.
Néanmoins, certaines opérations spéciales peuvent nécessiter d'être exécutées à la fin de vie d'un objet. Par exemple, une ressource du système d'exploitation possédée peut devoir être libérée ; une méthode d'un autre objet peut devoir être appelée ; un serveur tournant quelque part sur le réseau peut devoir être informé qu'une connexion va être fermée.
Le nom du destructeur est [c ~~this] et comme les constructeurs, n'a pas de type de retour.
[ = Le destructeur est appelé automatiquement
Le destructeur est exécuté dès que la vie de l'objet se termine. Comme vous pouvez vous en souvenir du chapitre sur les durées de vie et les opérations fondamentales, la durée de vie d'un objet se termine lorsque celui-ci quitte le bloc dans lequel il est défini.
La durée de vie d'une structure arrive à son terme lorsque l'on quitte le bloc de visibilité de l'objet, que ce soit normalement ou parce qu'une exception est lancée.
[code=d <<<
if (uneCondition) {
auto duree = Duree(7)
// ...
} // Le destructeur de 'duree' est appelé à cet endroit
>>>]
Les objets anonymes sont détruits à la fin de l'expression qui les a construits
[code=d <<<
moment.incrementer(Duree(5)); // l'objet Duree(5) est détruit
// à la fin de cette expression
>>>]
Tous les membres d'un objet structure sont détruits lorsque l'objet englobant est détruit.
]
[ = Exemple de destructeur
Concevons un type pour générer des documents XML simples. Les éléments XML sont définis par des chevrons. Ils peuvent contenir des données et d'autres éléments XML. Les éléments XML peuvent aussi avoir des attributs ; nous les ignorerons ici.
Notre but est de nous assurer qu'un élément qui a été ouvert par un tag [c <nom>] sera toujours fermé par un tag [c </nom>] :
[code <<<
<classe1> ← ouverture de l'élément XML extérieur
<note> ← ouverture de l'élément XML intérieur
57 ← les données
</note> ← fermeture de l'élément XML intérieur
</class1> ← fermeture de l'élément XML extérieur
>>>]
Une structure qui peut produire le résultat ci-dessus peut être définie par deux membres qui contiennent le tag de l'élément XML et le niveau d'indentation à utiliser lors de l'affichage.
[code=d <<<
struct ElementXML
{
string nom;
string indentation;
}
>>>]
Si les responsabilités d'ouverture et de fermeture de l'élément XML sont données au constructeur et au destructeur respectivement, le résultat attendu peut être produit en gérant les durées de vie des objets ElementXML. Par exemple, le constructeur peut afficher [c <balise>] et le destructeur peut afficher [c </balise>].
Voici la définition du constructeur pour produire la balise d'ouverture :
[code=d <<<
this(in string nom, in int niveau)
{
this.nom = nom;
this.indentation = chaineIndentation(niveau);
writeln(indentation, '<', nom, '>');
}
>>>]
Et voici la fonction chaineIndentation() :
[code=d <<<
import std.array;
// ...
string chaineIndentation(in int niveau)
{
return replicate(" ", niveau * 2);
}
>>>]
La fonction appelle [c replicate()] du module [c std.array] qui construit et renvoie une nouvelle chaine constituée de la valeur passée en paramètre répétée un certain nombre de fois.
Le destructeur peut être défini de la même manière que le constructeur afin de produire la balise fermante.
[code=d <<<
~this()
{
writeln(indentation, "</", nom, '>');
}
>>>]
Voici un code de test pour montrer l'effet des appels aux constructeur et destructeur automatique.
Voici un code de test pour montrer l'effet automatique des appels au constructeur et au destructeur.
[code=d <<<
import std.conv;
import std.random;
void main()
{
immutable classes = ElementXML("classes", 0);
foreach (idClasse; 0..2) {
immutable tagClasse = "classe" ~ to!string(idClasse);
immutable elementTag = ElementXML(tagClasse, 1);
foreach (i; 0..3)
{
immutable elementNote = ElementXML("note", 2);
immutable noteAleatoire = uniform(50, 101);
writeln(chaineIndentation(3), noteAleatoire);
}
}
}
>>>]
Notez que les objets [c ElementXML] sont créés dans trois blocs de portée dans le programme ci-dessus. Les tags d'ouverture et de fermeture des éléments XML sont produits seulement par le constructeur et le destructeur de ElementXML.
J'ai indiqué l'ouverture et la fermeture des tags des blocs extérieurs, intermédiaires et intérieurs par différentes couleurs :
[code=xml <<<
<classes>
<classe0>
<note>
72
</note>
<note>
97
</note>
<note>
90
</note>
</classe0>
<classe1>
<note>
77
</note>
<note>
87
</note>
<note>
56
</note>
</classe1>
</classes>
>>>]
L'élément classes est produit par la variable classes. Parce que cette variable est construite en premier dans la fonction [c main()], la sortie contient le résultat de sa construction est premier. Puisque c'est également la variable qui est détruite en dernier, avant de quitter [c main()], la sortie contient la sortie de l'appel du destructeur en dernier.
]
[ = Postcopie
Copier un objet consiste à construire un nouvel objet à partir d'un autre existant. La copie se déroule en deux étapes :
# La copie des membres de l'objet existant bit à bit. Cette étape est appelée ''blit'', labréviation anglaise de ''block transfer''.
# Faire d'autres ajustements sur le nouvel objet. Cette étape est appelée postcopie (''postblit'' en anglais).
La première étape est prise en charge automatiquement par le compilateur. Il copie les membres de l'objet existant vers les membres du nouvel objet :
[code=d <<<
auto dureeVoyageRetour = dureeVoyage; // copie
>>>]
Ne faites pas de confusion entre copie et affectation. Le mot-clé [c auto] ci-dessus est une indication qu'un nouvel objet est défini. Le nom du type réel aurait pu être renseigné à la place de auto.
Pour qu'une opération soit une affectation, l'objet à gauche doit être un objet existant. Par exemple, si a variable [c dureeVoyageRetour] avait déjà été défini :
[code=d <<<
dureeVoyageRetour = dureeVoyage; // affectation
>>>]
Il est parfois nécessaire de faire quelques ajustements aux membres du nouvel objet après le ''blit'' automatique. Ces opérations sont définies dans la fonction postcopie de la structure.
Puisqu'il s'agit de construction d'objet, le nom de la fonction postcopie est également [c this]. Pour le dissocier des autres constructeurs, sa liste de paramètre contient le mot-clé [c this] :
[code=d <<<
this(this)
{
// ...
}
>>>]
Nous avons défini un type [c Etudiant] dans le chapitre sur les structures, qui avait un problème sur la copie d'objets de ce type :
[code=d <<<
struct Etudiant
{
int nombre;
int[] notes;
}
>>>]
Étant une tranche, le membre notes de cette structure est un type référence. En conséquence, après la copie d'un objet [c Etudiant], les membres notes des deux objets (la copie et l'original) permettent l'accès aux même tableau d'entiers. La modification d'une note via un de ces objets est visible à travers l'autre objet :
[code=d <<<
auto etudiant1 = Etudiant(1, [70, 90, 85]);
auto etudiant2 = etudiant1; // copie
etudiant2.nombre = 2;
etudiant1.notes[0] += 5; // ceci change aussi la note du second etudiant
assert(etudiant2.notes[0] == 75);
>>>]
Pour éviter cette confusion, les éléments du membre notes du second objet doivent être séparés et n'appartenir qu'à cet objet. Ce genre d'ajustement est fait dans la postcopie :
[code=d <<<
struct Etudiant
{
int nombre;
int[] notes;
this(this)
{
notes = notes.dup;
}
}
>>>]
Souvenez-vous que tous les membres ont déjà été copiés automatiquement avant que [c this(this)] ne soit exécuté. La seule ligne de la postcopie ci-avant crée une copie des éléments du tableau original et en affecte une tranche à notes. Au final le nouvel objet obtient sa propre copie des notes.
Modifier les notes depuis le premier objet n'affecte plus le second objet :
[code=d <<<
etudiant1.notes[0] += 5;
assert(etudiant2.notes[0] == 70);
>>>]
]
[ = Désactiver la postcopie
La fonction postcopie peut également être désactivée avec [c @disable]. Les objets marqués ainsi ne peuvent être copiés :
[code=d <<<
struct Archive
{
// ...
@disable this(this);
}
// ...
auto a = Archive("enregistrements");
auto b = a; // Erreur de compilation
>>>]
Le compilateur n'autorise pas l'appel à la fonction postcopie désactivée
Erreur: struct Archive n'est pas copiable, car elle est annotée avec @disable
]
]
[ = Opérateur d'affectation
L'opérateur d'affectation donne une nouvelle valeur à un objet existant :
[code=d <<<
dureeVoyageRetour = dureeVoyage; // affectation
>>>]
L'affectation est plus compliquée que les autres fonctions spéciales parce qu'elle est une combinaison de deux opérations :
La destruction de l'objet de gauche
La copie de l'objet de droite vers l'objet de gauche.
Néanmoins, appliquer ces deux opérations dans cet ordre est risqué, car l'objet original serait détruit avant même de savoir que la copie sera un succès. Une exception lancée durant l'opération de copie pourrait laisser l'objet destination dans un état incohérent : pleinement détruit, mais en partie copié.
Pour cette raison, l'opérateur d'affectation automatiquement généré par le compilateur se montre prudent en procédant par étapes :
# Copie de l'objet de droite dans un temporaire.
C'est la moitié de l'opération d'affectation. Puisque l'objet de gauche n'est pas encore modifié, il restera intact si une exception est lancée durant cette opération.
# Destruction de l'objet de gauche.
C'est l'autre étape de l'opération d'affectation.
# Transfert de l'objet temporaire vers l'objet de gauche.
Aucune postcopie ni aucun destructeur ne sont exécutés durant cette opération. Au final, l'objet de gauche devient l'équivalent de l'objet temporaire.
Après les étapes ci-avant, l'objet temporaire disparaît et seuls l'objet de droite et sa copie (c'est-à-dire l'objet de gauche) restent.
Bien que l'opérateur d'affectation généré par le compilateur soit suffisant dans la plupart des cas, il peut être défini par le programmeur. Lorsque vous faites cela, gardez à l'esprit les causes potentielles d'exceptions et écrivez l'opérateur d'affectation pour qu'il se comporte correctement en présence d'exceptions.
La syntaxe de l'opérateur d'affectation est la suivante :
- Le nom de la fonction est opAssign.
- Le type du paramètre est le même que celui de la structure. Ce paramètre est souvent appelé [c rhs] (pour ''Right Hand Side'', côté droit en anglais).
- Le type de retour est le même que celui de la structure.
- La fonction se termine par [c return this].
Par exemple, considérons une structure [c Duree] simple, où l'opérateur d'affectation affiche un message :
[code=d <<<
struct Duree
{
int minute;
Duree opAssign(Duree rhs)
{
writefln("minute est modifié de %s à %s",
this.minute, rhs.minute);
this.minute = rhs.minute;
return this;
}
}
// ...
auto duree = Duree(100);
duree = Duree(200); // affectation
>>>]
Le résultat :
[output <<<
minute est modifié de 100 à 200.
>>>]
[ = Affectation d'autres types
Il est parfois commode d'affecter des valeurs de types différents du type de la structure. Par exemple, plutôt que de nécessiter la création d'un objet Duree sur le coté droit, il peut être utile d'affecter directement un entier :
[code=d <<<
duree = 300;
>>>]
C'est possible en définissant un autre opérateur d'affectation qui prend un entier en paramètre :
[code=d <<<
struct Duree
{
int minute;
Duree opAssign(Duree rhs)
{
writefln("minute est modifié de %s à %s",
this.minute, rhs.minute);
this.minute = rhs.minute;
return this;
}
Duree opAssign(int minute)
{
writefln("minute est remplacé par un entier");
this.minute = minute;
return this;
}
}
// ...
duree = Duree(200);
duree = 300;
>>>]
En résultat :
[output <<<
minute est modifié de 100 à 200
minute est remplacé par un entier
>>>]
Note : Bien que ce soit commode, affecter différents types entre eux peut semer la confusion et être source d'erreurs.
]
]
[ = En résumé
- Le constructeur ([c this]) prépare les objets à être utilisés. Le constructeur par défaut généré par le compilateur est suffisant dans la plupart des cas.
- Le comportement du constructeur par défaut ne peut être changé pour les structures. On peut néanmoins utiliser static opCall à la place.
- Les constructeurs avec un seul paramètre peuvent être utilisés pour les conversions de type avec to et cast.
- Le destructeur ([c ~~this]) exécute des opérations à la fin de vie d'un objet.
- La postcopie (this(this)) fait des ajustements à un objet après une copie automatique de ses membres.
- L'opérateur d'affectation (opAssign) permet la modification d'objets existants.
]

View File

@ -323,7 +323,7 @@ Une partie du pouvoir de [c foreach] vient du fait qu'elle peut être utilisée
1
>>>]
L'utilisation de [c foreach_reverse] n'est pas répandue parce que la fonction ``retro()`` fait la même chose. Nous verrons cette fonction dans un chapitre suivant.
L'utilisation de [c foreach_reverse] n'est pas répandue parce que la fonction d'intervalle ``retro()`` fait la même chose. Nous verrons cette fonction dans un chapitre suivant.
]
[ = Exercice

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -1,32 +1,33 @@
[set
title = "L'environnement du programme"
partAs = chapitre
title = "L'environnement du programme"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Nous avons vu que [c main] est une fonction. L'exécution du programme commence par [c main] et continue avec d'autres fonction depuis là. La définition de [c main] que nous avons utilisé jusqu'à maintenant est :
Nous avons vu que [c main()] est une fonction. L'exécution du programme commence par [c main()] et continue avec d'autres fonctions depuis là. La définition de [c main()] que nous avons utilisée jusqu'à maintenant est~ :
[code=d <<<
void main()
>>>]
Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas de valeur. Il n'est en fait pas possible de ne pas retourner de valeur, parce que les environnements qui démarrent les programmes s'attendent à avoir des valeurs de retours de leur part. Même s'il est possible de spécifier le type de retour de [c main] comme [c void], elle retourne quand même une valeur.
Selon cette définition, [c main()] ne prend pas de paramètre et ne retourne pas de valeur. En réalité, il est impossible de ne pas retourner de valeur, parce que les environnements qui démarrent les programmes s'attendent à avoir des valeurs de retour de leur part. Même s'il est possible de spécifier le type de retour de [c main()] comme [c void], elle retourne quand même une valeur.
[ = La valeur de retour de [c main]
[ = La valeur de retour de [c main()]
Les programmes sont toujours démarrés par une entité dans un environnement particulier. L'entité qui démarre le programme peut être le ''shell'' où l'utilisateur tape le nom du programme et appuie sur la touche Entrée, ou ça peut être un environnement de développement où le programmeur clique sur le bouton ''Exécuter'', etc.
Les programmes sont toujours démarrés par une entité dans un environnement particulier. L'entité qui démarre le programme peut être l'interpréteur de commandes (''shell'') où l'utilisateur tape le nom du programme et appuie sur la touche Entrée, ou ça peut être un environnement de développement où le programmeur clique sur le bouton ''Exécuter'', etc.
Le programme communique son code de retour à son environnement par la valeur de retour de [c main].
Le programme communique son code de retour à son environnement par la valeur de retour de [c main()].
Une valeur de retour de 0 veut dire que tout s'est bien passé et n'importe quelle autre valeur indique un certain type d'échec. Même si la valeur de retour appartient au programmeur, la valeur 0 veut toujours dire réussite, par convention.
Une valeur de retour de 0 veut dire que tout s'est bien passé et n'importe quelle autre valeur indique un certain type d'échec. Bien que le choix de la valeur de retour appartienne au programmeur, la valeur 0 veut toujours dire réussite, par convention.
[p Note : | Seules les valeurs dans l'intervalle ~[0,127~] sont portables ; tous les environnements ne prennent pas en charge d'autres valeurs.]
[p Note~ : | Seules les valeurs dans l'intervalle ~[0,127~] sont portables~ ; tous les environnements ne prennent pas en charge d'autres valeurs.]
Les valeurs autres que 0 peuvent avoir différents sens en fonction de chaque programme. Par exemple, le programme Unix [c ls], qui est utilisé pour lister le contenu des répertoires, retourne 1 pour les erreurs mineurs et 2 pour les erreurs sérieuses. Dans beaucoup d'environnements, la valeur de retour du programme qui a été exécuté le plus récemment dans la console peut être vu à travers la variable d'environnement [c $?]. Par exemple, quand on demande à [c ls] pour lister un fichier qui n'existe pas, sa valeur de retour non nulle peut être vue avec [c $?] comme ci-après.
Les valeurs autres que 0 peuvent avoir différents sens en fonction de chaque programme. Par exemple, le programme Unix [c ls], qui est utilisé pour lister le contenu des répertoires, retourne 1 pour les erreurs mineurs et 2 pour les erreurs sérieuses. Dans beaucoup d'environnements, la valeur de retour du programme qui a été exécuté le plus récemment dans la console peut être vu à travers la variable d'environnement [c $?]. Par exemple, quand on demande à [c ls] de lister un fichier qui n'existe pas, sa valeur de retour non nulle peut être vue avec [c $?] comme ci-dessous.
[p Note : | Dans la session en lignes de commande suivante, les lignes qui commencent par [c #] indiquent les lignes que l'utilisateur tape. Si vous voulez essayer les mêmes commandes, vous devez saisir le contenu de ces lignes sans le caractère [c #]. Aussi, les commences ci-après démarrent un programme nommé [c deneme] ; remplacer ce nom avec le nom de votre programme test.]
[p Note~ : | dans la session en ligne de commande suivante, les lignes qui commencent par [c #] indiquent les lignes que l'utilisateur tape. Si vous voulez essayer les mêmes commandes, vous devez saisir le contenu de ces lignes sans le caractère [c #]. En outre, les commandes tapées plus loin démarrent un programme nommé [c essai]~ ; remplacer ce nom avec le nom de votre programme test.]
De plus, même si les exemples suivant affichent des interactions dans une console Linux, elles seraient similaire mais pas exactement le même dans des consoles d'autres systèmes d'exploitation.
De plus, même si les exemples suivant affichent des interactions dans une console Linux, elles seraient similaires mais pas exactement les mêmes dans des consoles d'autres systèmes d'exploitation.
[code <<<
@ -38,11 +39,11 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
[ = [c main] retourne toujours une valeur
Certains programmes que nous avons écrit jusqu'à maintenant on levé des exceptions quand ils ne pouvaient pas continuer leurs tâches. D'après ce que nous avons vu jusqu'à maintenant, quand une exception est levée, le programme finit avec un message d'erreur [c object.Exception].
Certains programmes que nous avons écrits jusqu'à maintenant on levé des exceptions quand ils ne pouvaient pas continuer leurs tâches. D'après ce que nous avons vu jusqu'à maintenant, quand une exception est levée, le programme finit avec un message d'erreur [c object.Exception].
Quand cela arrive, même si [c main] a été définie comme retournant [c void], une valeur de 1 est automatiquement retournée à l'environnement du programme.
Quand cela arrive, même si [c main()] a été définie comme retournant [c void], une valeur de 1 est automatiquement retournée à l'environnement du programme.
Voyons ceci en action avec ce programme suivant qui se finit avec une exception :
Voyons ceci en action avec ce programme qui se finit avec une exception~ :
[code=d <<<
void main()
@ -51,15 +52,15 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
Même si le type de retour est spécifié comme [c void], la valeur de retour est 1 :
Même si le type de retour est spécifié comme [c void], la valeur de retour est 1~ :
[code <<<
# ./deneme
# ./essai
object.Exception: Il y a eu une erreur.
# echo $?
1
>>>]
De façon similaire, les fonction [c void main()] qui terminent correctement retournent aussi 0 automatiquement. Voyons ceci avec le programme suivant qui termine correctement :
De façon similaire, les fonction [c void main()] qui terminent correctement retournent aussi 0 automatiquement. Voyons ceci avec le programme suivant qui termine correctement~ :
[code=d <<<
import std.stdio;
@ -70,9 +71,9 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
Le programme retourne 0 :
Le programme retourne 0~ :
[code <<<
# ./deneme
# ./essai
Fait !
# echo $?
0
@ -81,7 +82,7 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
]
[ = Spécifier la valeur de retour
Retourner une valeur de retour spécifique depuis [c main] est la même chose que retourner une valeur depuis n'importe quelle fonction. Le type de retour doit être positionné à [c int] et la valeur doit être retournée par l'instruction [c return] :
Retourner une valeur de retour spécifique depuis [c main()] est la même chose que retourner une valeur depuis n'importe quelle fonction. Le type de retour doit être positionné à [c int] et la valeur doit être retournée par l'instruction [c return]~ :
[code=d <<<
@ -104,53 +105,52 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
Quand le nombre saisi est dans l'intervalle de validité, la valeur de retour du programme est 0 :
Quand le nombre saisi est dans l'intervalle de validité, la valeur de retour du programme est 0~ :
[code <<<
# ./deneme
# ./essai
Veuillez saisir un nombre entre 3 et 6 : 5
Merci d'avoir saisi 5.
# echo $?
0
>>>]
Quand le nombre est hors de l'intervalle de validité, la valeur de retour du programme est 111 :
Quand le nombre est hors de l'intervalle de validité, la valeur de retour du programme est 111~ :
[code <<<
# ./deneme
# ./essai
Veuillez saisir un nombre entre 3 et 6 : 10
ERREUR : 10 n'est pas valide !
# echo $?
$(DARK_GRAY $(HILITE 111))
)
111
>>>]
La valeur de 11 dans le programme précédent est arbitraire ; normalement 1 est utilisé pour le code d'échec.
La valeur de 111 dans le programme précédent est arbitraire~ ; normalement 1 est utilisé pour le code d'échec.
]
]
[ = Le flux standard d'erreur [c stderr]
Le programme ci-avant utilise le flux [c stderr]. Ce flux est un des trois flux standards. Il est utilisé pour écrire des messages d'erreurs :
- [c stdin] : flux d'entrée standard
- [c stdout] : flux de sortie standard
- [c stderr] : flux d'erreur standard
Le programme ci-avant utilise le flux [c stderr]. Il est utilisé pour écrire des messages d'erreurs. Ce flux est un des trois flux standards~ :
- [c stdin]~ : flux d'entrée standard~ ;
- [c stdout]~ : flux de sortie standard~ ;
- [c stderr]~ : flux d'erreur standard.
Quand un programme est lancé dans une console, normalement les messages qui sont écrits dans [c stdout] et [c stderr] apparaissent tous dans la console. Quand c'est nécessaire, il est possible de rediriger ces sorties individuellement. Ceci est hors du programme de ce chapitre et les détails peuvent varier pour chaque émulateur de terminal.
Quand un programme est lancé dans une console, normalement les messages qui sont écrits dans [c stdout] et [c stderr] apparaissent tous dans la console. Quand c'est nécessaire, il est possible de rediriger ces sorties individuellement. Ceci est hors du programme de ce chapitre et les détails peuvent varier pour chaque ''shell''.
]
[ = Paramètres de [c main]
[ = Paramètres de [c main()]
Il est commun pour les programmes de prendre des paramètres depuis l'environnement qui les ont démarré. Par exemple, nous avons déjà passé un fichier en option en ligne de commande à [c ls] ci-avant. Il y a deux options en ligne de commande dans la ligne suivante :
Il est courant que les programmes prennent des paramètres depuis l'environnement qui les a démarrés. Par exemple, nous venons de passer un nom de fichier en option en ligne de commande à [c ls]. Il y a deux options en ligne de commande dans la ligne suivante~ :
[code <<<
# ls -l deneme
-rwxr-xr-x 1 acehreli users 460668 Nov 6 20:38 deneme
# ls -l essai
-rwxr-xr-x 1 acehreli users 460668 Nov 6 20:38 essai
>>>]
L'ensemble des paramètres en ligne de commande et leurs significations sont entièrement définis par le programme. Chaque programme documente son usage, dont la signification de chaque paramètre.
Les arguments qui sont utilisés lors du démarrage d'un programme D sont passés à la fonction main de ce programme comme une tranche de [c string]s. Définir [c main] comme prenant un paramètre de type [c string~[~]] est suffisant pour avoir accès aux arguments du programme. Le nom de ce paramètre est souvent appelé [c args]. Le programme suivant affiche tous les arguments qui on été passés au programme :
Les arguments qui sont utilisés lors du démarrage d'un programme D sont passés à la fonction [c main()] de ce programme sous la forme d'une tranche de [c string]s. Il suffit de définir [c main()] comme prenant un paramètre de type [c string~[~]] pour avoir accès aux arguments du programme. Le nom de ce paramètre est souvent appelé [c args]. Le programme suivant affiche tous les arguments qui ont été passés au programme~ :
[code=d <<<
import std.stdio;
@ -163,11 +163,11 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
Lançons ce programme avec des arguments arbitraires :
Lançons ce programme avec des arguments arbitraires~ :
[code <<<
# ./deneme des arguments en ligne de commande 42 --une-option
Argument 0 : ./deneme
# ./essai des arguments en ligne de commande 42 --une-option
Argument 0 : ./essai
Argument 1 : des
Argument 2 : arguments
Argument 3 : en
@ -178,17 +178,17 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
Argument 8 : --une-option
>>>]
Le premier argument est toujours le nom du programme, tel que l'utilisateur l'a écrit. Les autres arguments apparaissent dans le même ordre dans lequel ils ont été saisis.
Comment sont utilisés les arguments appartient complètement au programme. Le programme suivant affiche ses deux arguments dans l'ordre inverse :
Le premier argument est toujours le nom du programme, tel que l'utilisateur l'a écrit. Les autres arguments apparaissent dans l'ordre dans lequel ils ont été saisis.
La manière d'utiliser les arguments est complètement du ressort du programme. Le programme suivant affiche ses deux arguments dans l'ordre inverse~ :
[code=d <<<
import std.stdio;
int main(string[] args)
{
if (args.length != 3) {
stderr.writefln("ERREUR ! utilisation correcte :\n"
stderr.writefln("ERREUR ! Utilisation correcte :\n"
" %s mot1 mot2", args[0]);
return 1;
}
@ -199,39 +199,39 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
Le programme affiche aussi son utilisation correcte s'il n'y a pas exactement deux mots :
Le programme affiche aussi son utilisation correcte s'il n'y a pas exactement deux mots~ :
[code <<<
# ./deneme
ERREUR ! utilisation correcte :
./deneme mot1 mot2
# ./deneme mot salut
# ./essai
ERREUR ! Utilisation correcte :
./essai mot1 mot2
# ./essai mot salut
salut mot
>>>]
]
[ = Options en ligne de commande et le module [c std.getopt]
C'est tout ce qu'il y a à savoir sur les paramètres et la valeur de retour de [c main]. Cependant, analyser les arguments est une tâche répétitive. Le module [c std.getopt] est conçu pour aider l'analyse des options en lignes de commande du programme.
C'est tout ce qu'il y a à savoir sur les paramètres et la valeur de retour de [c main()]. Cependant, analyser les arguments est une tâche répétitive. Le module [c std.getopt] est conçu pour aider à analyser les options en lignes de commande du programme.
Certains paramètres comme "mot" et "salut" ci-avant sont purement des données utilisées par le programme. D'autres types de paramètres sont appelés [* options en ligne de commande], et sont là pour changer le comportement du programme. Un exemple d'une option en ligne de commande qy'on a déjà rencontré est [c -l], qui a été passé à [c ls] ci-avant.
Certains paramètres comme «~ mot~ » et «~ salut~ » dans l'exemple précédent sont purement des données utilisées par le programme. D'autres types de paramètres sont appelés [* options en ligne de commande], et sont là pour changer le comportement du programme. Un exemple d'une option en ligne de commande qu'on a déjà rencontré est [c -l], qui a été passé à [c ls].
Les options en ligne de commande rendent les programmes plus utiles en supprimant le besoin pour un utilisateur humain d'interagir avec le programme pour obtenir de lui un comportement particulier. Avec les options en ligne de commande, les programmes peuvent être lancés à partir de scripts et leurs comportements peuvent être spéficiés à travers les options en ligne de commande.
Les options en ligne de commande rendent les programmes plus utiles en supprimant le besoin qu'un utilisateur humain interagisse avec le programme pour obtenir de lui un comportement particulier. Avec les options en ligne de commande, les programmes peuvent être lancés à partir de scripts et leurs comportements peuvent être spéficiés à travers les options en ligne de commande.
Même si la syntaxe et le sens des arguments en ligne de commande de chaque programme est spécifique à ce programme, leur format est plus ou moins standard. Par exemple, en POSIX, les options en ligne de commande commencent par [c <<<-->>>] suivi par le nom de l'option, et les valeurs viennent après des caractère [c =] :
Même si la syntaxe et le sens des arguments en ligne de commande de chaque programme est spécifique à ce programme, leur format est plus ou moins standard. Par exemple, en POSIX, les options en ligne de commande commencent par «~ [c <<<-->>>]~ » suivi par le nom de l'option, et les valeurs viennent après des caractères «~ [c =]~ »~ :
[code <<<
# ./deneme --an-option=17
# ./essai --an-option=17
>>>]
Le module [c std.getopt] simplifie analyse de tels options. Il a plus de capacité que ce qui est couvert dans cette section.
Le module [c std.getopt] simplifie l'analyse de telles options. Il offre plus de possibilités que celles qui vont être couvertes dans cette section.
Concevons un programme qui affiche des nombres aléatoires. Prenons le minimum, le maximum et le nombre total de ces nombres en arguments du programme. On s'attendra à la syntaxe suivante pour obtenir ces valeurs depuis la ligne de commande :
Concevons un programme qui affiche des nombres aléatoires. Prenons le minimum, le maximum et le nombre total de ces nombres en arguments du programme. On s'attendra à la syntaxe suivante pour obtenir ces valeurs depuis la ligne de commande~ :
[code <<<
# ./deneme --nombre=7 --minimum=10 --maximum=1
# ./essai --nombre=7 --minimum=10 --maximum=1
>>>]
La fonction [c getopt] analyse et affecte ces valeurs aux variables. De même que [c readf], les adresses des variables doivent être spécifiées avec l'opérateur [c &] :
La fonction [c getopt()] analyse et affecte ces valeurs aux variables. Comme quand on utilise [c readf()], on doit spécifier les adresses des variables au moyen de l'opérateur [c &]~ :
[code=d <<<
import std.stdio;
@ -258,32 +258,32 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
>>>]
[code <<<
# ./deneme --nombre=7 --minimum=10 --maximum=15
11 11 13 11 14 15 10
# ./essai --nombre=7 --minimum=10 --maximum=15
11 11 13 11 14 15 10
>>>]
La plupart des options en lignes de commande de la plupart des programmes ont aussi des syntaxes plus courte. Par exemple, [c -c] peut avoir le même sens que [c --nombre]. Une syntaxe alternative pour chaque option peut être spécifié après un caractère [c > |]. Il peut y avoir plus d'un raccourci par option :
La plupart des options en lignes de commande de la plupart des programmes ont aussi des syntaxes plus courte. Par exemple, [c -c] peut avoir le même sens que [c --nombre]. Une syntaxe alternative pour chaque option peut être spécifié après un caractère «~ [c >|]~ ». Il peut y avoir plus d'un raccourci par option~ :
[code=d <<<
[code=d <<<
getopt(args,
"nombre$(HILITE |c)", &nombre,
"minimum$(HILITE |n)", &minimum,
"maximum$(HILITE |x)", &maximum);
"nombre|c", &nombre,
"minimum|n", &minimum,
"maximum|x", &maximum);
>>>]
Il est commun d'utiliser un seul tiret pour la version courte et le caractère [c =] est généralement omis.
Il est commun d'utiliser un seul tiret pour la version courte et le caractère «~ [c =]~ » est généralement omis.
[code <<<
# ./deneme -c7 -n10 -x15
# ./essai -c7 -n10 -x15
11 13 10 15 14 15 14
>>>]
[c getopt()] convertit les arguments depuis le type [c string] vers le type de chaque variable. Par exemple, comme la variable [c nombre] ci-dessus est un [c int], [c getopt] convertit la valeur spécifiée avec l'argument [c nombre] en [c int]. Lorsque cela est nécessaire, de telles conversions peuvent aussi être faites explicitement avec [c to].
[c getopt()] convertit les arguments depuis le type [c string] vers le type de chaque variable. Par exemple, comme la variable [c nombre] ci-dessus est un [c int], [c getopt()] convertit la valeur spécifiée avec l'argument [c -~-nombre] en [c int]. Lorsque cela est nécessaire, de telles conversions peuvent aussi être faites explicitement avec [c to].
Jusqu'à maintenant, nous avons utilisé [c std.conv.to] seulement vers [c string]. [c to] peut en fait convertir depuis n'importe quel type vers n'importe quel type pourvu que cette conversion soit possible. Par exemple, le programme suivant utilise [c to] pour convertir ses arguments vers [c size_t] :
Jusqu'à maintenant, nous avons utilisé [c std.conv.to] seulement vers [c string]. [c to] peut en fait convertir depuis n'importe quel type vers n'importe quel type pourvu que cette conversion soit possible. Par exemple, le programme suivant utilise [c to] pour convertir ses arguments vers [c size_t]~ :
[code=d <<<
import std.stdio;
@ -291,12 +291,12 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
void main(string[] args)
{
// The default nombre is 10
// Le nombre par défaut est 10
size_t nombre = 10;
if (args.length > 1) {
// There is an argument
nombre = $(HILITE to!size_t)(args[1]);
// Il y a un argument
nombre = to!size_t(args[1]);
}
foreach (i; 0 .. nombre) {
@ -307,18 +307,18 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
Le programme produit 10 nombres quand il n'y a pas d'argument spécifié :
Le programme produit 10 nombres quand il n'y a pas d'argument spécifié~ :
[code <<<
# ./deneme
# ./essai
0 2 4 6 8 10 12 14 16 18
# ./deneme 3
# ./essai 3
0 2 4
>>>]
]
[ = Les variables d'environnement
L'environnement dans lequel le programme est démarré fournit des variables que le programme peut utiliser. On peut accéder aux variables d'environnement avec [c std.process.getenv]. Le programme suivant accède et affiche la variable d'environnement [c PATH].
L'environnement dans lequel le programme est démarré fournit des variables que le programme peut utiliser. On peut accéder aux variables d'environnement avec [c std.process.getenv]. Le programme suivant affiche la variable d'environnement [c PATH].
[code=d <<<
import std.stdio;
@ -330,15 +330,15 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
}
>>>]
La sortie :
La sortie~ :
[code <<<
# ./deneme
# ./essai
/usr/local/bin:/usr/bin:/home/acehreli/dmd/linux/bin
>>>]
[c std.process.environment] donne accès aux variables d'environnement à travers la syntaxe des tableaux associatifs :
[c std.process.environment] donne accès aux variables d'environnement à travers la syntaxe des tableaux associatifs~ :
[code=d <<<
import std.process;
@ -346,18 +346,18 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
writeln(environment["PATH"]);
>>>]
Cependant, [c environment] n'est en réalité pas un tableau associatif. Lorsque c'est nécessaire, [c environment] peut être convertie vers un tableau associatif avec [c toAA()] :
Cependant, [c environment] n'est en réalité pas un tableau associatif. Lorsque c'est nécessaire, [c environment] peut être converti vers un tableau associatif avec [c toAA()]~ :
[code=d <<<
string[string] envVars = environment.toAA();
>>>]
]
[ = Lancer d'autres programmes
Les programmes peuvent lancer d'autres programmes et devenir l'environnement de ces programmes. La fonction [c executeShell] de module [c std.process] permet cela.
Les programmes peuvent lancer d'autres programmes et devenir l'environnement pour ces programmes. Deux fonctions qui permettent cela sont [c system] et [c shell] du module [c std.process].
[c system] exécute ses paramètres comme si la commande avait été tapé dans la console. Elle retourne ensuite la valeur de cette commande :
[c executeShell] exécute son paramètre comme s'il avait été saisi dans le terminal. Elle retourne aussi bien le code de retour que la sortie de cette commande dans un n-uplet. Les n-uplets (''tuples'') sont des structures qui ressemblent aux tableaux~ ; nous les verrons plus tard dans le chapitre sur les n-upplets.
[code=d <<<
import std.stdio;
@ -365,62 +365,42 @@ Selon cette définition, [c main] ne prent pas de paramètre et ne retourne pas
void main()
{
immutable retVal = system("ls -l deneme");
const resultat = executeShell("ls -l deneme");
const codeRetour = resultat[0];
const sortie = resultat[1];
writefln("ls a retourné %s.", retVal);
writefln("ls a retourné %s.", codeRetour);
writefln("Sa sortie:\n%s", sortie);
}
>>>]
La sortie inclue la sortie de [c ls] suivie par la ligne que ce programme affiche :
Le résultat~ :
[code <<<
[output <<<
# ./deneme
-rwxr-xr-x 1 acehreli users 461107 Nov 7 17:33 deneme
ls has returned 0.
ls a retourné 0.
Sa sortie:
-rwxrwxr-x. 1 acehreli acehreli 1359178 Apr 21 15:01 deneme
>>>]
[c shell] exécute son paramètre dans un environnement shell et retourne la sortie standard de cette commande. Elle lève une exception si le programme n'a pas pu être lancé ou s'il a fini avec une valeur de retour non nulle :
[code=d <<<
import std.stdio;
import std.process;
void main()
{
immutable sortie = shell("ls -l deneme*");
writeln("La sortie de ls :");
writeln(sortie);
}
>>>]
La sortie:
[code <<<
# ./deneme
La sortie de ls :
-rwxrwxr-x 1 acehreli acehreli 923117 2012-03-25 11:19 deneme
-rwxr-xr-x 1 acehreli acehreli 1091303 2012-03-25 11:19 deneme.d
>>>]
]
[ = Résumé
- Même quand elle est définie avec un type de retour [c void], [c main] retourne automatiquement 0 quand en cas de réussite et 1 en cas d'échec.
- Même quand elle est définie avec un type de retour [c void], [c main()] retourne automatiquement 0 en cas de réussite et 1 en cas d'échec.
- [c stderr] est approprié pour afficher des messages d'erreurs.
- [c main] peut prendre des paramètres [c string~[~]].
- [c main()] peut prendre des paramètres [c string~[~]].
- [c std.getopt] facilite l'analyse des options en ligne de commande.
- [c std.process] facilite l'accès aux variables d'environnement et au lancement d'autres programmes.
- [c std.process] facilite l'accès aux variables d'environnement et le lancement d'autres programmes.
]
[ = Exercices
# [
Écrivez une calculatrice qui prend un opérateur et deux opérandes en argument de la ligne de commande. Le programme doit prendre en charge l'utilisation suivante :
Écrivez une calculatrice qui prend un opérateur et deux opérandes en arguments de la ligne de commande. Le programme doit prendre en charge l'utilisation suivante~ :
[code <<<
# ./deneme 3.4 x 7.8
# ./essai 3.4 x 7.8
26.52
>>>]
[p Note : | Parce que le caractère [c *] a une signification spéciale sur la plupart des consoles, j'ai utilisé [c x]. Vous pouvez toujours utiliser [c *] tant qu'il est échappé : [c \*] (NdT: ou [c "*"]).]
[p Note~ : | parce que le caractère «~ [c *]~ » a une signification spéciale sur la plupart des ''shells'', j'ai utilisé «~ [c x]~ ». Vous pouvez toujours utiliser «~ [c *]~ » tant qu'il est échappé ainsi~ : «~ [c \*]~ » (NdT~ : ou ainsi~ : «~ [c "*"]~ »).]
]
# Écrivez un programme qui demande à l'utilisateur quel programme lancer, démarre ce programme et retourne sa valeur de retour.

View File

@ -2,18 +2,19 @@
title = "Les paramètres de fonction"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
]
Ce chapitre couvre les différentes manières de définir des paramètres de fonction.
Certaines idées de ce chapitres sont déjà apparues dans les chapitres précédents. Par exemple, le mot-clé [c ref] que nous avons vu dans le [[part:foreach | chapitre sur les boucles [c foreach]]] rendait les éléments eux-même disponibles dans les boucles [c foreach] au lieu de copies de ces éléments.
Certaines idées de ce chapitre sont déjà apparues dans les chapitres précédents. Par exemple, le mot-clé [c ref] que nous avons vu dans le [[part:foreach | chapitre sur les boucles [c foreach]]] permettait d'accéder directement aux éléments eux-mêmes dans les boucles [c foreach] au lieu d'accéder à des copies de ces éléments.
De plus, nous avons couvert les mots-clés [c const] et [c 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 :
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~ :
[code=d <<<
double moyennePondérée(double noteDuQuiz, double notesFinales)
double moyennePondérée(double noteDuQuiz, double noteFinale)
{
return noteDuQuiz * 0.4 + noteFinale * 0.6;
}
@ -31,9 +32,9 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
[ = La plupart des paramètres sont copiés
Dans le code ci-avant, les deux variables qui sont passées en arguments à [c 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é, ce que la fonction utilise sont des copies de ces variables.
Dans le code qui précède, les deux variables sont passées en arguments à [c 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.-à-d. avoir un effet de bord). Supposons que la fonction suivante est écrite pour réduire l'énergie d'un personnage de jeu :
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.-à-d. avoir un effet de bord). Supposons que la fonction suivante soit écrite pour réduire l'énergie d'un personnage de jeu~ :
[code=d <<<
void reduireEnergie(double energie)
@ -42,7 +43,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Voici un programme qui teste [c reduireEnergie()] :
Voici un programme qui teste [c reduireEnergie()]~ :
[code=d <<<
import std.stdio;
@ -57,19 +58,19 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
double energie = 100;
reduireEnergie(energie);
writeln("Nouvelle energie: ", energie);
writeln("Nouvelle energie : ", energie);
}
>>>]
La sortie :
La sortie~ :
[output <<<
Nouvelle energie: 100 ← Non changée !
Nouvelle energie : 100 ← Non changée !
>>>]
Même si [c reduireEnergie()] divise la valeur de son paramètres par 4, la variable [c energie] dans [c main()] ne change pas. La raison à ceci est que la variable [c energie] dans [c main()] et le paramètre [c energie] de [c reduireEnergie()] sont distincts ; le paramètre est une copie de la variable de [c main()].
Même si [c reduireEnergie()] divise la valeur de son paramètre par 4, la variable [c energie] dans [c main()] ne change pas. La raison à ceci est que la variable [c energie] dans [c main()] et le paramètre [c energie] de [c reduireEnergie()] sont distincts~ ; le paramètre est une copie de la variable de [c main()].
Pour observer cela plus précisément, plaçons quelques [c writeln()] :
Pour observer cela plus précisément, plaçons quelques [c writeln()]~ :
[code=d <<<
import std.stdio;
@ -100,29 +101,30 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
Après l'appel de la fonction : 100 ← la variable reste la même
>>>]
]
[ = Les types objets ou référence ne sont pas copiés
Les éléments des tranches et des tableaux associatifs et les objets de classes ne sont pas copiés quand ils sont passés en paramètres. De telles variables sont passés 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.
[ = Les objets de type référence ne sont pas copiés
Étant des tranches, les chaînes sont également passées par référence :
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és 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~ :
[code=d <<<
import std.stdio;
void premiereLettreEnPoint(dchar[] str)
void premiereLettreEnPoint(dchar[] chaine)
{
str[0] = '.';
chaine[0] = '.';
}
void main()
{
dchar[] str = "abc"d.dup;
premiereLettreEnPoint(str);
writeln(str);
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 [c main()] :
La modification apportée au premier élément du paramètre affecte l'élément dans [c main()]~ :
[output <<<
.bc
@ -130,24 +132,24 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
]
[ = Les qualificatifs de paramètre
Les paramètres sont passés aux fonction selon les règles générales suivantes :
- les types valeur sont copiés
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 suivant changent la manière dont les paramètres sont passés et quelles opérations sur eux sont permises.
[ = [c in]
Nous avons vu que les fonctions sont une facilité qui produisent des valeurs et peuvent avoir des effets de bords. Le mot-clé [c 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. [c in] implique [c const] :
Nous avons vu que les fonctions sont un service qui produit des valeurs et peut avoir des effets de bords. Le mot-clé [c 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. [c in] implique [c const]~ :
[code=d <<<
import std.stdio;
double poidsTotal(in double totalActuel,
in double poids,
in double ajout)
in double poids,
in double quantitéAjoutée)
{
return totalActuel + (poids * ajout);
return totalActuel + (poids * quantitéAjoutée);
}
void main()
@ -156,7 +158,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Les paramètres [c in] ne peuvent pas être modifiés :
Les paramètres [c in] ne peuvent pas être modifiés~ :
[code=d <<<
void foo(in int valeur)
@ -168,11 +170,11 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
]
[ = [c out]
Nous avons vu que les fonction 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-upplet ou une structure. Nous verrons ces fonctionnalités dans des chapitres ultérieurs).
Nous avons vu que les fonction 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-upplet ou une structure. Nous verrons ces fonctionnalités dans des chapitres ultérieurs).
Le mot-clé [c out] permet aux fonctions de retourner des résultats à travers leurs paramètres. Quand les paramètres [c 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ée à travers un paramètre [c out] :
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ée à travers un paramètre [c out]~ :
[code=d <<<
import std.stdio;
@ -188,17 +190,17 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
int reste;
int resultat = diviser(7, 3, reste);
writeln("résultat : ", resultat, ", reste: ", reste);
writeln("résultat : ", resultat, ", reste : ", reste);
}
>>>]
Modifier le paramètre [c reste] de la fonction modifie la variable [c reste] dans [c main()] (leur noms n'ont pas besoin d'être les mêmes) :
Modifier le paramètre [c reste] de la fonction modifie la variable [c reste] dans [c main()] (leurs noms n'ont pas besoin d'être les mêmes)~ :
[output <<<
résultat: 2, reste: 1
résultat: 2, reste : 1
>>>]
Indépendamment de leurs valeurs au moment de l'appel, la valeur [c init] du type des paramètres [c out] leur est automatiquement affectée :
Indépendamment de leurs valeurs au moment de l'appel, la valeur [c init] du type des paramètres [c out] leur est automatiquement affectée~:
[code=d <<<
import std.stdio;
@ -218,7 +220,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
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 [c int], affectant la variable dans [c main()] :
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 [c int], affectant la variable dans [c main()]~ :
[output <<<
Avant l'appel de la fonction : 100
@ -226,14 +228,14 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
Après le retour de la fonction : 0
>>>]
Cela montre que les paramètres [c out] ne peuvent pas passer de valeur aux fonctions ; il sont strictement là pour transmettre des valeurs hors de la fonction.
Cela montre que les paramètres [c out] ne peuvent pas passer de valeur aux fonctions~ ; il sont strictement là pour transmettre des valeurs hors de la fonction.
Nous verrons dans des chapitres ultérieurs que retourner des n-upplets ou structures peut être mieux qu'utiliser des paramètres [c out].
]
[ = [c const]
Comme nous l'avons vu dans le chapitre précédent, [c const] garantie 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. [c const] rend également les fonction plus utiles en autorisant des variables [c const], [c immutable] et non [c immutable] à être passées en paramètres :
Comme nous l'avons vu dans le chapitre précédent, [c 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. [c const] rend également les fonction plus utiles en autorisant des variables [c const], [c immutable] et non [c immutable] à être passées en paramètres~ :
[code=d <<<
import std.stdio;
@ -251,7 +253,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
]
[ = [c immutable]
Comme nous l'avons vu dans le chapitre précédent, [c immutable] force certains arguments à être des éléments [c 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) :
Comme nous l'avons vu dans le chapitre précédent, [c immutable] force certains arguments à être des éléments [c 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)~ :
[code=d <<<
import std.stdio;
@ -279,14 +281,14 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Comme il apporte un prérequis sur le paramètre, le qualificatif [c immutable] devrait être utilisé que quand il est vraiment nécessaire. Étant plus accueillant, [c const] est plus utile.
Comme il demande un prérequis sur le paramètre, le qualificatif [c immutable] ne devrait être utilisé que quand il est vraiment nécessaire. Étant plus accueillant, [c const] est plus utile.
]
[ = [c ref]
Ce mot-clé permet de passer un paramètre par référence même s'il serait normalement passé par valeur.
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 [c reduireEnergie()] ci-avant modifie la variable qui lui est passé en argument, son paramètre doit être marqué avec [c ref] :
Pour que la fonction [c reduireEnergie()] vue plus tôt modifie la variable qui lui est passée en argument, son paramètre doit être marqué avec [c ref]~ :
[code=d <<<
import std.stdio;
@ -305,15 +307,15 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Cette fois, les modifications qui sont faites aux paramètres affectent la variable qui est passée à la fonction dans main() :
Cette fois, les modifications qui sont appliquées aux paramètres affectent la variable qui est passée à la fonction dans [c main()]~ :
[output <<<
Nouvelle energie: 25
>>>]
Comme on peut le remarquer, les paramètres [c ref] peuvent être utilisés aussi bien comme entrée que comme sortie. Les paramètres [c ref] peuvent aussi être vus comme des [* alias] des variables passées en argument. Le paramètre de fonction [c energie] ci-avant est un alias de la variable [c energie] dans [c main()].
Comme on peut le remarquer, les paramètres [c ref] peuvent être utilisés aussi bien comme entrée que comme sortie. Les paramètres [c ref] peuvent aussi être vus comme des [* alias] des variables passées en argument. Le paramètre de la fonction [c energie] ci-dessus est un alias de la variable [c energie] dans [c main()].
Tout comme les paramètres [c out], les paramètres [c ref] permettent aux fonctions d'avoir des effets de bords. En fait, [c reduireEnergie()] ne retourne pas une valeur ; elle ne fait qu'avoir un effet de bord à travers sont seul paramètre.
Tout comme les paramètres [c out], les paramètres [c ref] permettent aux fonctions d'avoir des effets de bords. En fait, [c 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 programmations fonctionnels ne permettent pas du tout les effets de bords. Les fonctions qui produisent des résultat uniquement par leur valeur de retour sont en effet plus faciles à comprendre, à écrire correctement et à maintenir.
@ -322,7 +324,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
[code=d <<<
import std.stdio;
double EnergieReduite(double energie)
double energieReduite(double energie)
{
return energie / 4;
}
@ -331,7 +333,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
{
double energie = 100;
energie = EnergieReduite(energie);
energie = energieReduite(energie);
writeln("Nouvelle energie: ", energie);
}
>>>]
@ -339,13 +341,19 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
Notez également le changement de nom de la fonction. Il ne s'agit plus d'un verbe, mais d'un groupe nominal.
]
[ = [c 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 [c auto ref] prend les valeurs de gauche par référence et les valeurs de droite par copie.
]
[ = [c inout]
Malgré son nom composé de [c in] et de [c 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 [c ref].
[c inout] transmet la mutabilité du paramètre au type de retour. Si le paramètre est mutable, [c cont] ou [c immutable], alors la valeur de retour est respectivement mutable, [c cont] ou [c immutable].
[c inout] transmet la mutabilité du paramètre au type de retour. Si le paramètre est mutable, [c const] ou [c immutable], alors la valeur de retour est respectivement mutable, [c const] ou [c immutable].
Pour voir l'utilité d'[c 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 :
Pour voir l'utilité d'[c 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~ :
[code=d <<<
import std.stdio;
@ -353,10 +361,10 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
int[] rognée(int[] tranche)
{
if (tranche.length) {
--tranche.length; // rogner depuis la fin
--tranche.length; // rogner depuis la fin
if (tranche.length) {
tranche = tranche[1 .. $]; // rogner depuis le début
tranche = tranche[1 .. $]; // rogner depuis le début
}
}
@ -376,9 +384,9 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
[6, 7, 8]
>>>]
Selon le chapitre précédent, pour que la fonction soit plus utile, son paramètre devrait plus être [c 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 [c tranche] lui-même parce c'est une copie de l'argument originel.)
Selon le chapitre précédent, pour que la fonction soit plus utile, son paramètre devrait être [c 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 [c 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 :
Cependant, définir la fonction de la façon suivante entraînerait une erreur de compilation~ :
[code=d <<<
int[] rognée(const(int)[] tranche)
@ -388,24 +396,24 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
L'erreur de compilation indique que la tranche de [c const(int)] ne peut pas être retournée comme une tranche de [c mutable int] :
L'erreur de compilation indique que la tranche de [c const(int)] ne peut pas être retournée comme une tranche de [c mutable int]~ :
[output <<<
Error: cannot implicitly convert expression (tranche) of type
const(int)[] to int[]
>>>]
On pourrait croire que spéficier le type de retour comme [c const(int)~[~]] peut être la solution :
On pourrait croire que spéficier le type de retour comme [c const(int)~[~]] peut être la solution~ :
[code=d <<<
const(int)[] rognée(const(int)[] tranche)
{
// ...
return tranche; // now compiles
return tranche; // compile maintenant
}
>>>]
Bien que le code peut maintenant être compilé, il apporte une limitation : même si la fonction est appelée avec une tranche d'éléments [c mutable], la tranche retournée contient des elements [c const]. Pour voir à quel point ceci est limitant, regardons au code suivant, qui essaie de modifier les éléments d'une tranche autres que ceux qui sont au debut ou à la fin :
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 [c mutable], la tranche retournée contient des elements [c 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 debut ou à la fin~ :
[code=d <<<
int[] nombres = [ 5, 6, 7, 8, 9 ];
@ -413,14 +421,14 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
milieu[] *= 10;
>>>]
On s'attendrait à ce que la tranche retournée de type [c <<< const(int)[] >>>] ne puisse pas être assignée à une tranche de type [c <<< int[] >>>] :
Comme on peut s'y attendre, la tranche retournée de type [c <<< const(int)[]>>>] ne peut pas être assignée à une tranche de type [c <<< int[]>>>]~ :
[code=d <<<
Error: cannot implicitly convert expression (rognée(nombres))
of type const(int)[] to int[]
>>>]
Cependant, comme la tranche de départ est constitué d'élément mutable, cette limitation peut être vue comme artificielle et malheureuse. [c 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 porte la mutabilité du premier au dernier :
Cependant, comme la tranche de départ est constitué d'élément mutable, cette limitation peut être vue comme artificielle et malheureuse. [c 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~ :
[code=d <<<
inout(int)[] rognée(inout(int)[] tranche)
@ -429,8 +437,8 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
return tranche;
}
>>>]
Avec ce changement, la même fonction peut maintenant être appelée avec les tranches [c mutable], [c const] et [c immutable] :
Avec ce changement, la même fonction peut maintenant être appelée avec des tranches mutable, [c const] et [c immutable]~ :
[code=d <<<
{
@ -461,21 +469,21 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
>>>]
]
[ = [c lazy]
[ = [c lazy] (paresseux)
Il est naturel de s'attendre à ce que les arguments soient évalués avant d'entrer dans les fonctions et utiliser ces arguments. Par exemple, la fonction [c ajouter()] ci-après est appelée avec les valeurs de retours de deux autres fonctions :
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 [c ajouter()] ci-dessous est appelée avec les valeurs de retours de deux autres fonctions~ :
[code=d <<<
resultat = ajouter(uneQuantité(), uneAutreQuantité());
>>>]
Pour qu'[ajouter] soit appelée, [c uneQuantité] et [c uneAutreQuantité] doivent être appelée avant. Autrement, les valeurs dont [c ajouter] a besoin ne seraient pas disponibles.
Pour qu'[c ajouter()] soit appelée, [c uneQuantité()] et [c uneAutreQuantité()] doivent être appelée avant. Autrement, les valeurs dont [c ajouter()] a besoin ne seraient pas disponibles.
Évaluer les arguments avant d'appeler une fonction est coûteux.
Évaluer les arguments avant d'appeler une fonction est ''non paresseux'' (''eager'').
Cependant, certains paramètres peuvent ne pas être utilisé du tout par une fonction selon certaines conditions. Dans de tels cas, les évaluations coûteuses des arguments seraient inutiles.
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 d'obtenir le nombre requis d'œufs depuis 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 :
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~ :
[code=d <<<
void faireOmelette(in int œufsRequis,
@ -498,7 +506,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
De plus, supposons qu'il y a une fonction qui calcule et retourne le nombre total d'œufs du voisinage. Pour des raisons pédagogiques, la fonction affiche aussi quelques informations :
De plus, supposons qu'il y a une fonction qui calcule et retourne le nombre total d'œufs du voisinage. Pour des raisons pédagogiques, la fonction affiche aussi quelques informations~ :
[code=d <<<
int nombreŒufs(in int[string] œufsDisponibles)
@ -506,7 +514,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
int resultat;
foreach (voisin, nombre; œufsDisponibles) {
writeln(voisin, ": ", nombre, " œufs");
writeln(voisin, " : ", nombre, " œufs");
resultat += nombre;
}
@ -519,7 +527,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
La fonction itère sur les élements d'un tableau associatif et somme le nombre d'œufs.
La fonction [c faireOmelette] peut être appelée avec la valeur de retour de [c nombreŒufs] comme dans le programme suivant :
La fonction [c faireOmelette()] peut être appelée avec la valeur de retour de [c nombreŒufs()] comme dans le programme suivant~ :
[code=d <<<
import std.stdio;
@ -532,20 +540,20 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Comme dans la sortie du programme, la fonction [c nombreŒufs] est d'abord exécutée et [c faireOmelette] est ensuite appelée :
Comme on peut le constater dans la sortie du programme, la fonction [c nombreŒufs()] est d'abord exécutée et [c faireOmelette()] est ensuite appelée~ :
[output <<<
Jane: 5 œufs ⎫
Bill: 7 œufs ⎬ Comptage des œufs chez les voisins
Jim: 3 œufs ⎭
Jane : 5 œufs ⎫
Bill : 7 œufs ⎬ Comptage des œufs chez les voisins
Jim : 3 œufs ⎭
Un total de 15 œufs disponible chez les voisins
Besoin de faire une omelette de 2 œufs
Prendre tous les œufs depuis le réfrigérateur
>>>]
Même s'il est possible de faire l'omelette de deux œufs avec les œufs du réfrigérateur uniquement, les œufs chez les voisins ont été coûteusement comptés.
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é [c lazy] ("paresseux") indique qu'une expression qui a été passé à une fonction comme paramètre sera évaluée seulement si et quand elle est nécessaire :
Le mot-clé [c 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~ :
[code=d <<<
void faireOmelette(in int œufsRequis,
@ -556,33 +564,33 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
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 :
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~ :
[output <<<
Besoin de faire une omelette de 2 œufs
Prendre tous les œufs dans le réfrigérateur
>>>]
Ce comptage serait toujours fait si nécessaire. Par exemple, considérons le cas de quand le nombre d'œufs requis est plus grand que le nombre d'œ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~ :
[code=d <<<
faireOmelette(9, 5, nombreŒufs(chezLesVoisins));
>>>]
Cette fois, le nombre total d'œufs chez les voisins est vraiment nécessaire :
Cette fois, le nombre total d'œufs chez les voisins est vraiment nécessaire~ :
[code=d <<<
Besoin de faire une omelette de 9 œufs.
Jane: 5 œufs
Bill: 7 œufs
Jim: 3 œufs
Jane : 5 œufs
Bill : 7 œufs
Jim : 3 œufs
Un total de 15 œufs disponible chez les voisins
Prendre 5 œufs dans le réfrigérateur et 4 œufs chez les voisins
>>>]
Les valeurs des paramètres [c lazy] sont évalués à chaque fois qu'ils sont utilisés dans la fonction.
Par exemple, parce que le paramètre [c lazy] de la fonction suivante est utilisé trois fois dans la fonction, l'expression qui donne sa valeur est évaluée trois fois :
Par exemple, parce que le paramètre [c lazy] de la fonction suivante est utilisé trois fois dans la fonction, l'expression qui donne sa valeur est évaluée trois fois~ :
[code=d <<<
import std.stdio;
@ -617,7 +625,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
]
[ = [c scope]
Ce mot-clé indique qu'un paramètre ne sera pas utilisé au delà de la portée de la fonction :
Ce mot-clé indique qu'un paramètre ne sera pas utilisé au delà de la portée de la fonction~ :
[code=d <<<
int[] trancheGlobale;
@ -635,14 +643,14 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
La fonction casse la promesse de [c 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 termine.
La fonction casse la promesse de [c 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 se soit terminée.
(Note: dmd 2.058, le compileur qui a été utilisé pour compiler les exemples de ce chapitre, ne prend pas en charge le mot-clé [c scope]. )
(Note~ : dmd 2.066.1, le compileur qui a été utilisé pour compiler les exemples de ce chapitre, ne prend pas en charge le mot-clé [c scope]. )
]
[ = [c shared]
Ce mot-clé nécessite que le paramètre soit partageable entre les fils d'exécutions :
Ce mot-clé nécessite que le paramètre soit partageable entre les fils d'exécutions~ :
[code=d <<<
void foo(shared int[] i)
@ -657,7 +665,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Le programme ci-avant ne peut pas être compilé parce que l'argument n'est pas partagé. Le programme peut être compilé avec les changements suivant :
Le programme ci-avant ne peut pas être compilé parce que l'argument n'est pas partagé. Le programme peut être compilé avec les changements suivants~ :
[code=d <<<
shared int[] nombres = [ 10, 20 ];
@ -674,19 +682,20 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
- 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).
- [c in] indique que le paramètre est seulement pour une entrée de données.
- [c ou] indique que le paramètre est seulement pour une sortie de données.
- [c out] indique que le paramètre est seulement pour une sortie de données.
- [c ref] indique que le paramètre est pour une entrée et une sortie de données.
- [c cont] garantie que le paramètre n'est pas modifié à l'intérieur de la fonction.
- [c 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.
- [c const] garantit que le paramètre n'est pas modifié à l'intérieur de la fonction.
- [c immutable] nécessite que l'argument soit immuable.
- [c 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.
- [c lazy] évalue le paramètre quand (et à chaque fois) que sa valeur est utilisée.
- [c scope] garantie qu'aucune référence au paramètre sera fuité par la fonction.
- [c lazy] évalue le paramètre quand (et à chaque fois que) sa valeur est utilisée.
- [c scope] garantit qu'aucune référence au paramètre ne sera «~ fuitée~ » par la fonction.
- [c shared] nécessite que le paramètre soit partagé.
]
[ = Exercice
Le programme suivant essaie d'échanger les valeurs de deux arguments :
Le programme suivant essaie d'échanger les valeurs de deux arguments~ :
[code=d <<<
import std.stdio;
@ -709,7 +718,7 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
}
>>>]
Le programme n'a aucun effet sur [c a] ni sur [c b] :
Le programme n'a aucun effet sur [c a] ni sur [c b]~ :
[output <<<
1 2 ← non échangés
@ -717,5 +726,5 @@ Cette fonction calcule la note moyenne en prenant 40% de la note du quiz et 60%
Corrigez la fonction pour que les valeurs de [c a] et [c b] soient échangées.
... [[part:corrections/parametres_de_fonctions | … La solution]]
[[part:corrections/parametres_de_fonctions | … La solution]]
]

View File

@ -2,12 +2,16 @@
title = "Portées ([c scope])"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Comme nous l'avons vu, les expressions qui doivent toujours être exécutées sont écrites dans le bloc [c finally], et les expressions qui doivent être exécutées quand il y a des conditions d'erreurs sont écrites dans des blocs [c catch].
On peut faire les observations suivantes sur l'utilisation de ces blocs :
- [c catch] et [c finally] ne peuvent pas être utilisés sans un bloc [c try].
On peut faire les observations suivantes sur l'utilisation de ces blocs~ :
- [
[c catch] et [c finally] ne peuvent pas être utilisés sans un bloc [c try].
]
- [
Certaines variables dont ces blocs ont besoin peuvent ne pas être accessibles à l'intérieur de ces blocs :
@ -15,49 +19,49 @@ On peut faire les observations suivantes sur l'utilisation de ces blocs :
void foo(ref int r)
{
try {
int ajouterFin = 42;
int nombreÀAjouter = 42;
r += ajouterFin;
mayThrow();
r += nombreÀAjouter;
peutLever();
} catch (Exception exc) {
r -= ajouterFin; // ERREUR de compilation
r -= nombreÀAjouter; // ERREUR de compilation
}
}
>>>]
Cette fonction modifie le paramètre référence puis annule cette modification quand une exception est levée. Malheureusement, [c ajouterFin] n'est accessible que dans le bloc [c try], là où il est défini.
Cette fonction modifie le paramètre référence puis annule cette modification quand une exception est levée. Malheureusement, [c nombreÀAjouter] n'est accessible que dans le bloc [c try], là où il est défini.
[p Note : | Ceci renvoie à la notion d'espace de noms ; comme pour la notion de durée de vie des objets, cela sera expliqué dans un chapitre ultérieur.]
[p Note~ : | ceci renvoie à la notion d'espace de noms~ ; comme pour la notion de durée de vie des objets, cela sera expliqué dans un chapitre ultérieur.]
]
- Écrire toutes les expressions potentiellement sans rapport dans un seul bloc [c finally] en bas sépare ces expressions du code auquel elles sont liés.
- Écrire toutes les expressions potentiellement sans rapport dans un seul bloc [c finally] en bas sépare ces expressions du code auquel elles sont liées.
Les instructions [c scope] ont la même fonctionnalité que les blocs [c catch] et [c finally] mais sont meilleurs sur beaucoup d'aspects. Les trois instructions [c scope] sont aussi relatifs aux expressions qui devraient être exécutées lorsque qu'on quitte des portées :
- [c scope(exit)] : l'expression est toujours exécutée lorsqu'on quite la portée, que ce soit normalement ou à cause d'une exception.
- [c scope(success)] : l'expression n'est exécutée que si l'on quitte la portée normalement.
- [c scope(failure)] : l'expression n'est exécutée que si l'on quitte la portée à cause d'une exception.
Les instructions [c scope] ont la même fonctionnalité que les blocs [c catch] et [c finally] mais sont meilleures sur beaucoup d'aspects. Les trois instructions [c scope] décrivent des expressions qui devraient être exécutées lorsque qu'on quitte des portées~ :
- [c scope(exit)]~ : l'expression est toujours exécutée lorsqu'on quite la portée, que ce soit normalement ou à cause d'une exception~ ;
- [c scope(success)]~ : l'expression n'est exécutée que si l'on quitte la portée normalement~ ;
- [c scope(failure)]~ : l'expression n'est exécutée que si l'on quitte la portée à cause d'une exception.
Même si ces instructions sont proches du mécanisme des exceptions, elles peuvent être utilisées sans bloc [c try-catch].
Comme exemple, écrivons la fonction c-avant avec une instruction [c scope(failure)] :
Comme exemple, ré-écrivons la fonction précédente avec une instruction [c scope(failure)]~ :
[code=d <<<
void foo(ref int r)
{
int ajouterFin = 42;
int nombreÀAjouter = 42;
r += ajouterFin;
scope(failure) r -= ajouterFin;
r += nombreÀAjouter;
scope(failure) r -= nombreÀAjouter;
mayThrow();
peutLever();
}
>>>]
L'instruction [c scope(failure)] assure que l'expression [c r -= ajouterFin] sera exécutée si on sort de la portée de la fonction à cause d'une exception. Un avantage de [c scope(failure)] est le fait que l'expression qui annule l'autre expression est écrite à côté de cette-ci.
L'instruction [c scope(failure)] assure que l'expression [c r -= nombreÀAjouter] sera exécutée si on sort de la portée de la fonction à cause d'une exception. Un avantage de [c scope(failure)] est le fait que l'expression qui annule l'autre expression est écrite à côté de celle-ci.
Les instructions [c scope] peuvent aussi être utilisées par blocs :
Les instructions [c scope] peuvent aussi être utilisées par blocs~ :
[code=d <<<
scope(exit) {
@ -65,7 +69,7 @@ Les instructions [c scope] peuvent aussi être utilisées par blocs :
}
>>>]
Voici une autre fonction qui teste ces trois instructions :
Voici une autre fonction qui teste ces trois instructions~ :
[code=d <<<
void test()
@ -85,7 +89,7 @@ Voici une autre fonction qui teste ces trois instructions :
}
>>>]
Si aucune exception n'est levée, la sortie de la fonction n'inclue que les expressions [c scope(exit)] et [c scope(success)] :
Si aucune exception n'est levée, la sortie de la fonction n'inclut que les expressions [c scope(exit)] et [c scope(success)]~ :
[output <<<
en quittant 2
@ -94,14 +98,14 @@ Si aucune exception n'est levée, la sortie de la fonction n'inclue que les expr
en quittant 1
>>>]
Si une exception est levée, la sortie inclue les expressions [c scope(exit)] et [c scope(failure)] :
Si une exception est levée, la sortie inclut les expressions [c scope(exit)] et [c scope(failure)]~ :
[output <<<
si levée 2
en quittant 2
si levée 1
en quittant 1
object.Exception@...: the error message
object.Exception@...: le message d'erreur
>>>]
Comme on peut le constater, les blocs des instructions [c scope] sont exécutés dans l'ordre inverse. C'est parce que du code plus bas peut dépendre de variables précédentes. Exécuter les instructions [c scope] dans l'ordre inverse permet d'annuler des effets de bords d'expressions précédentes dans un ordre consistant.
Comme on peut le constater, les blocs des instructions [c scope] sont exécutés dans l'ordre inverse de celui dans lequel ils sont écrits dans le programme. C'est parce que du code situé plus bas peut dépendre de variables qui le précèdent. Exécuter les instructions [c scope] dans l'ordre inverse permet d'annuler les effets de bords d'expressions précédentes dans un ordre cohérent.