Visite guidée du langage C#

C# (prononcez « Si Sharp ») est un langage de programmation moderne, orienté objet et de type sécurisé. C# permet aux développeurs de créer de nombreux types d’applications sécurisées et robustes qui s’exécutent dans .NET. C# prend sa source dans la famille de langages C et sera immédiatement reconnaissable aux programmeurs en C, C++, Java et JavaScript. Cette visite guidée offre une vue d’ensemble des principaux composants du langage dans C# 8 et antérieur. Si vous voulez explorer le langage à travers des exemples interactifs, essayez les tutoriels de présentation de C#.

C# est un langage de programmation orienté objet, orienté composant. C# fournit des constructions de langage qui prennent directement en charge ces concepts, ce qui fait de C# un langage naturel pour créer et utiliser des composants logiciels. Depuis son apparition, C# a ajouté des fonctionnalités pour prendre en charge de nouvelles charges de travail et des pratiques de conception logicielle émergentes. À la base, C# est un langage orienté objet. Vous définissez des types et leur comportement.

Plusieurs fonctionnalités C# permettent de créer des applications robustes et durables. Garbage Collection récupère automatiquement la mémoire occupée par des objets inutilisés inaccessibles. Les types nullables protègent contre les variables qui ne font pas référence à des objets alloués. La gestion des exceptions offre une approche structurée et extensible de la détection et de la récupération des erreurs. Les expressions lambda prennent en charge les techniques de programmation fonctionnelle. La syntaxe LINQ (Language Integrated Query) crée un modèle commun pour utiliser les données de n’importe quelle source. La prise en charge du langage pour les opérations asynchrones fournit une syntaxe pour créer des systèmes distribués. C# a un système de type unifié. Tous les types C#, y compris les types primitifs tels que int et double, héritent d’un seul type object racine. Tous les types partagent un ensemble d’opérations communes. Les valeurs de n’importe quel type peuvent être stockées, transportées et manipulées de manière cohérente. De plus, C# prend en charge à la fois les types référence et les types valeur définis par l’utilisateur. C# autorise l’allocation dynamique d’objets et le stockage in-line de structures légères. C# prend en charge les méthodes et les types génériques, qui offrent une cohérence des types et des performances accrues. C# fournit des itérateurs qui permettent aux implémenteurs de classes de collection de définir des comportements personnalisés pour le code client.

C# met l’accent sur la gestion de versions pour garantir que les programmes et les bibliothèques peuvent évoluer de manière compatible. Certains aspects de la conception de C# ont été directement influencés par la gestion de versions, notamment les modificateurs virtual et override distincts, les règles de résolution des surcharges de méthodes et la prise en charge des déclarations explicites de membres d’interface.

Architecture .NET

Les programmes C# s’exécutent sur .NET, un système d’exécution virtuel appelé common language runtime (CLR) et un ensemble de bibliothèques de classes. Le CLR est l’implémentation de Microsoft de la CLI (Common Language Infrastructure), qui est une norme internationale. La CLI est la base de la création d’environnements d’exécution et de développement où langages et bibliothèques fonctionnent ensemble de façon fluide.

Le code source écrit en C# est compilé dans un langage intermédiaire (IL) qui est conforme à la spécification CLI. Le code et les ressources IL, comme les bitmaps et les chaînes, sont stockés dans un assembly, en général avec une extension .dll. Un assembly contient un manifeste qui fournit des informations sur ses types, sa version et sa culture.

Lorsque le programme C# est exécuté, l’assembly est chargé dans le CLR. Le CLR effectue une compilation juste-à-temps (JIT, Just-In-Time) pour convertir le code IL en instructions machine natives. Le CLR fournit d’autres services liés au Garbage collection, à la gestion des exceptions et à la gestion des ressources. Le code exécuté par le CLR est parfois appelé « code managé ». Le « code non managé » est compilé dans le langage machine natif qui cible une plateforme spécifique.

L’interopérabilité des langages est une fonctionnalité clé de .NET. Le code IL produit par le compilateur C# est conforme à la spécification CTS (Common Type Specification). Le code IL généré à partir de C# peut interagir avec le code généré à partir des versions .NET de F#, Visual Basic et C++. Il existe plus de 20 autres langages conformes à la spécification CTS. Un seul assembly peut contenir plusieurs modules écrits dans différents langages .NET. Les types peuvent faire référence les uns aux autres comme s’ils avaient été écrits dans le même langage.

En plus des services d’exécution, .NET comprend également des bibliothèques complètes. Ces bibliothèques prennent en charge un grand nombre de charges de travail différentes. Elles sont organisées en espaces de noms qui fournissent un large éventail de fonctionnalités utiles. Les bibliothèques englobent tout, de l’entrée/la sortie des fichiers à la manipulation de chaînes et à l’analyse XML, en passant par les frameworks d’application web et les contrôles Windows Forms. Une application C# typique utilise beaucoup la bibliothèque de classes .NET pour gérer les tâches fastidieuses courantes.

Pour plus d’informations sur .NET, consultez Vue d’ensemble de .NET.

Hello World

Le programme « Hello, World » est souvent utilisé pour présenter un langage de programmation. Le voici en C# :

using System;

class Hello
{
    static void Main()
    {
        Console.WriteLine("Hello, World");
    }
}

Le programme « Hello, World » commence par une directive using qui fait référence à l’espace de noms System. Les espaces de noms représentent un moyen hiérarchique d’organiser les bibliothèques et les programmes C#. Les espaces de noms contiennent des types et d’autres espaces de noms ; par exemple, l’espace de noms System contient plusieurs types, notamment la classe Console référencée dans le programme, et d’autres espaces de noms, tels que IO et Collections. Une directive using qui fait référence à un espace de noms donné permet l’utilisation non qualifiée des types membres de cet espace de noms. En raison de la directive using, le programme peut utiliser Console.WriteLine comme raccourci pour System.Console.WriteLine.

La classe Hello déclarée par le programme « Hello, World » a un membre unique, la méthode nommée Main. La méthode Main est déclarée avec le modificateur static. Si les méthodes d’instance peuvent faire référence à une instance d’objet englobante particulière avec le mot clé this, les méthodes statiques fonctionnent sans référence à un objet particulier. Par convention, une méthode statique nommée Main sert de point d’entrée d’un programme C#.

La sortie du programme est générée par la méthode WriteLine de la classe Console dans l’espace de noms System. Cette classe est fournie par les bibliothèques de classes standard, qui, par défaut, sont référencées automatiquement par le compilateur.

Types et variables

Un type définit la structure et le comportement de toutes les données dans C#. La déclaration d’un type peut inclure ses membres, le type de base, les interfaces qu’il implémente et les opérations autorisées pour ce type. Une variable est un nom qui fait référence à une instance d’un type spécifique.

Il existe deux genres de types en C# : les types référence et les types valeur. Les variables des types valeur contiennent directement leurs données. Les variables des types référence stockent des références à leurs données, connues sous le nom d’objets. Avec les types référence, deux variables peuvent faire référence au même objet et les opérations sur une variable peuvent affecter l’objet référencé par l’autre variable. Avec les types valeur, les variables ont chacune leur propre copie de données, et les opérations sur une variable ne peuvent pas affecter l’autre (sauf pour les variables de paramètre ref et out).

Un identificateur est un nom de variable. Un identificateur est une séquence de caractères Unicode sans espace blanc. Un identificateur peut être un mot réservé C#, s’il est préfixé par @. L’utilisation d’un mot réservé comme identificateur peut être utile lors d’une interaction avec d’autres langages.

Les types valeur de C# sont divisés entre types simples, types enum, types struct, types valeur nullable et types valeur tuple. Les types référence de C# sont divisés entre types de classe, types d’interface, types de tableau et types de délégué.

La structure suivante offre une vue d’ensemble du système de types de C#.

Les programmes C# utilisent les déclarations de type pour créer de nouveaux types. Une déclaration de type spécifie le nom et les membres du nouveau type. Six catégories de types C# sont définissables par l’utilisateur : les types de classe, les types struct, les types d’interface, les types enum, les types de délégué et les types valeur tuple. Vous pouvez également déclarer des types record, soit record struct, soit record class. Les types d’enregistrement (record) ont des membres synthétisés par le compilateur. Vous utilisez les enregistrements principalement pour stocker des valeurs, avec un comportement associé minimal.

  • Un type class définit une structure de données qui contient des données membres (champs) et des fonctions membres (méthodes, propriétés, etc.). Les types de classes prennent en charge l’héritage unique et le polymorphisme, des mécanismes par lesquels les classes dérivées peuvent étendre et spécialiser les classes de base.
  • Un type struct est similaire à un type de classe dans la mesure où il représente une structure avec des membres de données et des membres de fonctions. Cependant, contrairement aux classes, les structs sont des types valeur et ne nécessitent généralement pas d’allocation de tas. Les types struct ne prennent pas en charge l’héritage spécifié par l’utilisateur, et tous les types struct héritent implicitement du type object.
  • Un type interface définit un contrat en tant que jeu nommé de membres publics. Une class ou un struct qui implémente une interface doivent fournir les implémentations des membres de l’interface. Une interface peut hériter de plusieurs interfaces de base, et une class ou struct peut implémenter plusieurs interfaces.
  • Un type delegate 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 semblables aux types de fonction fournis par les langages fonctionnels. Ils sont aussi similaires au concept de pointeurs de fonction que l’on trouve dans d’autres langages. Contrairement aux pointeurs de fonction, les délégués sont orientés objet et de type sécurisé.

Les types class, struct, interface et delegate prennent tous en charge les génériques, ce qui leur permet d’être paramétrés avec d’autres types.

C# prend en charge les tableaux unidimensionnels et multidimensionnels de tout type. Contrairement aux types mentionnés ci-dessus, les types de tableau n’ont pas à être déclarés avant de pouvoir être utilisés. Au lieu de cela, les types de tableaux sont construits en ajoutant des crochets à un nom de type. Par exemple, int[] est un tableau unidimensionnel de int, int[,] est un tableau bidimensionnel de int et int[][] est un tableau unidimensionnel de tableaux unidimensionnels, ou un tableau « en escalier », de int.

Les types nullables n’ont pas besoin de définition distincte. Pour chaque type T non-nullable, il existe un type T? nullable correspondant, qui peut contenir une valeur supplémentaire, null. Par exemple, int? est un type qui peut contenir n’importe quel entier 32 bits ou la valeur null, tandis que string? est un type qui peut contenir n’importe quelle string ou la valeur null.

Le système de types de C# est unifié afin qu’une valeur de n’importe quel type puisse être traitée comme object. Chaque type dans C# dérive directement ou indirectement du type object, et object est la classe de base fondamentale de tous les types. Les valeurs des types référence sont considérées comme des objets simplement en affichant les valeurs en tant que type object. Les valeurs des types valeur sont considérées comme des objets en effectuant des opérations de boxing et d’unboxing. Dans l’exemple suivant, une valeur int est convertie en object et à nouveau en int.

int i = 123;
object o = i;    // Boxing
int j = (int)o;  // Unboxing

Quand une valeur d’un type valeur est affectée à une référence object, une « box » est alloué pour contenir la valeur. Cette « box » est une instance d’un type référence et la valeur est copiée dans celle-ci. À l’inverse, lorsqu’une référence object est castée en type valeur, un contrôle est effectué pour s’assurer que l’object référencé est une « box » du type valeur correct. Si le contrôle réussit, la valeur dans la « box » est copiée dans le type valeur.

Le système de types unifié de C# signifie en effet que les types valeur sont traités comme références object « à la demande ». En raison de l’unification, les bibliothèques à usage général qui se servent du type object peuvent être utilisées avec tous les types qui dérivent de object, y compris les types référence et les types valeur.

Il existe plusieurs types de variables en C#, y compris les champs, les éléments de tableau, les variables locales et les paramètres. Les variables représentent des emplacements de stockage. Chaque variable a un type qui détermine quelles valeurs peuvent y être stockées, comme illustré ci-dessous.

  • Type de valeur n’acceptant pas Null
    • Une valeur de ce type exact
  • Types valeur Nullable
    • Une valeur null ou une valeur de ce type exact
  • object
    • Une référence null, une référence à un objet de tout type référence ou une référence à une valeur boxed de n’importe quel type valeur
  • Type de classe
    • Une référence null, une référence à une instance de ce type de classe ou une référence à une instance d’une classe dérivée de ce type de classe
  • Type d'interface
    • Une référence null, une référence à une instance d’un type de classe qui implémente ce type d’interface ou une référence à une valeur boxed d’un type valeur qui implémente ce type d’interface
  • Type tableau
    • Une référence null, une référence à une instance de ce type de tableau ou une instance d’un type de tableau compatible
  • Type délégué
    • Une référence null ou une référence à une instance d’un type délégué compatible

Structure du programme

Les concepts organisationnels clés en C# sont les programmes, les espaces de noms, les types, les membres et les assemblys. Les programmes déclarent des types qui contiennent des membres et peuvent être organisés en espaces de noms. Les classes, les structs et les interfaces sont des exemples de types. Les champs, méthodes, propriétés et événements sont des exemples de membres. Lorsque les programmes C# sont compilés, ils sont physiquement empaquetés dans des assemblys. Les assemblys ont généralement l’extension de fichier .exe ou .dll, selon qu’elles implémentent des applications ou des bibliothèques, respectivement.

Voici un petit exemple d’un assembly qui contient le code suivant :

namespace Acme.Collections;

public class Stack<T>
{
    Entry _top;

    public void Push(T data)
    {
        _top = new Entry(_top, data);
    }

    public T Pop()
    {
        if (_top == null)
        {
            throw new InvalidOperationException();
        }
        T result = _top.Data;
        _top = _top.Next;

        return result;
    }

    class Entry
    {
        public Entry Next { get; set; }
        public T Data { get; set; }

        public Entry(Entry next, T data)
        {
            Next = next;
            Data = data;
        }
    }
}

Le nom qualifié complet de cette classe est Acme.Collections.Stack. La classe contient plusieurs membres : un champ nommé _top, deux méthodes nommées Push et Pop, et une classe imbriquée nommée Entry. La classe Entry contient trois membres en plus : une propriété nommée Next, une propriété nommée Data et un constructeur. Stack est une classe générique. Il a un paramètre de type, T, qui est remplacé par un type concret lorsqu’il est utilisé.

Un stack est une collection FILO (« first in - last out »). De nouveaux éléments sont ajoutés en haut du stack. Lorsqu’un élément est supprimé, il est supprimé du haut du stack. L’exemple précédent déclare le type Stack qui définit le stockage et le comportement d’un stack. Vous pouvez déclarer une variable qui fait référence à une instance du type Stack pour utiliser cette fonctionnalité.

Les assemblys contiennent le code exécutable sous forme d’instructions de langage intermédiaire (IL) et des informations symboliques sous la forme de métadonnées. Avant son exécution, le compilateur juste-à-temps (JIT) du Common Language Runtime .NET convertit le code IL d’un assembly en code spécifique au processeur.

Comme un assembly est une unité de fonctionnalité autodescriptive contenant du code et des métadonnées, les directives #include et les fichiers d’en-tête ne sont pas nécessaires en C#. Les membres et types publics contenus dans un assembly particulier sont disponibles dans un programme C# par simple référence à cet assembly lors de la compilation du programme. Par exemple, ce programme utilise la classe Acme.Collections.Stack à partir de l’assembly acme.dll :

class Example
{
    public static void Main()
    {
        var s = new Acme.Collections.Stack<int>();
        s.Push(1); // stack contains 1
        s.Push(10); // stack contains 1, 10
        s.Push(100); // stack contains 1, 10, 100
        Console.WriteLine(s.Pop()); // stack contains 1, 10
        Console.WriteLine(s.Pop()); // stack contains 1
        Console.WriteLine(s.Pop()); // stack is empty
    }
}

Pour compiler ce programme, vous devez référencer l’assembly contenant la classe stack définie dans l’exemple précédent.

Les programmes C# peuvent être stockés dans plusieurs fichiers sources. Lorsqu’un programme C# est compilé, tous les fichiers sources sont traités ensemble et peuvent librement se référencer entre eux. Sur le plan conceptuel, c’est comme si tous les fichiers sources étaient concaténés dans un seul grand fichier avant d’être traités. Les déclarations anticipées ne sont jamais nécessaires en C#, car, à de rares exceptions près, l’ordre de déclaration n’a pas d’importance. C# ne limite pas un fichier source à la déclaration d’un seul type public et n’a pas besoin non plus que le nom du fichier source corresponde à un type déclaré dans ce fichier.

D’autres articles de cette visite guidée expliquent ces blocs organisationnels.