Domaines principaux du langage C#

Cet article présente les principales fonctionnalités du langage C#.

Tableaux, collections et LINQ

C# et .NET fournissent de nombreux types de collection différents. Les tableaux ont une syntaxe définie par le langage. Les types de collection génériques sont répertoriés dans l’espace de noms System.Collections.Generic. Les collections spécialisées incluent System.Span<T> pour l’accès à la mémoire continue sur la frame de pile et System.Memory<T> pour l’accès à la mémoire continue sur le tas managé. Toutes les collections, y compris les tableaux, Span<T> et Memory<T> partagent un principe unifié pour l’itération. Vous utilisez l’interface System.Collections.Generic.IEnumerable<T>. Ce principe d’unification signifie que tous les types de collection peuvent être utilisés avec des requêtes LINQ ou d’autres algorithmes. Vous écrivez des méthodes à l’aide de IEnumerable<T> et ces algorithmes utilisent n’importe quelle collection.

Tableaux

Un tableau est une structure de données qui contient un certain nombre de variables qui sont accessibles par des indices calculés. Les variables contenues dans un tableau, également appelées les éléments du tableau, sont du même type. Ce type est appelé le type d’élément du tableau.

Les types tableau sont des types référence, et la déclaration d’une variable tableau réserve simplement un espace pour une référence à une instance de tableau. Les instances de tableau réelles sont créées dynamiquement au moment de l’exécution à l’aide de l’opérateur new. L’opération new spécifie la longueur de la nouvelle instance de tableau, qui reste ensuite fixe pour la durée de vie de l’instance. Les indices des éléments d’un tableau vont de 0 à Length - 1. L’opérateur new initialise automatiquement les éléments d’un tableau à leur valeur par défaut, c'est-à-dire, par exemple, zéro pour tous les types numériques et null pour tous les types référence.

L’exemple suivant crée un tableau d’éléments int, initialise le tableau et imprime le contenu du tableau.

int[] a = new int[10];
for (int i = 0; i < a.Length; i++)
{
    a[i] = i * i;
}
for (int i = 0; i < a.Length; i++)
{
    Console.WriteLine($"a[{i}] = {a[i]}");
}

Cet exemple crée et utilise un tableau unidimensionnel. C# prend également en charge les tableaux multidimensionnels. Le nombre de dimensions d’un type tableau, également appelé rang du type tableau, est de un plus le nombre de virgules entre les crochets du type tableau. L’exemple suivant alloue respectivement des tableaux à une seule, deux et trois dimensions, respectivement.

int[] a1 = new int[10];
int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];

Le tableau a1 contient 10 éléments, le tableau a2 en contient 50 (10 x 5) et le tableau a3 en contient 100 (10 × 5 × 2). Le type d’élément d’un tableau peut être de n’importe quel type, y compris un type tableau. Un tableau avec des éléments d’un type tableau est parfois appelé un tableau en escalier, car les longueurs des tableaux d’éléments ne sont pas nécessairement les mêmes. L’exemple suivant alloue un tableau de tableaux de int :

int[][] a = new int[3][];
a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];

La première ligne crée un tableau avec trois éléments, chacun de type int[] et chacun avec une valeur initiale de null. Les lignes suivantes initialisent ensuite les trois éléments avec des références aux instances individuelles de tableau de longueurs variables.

Les initialiseurs de collection fournissent une syntaxe cohérente pour initialiser des éléments dans un tableau ou une collection. L’exemple suivant alloue et initialise un int[] avec trois éléments.

int[] a = [1, 2, 3];

La longueur du tableau est déduite du nombre d’expressions entre [ et ]. L’instruction foreach peut être utilisée pour énumérer les éléments de n’importe quelle collection. Le code suivant énumère le tableau de l’exemple précédent :

foreach (int item in a)
{
    Console.WriteLine(item);
}

L’instruction foreach utilise l’interface IEnumerable<T>, afin qu’elle puisse fonctionner avec n’importe quelle collection.

Interpolation de chaîne

L’interpolation de chaîne C# vous permet de mettre en forme des chaînes en définissant des expressions dont les résultats sont placés dans une chaîne de format. Par exemple, l’exemple suivant imprime la température sur un jour donné à partir d’un ensemble de données météorologiques :

Console.WriteLine($"The low and high temperature on {weatherData.Date:MM-dd-yyyy}");
Console.WriteLine($"    was {weatherData.LowTemp} and {weatherData.HighTemp}.");
// Output (similar to):
// The low and high temperature on 08-11-2020
//     was 5 and 30.

Une chaîne interpolée est déclarée à l’aide du jeton $. L’interpolation de chaîne évalue les expressions entre { et }, puis convertit le résultat en une string et remplace le texte entre les crochets par le résultat de la chaîne de l’expression. : dans la première expression, {weatherData.Date:MM-dd-yyyy} spécifie la chaîne de format. Dans l’exemple précédent, il spécifie que la date doit être imprimée au format « MM-jj-aaaa ».

Critères spéciaux

Le langage C# fournit des expressions de critères spéciaux pour interroger l’état d’un objet et exécuter du code en fonction de cet état. Vous pouvez inspecter les types et les valeurs des propriétés et des champs pour déterminer quelle action effectuer. Vous pouvez également inspecter les éléments d’une liste ou d’un tableau. L’expression switch est l’expression principale pour les critères spéciaux.

Délégués et expressions lambda

Un type délégué représente des références aux méthodes avec une liste de paramètres et un type de retour particuliers. Les délégués permettent de traiter les méthodes en tant qu’entités qui peuvent être affectées à des variables et passées comme paramètres. Les délégués sont similaires au concept de pointeurs de fonction trouvés dans d’autres langages. Contrairement aux pointeurs de fonction, les délégués sont orientés objet et de type sécurisé.

L’exemple suivant déclare et utilise un type délégué nommé Function.

delegate double Function(double x);

class Multiplier
{
    double _factor;

    public Multiplier(double factor) => _factor = factor;

    public double Multiply(double x) => x * _factor;
}

class DelegateExample
{
    static double[] Apply(double[] a, Function f)
    {
        var result = new double[a.Length];
        for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
        return result;
    }

    public static void Main()
    {
        double[] a = { 0.0, 0.5, 1.0 };
        double[] squares = Apply(a, (x) => x * x);
        double[] sines = Apply(a, Math.Sin);
        Multiplier m = new(2.0);
        double[] doubles = Apply(a, m.Multiply);
    }
}

Une instance du type délégué Function peut faire référence à toute méthode qui accepte un argument double et retourne une valeur double. La méthode Apply applique une Function donnée aux éléments d’un double[], en retournant un double[] avec les résultats. Dans la méthode Main, Apply sert à appliquer trois fonctions différentes à un double[].

Un délégué peut référencer une expression lambda pour créer une fonction anonyme (par exemple (x) => x * x dans l’exemple précédent), une méthode statique (par exemple Math.Sin dans l’exemple précédent) ou une méthode d’instance (comme m.Multiply dans l’exemple précédent). Un délégué qui référence une méthode d’instance référence aussi un objet particulier, et quand la méthode d’instance est appelée via le délégué, cet objet devient this dans l’appel.

Les délégués peuvent également être créés à l’aide de fonctions anonymes ou d’expressions lambda, qui sont des « méthodes inline » créées lors de la déclaration. Les fonctions anonymes peuvent voir les variables locales des méthodes qui l’entourent. L’exemple suivant ne crée pas de classe :

double[] doubles = Apply(a, (double x) => x * 2.0);

Un délégué ne sait pas ou ne s’intéresse pas à la classe de la méthode qu’il référence. La méthode référencée doit avoir les mêmes paramètres et le même type de retour que le délégué.

async / await

C# prend en charge les programmes asynchrones avec deux mots clés : async et await. Vous ajoutez le modificateur async à une déclaration de méthode pour déclarer que la méthode est asynchrone. L’opérateur await indique au compilateur d’attendre de façon asynchrone qu’un résultat se termine. Le contrôle est retourné à l’appelant et la méthode retourne une structure qui gère l’état du travail asynchrone. La structure est généralement un System.Threading.Tasks.Task<TResult>, mais peut être n’importe quel type qui prend en charge le modèle awaiter. Ces fonctionnalités vous permettent d’écrire du code qui lit comme son équivalent synchrone, mais s’exécute de manière asynchrone. Par exemple, le code suivant télécharge la page d’accueil de Microsoft docs :

public async Task<int> RetrieveDocsHomePage()
{
    var client = new HttpClient();
    byte[] content = await client.GetByteArrayAsync("https://learn.microsoft.com/");

    Console.WriteLine($"{nameof(RetrieveDocsHomePage)}: Finished downloading.");
    return content.Length;
}

Ce petit exemple montre les principales fonctionnalités de la programmation asynchrone :

  • La déclaration de méthode inclut le modificateur async.
  • Le corps de la méthode awaits est le retour de la méthode GetByteArrayAsync.
  • Le type spécifié dans l’instruction return correspond à l’argument de type dans la déclaration Task<T> de la méthode. (Une méthode qui retourne une Task utiliserait des instructions return sans argument).

Le mot clé async et l’opérateur await fournissent une abstraction au niveau du langage sur des primitives de niveau inférieur qui prennent en charge les opérations asynchrones. Le compilateur et la bibliothèque peuvent s’appuyer sur des interruptions réseau, des événements de système d’exploitation, des interruptions matérielles ou d’autres primitives pour déclencher l’achèvement d’une opération asynchrone. Le compilateur génère du code pour mettre à jour l’état de n’importe quel objet Task et transférer le contrôle vers le code qui doit s’exécuter lorsque la tâche attendue se termine.

Vous pouvez en savoir plus sur les mécanismes utilisés en consultant la section sur la programmation asynchrone en C#.

Attributs

Les types, membres et autres entités d’un programme C# prennent en charge les modificateurs qui contrôlent certains aspects de leur comportement. Par exemple, l’accessibilité d’une méthode est contrôlée à l’aide des modificateurs public, protected, internal et private. C# généralise cette fonctionnalité pour que les types d’informations déclaratives définis par l’utilisateur puissent être associés aux entités de programme et récupérés au moment de l’exécution. Les programmes spécifient ces informations déclaratives en définissant et en utilisant des attributs.

L’exemple suivant déclare un attribut HelpAttribute qui peut être placé sur des entités de programme pour fournir des liens vers leur documentation associée.

public class HelpAttribute : Attribute
{
    string _url;
    string _topic;

    public HelpAttribute(string url) => _url = url;

    public string Url => _url;

    public string Topic
    {
        get => _topic;
        set => _topic = value;
    }
}

Toutes les classes d’attributs dérivent de la classe de base Attribute fournie par la bibliothèque .NET. Les attributs peuvent être appliqués en donnant leur nom, ainsi que les éventuels arguments, à l’intérieur de crochets juste avant la déclaration associée. Si le nom d’un attribut se termine en Attribute, cette partie du nom peut être omise lorsque l’attribut est référencé. Par exemple, HelpAttribute peut être utilisé comme suit.

[Help("https://learn.microsoft.com/dotnet/csharp/tour-of-csharp/features")]
public class Widget
{
    [Help("https://learn.microsoft.com/dotnet/csharp/tour-of-csharp/features",
    Topic = "Display")]
    public void Display(string text) { }
}

Cet exemple joint un HelpAttribute à la classe Widget. Il ajoute un autre HelpAttribute à la méthode Display dans la classe. Les constructeurs publics d’une classe d’attributs contrôlent les informations qui doivent être fournies lorsque l’attribut est joint à une entité de programme. Des informations supplémentaires peuvent être fournies en référençant les propriétés en lecture-écriture publiques de la classe d’attributs (comme la référence à la propriété Topic, précédemment).

Les métadonnées définies par les attributs peuvent être lues et manipulées par réflexion lors de l’exécution. Lorsqu’un attribut particulier est demandé à l’aide de cette technique, le constructeur de la classe d’attributs est appelé avec les informations fournies dans la source du programme. L’instance d’attribut résultante est retournée. Si des informations supplémentaires ont été fournies via les propriétés, ces propriétés sont définies sur les valeurs données avant que le renvoi de l’instance de l’attribut.

L’exemple de code suivant montre comment obtenir les HelpAttribute instances associées à la classe Widget et sa méthode Display.

Type widgetType = typeof(Widget);

object[] widgetClassAttributes = widgetType.GetCustomAttributes(typeof(HelpAttribute), false);

if (widgetClassAttributes.Length > 0)
{
    HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];
    Console.WriteLine($"Widget class help URL : {attr.Url} - Related topic : {attr.Topic}");
}

System.Reflection.MethodInfo displayMethod = widgetType.GetMethod(nameof(Widget.Display));

object[] displayMethodAttributes = displayMethod.GetCustomAttributes(typeof(HelpAttribute), false);

if (displayMethodAttributes.Length > 0)
{
    HelpAttribute attr = (HelpAttribute)displayMethodAttributes[0];
    Console.WriteLine($"Display method help URL : {attr.Url} - Related topic : {attr.Topic}");
}

En savoir plus

Vous pouvez en savoir plus sur C# en essayant l’un de nos didacticiels.