Leçon 02

Introduction à la programmation C# · Module 7, leçon 4 sur 4

Regrouper des données : struct

Créer son propre type pour regrouper les données d'une même chose : déclarer une structure, créer des fiches, en faire des tableaux, et les passer à des méthodes.

Résultat attendu

À la fin de la leçon, vous remplacez des tableaux parallèles par un tableau de structures, que vous triez, parcourez et passez à des méthodes.

Dans cette leçon
  1. L'essentiel
  2. Exercices
  3. Solutions
  4. Je fais le point

Ce que vous allez faire

  • Déclarer une structure et ses champs.
  • Créer une fiche et lire ou modifier ses champs.
  • Remplacer des tableaux parallèles par un tableau de structures.
  • Passer une structure à une méthode et en renvoyer une.

Vous aurez réussi si…

  • chaque champ utilisé hors de la structure est déclaré public ;
  • votre structure est déclarée à la toute fin du fichier ;
  • vous savez qu'une structure, comme un int, est copiée à l'affectation et au passage en paramètre.

L'essentiel

Depuis la leçon 6.1, un sentier de randonnée se décrit avec des tableaux parallèles :

C# · Tableaux parallèles
string[] noms = { "Lac des Castors", "Mont Pointu", "Ruisseau Vert" };
double[] longueurs = { 3.2, 7.8, 5.5 };
int[] deniveles = { 60, 540, 280 };

Ça fonctionne, mais rien dans le code ne dit que noms[1], longueurs[1] et deniveles[1] décrivent le même sentier. À chaque tri, il faut échanger dans les trois tableaux (leçon 6.5) ; un oubli, et les données se mélangent. Une structure crée un nouveau type qui regroupe ces données en une seule fiche.

Déclarer une structure

C# · Program.cs
Sentier s = new Sentier { Nom = "Mont Pointu", LongueurKm = 7.8, Denivele = 540 };
Console.WriteLine($"{s.Nom} : {s.LongueurKm} km, {s.Denivele} m de dénivelé");

s.Denivele = 545;   // on modifie un seul champ
Console.WriteLine(s.Denivele);

// ---------- Types ----------

struct Sentier
{
    public string Nom;
    public double LongueurKm;
    public int Denivele;
}
ÉlémentRôle
struct Sentier { … }Déclare un nouveau type, nommé en PascalCase comme une méthode
public double LongueurKm;Un champ : une variable qui fait partie de chaque fiche. public le rend utilisable hors de la structure
new Sentier { Nom = …, … }Crée une fiche et donne une valeur à ses champs
s.DeniveleLit ou modifie un champ, avec un point, comme texte.Length

Dans un Program.cs comme ceux du cours, les structures se déclarent à la toute fin du fichier, après les méthodes. Les champs non initialisés par new valent 0, false ou null, comme les cases d'un nouveau tableau.

Un tableau de structures

Trois tableaux parallèlesnomslongueursdeniveles0Lac des Castors3,2601Mont Pointu7,85402Ruisseau Vert5,5280à garder alignés à chaque tri,ajout ou retraitUn tableau de structuressentiers (Sentier[])0Lac des Castors · 3,2 km · 60 m1Mont Pointu · 7,8 km · 540 m2Ruisseau Vert · 5,5 km · 280 mchaque case est une fiche complète :sentiers[1].Denivele → 540
Trois tableaux à synchroniser, ou un seul tableau dont chaque case est une fiche complète.

Un tableau peut contenir des structures. Le tri de la leçon 6.5 y gagne beaucoup : un seul échange déplace la fiche entière, avec tous ses champs.

C# · Program.cs
Sentier[] sentiers =
{
    new Sentier { Nom = "Lac des Castors", LongueurKm = 3.2, Denivele = 60 },
    new Sentier { Nom = "Mont Pointu", LongueurKm = 7.8, Denivele = 540 },
    new Sentier { Nom = "Ruisseau Vert", LongueurKm = 5.5, Denivele = 280 },
    new Sentier { Nom = "Crête des Pins", LongueurKm = 11.4, Denivele = 720 },
    new Sentier { Nom = "Boucle du Moulin", LongueurKm = 2.6, Denivele = 35 }
};

// Tri par longueur croissante : un seul échange déplace toute la fiche
for (int passe = 0; passe < sentiers.Length - 1; passe++)
{
    for (int j = 0; j < sentiers.Length - 1 - passe; j++)
    {
        if (sentiers[j].LongueurKm > sentiers[j + 1].LongueurKm)
        {
            Sentier temporaire = sentiers[j];
            sentiers[j] = sentiers[j + 1];
            sentiers[j + 1] = temporaire;
        }
    }
}

double totalKm = 0;
foreach (Sentier s in sentiers)
{
    AfficherSentier(s);
    totalKm += s.LongueurKm;
}
Console.WriteLine($"Réseau : {totalKm} km");

// ---------- Méthodes ----------

static void AfficherSentier(Sentier s)
{
    Console.WriteLine($"{s.Nom} : {s.LongueurKm} km, {s.Denivele} m ({ObtenirDifficulte(s)})");
}

static string ObtenirDifficulte(Sentier s)
{
    if (s.Denivele < 200)
    {
        return "facile";
    }
    if (s.Denivele < 500)
    {
        return "intermédiaire";
    }
    return "difficile";
}

// ---------- Types ----------

struct Sentier
{
    public string Nom;
    public double LongueurKm;
    public int Denivele;
}
Sortie
Boucle du Moulin : 2,6 km, 35 m (facile)
Lac des Castors : 3,2 km, 60 m (facile)
Ruisseau Vert : 5,5 km, 280 m (intermédiaire)
Mont Pointu : 7,8 km, 540 m (difficile)
Crête des Pins : 11,4 km, 720 m (difficile)
Réseau : 30,5 km

La comparaison lit un champ, sentiers[j].LongueurKm, mais l'échange porte sur la fiche entière, avec une variable temporaire de type Sentier. Les méthodes reçoivent un seul paramètre, Sentier s, au lieu de trois.

Une structure est copiée

Une structure se comporte comme un int : l'affectation et le passage en paramètre font une copie de toute la fiche (leçon 7.2).

C# · Program.cs
Sentier a = new Sentier { Nom = "Ruisseau Vert", LongueurKm = 5.5, Denivele = 180 };
Sentier b = a;          // b est une copie complète de a
b.LongueurKm = 6.0;
Console.WriteLine($"{a.LongueurKm} {b.LongueurKm}");   // 5,5 6

Allonger(a);
Console.WriteLine(a.LongueurKm);                       // 5,5 : la méthode a reçu une copie
a = AllongerEtRenvoyer(a, 1.5);
Console.WriteLine(a.LongueurKm);                       // 7

static void Allonger(Sentier s)
{
    s.LongueurKm += 1;
}

static Sentier AllongerEtRenvoyer(Sentier s, double km)
{
    s.LongueurKm += km;
    return s;
}

struct Sentier
{
    public string Nom;
    public double LongueurKm;
    public int Denivele;
}

Pour qu'une méthode change une fiche, elle la renvoie modifiée (return), ou la reçoit avec ref. La même règle explique deux refus du compilateur : on ne peut pas modifier un champ d'une fiche lue dans une List, ni la variable d'un foreach, puisque ce sont des copies.

Structure ou classe ?

C# offre aussi les classes, qui ressemblent aux structures mais ne sont pas copiées. Elles sont au cœur de la programmation orientée objet, le sujet d'un prochain cours. Pour regrouper quelques données simples, la structure suffit.

Quatre pièges fréquents
  • Oublier public : un champ sans public est inaccessible hors de la structure (CS0122, « … est inaccessible en raison de son niveau de protection »).
  • Déclarer la structure avant le programme principal : CS8803 (« Les instructions de niveau supérieur doivent précéder les déclarations d'espace de noms et de type »).
  • Modifier un champ dans une liste : panier[0].Prix = 2.99; donne CS1612. On sort la fiche dans une variable, on la modifie, puis on la remet dans la liste. Dans un tableau, en revanche, tableau[0].Prix = 2.99; fonctionne.
  • Croire qu'une méthode modifie la fiche reçue : sans ref ni return, elle modifie une copie, sans aucun message.

Exercices

GuidéExercice 1 : Prévoir les résultats

C# · Program.cs
Point a = new Point { X = 1, Y = 2 };
Point b = a;
b.X = 10;
Console.WriteLine($"{a.X} {b.X}");

Deplacer(a);
Console.WriteLine(a.X);

DeplacerRef(ref a);
Console.WriteLine(a.X);

Point[] points = new Point[2];
points[1].Y = 7;
Console.WriteLine($"{points[0].Y} {points[1].Y}");

static void Deplacer(Point p)
{
    p.X += 5;
}

static void DeplacerRef(ref Point p)
{
    p.X += 5;
}

struct Point
{
    public int X;
    public int Y;
}

Prévoyez les quatre lignes affichées, puis vérifiez en exécutant.

Vous avez réussi si vos quatre prévisions sont justes, et si vous expliquez pourquoi la deuxième ligne affiche 1 et la troisième 6.

GuidéExercice 2 : Les colis

Règle : « L'envoi d'un colis coûte 5 , plus 2  par kilogramme. »

Déclarez une structure Colis (destination, poids en kg). Créez un tableau de trois colis : Sherbrooke 1,5 kg, Rimouski 4 kg et Gatineau 0,8 kg. Affichez les frais de chacun, puis le total.

Vous avez réussi si les frais sont 8,00 , 13,00  et 6,60 , pour un total de 27,60 , et si les tarifs sont des constantes.

Semi-guidéExercice 3 : Des méthodes pour un rectangle

Déclarez une structure Rectangle (largeur, hauteur) et trois méthodes : CreerRectangle(double largeur, double hauteur), qui renvoie un Rectangle ; CalculerAire(Rectangle r) ; EstCarre(Rectangle r), qui renvoie un bool.

Vous avez réussi si un terrain de 30 × 20 donne 600 m² et n'est pas carré, et une case de 4 × 4 donne 16 m² et est carrée.

AutonomeExercice 4 : Le panier

Ce programme doit mettre le cahier en solde à 2,99 , ajouter un crayon, puis afficher le panier. Il ne compile pas et, une fois compilé, il reste une erreur de logique.

C# · Program.cs (avec erreurs)
List<Article> panier = new List<Article>
{
    new Article { Nom = "Cahier", Prix = 3.50, Quantite = 4 },
    new Article { Nom = "Crayon", Prix = 1.25, Quantite = 10 }
};

// Le cahier est en solde
panier[0].Prix = 2.99;

// On ajoute un crayon
AjouterUn(panier[1]);

foreach (Article a in panier)
{
    Console.WriteLine($"{a.Nom} : {a.Quantite} × {a.Prix:C} = {a.Quantite * a.Prix:C}");
}

static void AjouterUn(Article a)
{
    a.Quantite = a.Quantite + 1;
}

struct Article
{
    public string Nom;
    public double Prix;
    int Quantite;
}

Corrigez les deux erreurs de compilation, puis l'erreur de logique.

Vous avez réussi si le programme affiche « Cahier : 4 × 2,99  = 11,96  » et « Crayon : 11 × 1,25  = 13,75  ».

DéfiExercice 5 : Fini les tableaux parallèles

À la leçon 6.5, le classement de Léa, Samuel, Inès, Hugo et Maya (notes 72,5, 88, 64, 91 et 79) demandait deux échanges par inversion. Réécrivez-le avec une structure Etudiant et un tableau de structures, triez en ordre décroissant de note, puis ajoutez une méthode CalculerMoyenne(Etudiant[] etudiants).

Vous avez réussi si le classement est Hugo, Samuel, Maya, Léa, Inès, si la moyenne est 78,9, et si chaque inversion ne fait qu'un seul échange.

Solutions

Exercice 1 : Prévoir les résultats

1 10 ; 1 ; 6 ; 0 7.

b = a copie la fiche : changer b.X ne change pas a.X. Deplacer reçoit une copie, donc a.X reste 1. DeplacerRef reçoit a lui-même avec ref : a.X passe à 6. Dans un tableau, points[1].Y = 7 modifie directement la case ; la case 0 garde la valeur par défaut, 0.

Exercice 2 : Les colis
C# · Program.cs
const double FraisBase = 5.00;
const double FraisParKg = 2.00;

Colis[] colis =
{
    new Colis { Destination = "Sherbrooke", PoidsKg = 1.5 },
    new Colis { Destination = "Rimouski", PoidsKg = 4.0 },
    new Colis { Destination = "Gatineau", PoidsKg = 0.8 }
};

double total = 0;
for (int i = 0; i < colis.Length; i++)
{
    double frais = FraisBase + FraisParKg * colis[i].PoidsKg;
    Console.WriteLine($"{colis[i].Destination} ({colis[i].PoidsKg} kg) : {frais:C}");
    total += frais;
}
Console.WriteLine($"Total : {total:C}");

struct Colis
{
    public string Destination;
    public double PoidsKg;
}
Exercice 3 : Des méthodes pour un rectangle
C# · Program.cs
Rectangle terrain = CreerRectangle(30, 20);
Rectangle case1 = CreerRectangle(4, 4);

Console.WriteLine($"Terrain : {CalculerAire(terrain)} m², carré : {EstCarre(terrain)}");
Console.WriteLine($"Case : {CalculerAire(case1)} m², carré : {EstCarre(case1)}");

static Rectangle CreerRectangle(double largeur, double hauteur)
{
    return new Rectangle { Largeur = largeur, Hauteur = hauteur };
}

static double CalculerAire(Rectangle r)
{
    return r.Largeur * r.Hauteur;
}

static bool EstCarre(Rectangle r)
{
    return r.Largeur == r.Hauteur;
}

struct Rectangle
{
    public double Largeur;
    public double Hauteur;
}

CreerRectangle renvoie une structure : une méthode peut renvoyer n'importe quel type, y compris le vôtre. Les deux autres méthodes reçoivent un seul paramètre au lieu de deux : la structure voyage comme un tout.

Exercice 4 : Le panier
  1. CS0122 : le champ Quantite n'est pas public.
  2. CS1612 : panier[0].Prix = 2.99; modifie une copie lue dans la liste. On sort la fiche, on la modifie, on la remet.
  3. La logique : AjouterUn modifie une copie ; le crayon reste à 10. La méthode renvoie maintenant la fiche modifiée, que l'appel remet dans la liste.
C# · Program.cs (corrigé)
List<Article> panier = new List<Article>
{
    new Article { Nom = "Cahier", Prix = 3.50, Quantite = 4 },
    new Article { Nom = "Crayon", Prix = 1.25, Quantite = 10 }
};

// Le cahier est en solde : on sort la fiche, on la modifie, on la remet
Article cahier = panier[0];
cahier.Prix = 2.99;
panier[0] = cahier;

// On ajoute un crayon : la méthode renvoie la fiche modifiée
panier[1] = AjouterUn(panier[1]);

foreach (Article a in panier)
{
    Console.WriteLine($"{a.Nom} : {a.Quantite} × {a.Prix:C} = {a.Quantite * a.Prix:C}");
}

static Article AjouterUn(Article a)
{
    a.Quantite = a.Quantite + 1;
    return a;
}

struct Article
{
    public string Nom;
    public double Prix;
    public int Quantite;
}
Exercice 5 : Fini les tableaux parallèles
C# · Program.cs
Etudiant[] groupe =
{
    new Etudiant { Nom = "Léa", Note = 72.5 },
    new Etudiant { Nom = "Samuel", Note = 88 },
    new Etudiant { Nom = "Inès", Note = 64 },
    new Etudiant { Nom = "Hugo", Note = 91 },
    new Etudiant { Nom = "Maya", Note = 79 }
};

// Tri décroissant : un seul échange par inversion, au lieu de deux
for (int passe = 0; passe < groupe.Length - 1; passe++)
{
    for (int j = 0; j < groupe.Length - 1 - passe; j++)
    {
        if (groupe[j].Note < groupe[j + 1].Note)
        {
            Etudiant temporaire = groupe[j];
            groupe[j] = groupe[j + 1];
            groupe[j + 1] = temporaire;
        }
    }
}

for (int i = 0; i < groupe.Length; i++)
{
    Console.WriteLine($"{i + 1}. {groupe[i].Nom} : {groupe[i].Note}");
}
Console.WriteLine($"Moyenne : {CalculerMoyenne(groupe):F1}");

static double CalculerMoyenne(Etudiant[] etudiants)
{
    double somme = 0;
    foreach (Etudiant e in etudiants)
    {
        somme += e.Note;
    }
    return somme / etudiants.Length;
}

struct Etudiant
{
    public string Nom;
    public double Note;
}

La variable temporaire est de type Etudiant : l'échange déplace le nom et la note ensemble, et il devient impossible de les désynchroniser. CalculerMoyenne parcourt le tableau avec foreach, en lecture seule, et lit le champ Note de chaque fiche.

Je fais le point

  • Je sais déclarer une structure avec des champs public, à la fin du fichier.
  • Je sais créer une fiche avec new et lire ou modifier ses champs avec un point.
  • Je sais remplacer des tableaux parallèles par un tableau de structures, et le trier.
  • Je sais passer une structure à une méthode et en renvoyer une.
  • Je sais qu'une structure est copiée, et comment modifier une fiche dans une liste.