Le but de cet article est de résumer et d’expliquer les fonctionnalités de C# 13.0. Dans un premier temps, on explicitera le contexte de C# 13.0 par rapport aux autres composants (frameworks, IDE, compilateur etc…) qui permettent de l’utiliser. Ensuite, on rentrera dans le détail des fonctionnalités.
Précisions sur les versions de C#
Chronologie des releases
id=”cs12-Lien_entre_la_version_C_et_le_compilateur”>Lien entre la version C# et le compilateur
Limiter la version C# à compiler
Fonctionnalités C# 13
Params Collection
Attribut OverloadResolutionPriority
Utilisation de partial avec des propriétés (C# 13), indexers (C# 13), des évènements (C# 14) et des constructeurs (C# 14)
Index de fin d’une structure utilisable dans un initializer
Utilisation d’index dans un initializer à partir de C# 6
Index de fin à partir de C# 8
Utilisation d’index de fin dans un initializer à partir de C# 13
Nouvelle séquence d’échappement \e
Précisions sur les versions de C#
Depuis C# 8.0, les évolutions fonctionnelles de C# se font pour .NET seulement (anciennement appelé .NET Core). Le framework .NET est toujours supporté toutefois les nouvelles fonctionnalités ne sont pas implémentées pour cet environnement.
Comme les environnements du framework .NET et de .NET ne subsistent plus en parallèle, l’approche .NET Standard n’a plus d’intérêt. .NET Standard s’arrête donc à la version 2.1. Les versions 5.0, 6.0, 7.0, 8.0 et 9.0 de .NET implémentent .NET Standard de la version 1.0 à 2.1 toutefois il est désormais conseillé de cibler une version de .NET plutôt que .NET Standard.
Chronologie des releases
Ce tableau permet de résumer les dates de sorties de C# 13.0, de Visual Studio, du compilateur Roslyn et des versions .NET.
| Date | Version C# | Version Visual Studio | Version .NET | Compilateur | Support |
|---|---|---|---|---|---|
| Novembre 2021 | C# 10.0 | VS2022 (17.0) | .NET 6.0 (NET Standard 1.0⇒2.1)* |
Roslyn 4.0.1 | Novembre 2024 (LTS**) |
| Décembre 2021 | Roslyn 4.1.0 | ||||
| Février 2022 | VS2022 (17.1) | ||||
| Avril 2022 | Roslyn 4.2.0 | ||||
| Mai 2022 | VS2022 (17.2) | ||||
| Août 2022 | VS2022 (17.3) | Roslyn 4.3.1 | |||
| Novembre 2022 | C# 11.0 | VS2022 (17.4) | .NET 7.0 (NET Standard 1.0⇒2.1)* |
Roslyn 4.4.0 | Mai 2024 |
| Février 2023 | VS2022 (17.5) | Roslyn 4.5.0 | |||
| Mai 2023 | VS2022 (17.6) | Roslyn 4.6.0 | |||
| Août 2023 | VS2022 (17.7) | Roslyn 4.7.0 | |||
| Novembre 2023 | C# 12.0 | VS2022 (17.8) | .NET 8.0 (NET Standard 1.0⇒2.1)* |
Roslyn 4.8.0 | Novembre 2026 (LTS**) |
| Février 2024 | VS2022 (17.9) | Roslyn 4.9.2 | |||
| Mai 2024 | VS2022 (17.10) | Roslyn 4.10.0 | |||
| Août 2024 | VS2022 (17.11) | Roslyn 4.11.0 | |||
| Novembre 2024 | C# 13.0 | VS2022 (17.12) | .NET 9.0 | Roslyn 4.12.0 | Novembre 2026 |
| Février 2025 | VS2022 (17.13) | Roslyn 4.13.0 | |||
| Mai 2025 | VS2022 (17.14) | Roslyn 4.14.0 | |||
| Novembre 2025 | C# 14.0 | VS2026 (18.0) | .NET 10.0 | Roslyn 5.0.0 | Novembre 2028 (LTS**) |
| Décembre 2025 | VS2026 (18.1) | ||||
| Janvier 2026 | VS2026 (18.2) | ||||
| Février 2026 | VS2026 (18.3) | ||||
| Novembre 2026 ? | C# 15.0 | VS2026 (18.?) | .NET 11.0 | Roslyn 5.?.? | Novembre 2028 ? |
- *: .NET Standard n’est plus nécessaire puisque les 2 environnements framework .NET et .NET Core n’évoluent plus fonctionnellement. Ils ont laissé place à l’environnement uniformisé .NET (voir .NET 5+ and .NET Standard pour plus de détails).
- **: LTS pour Long Term Support. Ces versions correspondent au support étendu à 2 ans après la 1ère release contrairement aux autres versions qui sont limitées à 1 an.
Lien entre la version C# et le compilateur
Le tableau précédent permet d’indiquer la version de C# dans le contexte des frameworks de façon à avoir une idée des sorties des autres éléments de l’environnement .NET. Toutefois, la version de C# est liée à la version du compilateur C#. Le compilateur est ensuite livré avec Visual Studio (depuis Visual Studio 2017 15.3) ou avec le SDK .NET.
Le chemin du compilateur est lié au composant avec lequel il est livré:
- Avec Visual Studio: par exemple pour Visual Studio 2022 Professional:
C:\Program Files (x86)\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\Roslyn\csc.exe - Avec les Build tools: par exemple pour les Build Tools for Visual Studio 2022:
C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\Roslyn\csc.exe - Avec le SDK .NET:
- Sur Linux:
/usr/share/dotnet/sdk/<version>/Roslyn/bincore/csc.dll - Sur Windows:
C:\Program Files\dotnet\sdk\<version>\Roslyn\bincore\csc.dll
- Sur Linux:
On peut connaître la version du compilateur en tapant:
csc -help
On peut savoir quelles sont les versions de C# que le compilateur peut gérer en exécutant:
csc -langversion:?
Limiter la version C# à compiler
Par défaut, le compilateur compile dans les versions suivantes de C#:
- .NET 10.0: C# 14.0
- .NET 9.0: C# 13.0
- .NET 8.0: C# 12.0
- .NET 7.0: C# 11.0
- .NET 6.0: C# 10.0
- .NET 5.0: C# 9.0
- Framework .NET: C# 7.3
- .NET Core 3.x: C# 8.0
- .NET Core 2.x: C# 7.3
- .NET Standard 2.1: C# 8.0
- .NET Standard 2.0: C# 7.3
- .NET Standard 1.x: C# 7.3
On peut volontairement limiter la version C# que le compilateur va traiter:
- Dans Visual Studio: dans les propriétés du projet ⇒ Onglet Build ⇒ Advanced ⇒ Paramètre Language version.
- En éditant directement le fichier
.csprojdu projet et en indiquant la version avec le paramètreLangVersion:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net9.0</TargetFramework> <LangVersion>13.0</LangVersion> </PropertyGroup> </Project>
Fonctionnalités C# 13
Les fonctionnalités les plus basiques de C# 13.0 sont présentées dans cet article. D’autres fonctionnalités plus avancées sont présentées dans les articles suivants:
- Nouveau Lock qui est une des fonctionnalités principales de C# 13.
- Améliorations concernant les variables
refet lesref struct.
Params Collection
Cette fonctionnalité est assez directe. Avant C# 13, le mot-clé params ne pouvait être utilisé qu’avec un tableau:
public void AddItems(params string[] newItems)
{
items.AddRange(newItems);
}
A partir de C# 13, on peut désormais utiliser d’autres structures comme List<T>, Span<T>, ReadOnlySpan<T>, IEnumerable<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyList<T>, par exemple:
internal class ParamsCollections
{
private readonly List<string> items = new List<string>();
// Before C# 13.0
public void AddItems(params string[] newItems)
{
items.AddRange(newItems);
}
// From C# 13.0
public void AddItems(params IEnumerable<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params List<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params IList<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params ICollection<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params IReadOnlyList<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params IReadOnlyCollection<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params Span<string> newItems)
{
items.AddRange(newItems);
}
public void AddItems(params ReadOnlySpan<string> newItems)
{
items.AddRange(newItems);
}
}
Attribut OverloadResolutionPriority
L’utilisation de l’attribut OverloadResolutionPriority permet d’indiquer un ordre de priorité différent de l’ordre par défaut pour la résolution de surcharge de fonctions.
L’attribut prend en paramètre un entier indiquant la priorité: plus le chiffre est élevé et plus la fonction est prioritaire dans l’ordre de résolution. Par défaut, si l’attribut n’est pas présent, l’ordre de priorité est 0.
Par exemple si on considère:
internal class OverloadResolutionPriorityAttributeExample
{
private string title;
public void SetTitle(string newTitle)
{
title = newTitle;
Console.WriteLine("Called overload: SetTitle(string newTitle)");
}
public void SetTitle(ReadOnlySpan<char> newTitle)
{
title = newTitle.ToString();
Console.WriteLine("Called overload: SetTitle(ReadOnlySpan<char> newTitle)");
}
public void ExecuteMe()
{
SetTitle("OK") ;
}
}
Si on exécute ExecuteMe(), la surcharge choisie sera void SetTitle(string newTitle). Si on ajoute des attributs OverloadResolutionPriority à ces fonctions, comme suit:
internal class OverloadResolutionPriorityAttributeExample
{
private string title;
[OverloadResolutionPriority(1)]
public void SetTitle(string newTitle)
{
title = newTitle;
Console.WriteLine("Called overload: SetTitle(string newTitle)");
}
[OverloadResolutionPriority(2)]
public void SetTitle(ReadOnlySpan<char> newTitle)
{
title = newTitle.ToString();
Console.WriteLine("Called overload: SetTitle(ReadOnlySpan<char> newTitle)");
}
public void ExecuteMe()
{
SetTitle("OK") ;
}
}
La surcharge exécutée sera SetTitle(ReadOnlySpan<char> newTitle).
Utilisation de partial avec des propriétés (C# 13), indexers (C# 13), des évènements (C# 14) et des constructeurs (C# 14)
Le mot clé partial permet de séparer l’implémentation d’une classe, d’une struct, d’une interface ou d’un record. Lorsque la définition d’un de ces objets est séparée, les données et fonctions membres sont additionnées à la définition finale de l’objet.
Par exemple, la définition de la classe PartialExample peut être séparée dans des fichiers différents:
// Avant C# 13
// Partie 1
internal partial class PartialExample
{
private readonly Dictionary<string, string> stringData = new Dictionary<string, string>();
public bool TryGetString(string key, out string value)
{
return stringData.TryGetValue(key, out value);
}
}
// Partie 2
internal partial class PartialExample
{
private readonly Dictionary<string, int> intData = new Dictionary<string, int>();
public bool TryGetInt(string key, out int value)
{
return intData.TryGetValue(key, out value);
}
}
Les 2 parties de la classe peuvent se trouver dans des fichiers séparés ou dans le même fichier.
La définition finale de la classe PartialExample contient les membres des 2 parties:
var partialExample = new PartialExample();
partialExample.TryGetString("testKey", out string stringValue);
partialExample.TryGetInt("testKey", out int intValue);
Le mot-clé partial peut s’appliquer aux fonctions et permet d’indiquer la déclaration d’une fonction dans une partie et l’implémentation de cette fonction dans une autre partie:
// Partie 1
internal partial class PartialExample
{
// [...]
// Déclaration fonction
public partial bool TryGetValue(string key, out string value);
}
// Partie 2
internal partial class PartialExample
{
// [...]
// Définition fonction
public partial bool TryGetValue(string key, out string value)
{
return stringData.TryGetValue(key, out value);
}
}
A partir de C# 13, on peut appliquer partial à des propriétés, par exemple:
// Partie 1
internal partial class PartialExample
{
// [...]
// Properties (C# 13)
public partial IEnumerable<string> DataKeys { get; }
}
// Partie 2
internal partial class PartialExample
{
// [...]
public partial IEnumerable<string> DataKeys => stringData.Keys;
}
Depuis C# 13, partial peut aussi être appliqué à des indexers:
// Partie 1
internal partial class PartialExample
{
// [...]
// Indexers (C# 13)
public partial string this[string key] { get; set; }
}
// Partie 2
internal partial class PartialExample
{
// [...]
public partial string this[string key]
{
get => stringData[key];
set => stringData[key] = value;
}
}
A partir de C# 14, partial peut s’appliquer aux constructeurs. On peut ainsi séparer la déclaration d’un constructeur de sa définition:
// Partie 1
internal partial class PartialExample
{
// [...]
// Déclaration d'un constructor (C# 14)
public partial PartialExample(IDictionary<string, string> data);
}
// Partie 2
internal partial class PartialExample
{
// [...]
// Définition du constructeur
public partial PartialExample(IDictionary<string, string> data)
{
foreach (var kvp in data)
stringData[kvp.Key] = kvp.Value;
}
}
Encore à partir de C# 14, on peut désormais utiliser partial pour des évènements:
// Partie 1
internal partial class PartialExample
{
// [...]
// Evènement (C# 14)
public partial event EventHandler DataAdded;
}
// Partie 2
internal partial class PartialExample
{
// [...]
public partial event EventHandler DataAdded
{
add { }
remove { }
}
}
Index de fin d’une structure utilisable dans un initializer
Cette fonctionnalité n’est pas très utile car très restrictive. Elle permet d’initialiser une structure de données dans l’initializer d’un objet en utilisant un index de fin. Mais attention, l’initializer est celui d’un objet contenant la structure de données et non pas l’initializer de la structure de données.
Utilisation d’index dans un initializer à partir de C# 6
A partir de C# 6, on pouvait déjà utiliser un index pour initialiser une structure de données membre d’un objet.
Par exemple, si on considère la classe suivante:
public class ContainingObject
{
public int[] NumberArray = new int[5];
public List<int> NumberList = new List<int>(5);
public List<int> NumberListWithElements = new List<int> { 0, 0, 0, 0, 0 };
}
On peut instancier cette classe en utilisant un initializer et affecter directement les structures de données dans l’initializer de la classe:
var containingObject = new ContainingObject
{
NumberArray = {
[0] = 1,
[1] = 2,
[2] = 3,
[3] = 4,
[4] = 5,
}
};
// Le tableau contient:
// | Indexes: | 0 | 1 | 2 | 3 | 4 |
// | Values: | 1 | 2 | 3 | 4 | 5 |
Mais attention, pour que cette notation puisse être utilisée, il faut que le tableau soit déjà instancié à la bonne taille ce qui est fait dans le corps de la classe ContainingObject:
public int[] NumberArray = new int[5];
Si on utilise la même notation pour le membre NumberList ça ne marche pas car les index ne sont pas accessibles:
var containingObject = new ContainingObject
{
NumberList =
{
[0] = 1,
[1] = 2, // ATTENTION: OutOfRange exception
[2] = 3,
[3] = 4,
[4] = 5,
}
};
Cette exécution échoue car les index ne sont pas accessibles, une exception de type OutOfRangeException est lancée.
De même si on essaie d’instancier directement une liste de cette façon, une exception OutOfRange sera aussi lancée:
var integers =
new List<int>(5)
{
[0] = 1,
[1] = 2,
[2] = 3, // ATTENTION: OutOfRange exception
[3] = 4,
[4] = 5,
};
Par contre pour le membre NumberListWithElements, cette instantiation réussit car les index existent:
var containingObject = new ContainingObject
{
NumberListWithElements =
{
[0] = 1,
[1] = 2,
[2] = 3, // OK
[3] = 4,
[4] = 5,
}
};
// Le tableau contient:
// | Indexes: | 0 | 1 | 2 | 3 | 4 |
// | Values: | 1 | 2 | 3 | 4 | 5 |
Index de fin à partir de C# 8
A partir de C# 8, est apparu l’index de fin utilisable pour des structures de données [^..], par exemple:
var integers = new List<int> { 1, 2, 3, 4, 5, 6 };
Console.WriteLine(integers[^1]); // 6
Console.WriteLine(integers[^2]); // 5
Console.WriteLine(integers[^6]); // 1
Console.WriteLine(integers[^0]); // ERREUR: out of range exception
Console.WriteLine(integers[^7]); // ERREUR: out of range exception
Pour plus de détails sur cet index de fin: C# 8 – Index et plage de structure de données.
Utilisation d’index de fin dans un initializer à partir de C# 13
C# 13 permet d’utiliser des index de fin pour initialiser un membre dans l’initializer d’un objet. Cependant les restrictions seront les mêmes que pour les index normaux (voir plus haut).
Ainsi, si on peut instancier directement un tableau en tant que membre avec des index de fin:
var containingObject = new ContainingObject
{
NumberArray = {
[^1] = 1,
[^2] = 2,
[^3] = 3,
[^4] = 4,
[^5] = 5,
}
};
// Le tableau contient:
// | Indexes: | 0 | 1 | 2 | 3 | 4 |
// | Values: | 5 | 4 | 3 | 2 | 1 |
De même si une liste est instanciée et que les index existent comme pour NumberListWithElements, on peut écrire:
var containingObject = new ContainingObject
{
NumberListWithElements = {
[^1] = 1,
[^2] = 2,
[^3] = 3, // OK
[^4] = 4,
[^5] = 5,
}
};
// La liste contient:
// | Indexes: | 0 | 1 | 2 | 3 | 4 |
// | Values: | 5 | 4 | 3 | 2 | 1 |
En revanche si la liste ne contient pas d’éléments au préalable, les index n’existent pas et une exception de type OutOfRange est lancée:
var containingObject = new ContainingObject
{
NumberList =
{
[^1] = 1,
[^2] = 2,
[^3] = 3, // ATTENTION: OutOfRange exception
[^4] = 4,
[^5] = 5,
}
};
A cause de cette restriction, cette fonctionnalité paraît peu utile et peut facilement amener à des erreurs lors de l’exécution.
Nouvelle séquence d’échappement \e
Pour représenter le caractère Unicode d’échappement escape, on devait utiliser \u001b ou \x1b:
\u001b: dans les chaînes de caractères,\uest utilisé pour permettre d’écrire un caractère Unicode quelconque avec la syntaxe:\u<code du caractère sur 4 octets>.
Ainsi pour écrire le caractère Unicode escape correspondant au code1b, on utilise le code001bsur 4 octets.\x1b:\xest utilisé pour permettre un caractère Unicode quelconque dont le code contient un nombre variable d’octets avec la syntaxe:\x<code du caractère avec un nombre variable d'octets>.
On peut utiliser\xpour représenter des caractères avec un code sur:- 2 octets: par exemple
\xe7correspondant au codee7(qui est le caractère ç); - 3 octets: par exemple
\x3b2correspondant au code3b2(qui est le caractère β); - 4 octets: par exemple
\x24d4correspondant au code24d4(qui est le caractère ⓔ).
- 2 octets: par exemple
Voir Unicode en 5 min pour plus de détails sur les caractères Unicode.
Avant C# 13, pour représenter le caractère Unicode escape avec \x il faudrait utiliser \x1b or cette syntaxe prête à confusion. Par exemple \x1b44 peut être considéré comme:
- Le caractère avec le code Unicode
U+001Bsuivi de"44"ou - Le caractère avec le code Unicode
U+1B44.
Pour éviter cette confusion, C# 13 introduit \e correspondant au caractère d’échappement escape.
Par exemple, la séquence [32m est un code d’échappement ANSI utilisé pour changer la couleur du texte dans les terminaux qui prennent en charge les séquences d’échappement ANSI. Plus précisément, 32m définit la couleur du texte en vert. De même, [0m est utilisé pour réinitialiser la mise en forme du texte aux paramètres par défaut.
Si on utilise \u001b pour échapper le caractère escape alors on peut écrire:
string text = "\u001b[32mTexte affiché en vert\u001b[0m";
Console.WriteLine(text);
Avec la nouvelle séquence d’échappement \e, on peut désormais écrire plus simplement:
string text = "\e[32mTexte affiché en vert\e[0m";
Console.WriteLine(text);
L’affichage de ce code dans un terminal qui prend en charge les séquences d’échappement ANSI affichera le texte:
Texte affiché en vert
Autres fonctionnalités
Les autres fonctionnalités sont traitées dans d’autres articles:
- What’s new in C# 13: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13
- What’s new in C# 14: https://learn.microsoft.com/fr-fr/dotnet/csharp/whats-new/csharp-14
- OverloadResolutionPriorityAttribute Class: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.overloadresolutionpriorityattribute
- Method group natural type improvements: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-13.0/method-group-natural-type-improvements
- Partial member: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/partial-member
- Partial Classes and Members: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods
- String escape sequences: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/#string-escape-sequences
- C# Escape Characters: https://medium.com/@AIbatros/c-escape-characters-124b5b2607c8
- Initialiseurs d’objets et de collection: https://learn.microsoft.com/fr-fr/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers
- Memory Efficiency: ref and unsafe Feature in C# 13: https://www.thetechplatform.com/post/memory-efficiency-ref-and-unsafe-feature-in-c-13
- Some more C# 13: https://developers.redhat.com/articles/2025/04/21/c-13-advanced-features#