Les sous-programmes
Découper un programme en morceaux réutilisables.
Après cette fiche, vous saurez :
- écrire une procédure
- écrire une fonction qui renvoie un résultat
- passer des informations par des paramètres
- découper un problème en sous-problèmes
1Le problème du programme trop long
Vos programmes commencent à faire cinquante ou soixante lignes. Ils deviennent difficiles à relire, et certains morceaux se répètent.
Console.WriteLine("+-----------------------------+"); Console.WriteLine(" TICKET"); Console.WriteLine("+-----------------------------+"); // ... vingt lignes de calculs ... Console.WriteLine("+-----------------------------+"); Console.WriteLine(" TOTAL"); Console.WriteLine("+-----------------------------+");
La solution : découper le programme en morceaux nommés, qu'on appelle quand on en a besoin. Ces morceaux s'appellent des sous-programmes.
À retenir
Il en existe deux sortes.
- Une procédure fait quelque chose. Elle ne renvoie rien.
- Une fonction calcule quelque chose et renvoie un résultat.
Vous en utilisez déjà depuis la fiche 3 : Console.WriteLine est une
procédure, int.Parse est une fonction.
2Écrire une procédure
// Définition de la procédure void AfficherTitre() { Console.WriteLine("+-----------------------------+"); Console.WriteLine(" TICKET"); Console.WriteLine("+-----------------------------+"); } // Appel : on l'utilise AfficherTitre(); Console.WriteLine("Contenu du ticket..."); AfficherTitre();
| Morceau | Rôle |
|---|---|
void | « vide » : cette procédure ne renvoie rien |
AfficherTitre | Le nom que vous choisissez |
() | Les parenthèses, obligatoires même vides |
{ … } | Ce que fait la procédure |
Écrite une fois, appelée deux fois. Et si le cadre doit changer, une seule modification suffit.
Attention
Où écrire ses sous-programmes ?
Dans un projet .NET moderne, placez-les à la fin de votre fichier
Program.cs, après le reste du code.
// D'abord le programme principal Console.WriteLine("Bonjour"); AfficherTitre(); Console.WriteLine("Au revoir"); // Puis les sous-programmes, tout en bas void AfficherTitre() { Console.WriteLine("=== TICKET ==="); }
C'est contre-intuitif, mais l'ordre n'a pas d'importance : le compilateur lit tout
le fichier avant d'exécuter quoi que ce soit. Il connaît donc déjà
AfficherTitre quand il arrive à l'appel.
3Les paramètres
Une procédure devient vraiment utile quand on peut lui donner des informations.
void AfficherTitre(string texte) { Console.WriteLine("+-----------------------------+"); Console.WriteLine($" {texte}"); Console.WriteLine("+-----------------------------+"); } AfficherTitre("TICKET DE CAISSE"); AfficherTitre("RECAPITULATIF"); AfficherTitre("MERCI DE VOTRE VISITE");
+-----------------------------+ TICKET DE CAISSE +-----------------------------+ +-----------------------------+ RECAPITULATIF +-----------------------------+ +-----------------------------+ MERCI DE VOTRE VISITE +-----------------------------+
string texte est un paramètre : une boîte que la procédure
reçoit à chaque appel.
On peut en mettre plusieurs, séparés par des virgules :
void AfficherLigne(string article, int quantite, double prix) { double sousTotal = prix * quantite; Console.WriteLine($" {quantite} x {article} : {sousTotal:F2} euros"); } AfficherLigne("Pain", 2, 1.50); AfficherLigne("Croissant", 4, 1.20);
2 x Pain : 3,00 euros 4 x Croissant : 4,80 euros
À retenir
L'ordre des paramètres compte.
À l'appel, les valeurs sont distribuées dans l'ordre : la première va dans
article, la deuxième dans quantite, la troisième dans
prix.
Inverser deux valeurs du même type produit un résultat faux, sans erreur de compilation. Faites attention.
4Écrire une fonction
Une fonction renvoie un résultat. Deux différences avec une procédure :
- au lieu de
void, on écrit le type du résultat ; - à l'intérieur, on utilise
returnpour renvoyer la valeur.
double CalculerTva(double prixHT) { double tva = prixHT * 0.21; return tva; } // Utilisation double montant = CalculerTva(100); Console.WriteLine($"TVA : {montant:F2} euros"); // Ou directement dans un affichage Console.WriteLine($"TVA : {CalculerTva(250):F2} euros");
TVA : 21,00 euros TVA : 52,50 euros
| Procédure | Fonction | |
|---|---|---|
| Commence par | void | un type : int, double, string… |
| Contient | des actions | un return |
| S'utilise | seule, sur sa ligne | comme une valeur |
| Exemple | AfficherTitre(); | double t = CalculerTva(100); |
Attention
return arrête immédiatement la fonction.
double CalculerTva(double prixHT) { return prixHT * 0.21; Console.WriteLine("Calcul terminé"); // ne s'exécutera JAMAIS }
Tout ce qui suit un return est ignoré. Visual Studio vous préviendra
d'ailleurs que ce code est inaccessible.
Une fonction qui décide
bool EstMajeur(int age) { if (age >= 18) { return true; } else { return false; } } // Utilisation if (EstMajeur(20)) { Console.WriteLine("Entrée autorisée"); }
Une fonction peut renvoyer un bool. On l'utilise alors directement dans
un if, ce qui rend le code très lisible.
À retenir
Cette fonction peut d'ailleurs s'écrire en une ligne :
bool EstMajeur(int age) { return age >= 18; }
age >= 18 est déjà une réponse vrai/faux. Inutile de passer par un
if pour renvoyer true ou false.
5La portée des variables
Une variable créée dans un sous-programme n'existe pas ailleurs.
void Calculer() { int resultat = 42; } Calculer(); Console.WriteLine(resultat); // ERREUR : resultat n'existe pas ici
À retenir
Chaque sous-programme a sa propre mémoire.
Les variables qu'il crée naissent quand il démarre et disparaissent quand il se termine.
C'est une bonne chose : cela évite qu'un morceau de programme abîme les variables d'un autre. Chacun travaille dans son coin.
Pour faire sortir une information, il n'y a qu'un moyen : la
return d'une fonction.
6Découper un problème
C'est le vrai bénéfice, et c'est ce qu'on évalue.
Devant un problème compliqué, on ne l'attaque pas d'un bloc. On le découpe en morceaux qu'on sait résoudre.
// Le programme principal se lit comme un résumé AfficherEntete(); double prix = DemanderPrix(); int quantite = DemanderQuantite(); double totalHT = prix * quantite; double tva = CalculerTva(totalHT); AfficherTicket(totalHT, tva); AfficherPied(); // --- Les sous-programmes, en dessous --- void AfficherEntete() { Console.WriteLine("=== CAISSE ==="); Console.WriteLine(); } double DemanderPrix() { Console.Write("Prix unitaire : "); return double.Parse(Console.ReadLine()); } int DemanderQuantite() { Console.Write("Quantité : "); return int.Parse(Console.ReadLine()); } double CalculerTva(double montantHT) { return montantHT * 0.21; } void AfficherTicket(double ht, double tva) { Console.WriteLine(); Console.WriteLine($" Total HT : {ht:F2} euros"); Console.WriteLine($" TVA : {tva:F2} euros"); Console.WriteLine($" A PAYER : {ht + tva:F2} euros"); } void AfficherPied() { Console.WriteLine(); Console.WriteLine("Merci de votre visite !"); }
À retenir
Regardez les huit premières lignes. Elles se lisent comme le résumé du programme, en français presque courant.
C'est la marque d'un bon découpage : le programme principal raconte ce qui se passe, et les sous-programmes disent comment.
Trois règles simples :
- un sous-programme fait une seule chose ;
- son nom est un verbe qui dit ce qu'il fait ;
- s'il dépasse quinze lignes, il en cache probablement deux.
→Exercices
Recopiez ce programme et lancez-le.
AfficherSeparateur(); Console.WriteLine("Bienvenue !"); AfficherSeparateur(); Console.WriteLine("Contenu du programme..."); AfficherSeparateur(); Console.WriteLine("Au revoir !"); AfficherSeparateur(); Console.ReadKey(); void AfficherSeparateur() { Console.WriteLine("============================="); }
- Combien de fois la ligne de séparation s'affiche-t-elle ?
- Combien de fois l'avez-vous écrite ?
- Remplacez les
=par des*. Combien de lignes modifiez-vous ? - Que se passe-t-il si vous déplacez
AfficherSeparateurtout en haut du fichier ?
Solution
============================= Bienvenue ! ============================= Contenu du programme... ============================= Au revoir ! =============================
1. Quatre fois. 2. Une seule fois. 3. Une seule ligne.
C'est exactement l'intérêt d'un sous-programme : écrire une fois, utiliser partout, modifier à un seul endroit.
4. Le programme ne compile plus, ou se comporte bizarrement.
Dans un projet .NET moderne, le code principal doit venir avant les déclarations de sous-programmes. C'est une contrainte de cette écriture simplifiée.
La règle à retenir : vos sous-programmes vont tout en bas du fichier,
après le Console.ReadKey().
Voici un programme qui fonctionne, mais qui est écrit d'un bloc.
Console.Write("Prix hors TVA : "); double prixHT = double.Parse(Console.ReadLine()); Console.Write("Quantité : "); int quantite = int.Parse(Console.ReadLine()); double totalHT = prixHT * quantite; double tva = totalHT * 0.21; double totalTTC = totalHT + tva; Console.WriteLine(); Console.WriteLine($"Total HT : {totalHT:F2} euros"); Console.WriteLine($"TVA : {tva:F2} euros"); Console.WriteLine($"Total TTC : {totalTTC:F2} euros"); Console.ReadKey();
- Créez une fonction
CalculerTvaqui reçoit un montant et renvoie la TVA. - Créez une fonction
CalculerTotalTTCqui reçoit un montant HT et renvoie le total TTC. Elle doit utiliserCalculerTva. - Créez une procédure
AfficherResultatqui reçoit les trois montants et les affiche. - Le programme principal doit tenir en moins de dix lignes.
Solution
Console.Write("Prix hors TVA : "); double prixHT = double.Parse(Console.ReadLine()); Console.Write("Quantité : "); int quantite = int.Parse(Console.ReadLine()); double totalHT = prixHT * quantite; double tva = CalculerTva(totalHT); double totalTTC = CalculerTotalTTC(totalHT); AfficherResultat(totalHT, tva, totalTTC); Console.ReadKey(); // ----- Les sous-programmes ----- double CalculerTva(double montantHT) { return montantHT * 0.21; } double CalculerTotalTTC(double montantHT) { return montantHT + CalculerTva(montantHT); } void AfficherResultat(double ht, double tva, double ttc) { Console.WriteLine(); Console.WriteLine($"Total HT : {ht:F2} euros"); Console.WriteLine($"TVA : {tva:F2} euros"); Console.WriteLine($"Total TTC : {ttc:F2} euros"); }
Le point intéressant : CalculerTotalTTC appelle
CalculerTva. Un sous-programme peut parfaitement en utiliser un autre.
C'est même souhaitable : le taux de 21 % n'est écrit qu'à un seul endroit. S'il change, une ligne suffit.
Comparez les deux versions. Le programme principal fait maintenant dix lignes et se lit comme un résumé : je demande, je calcule, j'affiche. Les détails sont rangés en dessous.
Sur les noms de paramètres : la fonction reçoit montantHT, alors
qu'on lui passe totalHT. Ce n'est pas un problème : à l'intérieur du
sous-programme, la valeur porte le nom du paramètre. Les deux boîtes sont
différentes.
Écrivez un jeu de dés en découpant proprement.
- Une procédure
AfficherReglesqui explique le jeu. - Une fonction
LancerDequi reçoit unRandomet renvoie un nombre entre 1 et 6. - Une fonction
DemanderNombreDeToursqui pose la question et renvoie la réponse. - Une procédure
AfficherResultatqui reçoit le total et le nombre de tours, et affiche la moyenne. - Le programme principal lance les dés dans une boucle et cumule le total.
Aide : créez le Random une seule fois, dans le programme
principal : Random de = new Random();. Ensuite, de.Next(1, 7)
donne un entier entre 1 et 6. La borne haute est exclue.
Solution
AfficherRegles(); Random de = new Random(); int nbTours = DemanderNombreDeTours(); int total = 0; Console.WriteLine(); for (int i = 1; i <= nbTours; i++) { int resultat = LancerDe(de); total = total + resultat; Console.WriteLine($"Lancer {i} : {resultat}"); } AfficherResultat(total, nbTours); Console.ReadKey(); // ----- Les sous-programmes ----- void AfficherRegles() { Console.WriteLine("=== LE LANCER DE DÉS ==="); Console.WriteLine(); Console.WriteLine("Je lance un dé à six faces autant de fois"); Console.WriteLine("que vous le souhaitez, puis je calcule la moyenne."); Console.WriteLine(); } int LancerDe(Random generateur) { return generateur.Next(1, 7); } int DemanderNombreDeTours() { Console.Write("Combien de lancers ? "); return int.Parse(Console.ReadLine()); } void AfficherResultat(int total, int nombre) { double moyenne = (double)total / nombre; Console.WriteLine(); Console.WriteLine("+-------------------------------+"); Console.WriteLine($" Nombre de lancers : {nombre}"); Console.WriteLine($" Total des points : {total}"); Console.WriteLine($" Moyenne : {moyenne:F2}"); Console.WriteLine("+-------------------------------+"); }
=== LE LANCER DE DÉS ===
Je lance un dé à six faces autant de fois
que vous le souhaitez, puis je calcule la moyenne.
Combien de lancers ? 5
Lancer 1 : 4
Lancer 2 : 2
Lancer 3 : 6
Lancer 4 : 3
Lancer 5 : 5
+-------------------------------+
Nombre de lancers : 5
Total des points : 20
Moyenne : 4,00
+-------------------------------+Le programme principal fait treize lignes et se lit comme le déroulement du jeu : j'explique, je demande, je lance en boucle, j'affiche. C'est exactement l'objectif du découpage.
Trois points techniques.
generateur.Next(1, 7) : la borne haute est exclue. Pour
obtenir 6, il faut écrire 7. C'est un piège classique.
Le Random est créé une seule fois dans le programme principal, puis
transmis à LancerDe par un paramètre. Une variable du programme principal
n'est pas visible dans un sous-programme (section 5) : c'est le même principe que
pour total et nombre. Ici, la fonction LancerDe
appelle le même Random à chaque lancer.
(double)total / nombre dans AfficherResultat : deux
int donneraient une division entière et une moyenne fausse. Rappel de la
fiche 7.
moyenne est déclarée dans la procédure. Elle n'a pas besoin
d'exister ailleurs, donc c'est sa place.
Pour aller plus loin : ajoutez une fonction EstUnSix qui renvoie
un bool, et comptez les six obtenus.
Je sais faire
- Je sais écrire une procédure et l'appeler.
- Je sais écrire une fonction avec un return.
- Je connais la différence entre une procédure et une fonction.
- Je sais passer des paramètres.
- Je sais découper un problème en morceaux.
Une case qui coince ? Relisez la partie concernée avant de passer à la fiche suivante.