⌂ Programmation
Fiche 12 sur 13

Les sous-programmes

Découper un programme en morceaux réutilisables.

Après cette fiche, vous saurez :

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.

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();
MorceauRôle
void« vide » : cette procédure ne renvoie rien
AfficherTitreLe 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");
Ce que l'ordinateur affiche
+-----------------------------+
   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);
Ce que l'ordinateur affiche
  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 :

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");
Ce que l'ordinateur affiche
TVA : 21,00 euros
TVA : 52,50 euros
ProcédureFonction
Commence parvoidun type : int, double, string…
Contientdes actionsun return
S'utiliseseule, sur sa lignecomme une valeur
ExempleAfficherTitre();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 :

→Exercices

1Première procédureRecopier

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("=============================");
}
  1. Combien de fois la ligne de séparation s'affiche-t-elle ?
  2. Combien de fois l'avez-vous écrite ?
  3. Remplacez les = par des *. Combien de lignes modifiez-vous ?
  4. Que se passe-t-il si vous déplacez AfficherSeparateur tout en haut du fichier ?

Solution

Ce que l'ordinateur affiche
=============================
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().

2Transformer en fonctionsModifier

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();
  1. Créez une fonction CalculerTva qui reçoit un montant et renvoie la TVA.
  2. Créez une fonction CalculerTotalTTC qui reçoit un montant HT et renvoie le total TTC. Elle doit utiliser CalculerTva.
  3. Créez une procédure AfficherResultat qui reçoit les trois montants et les affiche.
  4. 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.

3Le jeu de dés découpéCréer

Écrivez un jeu de dés en découpant proprement.

  1. Une procédure AfficherRegles qui explique le jeu.
  2. Une fonction LancerDe qui reçoit un Random et renvoie un nombre entre 1 et 6.
  3. Une fonction DemanderNombreDeTours qui pose la question et renvoie la réponse.
  4. Une procédure AfficherResultat qui reçoit le total et le nombre de tours, et affiche la moyenne.
  5. 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("+-------------------------------+");
}
Ce que l'ordinateur affiche
=== 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

Une case qui coince ? Relisez la partie concernée avant de passer à la fiche suivante.

← Fiche 11 Fiche 13 : Fonctionnalités bonus →