C# · Le langage

Nouveautés, de C# 8 à C# 15

Ce que chaque version a ajouté, et ce qui vaut la peine d'être adopté.

Vérifié en septembre 2026 · .NET 10, et .NET 11 RC 1 pour C# 15 · environ 16 min

De C# 8, sorti en septembre 2019 avec .NET Core 3.0, à C# 14, sorti en novembre 2025 avec .NET 10, le langage a reçu une version par an, chaque fois liée à une version de .NET. Ce cours est une carte : pour chaque version, sa date, son .NET, les ajouts qui changent le code qu'on écrit, et un avis — à adopter, à connaître, rarement utile. Les notions qui ont leur cours, du nullable aux spans, y sont renvoyées plutôt que réexpliquées. Chaque exemple de C# 8 à C# 14 compile avec le SDK 10.0.300 sous la LangVersion qui introduit l'ajout ; ceux de C# 15, qui accompagne .NET 11 encore en release candidate et ferme la carte, compilent avec le SDK 11.0.100-rc.1.26425.128 sur net11.0. Les erreurs citées sont celles du compilateur nommé, en anglais.

Lire une version de C#

La version du langage découle de la cible du projet : un projet net10.0 sans LangVersion compile en C# 14, un projet net8.0 en C# 12, et tout projet .NET Framework en C# 7.3. La documentation déclare non pris en charge l'usage d'une version plus récente que celle de la cible, parce que certains ajouts s'appuient sur le runtime ou sur la bibliothèque : les membres d'interface par défaut exigent le runtime de .NET Core 3.0, les index et plages les types Index et Range, les unions et les hiérarchies fermées de C# 15 des types livrés avec .NET 11.

VersionSortie.NETVersion par défaut de
C# 8septembre 2019.NET Core 3.0netcoreapp3.x, netstandard2.1
C# 9novembre 2020.NET 5net5.0
C# 10novembre 2021.NET 6net6.0
C# 11novembre 2022.NET 7net7.0
C# 12novembre 2023.NET 8net8.0
C# 13novembre 2024.NET 9net9.0
C# 14novembre 2025.NET 10net10.0
C# 15 pas encore sorti : RC 1 le 8 septembre 2026, sortie annoncée le 10 novembre 2026 .NET 11 (release candidate)net11.0

Pour savoir ce que compile réellement un projet, la directive #error version fait échouer la compilation avec un message qui nomme le compilateur et la version retenue.

// Program.cs d'un projet net10.0 sans LangVersion, compile par le SDK 10.0.300.
#error version

// error CS1029: #error: 'version'
// error CS8304: Compiler version: '5.6.0-2.26230.102 (caa81fa4971f74880cdab61990cb1b11420939ec)'.
//   Language version: 14.0. Compiler path: 'C:\Program Files\dotnet\sdk\10.0.300\Roslyn\bincore\Microsoft.CodeAnalysis.CSharp.dll'.

// Le meme fichier avec <LangVersion>preview</LangVersion> : Language version: preview.

// Le meme fichier dans un projet net11.0, compile par le SDK 11.0.100-rc.1.26425.128 :
// error CS1029: #error: 'version'
// error CS8304: Compiler version: '5.11.0-1.26425.128 (3551975be08744f0418857c5bed8ab1545c5dd47)'.
//   Language version: 15.0. Compiler path: 'C:\Program Files\dotnet\sdk\11.0.100-rc.1.26425.128\Roslyn\bincore\Microsoft.CodeAnalysis.CSharp.dll'.

LangVersion sert à deux choses : figer une version plus ancienne, et essayer la suivante avec preview. La documentation déconseille latest, dont la valeur dépend du compilateur installé : un même dépôt compilerait différemment d'une machine à l'autre. Monter de version, c'est donc changer de cible, et la carte qui suit dit ce qu'un projet gagne à passer de net8.0 à net10.0.

C# 8 et 9 : le nullable, les records, moins de cérémonie

C# 8 est la première version écrite pour .NET Core, et une partie de ses ajouts n'existe que là. Le plus lourd de conséquences est le type référence nullable, présenté dans Bases du langage : une annotation que vérifie l'analyse de flux, sans effet à l'exécution. Les modèles de projet du SDK 10 l'activent ; sur un code existant, #nullable enable fichier par fichier évite un mur d'avertissements. Les expressions switch ouvrent le pattern matching du même cours, les membres d'interface par défaut sont traités dans Programmation orientée objet, les flux asynchrones dans Parallélisme et flux. Restent des ajouts plus petits, qu'on écrit tous les jours.

// LangVersion 8.0 : pas encore d'instructions de niveau superieur, d'ou Main.
using System;
using System.IO;

static class Programme
{
    static void Main()
    {
        string[] etapes = { "creee", "payee", "expediee", "livree" };

        // Index depuis la fin, et plage : [1..3] prend les elements 1 et 2.
        // Sur un tableau, la plage est une copie ; sur un span, une vue.
        Console.WriteLine(etapes[^1]);
        Console.WriteLine(string.Join(", ", etapes[1..3]));

        // ??= n'affecte que si la variable est nulle.
        string? note = null;
        note ??= "sans note";
        Console.WriteLine(note);

        // Declaration using : liberee a la fin du bloc englobant, sans accolades.
        using var journal = new StringWriter();
        journal.Write("ecrit");
        Console.WriteLine(journal);
    }
}
// livree
// payee, expediee
// sans note
// ecrit

// Sous LangVersion 7.3 (et <Nullable>disable</Nullable>, sinon CS8630 d'abord),
// cinq erreurs CS8370, dont la premiere :
// error CS8370: Feature 'index operator' is not available in C# 7.3. Please use language version 8.0 or greater.

C# 9, en novembre 2020 avec .NET 5, retire de la cérémonie : les instructions de niveau supérieur suppriment la classe Program, new() déduit le type de la cible, et les motifs relationnels et logiques complètent le pattern matching. Les records, son ajout majeur, ont leur section dans Bases du langage ; l'accesseur init sur lequel ils reposent sert aussi à une classe ordinaire : la propriété s'écrit dans l'initialiseur, puis plus jamais.

// LangVersion 9.0 : instructions de niveau superieur, plus de classe Program.
using System;
using System.Collections.Generic;

// new() prend le type de la cible.
List<Commande> commandes = new()
{
    new() { Reference = "C-1", Montant = 40m },
    new() { Reference = "C-2", Montant = 1200m },
};

foreach (var c in commandes)
{
    Console.WriteLine($"{c.Reference} : {Palier(c.Montant)}");
}
// C-1 : standard
// C-2 : a valider

// commandes[0].Montant = 0m;
// error CS8852: Init-only property or indexer 'Commande.Montant' can only be assigned in an object
//   initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.

// Motifs relationnels et logiques.
static string Palier(decimal montant) => montant switch
{
    < 0m => "invalide",
    >= 0m and < 1000m => "standard",
    _ => "a valider",
};

class Commande
{
    public string Reference { get; init; } = "";
    public decimal Montant { get; init; }
}

// Sous LangVersion 8.0 :
// error CS8400: Feature 'init-only setters' is not available in C# 8.0. Please use language version 9.0 or greater.
AjoutAvisPourquoi
types référence nullables (8)à adopterle seul contrôle des null que fait le compilateur
expressions switch, motifs (8, 9)à adopterexhaustivité vérifiée ; cours Bases du langage
déclaration using, ??=, index ^ (8)à adoptermoins de code, même sens
plages .. (8)à adopteren sachant qu'une plage de tableau copie
flux asynchrones (8)à adopterdès que les données arrivent en flux
membres d'interface par défaut (8)à connaîtrefaire évoluer une interface publiée
records, init (9)à adoptervaleurs immuables ; cours Bases du langage
instructions de niveau supérieur, new() (9)à adopter pour Program.cs ; new() quand le type est écrit à gauche
lambdas static (9)à connaîtreinterdit toute capture ; cours Generics, delegates et events
pointeurs de fonction, nint, initialiseurs de module (9)rarement utileinteropérabilité et générateurs de code

L'avis se lit ainsi : à adopter, ce qu'un code neuf devrait écrire partout ; à connaître, ce qu'on rencontre et qu'on sait lire sans en faire une habitude ; rarement utile, ce qui sert un cas précis. La déclaration using garde une limite : la ressource vit jusqu'à la fin du bloc, et l'instruction using à accolades reste la bonne forme quand il faut la rendre plus tôt.

C# 10 et 11 : des fichiers plus courts, des objets complets

C# 10, en novembre 2021 avec .NET 6, réduit la place que prend l'organisation du code. global using déclare un import pour tout le projet — l'option ImplicitUsings des modèles du SDK en génère une série — et l'espace de noms de portée fichier retire un niveau d'indentation à chaque fichier. La même version apporte record struct, le motif de propriété étendu et les chaînes interpolées constantes, et donne aux lambdas un type naturel, que montre le cours Generics, delegates et events. L'exemple réunit trois fichiers d'un même projet.

// Trois fichiers d'un meme projet : Usings.cs, Frais.cs et Program.cs.

// Usings.cs : un global using vaut pour tous les fichiers du projet.
global using System;

// Frais.cs : espace de noms de portee fichier, sans accolades ni indentation.
namespace Boutique;

public readonly record struct Adresse(string Ville, string Pays);

public record Commande(string Reference, Adresse Livraison);

public static class Frais
{
    const string Devise = "EUR";

    // Chaine interpolee constante : tous les trous sont des constantes string.
    public const string Libelle = $"Frais de port en {Devise}";

    // Motif de propriete etendu : Livraison.Pays au lieu de Livraison: { Pays: ... }.
    public static decimal Calculer(Commande c) => c switch
    {
        { Livraison.Pays: "FR" } => 4.90m,
        { Livraison.Pays: "BE" or "LU" } => 9.90m,
        _ => 19.90m,
    };
}

// Program.cs : aucun using System ecrit, il vient du global using.
using Boutique;

var commande = new Commande("C-3", new Adresse("Namur", "BE"));
Console.WriteLine($"{Frais.Libelle} : {Frais.Calculer(commande)}");
// Frais de port en EUR : 9,90   (culture fr-FR)

// Sous LangVersion 9.0, quatre erreurs CS8773, dont :
// error CS8773: Feature 'file-scoped namespace' is not available in C# 9.0. Please use language version 10.0 or greater.

C# 11, en novembre 2022 avec .NET 7, est plus hétéroclite. Les chaînes brutes et les motifs de liste sont décrits dans Bases du langage, les membres statiques abstraits d'interface, qui fondent les maths génériques, dans Generics, delegates et events. L'ajout qui change le plus le code courant est required. Avant lui, une propriété init obligatoire recevait une valeur factice pour contenter le nullable, et rien n'empêchait l'appelant de l'oublier :

// LangVersion 10.0. Rien n'oblige l'appelant a renseigner Email.
using System;

var client = new Client { Nom = "Durand" };
Console.WriteLine($"[{client.Email}] {client.Email.Length}");
// [] 0 : l'oubli compile, et la valeur factice circule comme une vraie adresse.

class Client
{
    public string Nom { get; init; } = "";

    // "" ne sert qu'a faire taire l'avertissement du nullable (CS8618).
    public string Email { get; init; } = "";
}
// LangVersion 11.0 : required oblige chaque initialiseur a renseigner la propriete.
using System;

// var incomplet = new Client { Nom = "Durand" };
// error CS9035: Required member 'Client.Email' must be set in the object initializer or attribute constructor.

var client = new Client { Nom = "Durand", Email = "[email protected]" };
Console.WriteLine($"[{client.Email}] {client.Email.Length}");
// [[email protected]] 17

class Client
{
    // Plus de valeur factice : le nullable sait que l'initialiseur fournira la valeur.
    public required string Nom { get; init; }
    public required string Email { get; init; }
}

// Sous LangVersion 10.0 :
// error CS8936: Feature 'required members' is not available in C# 10.0. Please use language version 11.0 or greater.

required déplace le contrôle à l'endroit où l'objet se construit : chaque initialiseur doit fournir la valeur. Un constructeur marqué [SetsRequiredMembers] lève l'obligation pour ses appelants, sans que le compilateur vérifie qu'il renseigne tout : s'il oublie Email, seul l'avertissement du nullable, CS8618, le signale.

AjoutAvisPourquoi
global using, espace de noms de portée fichier (10)à adopterun niveau d'indentation et des imports répétés en moins
motif de propriété étendu (10)à adopterplus lisible que les accolades imbriquées
record struct, type naturel des lambdas (10)à connaîtrecours Bases du langage, Generics, delegates et events
gestionnaires de chaînes interpolées (10)à connaîtrel'interpolation n'alloue plus de object[] ; cours Bases du langage
chaînes interpolées constantes (10)rarement utilearguments d'attribut, constantes composées
required (11)à adopterl'oubli devient une erreur de compilation
chaînes brutes, motifs de liste (11)à adopterJSON et SQL sans échappement ; cours Bases du langage
membres statiques abstraits d'interface (11)à connaîtreécrire du code numérique générique ; surtout les bibliothèques
littéraux UTF-8 u8, types file, attributs génériques (11) rarement utileprotocoles binaires, générateurs de code

C# 12 et 13 : collections et constructeurs principaux

C# 12, en novembre 2023 avec .NET 8, généralise deux syntaxes. L'expression de collection, [1, 2, 3], s'écrit pour un tableau, une liste, un span ou une interface, et .. y étale une autre collection ; le type vient de la cible. L'alias using accepte désormais n'importe quel type, un tuple compris. C# 13, en novembre 2024 avec .NET 9, prolonge avec params sur un span ou une collection. Le compilateur peut alors se passer du tableau : en Release, après échauffement, trois entiers passés à params ReadOnlySpan<int> n'allouent rien, là où params int[] alloue à chaque appel un tableau de trois entiers, 40 octets en x64.

// C# 12 : alias de n'importe quel type, ici un tuple.
using System;
using System.Collections.Generic;
using Ligne = (string Reference, int Quantite);

// C# 12 : une expression de collection prend le type de sa cible ; .. etale.
int[] premieres = [3, 1];
List<int> quantites = [.. premieres, 2];
Ligne[] lignes = [("USB-64", 3), ("SSD-1T", 1)];

// C# 13 : params accepte d'autres collections qu'un tableau, ici un span.
Console.WriteLine(Total(3, 1, 2));
Console.WriteLine(Total([.. quantites, 4]));
Console.WriteLine(lignes[^1].Reference);
// 6
// 10
// SSD-1T

static int Total(params ReadOnlySpan<int> valeurs)
{
    int total = 0;
    foreach (int v in valeurs)
    {
        total += v;
    }
    return total;
}

// Sous LangVersion 12.0 :
// error CS9202: Feature 'params collections' is not available in C# 12.0. Please use language version 13.0 or greater.
// Sous LangVersion 11.0, des l'alias :
// error CS9058: Feature 'using type alias' is not available in C# 11.0. Please use language version 12.0 or greater.

Le constructeur principal, l'autre ajout de C# 12, a son piège. Sur une classe, ses paramètres ne deviennent ni propriétés, comme sur un record, ni champs readonly : ce sont des paramètres, modifiables, que le compilateur range dans un champ caché dès qu'un membre s'en sert. Utilisé à la fois pour initialiser une propriété et dans une méthode, le même paramètre donne deux copies de l'état, qui divergent, avec pour seul signal un avertissement :

// LangVersion 12.0.
using System;

var compte = new Compte("FR76-001", 100m);
compte.Crediter(50m);
Console.WriteLine($"{compte.Solde} / {compte.Resume()}");
// 100 / FR76-001 : 150

// Le parametre solde sert deux fois : il initialise la propriete Solde, et les
// methodes le capturent. Deux copies du meme etat, qui divergent au premier credit.
// warning CS9124: Parameter 'decimal solde' is captured into the state of the enclosing type
//   and its value is also used to initialize a field, property, or event.
class Compte(string iban, decimal solde)
{
    public decimal Solde { get; } = solde;

    public void Crediter(decimal montant) => solde += montant;

    public string Resume() => $"{iban} : {solde}";
}
// LangVersion 12.0.
using System;

var compte = new Compte("FR76-001", 100m);
compte.Crediter(50m);
Console.WriteLine($"{compte.Solde} / {compte.Resume()}");
// 150 / FR76-001 : 150

// Les parametres ne servent qu'a initialiser : l'etat vit dans un seul membre, declare.
class Compte(string iban, decimal soldeInitial)
{
    public string Iban { get; } = iban;

    public decimal Solde { get; private set; } = soldeInitial;

    public void Crediter(decimal montant) => Solde += montant;

    public string Resume() => $"{Iban} : {Solde}";
}

// Sous LangVersion 11.0 :
// error CS9058: Feature 'primary constructors' is not available in C# 11.0. Please use language version 12.0 or greater.

La règle : un paramètre de constructeur principal sert soit à initialiser des membres, soit directement dans les méthodes, jamais aux deux. L'injection de dépendances est le cas où la seconde forme convient, le service reçu n'étant que lu. Le reste de C# 13 vise des cas précis : le type Lock, présenté dans Multithreading et concurrence ; les ref struct dans les méthodes async et comme arguments de type, traitées dans Mémoire et performance et dans Generics, delegates et events ; les propriétés partial, faites pour les générateurs de code.

AjoutAvisPourquoi
expressions de collection (12)à adopterune syntaxe pour tous les types de collection
constructeurs principaux (12)à connaîtreutiles pour l'injection, piégeux pour l'état
alias de tout type (12)à connaîtrenommer un tuple local
paramètres de lambda par défaut, tableaux inline, ref readonly (12) rarement utileAPI bas niveau et performance
params collections (13)à adopterdans une API, params ReadOnlySpan<T> évite le tableau
type Lock (13)à adopterpour tout nouveau verrou ; cours Multithreading et concurrence
ref struct assouplies, allows ref struct (13)à connaîtrecours Mémoire et performance
propriétés partial, OverloadResolutionPriority (13)rarement utilegénérateurs de code, auteurs de bibliothèques

C# 14 : field, membres d'extension, ?.=

C# 14 accompagne .NET 10, sorti en novembre 2025. Le mot-clé field désigne, dans un accesseur, le champ que le compilateur génère : une propriété qui valide ou normalise sa valeur n'a plus besoin d'un champ déclaré. Les blocs extension déclarent des propriétés d'extension et des membres statiques d'extension, que l'ancienne syntaxe en this ne permettait pas ; les méthodes d'extension existantes restent valides. nameof accepte enfin un générique non lié.

// LangVersion 14.0.
using System;
using System.Collections.Generic;
using System.Linq;

var panier = new Panier { Client = "  Durand " };
panier.Lignes.Add(12m);
panier.Lignes.Add(90m);

Console.WriteLine($"[{panier.Client}]");
Console.WriteLine($"{panier.Lignes.Total} {panier.Lignes.EstVide} {List<decimal>.Vide.Count}");
Console.WriteLine(nameof(List<>));
// [Durand]
// 102 False 0
// List

class Panier
{
    public List<decimal> Lignes { get; } = [];

    // field designe le champ que le compilateur genere : plus de _client a declarer.
    public string Client
    {
        get;
        set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
    } = "";
}

static class Extensions
{
    // Bloc d'extension : le recepteur est nomme une fois pour tous ses membres.
    extension(List<decimal> lignes)
    {
        public decimal Total => lignes.Sum();
        public bool EstVide => lignes.Count == 0;
    }

    // Sans nom de recepteur : des membres statiques, appeles sur le type.
    extension(List<decimal>)
    {
        public static List<decimal> Vide => [];
    }
}

// Sous LangVersion 13.0, ce fichier echoue des l'analyse du bloc extension,
// avec six erreurs de syntaxe (CS1513, CS1002, CS8803, CS1022), dont :
// error CS1513: } expected
// error CS8803: Top-level statements must precede namespace and type declarations.
// La propriete Client seule, dans un fichier sans bloc extension, donnerait :
// error CS9260: Feature 'field keyword' is not available in C# 13.0. Please use language version 14.0 or greater.

Sous C# 13, le bloc extension n'est pas même reconnu : le compilateur rapporte six erreurs de syntaxe, dont CS1513 et CS8803, au lieu d'un message de version. Le mot-clé field est contextuel, et c'est à la migration qu'il mord. Une classe qui avait déjà un membre nommé field et s'en servait dans un accesseur change de sens en passant à C# 14, avec un simple avertissement :

// Code ecrit pour C# 13 : un champ prive s'appelle field.
using System;

var capteur = new Capteur();
capteur.Seuil = 5;
Console.WriteLine($"{capteur.Seuil} {capteur.LireChamp()}");
// LangVersion 13.0 : 5 5
// LangVersion 14.0 : 5 99. Les accesseurs ecrivent dans un champ genere, plus dans le membre.
// warning CS9258: In language version 14.0, the 'field' keyword binds to a synthesized backing field
//   for the property. To avoid generating a synthesized backing field, and to refer to the existing
//   member, use 'this.field' or '@field' instead.

class Capteur
{
    private int field = 99;

    public int Seuil
    {
        get => field;
        set => field = value;
    }

    public int LireChamp() => this.field;
}
// Le membre est renomme : plus rien ne s'appelle field, le sens ne depend plus de la version.
using System;

var capteur = new Capteur();
capteur.Seuil = 5;
Console.WriteLine($"{capteur.Seuil} {capteur.LireChamp()}");
// LangVersion 13.0 et 14.0 : 5 5, sans avertissement.

class Capteur
{
    private int seuil = 99;

    public int Seuil
    {
        get => seuil;
        set => seuil = value;
    }

    public int LireChamp() => seuil;
}

Le message propose this.field ou @field ; renommer le membre est la seule correction qui ne laisse pas le piège au lecteur suivant. Un projet qui traite les avertissements comme des erreurs est protégé ; les autres doivent lire CS9258 en montant vers net10.0.

L'affectation conditionnelle ?.= place l'opérateur ?. à gauche d'une affectation : la partie droite n'est évaluée que si la cible existe, ce qu'une lecture rapide ne devine pas quand cette partie a un effet de bord. ++ et -- restent refusés. La même version admet un modificateur sur un paramètre de lambda sans type écrit.

// LangVersion 14.0.
using System;

var durand = new Client("C-1");
Client? Trouver(string id) => id == durand.Id ? durand : null;

int appels = 0;
DateTime Horodater()
{
    appels++;
    return new DateTime(2026, 9, 26);
}

// ?.= : la partie droite n'est evaluee que si la partie gauche n'est pas nulle.
Trouver("C-1")?.DerniereVisite = Horodater();
Trouver("C-9")?.DerniereVisite = Horodater();
Trouver("C-1")?.Visites += 1;
Console.WriteLine($"{appels} {durand.Visites} {durand.DerniereVisite:yyyy-MM-dd}");
// 1 1 2026-09-26

// Trouver("C-1")?.Visites++;
// error CS1059: The operand of an increment or decrement operator must be a variable, property or indexer

// Modificateur sur un parametre de lambda, sans ecrire son type.
Analyse<int> analyser = (texte, out valeur) => int.TryParse(texte, out valeur);
Console.WriteLine(analyser("42", out int n) ? n : -1);
// 42

delegate bool Analyse<T>(string texte, out T valeur);

class Client(string id)
{
    public string Id { get; } = id;
    public DateTime? DerniereVisite { get; set; }
    public int Visites { get; set; }
}

// Sous LangVersion 13.0 :
// error CS9260: Feature 'null conditional assignment' is not available in C# 13.0. Please use language version 14.0 or greater.
// error CS9260: Feature 'simple lambda parameter modifiers' is not available in C# 13.0. Please use language version 14.0 or greater.

Les spans de première classe, qui font entrer dans le langage les conversions vers Span<T> et ReadOnlySpan<T>, sont montrés dans Mémoire et performance. Les constructeurs et événements partial et les opérateurs d'affectation composée définis par l'utilisateur servent surtout aux générateurs de code et aux types numériques. Enfin, le compilateur ignore désormais les directives #: et #!, que lit le SDK pour les applications fichier lancées par dotnet run app.cs : #:package, #:sdk, #:property en tête du fichier remplacent le projet.

AjoutAvisPourquoi
fieldà adopterune propriété validée sans champ déclaré ; vérifier CS9258
membres d'extensionà adopterpropriétés et membres statiques d'extension
affectation conditionnelle ?.=à connaîtrecourte, mais l'évaluation conditionnelle se lit mal
spans de première classeà connaîtrele bénéfice vient sans rien écrire ; cours Mémoire et performance
nameof sur générique non lié, modificateurs de lambdaà connaîtrepetites commodités
constructeurs et événements partial, affectation composéerarement utilegénérateurs de code, types numériques
directives #: des applications fichierà connaîtrescripts et prototypes sans fichier projet

C# 15, en release candidate avec .NET 11

C# 15 sortira avec .NET 11, dont la sortie est annoncée le 10 novembre 2026. Depuis la release candidate 1 du 8 septembre 2026, c'est la version par défaut de net11.0 et ses ajouts sont stabilisés, sauf la sûreté mémoire, qui reste une préversion indépendante : ses assouplissements sur les pointeurs demandent preview, ses nouvelles règles une option du compilateur en plus. La page What's new in C# 15 de learn.microsoft.com le présente encore, au 26 septembre 2026, comme une préversion. Chaque ajout est arrivé dans une préversion de .NET 11 ; les notes de la RC 1 comptent aussi parmi eux les membres statiques d'interface non virtuels (non-virtual static interface members), que la page What's new ne décrit pas.

AjoutArrivé enSDK 10.0.300, net10.0, LangVersion previewSDK 11.0.100-rc.1, net11.0, C# 15
arguments d'expression de collectionpréversion 1, 10 février 2026compile et s'exécutecompile et s'exécute
unions préversion 2, 10 mars 2026 : compilateur seul, types de support à écrire soi-même ; préversion 5, 9 juin 2026 : déclarations et motifs d'union ; types de support dans le framework en préversion 6, 14 juillet 2026 (en préversion 5 selon learn) syntaxe reconnue, CS0518 et CS0656 : types absents de net10.0compile et s'exécute
hiérarchies fermées, closedpréversion 5, 9 juin 2026mot-clé inconnu (CS1002)compile et s'exécute ; CS8509 sur un cas oublié
sûreté mémoire, pointeurs hors unsafepréversion 5, 9 juin 2026 ; encore préversion à la RC 1refusé (CS0214) refusé en C# 15 (CS0214) ; sous preview, déclaration et & compilent, le déréférencement reste refusé (CS9360)
indexeurs d'extensionpréversion 6, 14 juillet 2026refusé (CS9282)compile et s'exécute
break et continue étiquetéspréversion 7, 11 août 2026syntaxe inconnue (CS1002)compile et s'exécute

Le SDK 11.0.100-rc.1.26425.128 embarque le compilateur 5.11.0-1.26425.128, qui retient C# 15 pour un projet net11.0 sans LangVersion : tous les exemples de cette section y compilent et s'exécutent sans option. Sous LangVersion 14.0, il refuse arguments de collection, closed, étiquettes et indexeurs d'extension par CS9327 ; union n'y est pas un mot-clé et produit des erreurs de syntaxe. Celui du SDK 10.0.300, 5.6.0-2.26230.102, ne connaît pas LangVersion 15.0 (CS1617) et, sous preview, ne compile que les arguments d'expression de collection : un élément with(...) en tête transmet ses arguments au constructeur de la collection, un comparateur ou une capacité.

// SDK 11.0.100-rc.1.26425.128, net11.0 sans LangVersion, donc C# 15.
// Meme sortie avec le SDK 10.0.300, net10.0 et <LangVersion>preview</LangVersion>.
using System;
using System.Collections.Generic;

string[] references = ["usb-64", "USB-64", "ssd-1t"];

// with(...) en tete : les arguments du constructeur de la collection.
HashSet<string> uniques = [with(StringComparer.OrdinalIgnoreCase), .. references];
List<string> copie = [with(capacity: 10), .. references];

Console.WriteLine($"{uniques.Count} {copie.Count} {copie.Capacity}");
// 2 3 10

// Sous LangVersion 14.0, avec le SDK 11, une fois par element with(...) :
// error CS9327: Feature 'collection expression arguments' is not available in C# 14.0. Please use language version 15.0 or greater.
// Avec le SDK 10.0.300, sous LangVersion 14.0 :
// error CS8652: The feature 'collection expression arguments' is currently in Preview and *unsupported*.
//   To use Preview features, use the 'preview' language version.

Les unions déclarent un ensemble fermé de types de cas, qui n'ont pas à partager d'ancêtre ; chaque cas s'y convertit implicitement, et une expression switch qui les traite tous est exhaustive sans bras _. C'est ce que le patron Result du cours Exceptions et gestion d'erreur écrit à la main. Sous net10.0, même en preview et même avec le SDK 11, le compilateur réclame deux types livrés avec .NET 11.

// SDK 11.0.100-rc.1.26425.128, net11.0, C# 15.
using System;

Paiement paiement = new Virement("FR76-001"); // conversion implicite depuis un cas
Console.WriteLine(Decrire(paiement));
// virement FR76-001

// Exhaustive sans bras _ : le compilateur connait la liste des cas.
static string Decrire(Paiement p) => p switch
{
    Carte c => $"carte {c.Numero}",
    Virement v => $"virement {v.Iban}",
};

public record Carte(string Numero);
public record Virement(string Iban);
public union Paiement(Carte, Virement);

// Sans le bras Virement, la compilation passe avec un avertissement :
// warning CS8509: The switch expression does not handle all possible values of its input type
//   (it is not exhaustive). For example, the pattern 'Virement' is not covered.

// Sous net10.0, meme en preview, avec le SDK 10.0.300 comme avec le SDK 11 :
// error CS0518: Predefined type 'System.Runtime.CompilerServices.IUnion' is not defined or imported
// error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.UnionAttribute..ctor'

Les hiérarchies fermées donnent la même exhaustivité à des classes : une classe closed, implicitement abstraite, ne se dérive directement que dans son assembly. La restriction ne descend pas plus bas : un descendant qui n'est ni closed ni sealed se dérive ailleurs. Sous net10.0, le SDK 11 réclame là aussi un attribut de .NET 11, IsClosedTypeAttribute (CS0656).

// SDK 11.0.100-rc.1.26425.128, net11.0, C# 15.
using System;

Console.WriteLine(Decrire(new Ouverte(40)));
// ouverte a 40 %

// Exhaustive sans bras _ : Fermee et Ouverte sont les seuls descendants directs.
static string Decrire(Vanne v) => v switch
{
    Fermee => "fermee",
    Ouverte(var pourcent) => $"ouverte a {pourcent} %",
};

// closed : implicitement abstraite, derivable directement dans cet assembly seulement.
public closed record Vanne;
public sealed record Fermee : Vanne;
public sealed record Ouverte(int Pourcent) : Vanne;

// Ajouter public sealed record Bloquee : Vanne; laisse compiler, avec un avertissement :
// warning CS8509: The switch expression does not handle all possible values of its input type
//   (it is not exhaustive). For example, the pattern 'Bloquee' is not covered.

// Deriver Vanne depuis un autre assembly :
// error CS9382: 'Bloquee': cannot use a closed type 'Vanne' from another assembly as a base type.

Les étiquettes de break et continue nomment la boucle visée, là où il fallait un booléen testé à chaque niveau ou un goto ; les indexeurs d'extension complètent les membres d'extension de C# 14.

// SDK 11.0.100-rc.1.26425.128, net11.0, C# 15.
using System;
using System.Collections.Generic;
using System.Linq;

int[][] lots = [[3, 0, 5], [2, 4], [7, -1, 8], [9]];

// Un zero invalide le lot entier ; un negatif arrete tout.
externe: foreach (int[] lot in lots)
{
    int somme = 0;
    foreach (int n in lot)
    {
        if (n == 0) continue externe;
        if (n < 0) break externe;
        somme += n;
    }
    Console.WriteLine($"lot de {lot.Length} : {somme}");
}

// Un indexeur d'extension : une sequence s'indexe comme un tableau.
IEnumerable<int> impairs = Enumerable.Range(1, 10).Where(n => n % 2 == 1);
Console.WriteLine(impairs[2]);

// Sortie :
// lot de 2 : 6
// 5

public static class IndexeurSequence
{
    extension(IEnumerable<int> sequence)
    {
        public int this[int index] => sequence.ElementAt(index);
    }
}

L'avis reste provisoire jusqu'à la sortie, et .NET 11 est une version STS, prise en charge deux ans : beaucoup d'équipes resteront sur .NET 10, LTS, et donc sur C# 14. Unions et hiérarchies fermées sont à adopter dès qu'un projet cible net11.0 : sans elles, un switch sur une hiérarchie ouverte n'est exhaustif qu'avec un bras _, où un cas ajouté plus tard tombe sans bruit ; avec elles, le cas oublié devient un avertissement de compilation. Les arguments de collection, les étiquettes de break et les indexeurs d'extension sont à connaître ; la sûreté mémoire ne concerne que le code qui manipule des pointeurs.

Ce cours vous a servi ? Offrir un café Signaler une erreur