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.
Elle est déterminée par le <TargetFramework> : net8.0 → C# 12,
net9.0 → C# 13, net10.0 → C# 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
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; }
}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éclaration | Nature | Quand l'utiliser |
|---|---|---|
record Client(...) | Classe (référence), immuable | DTO, message, événement, valeur métier |
record struct Point(...) | Struct (valeur), modifiable | Petite valeur numérique très fréquente |
readonly record struct Argent(...) | Struct immuable | Le meilleur défaut pour une petite valeur |
class Commande | Classe classique | Entité avec identité et comportement mutable |
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érentsL'é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)
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();
}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"
};
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
private string _nom = "";
public string Nom
{
get => _nom;
set => _nom = value?.Trim()
?? throw new ArgumentNullException(nameof(value));
}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"
// 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
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
};
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
| Version | Framework | À retenir |
|---|---|---|
| C# 10 | net6.0 | namespace de fichier, global using, record struct |
| C# 11 | net7.0 | chaînes brutes, required, motifs de liste, membres statiques abstraits |
| C# 12 | net8.0 | constructeurs primaires, expressions de collection […], alias de type |
| C# 13 | net9.0 | params collections, nouveau lock, \e |
| C# 14 | net10.0 | membres d'extension, mot-clé field, affectation conditionnelle au null |
| C# 15 | net11.0 (aperçu) | unions, closed, indexeurs d'extension, break/continue étiquetés |
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 ?
2. Dans public class Service(AppDbContext db), db est-il une propriété publique ?
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 ?
4. Apport principal des membres d'extension de C# 14 ?
"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 ?
Fiches de révision
record ?with ?[1, 2, ..autres] ?union vs closed ?union = plusieurs types sans lien. closed = hiérarchie dont on connaît tous les descendants.- record pour transporter des données : égalité, affichage,
withofferts. - Constructeurs primaires : la fin du copier-coller des dépendances.
- Pattern matching : la fonctionnalité qui améliore le plus la lisibilité, avec exhaustivité vérifiée.
- C# 14 : membres d'extension +
field. C# 15 : unions +closed. - Critère d'adoption unique : est-ce que ça se relit plus vite ?