02 juillet 2025
Dans cet article, nous allons examiner certaines des nouvelles fonctionnalités introduites dans C# 11 et 12 qui aident à simplifier votre code et à rendre le développement plus fluide. Ces mises à jour ne sont peut-être pas révolutionnaires, mais elles sont pratiques et conçues pour gagner du temps en réduisant la complexité inutile. Nous verrons comment de petits changements peuvent conduire à des solutions plus propres et plus efficaces dans les tâches de codage quotidiennes.
Construire des chaînes avec un contenu complexe a historiquement posé un défi en C#. Les développeurs doivent souvent gérer l'échappement des caractères spéciaux, des sauts de ligne et des guillemets, ce qui conduit à un code verbeux et souvent illisible. Ce processus devient particulièrement lourd lorsqu'il s'agit de formats comme JSON, XML ou des expressions régulières intégrées directement dans les fichiers source.
C# 11 a introduit les chaînes de caractères brutes pour répondre directement à ce problème. Cette fonctionnalité permet aux chaînes de s'étendre sur plusieurs lignes et de contenir pratiquement n'importe quel caractère, y compris des guillemets et des barres obliques inverses intégrés, sans avoir besoin de séquences d'échappement. Une chaîne brute commence et se termine par au moins trois guillemets doubles ("""
).
Avant C# 11 :
string oldJson = "{\r\n \"name\": \"Alice\",\r\n \"age\": 30\r\n}";
Console.WriteLine(oldJson);
Avec C# 11 :
string newJson = """
{
"name": "Alice",
"age": 30
}
""";
Console.WriteLine(newJson);
Tout espace blanc précédant les guillemets de fermeture définit l'indentation minimale pour la chaîne, que le compilateur supprime de la sortie finale. Les chaînes de caractères brutes améliorent considérablement la lisibilité des chaînes et réduisent la probabilité d'erreurs de syntaxe.
La correspondance de motifs en C# a considérablement évolué, avec C# 11 introduisant les modèles de liste pour permettre la correspondance de séquences dans les tableaux ou les listes. Cette amélioration permet aux développeurs d'inspecter la structure et le contenu des collections de manière concise et expressive.
Auparavant, valider la structure d'une collection nécessitait des vérifications manuelles sur la longueur et les indices individuels, ce qui conduisait à un code verbeux et moins maintenable. Les modèles de liste répondent à cela en prenant en charge des sous-modèles tels que les motifs constants, de type, de propriété et relationnels. Les fonctionnalités clés incluent le motif de rejet (_
) pour correspondre à un seul élément quelconque et le motif de plage (..
) pour correspondre à une séquence de zéro ou plusieurs éléments.
Avant C# 11 :
int[] numbers = { 1, 2, 3 };
if (numbers != null && numbers.Length == 3 &&
numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
Console.WriteLine("Le tableau contient exactement 1, 2, 3.");
}
if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
Console.WriteLine("Le tableau a 2 comme deuxième élément.");
}
Avec C# 11 :
int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3])
{
Console.WriteLine("Le tableau contient exactement 1, 2, 3.");
}
if (numbers is [_, 2, ..])
{
Console.WriteLine("Le tableau a 2 comme deuxième élément.");
}
Les modèles de liste simplifient la validation des séquences en une forme compacte et lisible, réduisant considérablement le nombre de lignes de code nécessaires pour de telles opérations.
L'initialisation d'objets peut parfois conduire à un état indésirable où des propriétés ou des champs essentiels restent non assignés. Traditionnellement, les développeurs imposent une initialisation obligatoire par le biais de constructeurs qui acceptent tous les paramètres requis ou en ajoutant des vérifications défensives dans les méthodes.
C# 11 introduit le modificateur required
pour les propriétés et les champs, un mécanisme d'application au moment de la compilation. Lorsqu'un membre est marqué comme required
, le compilateur s'assure qu'il reçoit une valeur lors de la création de l'objet, soit par un constructeur, soit par un initialiseur d'objet. Cela garantit que les instances d'un type sont toujours dans un état valide et entièrement initialisé, évitant les bugs courants liés à des données manquantes.
Avant C# 11 :
public class OldPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void DisplayName() => Console.WriteLine($"Nom : {FirstName} {LastName}");
}
// Utilisation :
var person = new OldPerson(); // Pas d'erreur de compilation, mais crée un objet potentiellement invalide
person.DisplayName();
Avec C# 11 :
public class NewPerson
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public void DisplayName() => Console.WriteLine($"Nom : {FirstName} {LastName}");
}
// Utilisation :
// var person = new NewPerson(); // Erreur de compilation - propriétés requises manquantes
// var person = new NewPerson { FirstName = "John" }; // Erreur de compilation - LastName manquant
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // OK
person.DisplayName();
Les membres obligatoires éliminent les surprises au moment de l'exécution en imposant l'initialisation au moment de la compilation, réduisant le besoin de vérifications manuelles. Cette fonctionnalité améliore la fiabilité du code avec moins de codage défensif, permettant aux développeurs de se concentrer sur la fonctionnalité plutôt que sur la validation.
C# 12 introduit les constructeurs primaires pour toutes les classes et structures, élargissant une fonctionnalité autrefois exclusive aux types d'enregistrement. Cela permet de déclarer les paramètres du constructeur directement dans la définition du type, en les définissant automatiquement comme des champs ou des propriétés à travers toute la classe. Contrairement aux constructeurs traditionnels, cette approche évite les déclarations explicites de champs et les affectations manuelles.
Le problème clé résolu ici est la répétition de code passe-partout dans l'initialisation d'objets. Auparavant, les développeurs devaient définir des champs privés et mapper explicitement les arguments du constructeur à ces derniers, augmentant inutilement la taille du code. Les constructeurs primaires simplifient cela, intégrant la logique d'initialisation directement dans la signature du type.
Avant C# 12 :
public class OldProduct
{
private readonly int _productId;
private readonly string _productName;
public OldProduct(int productId, string productName)
{
_productId = productId;
_productName = productName;
}
public string PrintDetails() => $"ID Produit : {_productId}, Nom : {_productName}";
}
// Utilisation :
OldProduct oldProd = new OldProduct(101, "Ordinateur portable");
oldProd.PrintDetails();
Avec C# 12 :
public class NewProduct(int productId, string productName)
{
public string PrintDetails() => $"ID Produit : {productId}, Nom : {productName}";
}
// Utilisation :
NewProduct newProd = new NewProduct(102, "Clavier");
newProd.PrintDetails();
Les constructeurs primaires rendent la définition des types centrés sur les données incroyablement succincte. Ils améliorent la lisibilité en plaçant les paramètres de construction essentiels juste à côté du nom du type, rendant les dépendances de la classe ou de la structure claires d'un coup d'œil.
L'initialisation des collections en C# a historiquement impliqué diverses syntaxes selon le type de collection, comme new List<T> { ... }
pour les listes ou new T[] { ... }
pour les tableaux. Combiner ou fusionner des collections existantes en une nouvelle nécessitait souvent des boucles itératives ou des méthodes LINQ comme Concat()
, ajoutant du surpoids et de la verbosité.
C# 12 introduit les expressions de collection, une syntaxe unifiée et concise pour créer et initialiser une large gamme de types de collections. En utilisant une syntaxe simple [...]
, les développeurs peuvent créer des tableaux, des listes, des Span<T>
, et d'autres types similaires à des collections. Le nouvel élément de dispersion (..
) permet d'intégrer des éléments de collections existantes directement dans une nouvelle expression de collection, éliminant le besoin de concaténation manuelle.
Avant C# 12 :
// Initialisation de différentes collections
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };
// Combinaison de collections
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);
Console.WriteLine(string.Join(", ", allNumbers));
Avec C# 12 :
// Initialisation de différentes collections
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];
// Combinaison de collections avec l'opérateur de dispersion
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];
Console.WriteLine(string.Join(", ", allNumbers));
Les expressions de collection réduisent la verbosité de l'initialisation et de la combinaison des collections, offrant une syntaxe plus propre et plus intuitive. Cette efficacité accélère le codage et améliore la lisibilité, soutenant le principe d'avoir un impact plus important avec moins de lignes.
Les expressions lambda, un pilier de la programmation fonctionnelle en C#, ont historiquement manqué la capacité de définir des valeurs par défaut pour leurs paramètres. Si un lambda devait gérer des arguments optionnels ou fournir des valeurs de secours, les développeurs devaient recourir à une logique conditionnelle dans le corps du lambda ou définir plusieurs surcharges, même si les lambdas ne prennent pas directement en charge les surcharges.
C# 12 comble cette lacune en permettant des valeurs par défaut pour les paramètres dans les expressions lambda. La syntaxe et le comportement reflètent ceux des paramètres de méthode ou de fonction locale, offrant une manière plus fluide et concise de définir des fonctions lambda flexibles.
Avant C# 12 :
// Lambda sans paramètres par défaut.
// Si une valeur par défaut était souhaitée pour 'y', souvent un wrapper ou une logique conditionnelle était nécessaire :
Func<int, int, int> addOld = (x, y) => x + y;
Func<int, int> addWithDefaultOld = x => addOld(x, 10); // Une solution de contournement courante
Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));
Avec C# 12 :
// Lambda avec paramètres par défaut
Func<int, int, int> addNew = (x, y = 10) => x + y;
Console.WriteLine(addNew(5, 3)); // y est 3
Console.WriteLine(addNew(5)); // y prend la valeur par défaut 10
L'introduction de paramètres par défaut pour les lambdas améliore considérablement leur flexibilité et leur expressivité. Cela réduit le besoin de définitions de lambda redondantes ou de logique conditionnelle interne.
C# 11 et 12 offrent un ensemble de fonctionnalités convaincantes qui tiennent la promesse de “Écrire Moins, Faire Plus”. Des chaînes de caractères brutes et des modèles de liste de C# 11 aux constructeurs primaires et expressions de collection de C# 12, ces avancées répondent à de véritables frustrations dans le codage quotidien. Elles éliminent la syntaxe inutile, améliorent la lisibilité et imposent des modèles plus sûrs, améliorant directement les flux de travail dans le développement logiciel et les projets de conversion de code. Chaque innovation — qu'il s'agisse d'imposer des membres obligatoires ou de simplifier la configuration des collections — réduit les frappes tout en maximisant la clarté et en minimisant les risques d'erreur.