Accueil › Les fondations › Chapitre 10

C# moderne (12 → 15)

C# gagne des fonctionnalités chaque année. La plupart ne sont pas des nouveautés « pour experts » : ce sont des raccourcis qui suppriment du code répétitif. Ce chapitre présente celles qui changent réellement l'écriture quotidienne, toujours sous la forme avant / après.

Quelle version de C# ai-je ?

Elle est déterminée par le <TargetFramework> : net8.0 → C# 12, net9.0 → C# 13, net10.0C# 14, net11.0 → C# 15. Forcer <LangVersion> est possible mais déconseillé : certaines fonctionnalités ont besoin du runtime correspondant.

record : le transporteur de données

Avant — 40 lignes
public class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) { X = x; Y = y; }

    public override bool Equals(object? o) =>
        o is Point p && p.X == X && p.Y == Y;
    public override int GetHashCode() =>
        HashCode.Combine(X, Y);
    public override string ToString() =>
        $"Point {{ X = {X}, Y = {Y} }}";
    public Point AvecX(int x) => new(x, Y);
    public void Deconstruct(out int x, out int y)
        { x = X; y = Y; }
}
Après — 1 ligne
public record Point(int X, int Y);

// Et tout est là :
var a = new Point(1, 2);
var b = a with { X = 5 };        // copie modifiée
var (x, y) = a;                   // déconstruction
Console.WriteLine(a);             // Point { X = 1, Y = 2 }
Console.WriteLine(a == new Point(1, 2));  // True
DéclarationNatureQuand l'utiliser
record Client(...)Classe (référence), immuableDTO, message, événement, valeur métier
record struct Point(...)Struct (valeur), modifiablePetite valeur numérique très fréquente
readonly record struct Argent(...)Struct immuableLe meilleur défaut pour une petite valeur
class CommandeClasse classiqueEntité avec identité et comportement mutable
Piège : record + collection = égalité trompeuse
record Panier(List<string> Articles);
var p1 = new Panier(["a"]);
var p2 = new Panier(["a"]);
Console.WriteLine(p1 == p2);   // False ! ← les deux LISTES sont des objets différents

L'égalité générée compare les membres, et pour une liste cela signifie comparer les références. Utilise ImmutableArray<T> avec un comparateur, ou compare explicitement les contenus.

Constructeurs primaires (C# 12)

Avant
public class ServiceCommande
{
    private readonly AppDbContext _db;
    private readonly ILogger<ServiceCommande> _log;
    private readonly IEnvoyeurEmail _mail;

    public ServiceCommande(
        AppDbContext db,
        ILogger<ServiceCommande> log,
        IEnvoyeurEmail mail)
    {
        _db = db;
        _log = log;
        _mail = mail;
    }

    public Task Traiter() => _db.SaveChangesAsync();
}
Après
public class ServiceCommande(
    AppDbContext db,
    ILogger<ServiceCommande> log,
    IEnvoyeurEmail mail)
{
    public Task Traiter() => db.SaveChangesAsync();
}

// Les paramètres sont utilisables dans tout
// le corps de la classe. Idéal avec
// l'injection de dépendances.

⚠️ Différence avec un record : les paramètres d'un constructeur primaire de classe ne deviennent pas des propriétés publiques. Ils sont capturés comme champs privés — ce qui est exactement ce qu'on veut pour des dépendances.

Expressions de collection (C# 12, enrichies en C# 15)

// Avant                                    // Après
var a = new List<int> { 1, 2, 3 };          List<int> a = [1, 2, 3];
var b = new int[] { };                      int[] b = [];
var c = new[] { 1, 2 }.Concat(autres);      int[] c = [1, 2, ..autres];   // ← étalement (spread)

// Fonctionne pour tout ce qui est « collection-compatible »
Span<byte> tampon = [0, 1, 2];
ImmutableArray<string> noms = ["Ada", "Alan"];
int[][] grille = [[1, 2], [3, 4]];

// C# 15 : passer des arguments au constructeur sous-jacent
List<string> grande = [with(capacity: 1000), .. source];
HashSet<string> insensible = [with(StringComparer.OrdinalIgnoreCase), "Bonjour", "BONJOUR"];
// → un seul élément : les deux chaînes sont égales pour ce comparateur

Pattern matching : le couteau suisse

C'est la fonctionnalité qui transforme le plus la lisibilité. On teste la forme d'une valeur et on en extrait des morceaux en une expression.

// --- 1. Motif de type, avec variable ---
if (forme is Cercle c) Console.WriteLine(c.Rayon);
if (o is not string) return;

// --- 2. switch expression (remplace 90 % des switch classiques) ---
decimal tarif = passager switch
{
    { Age: < 4 }                        => 0m,                    // motif de propriété
    { Age: < 18 } or { Statut: "etudiant" } => 5m,                // motifs logiques
    { Age: >= 65 }                      => 8m,                    // motif relationnel
    { Abonne: true, Age: >= 18 and < 65 } => 10m,                 // combinaison
    _                                   => 12m                     // cas par défaut
};

// --- 3. Motif avec déconstruction et garde when ---
var description = point switch
{
    (0, 0)                     => "origine",
    (var x, 0)                 => $"sur l'axe X à {x}",
    (0, var y)                 => $"sur l'axe Y à {y}",
    var (x, y) when x == y     => "sur la diagonale",
    _                          => "quelque part"
};

// --- 4. Motifs de liste (C# 11) ---
var analyse = segments switch
{
    []                     => "vide",
    [var seul]             => $"un seul : {seul}",
    ["api", .. var reste]  => $"appel API vers {string.Join('/', reste)}",
    [_, _, ..]             => "au moins deux segments"
};

// --- 5. Motif imbriqué dans une hiérarchie ---
string Etat(Commande cmd) => cmd switch
{
    { Statut: Statut.Payee, Livraison: { Transporteur: "DHL", Suivi: not null } } => "en route",
    { Statut: Statut.Payee } => "à expédier",
    { Statut: Statut.Annulee, Remboursement: null } => "à rembourser",
    _ => "en attente"
};
Bon réflexe : le compilateur vérifie l'exhaustivité

Sur une enum ou une hiérarchie closed (C# 15), une switch expression qui oublie un cas déclenche un avertissement. Un switch classique avec des if/else ne dira jamais rien. C'est un filet de sécurité gratuit à chaque ajout de cas métier.

Le confort d'écriture (C# 10 → 13)

// namespace de fichier : un niveau d'indentation en moins dans TOUT le fichier
namespace Boutique.Domaine;

// using globaux : déclarés une fois pour tout le projet (GlobalUsings.cs)
global using Microsoft.EntityFrameworkCore;

// new implicite (target-typed)
Dictionary<string, List<int>> index = new();          // au lieu de répéter le type

// chaînes brutes : ni échappement, ni concaténation
var json = """
    { "nom": "Ada", "chemin": "C:\Temp\x" }
    """;
var requete = $$"""
    SELECT * FROM Clients WHERE Ville = '{{ville}}'
    """;

// interpolation et formatage
Console.WriteLine($"Total : {total:C} au {date:dd/MM/yyyy}");
Console.WriteLine($"{nom,-20}{prix,10:N2}");           // alignement en colonnes

// nameof : le nom d'un symbole, vérifié par le compilateur (résiste au renommage)
ArgumentNullException.ThrowIfNull(commande, nameof(commande));

// params collections (C# 13) : plus seulement des tableaux
static int Somme(params ReadOnlySpan<int> valeurs) { /* zéro allocation */ }
Somme(1, 2, 3);

// alias de n'importe quel type (C# 12)
using Coordonnees = (double Latitude, double Longitude);

C# 14 .NET 10

Le mot-clé field

Avant : champ de soutien à la main
private string _nom = "";
public string Nom
{
    get => _nom;
    set => _nom = value?.Trim()
        ?? throw new ArgumentNullException(nameof(value));
}
Après
public string Nom
{
    get;
    set => field = value?.Trim()
        ?? throw new ArgumentNullException(nameof(value));
}
// « field » désigne le champ généré :
// plus besoin de le déclarer ni de le nommer.

Membres d'extension (le grand ajout de C# 14)

Les méthodes d'extension existaient depuis 2007. C# 14 généralise le mécanisme aux propriétés, aux membres statiques, et regroupe tout dans un bloc extension lisible.

public static class ExtensionsChaine
{
    // Ancienne syntaxe, toujours valable
    public static bool EstVide(this string s) => s.Length == 0;

    // Nouvelle syntaxe : un bloc pour un type receveur
    extension(string texte)
    {
        public bool EstEmail => texte.Contains('@');            // PROPRIÉTÉ d'extension
        public string Tronquer(int max) =>
            texte.Length <= max ? texte : texte[..max] + "…";   // méthode
    }

    extension(string)                                           // membres STATIQUES d'extension
    {
        public static string Vide => "";
        public static string DeChiffres(int n) => new('0', n);
    }
}

// Utilisation : comme si ces membres appartenaient à string
"a@b.fr".EstEmail            // true
"un texte très long".Tronquer(6)   // "un tex…"
string.DeChiffres(4)         // "0000"
Autres apports de C# 14
// Affectation conditionnelle au null : n'affecte que si la cible n'est pas null
client?.Adresse = nouvelleAdresse;         // avant : if (client is not null) { ... }

// nameof sur un générique non lié
Console.WriteLine(nameof(List<>));         // "List"

// Modificateurs sur les paramètres de lambda
var essai = (out int r) => { r = 42; return true; };

// Opérateurs d'affectation composée définis par l'utilisateur (+= optimisé sans allocation)
public void operator +=(Vecteur autre) { X += autre.X; Y += autre.Y; }

C# 15 .NET 11, aperçu

Statut

C# 15 accompagne .NET 11, attendu le 10 novembre 2026. Ce qui suit correspond aux versions d'aperçu et peut encore changer. Détails complets au chapitre 22.

Types union

public record class Chat(string Nom);
public record class Chien(string Nom);
public record class Oiseau(string Nom);

public union Animal(Chat, Chien, Oiseau);      // ← « exactement l'un des trois »

Animal a = new Chien("Rex");                   // conversion implicite

string nom = a switch                          // exhaustivité vérifiée : pas besoin de « _ »
{
    Chien d => d.Nom,
    Chat c  => c.Nom,
    Oiseau o => o.Nom
};

C'est la réponse native au motif « résultat » du chapitre 9 : public union Resultat(Succes, ErreurValidation, ErreurTechnique); — et le compilateur t'oblige à traiter les trois cas. ASP.NET Core 11 sait sérialiser les unions en JSON et les décrit en anyOf dans OpenAPI.

Hiérarchies fermées

public closed record class EtatPorte;                     // dérivable seulement dans CET assembly
public record class Fermee : EtatPorte;
public record class Ouverte(float Pourcentage) : EtatPorte;

string Decrire(EtatPorte e) => e switch
{
    Fermee => "fermée",
    Ouverte(var p) => $"ouverte à {p} %"
    // aucun « _ » nécessaire : le compilateur connaît tous les descendants
};
Et aussi : break/continue étiquetés
exterieur: for (int l = 0; l < grille.Hauteur; l++)
{
    for (int c = 0; c < grille.Largeur; c++)
    {
        if (grille[l, c].Bloque) continue exterieur;   // passe à la ligne suivante
        if (grille[l, c].Arrivee) break exterieur;     // sort des DEUX boucles
    }
}
// Remplace les drapeaux booléens et les goto

Récapitulatif par version

VersionFrameworkÀ retenir
C# 10net6.0namespace de fichier, global using, record struct
C# 11net7.0chaînes brutes, required, motifs de liste, membres statiques abstraits
C# 12net8.0constructeurs primaires, expressions de collection […], alias de type
C# 13net9.0params collections, nouveau lock, \e
C# 14net10.0membres d'extension, mot-clé field, affectation conditionnelle au null
C# 15net11.0 (aperçu)unions, closed, indexeurs d'extension, break/continue étiquetés
Ne cours pas après la syntaxe

Une fonctionnalité récente est utile quand elle supprime du bruit. Si une expression de collection ou un motif de liste rend une ligne plus difficile à relire dans six mois, garde la version explicite. Le seul critère qui vaille : est-ce que le prochain lecteur comprend plus vite ?

Vérifie que c'est passé

🎯 Quiz — 5 questions

1. Que donne var b = a with { Prix = 10 }; sur un record ?

C'est la « copie modifiée » (non-destructive mutation), le motif de base du travail avec des données immuables.

2. Dans public class Service(AppDbContext db), db est-il une propriété publique ?

Différence importante avec record, où les paramètres positionnels deviennent des propriétés publiques. Pour des dépendances injectées, le comportement de la classe est exactement celui souhaité.

3. Quel intérêt principal d'une switch expression sur une enum par rapport à des if/else ?

Ce filet de sécurité est décisif : ajoute une valeur à l'enum et le compilateur te montre tous les endroits à mettre à jour.

4. Apport principal des membres d'extension de C# 14 ?

On peut désormais écrire "a@b.fr".EstEmail (propriété) ou string.DeChiffres(4) (statique) sur un type qu'on ne possède pas.

5. À quoi sert une union en C# 15 ?

Rien à voir avec les unions de C (qui superposent des octets). C'est un type somme, comme dans F# ou Rust, idéal pour modéliser un résultat ou un état.

Fiches de révision

Quand utiliser record ?
DTO, message, valeur métier : dès qu'on veut l'égalité par contenu et l'immuabilité.
with ?
Crée une copie modifiée sans toucher l'original.
Constructeur primaire de classe ?
Paramètres capturés en privé, utilisables dans tout le corps. Parfait pour les dépendances.
[1, 2, ..autres] ?
Expression de collection avec étalement : liste, tableau, span, immuable…
Nouveauté phare de C# 14 ?
Les membres d'extension : propriétés et membres statiques ajoutés à un type existant.
union vs closed ?
union = plusieurs types sans lien. closed = hiérarchie dont on connaît tous les descendants.
À retenir