Système de types C#

C# est un langage fortement typé. Chaque variable et chaque constante ont un type, tout comme chaque expression qui fournit une valeur. Chaque déclaration de méthode spécifie un nom, le type et le genre (valeur, référence ou sortie) pour chaque paramètre d’entrée et pour la valeur de retour. La bibliothèque de classes .NET définit des types numériques intégrés et des types complexes qui représentent un large éventail de constructions. Il s’agit notamment du système de fichiers, des connexions réseau, des collections et des tableaux d’objets et des dates. Un programme C# classique utilise des types de la bibliothèque de classes et des types définis par l’utilisateur qui modélisent les concepts propres au domaine du problème du programme.

Les informations stockées dans un type peuvent inclure les éléments suivants :

  • Espace de stockage nécessaire pour une variable du type.
  • Valeurs minimale et maximale que le type peut représenter.
  • Membres (méthodes, champs, événements, etc.) que le type contient.
  • Type de base dont le type est hérité.
  • Interface(s) qu’il implémente.
  • Sortes d’opérations autorisées.

Le compilateur utilise les informations de type pour s’assurer que toutes les opérations qui sont effectuées dans votre code sont de type safe. Par exemple, si vous déclarez une variable de type int, le compilateur vous permet d’utiliser la variable dans des opérations d’addition et de soustraction. Si vous essayez d’effectuer ces mêmes opérations avec une variable de type bool, le compilateur génère une erreur, comme illustré dans l’exemple suivant :

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

Notes

Développeurs C et C++ : notez que dans C#, bool n’est pas convertible en int.

Le compilateur incorpore les informations de type dans le fichier exécutable sous forme de métadonnées. Le common language runtime (CLR) utilise ces métadonnées au moment de l’exécution pour garantir que le type est sécurisé lorsqu’il alloue et libère de la mémoire.

Spécification de types dans les déclarations de variable

Lorsque vous déclarez une variable ou une constante dans un programme, vous devez spécifier son type ou utiliser le mot clé var pour permettre au compilateur de déduire le type. L’exemple suivant montre des déclarations de variable qui utilisent des types numériques intégrés et des types complexes définis par l’utilisateur :

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
            where item <= limit
            select item;

Les types de paramètres de méthode et de valeurs renvoyée sont spécifiés dans la déclaration de méthode. La signature suivante présente une méthode qui requiert int comme argument d’entrée et retourne une chaîne :

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };

Après avoir déclaré une variable, vous ne pouvez pas la redéclarer avec un nouveau type et vous ne pouvez pas affecter une valeur non compatible avec son type déclaré. Par exemple, vous ne pouvez pas déclarer une variable de type int et lui assigner la valeur booléenne true. Toutefois, les valeurs peuvent être converties en d’autres types, par exemple quand elles sont assignées à de nouvelles variables ou passées en tant qu’arguments de méthode. Une conversion de type qui ne cause pas de perte de données est effectuée automatiquement par le compilateur. Une conversion susceptible de causer la perte de données exige une variable cast dans le code source.

Pour plus d’informations, consultez Cast et conversions de types.

Types intégrés

C# fournit un ensemble standard de types intégrés. Ils représentent des entiers, des valeurs à virgule flottante, des expressions booléennes, des caractères de texte, des valeurs décimales et d’autres types de données. Il existe également des types string et object intégrés. Ces types peuvent être utilisés dans n’importe quel programme C#. Pour obtenir la liste complète des types intégrés, consultez Types intégrés.

Types personnalisés

Vous utilisez les constructions struct, class, interface, enum, et record pour créer vos propres types personnalisés. La bibliothèque de classes .NET est une collection de types personnalisés que vous pouvez utiliser dans vos propres applications. Par défaut, les types les plus fréquemment utilisés dans la bibliothèque de classes sont disponibles dans tous les programmes C#. Les autres types sont disponibles uniquement si vous ajoutez explicitement une référence de projet à l’assembly qui les définit. Une fois que le compilateur a une référence à l’assembly, vous pouvez déclarer des variables (et des constantes) des types déclarés dans cet assembly dans le code source. Pour plus d’informations, consultez Bibliothèque de classes .NET.

Système de type commun

Il est important de comprendre deux points fondamentaux concernant le système de type dans .NET :

  • Il prend en charge le principe d’héritage. Les types peuvent dériver d’autres types, appelés types de base. Le type dérivé hérite (avec certaines restrictions) des méthodes, des propriétés et des autres membres du type de base. Le type de base peut à son tour dériver d’un autre type, auquel cas le type dérivé hérite des membres des deux types de base dans sa hiérarchie d’héritage. Tous les types, y compris les types numériques intégrés tels que System.Int32 (mot clé C# : int), dérivent au final d’un seul type de base, qui est System.Object (mot clé C# : object). Cette hiérarchie de types unifiée est appelée Système de type commun (CTS). Pour plus d’informations sur l’héritage dans C#, consultez Héritage.
  • Chaque type du CTS est défini comme type valeur ou type référence. Cela inclut tous les types personnalisés dans la bibliothèque de classes .NET, ainsi que les types définis par l’utilisateur. Les types que vous définissez à l’aide du mot clé struct sont des types valeur ; tous les types numériques intégrés sont structs. Les types que vous définissez à l’aide du mot clé class ou record sont des types référence. Les types référence et les types valeur ne suivent pas les mêmes règles de compilation et ont un comportement différent au moment de l’exécution.

L’illustration suivante montre la relation entre les types valeur et les types référence dans le CTS.

Capture d’écran montrant des types valeur et des types référence dans CTS.

Notes

Vous pouvez voir que les types couramment utilisés sont tous organisés dans l’espace de noms System. Toutefois, l’espace de noms qui contient un type n’a aucune incidence sur le fait qu’il s’agisse d’un type valeur ou d’un type référence.

Les classes et les structs sont deux des constructions de base du système de type commun dans .NET. C# 9 ajoute des enregistrements, qui sont une sorte de classe. Chacun est en substance une structure de données qui encapsule un ensemble de données et de comportements constituant une unité logique. Les données et les comportements sont les membres de la classe, du struct ou de l’enregistrement. Les membres incluent ses méthodes, propriétés, événements, etc., comme indiqué plus loin dans cet article.

Une déclaration de classe, de structure ou d’enregistrement est semblable à un plan utilisé pour créer des instances ou des objets au moment de l’exécution. Si vous définissez une classe, un struct ou un enregistrement nommé Person, Person est le nom du type. Si vous déclarez et initialisez une variable p de type Person, p est dit objet ou instance de Person. Plusieurs instances du même type Person peuvent être créées, et chaque instance peut avoir des valeurs différentes dans ses propriétés et champs.

Une classe est un type de référence. Lorsqu’un objet du type est créé, la variable à laquelle l’objet est affecté conserve uniquement une référence à cette mémoire. Lorsque la référence d’objet est affectée à une variable, la nouvelle variable fait référence à l’objet d’origine. Les modifications apportées à une variable sont répercutées sur l’autre variable, car toutes deux font référence aux mêmes données.

Un struct est un type valeur. Lorsqu'un struct est créé, la variable à laquelle le struct est assigné contient les données réelles du struct. Lorsque le struct est affecté à une nouvelle variable, il est copié. La nouvelle variable et la variable d’origine contiennent par conséquent deux copies distinctes des mêmes données. Les modifications apportées à une copie n’affectent pas l’autre copie.

Les types d’enregistrements peuvent être des types référence (record class) ou des types valeur (record struct).

En général, les classes sont utilisées pour modéliser un comportement plus complexe. Les classes stockent généralement les données destinées à être modifiées après la création d’un objet de classe. Les structs sont mieux adaptés aux petites structures de données. Les structs stockent généralement des données qui ne sont pas destinées à être modifiées après la création du struct. Les types d’enregistrements sont des structures de données avec des membres synthétisés supplémentaires du compilateur. Les enregistrements stockent généralement des données qui ne sont pas destinées à être modifiées après la création de l’objet.

Types de valeur

Les types valeur dérivent de System.ValueType, qui dérive de System.Object. Les types qui dérivent de System.ValueType ont un comportement spécial dans le CLR. Les variables de type valeur contiennent directement leurs valeurs. La mémoire d’un struct est allouée inline dans n’importe quel contexte où la variable est déclarée. Il n’existe pas d’allocation de tas ou de surcharge de garbage collection pour les variables de type valeur. Vous pouvez déclarer des types record struct qui sont des types valeur et inclure les membres synthétisés pour les enregistrements.

Il existe deux catégories de types valeur : struct et enum.

Les types numériques intégrés sont des structs et ont des champs et des méthodes auxquels vous pouvez accéder :

// constant field on type byte.
byte b = byte.MaxValue;

Mais vous les déclarez et leur assignez des valeurs comme s’il s’agissait de types non agrégés simples :

byte num = 0xA;
int i = 5;
char c = 'Z';

Les types valeur sont sealed. Vous ne pouvez pas dériver un type à partir d’un type valeur, par exemple System.Int32. Vous ne pouvez pas définir un struct pour hériter d’une classe ou d’un struct défini par l’utilisateur, car un struct peut uniquement hériter de System.ValueType. Toutefois, un struct peut implémenter une ou plusieurs interfaces. Vous pouvez convertir un type de struct en n’importe quel type d’interface qu’il implémente. Ce cast entraîne une opération de boxing pour envelopper le struct dans un objet de type référence sur le tas managé. Les opérations de boxing se produisent quand vous passez un type valeur à une méthode qui prend un System.Object ou tout type d’interface comme paramètre d’entrée. Pour plus d’informations, consultez Conversion boxing et unboxing.

Vous utilisez le mot clé struct pour créer vos propres types valeur personnalisés. En règle générale, un struct est utilisé comme conteneur pour un petit jeu de variables connexes, comme le montre l'exemple suivant :

public struct Coords
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

Pour plus d’informations sur les structs, consultez Types de structure. Pour plus d’informations sur les types valeur, consultez Types valeur.

L’autre catégorie de types valeur est enum. Un enum définit un jeu de constantes intégrales nommées. Par exemple l’énumération System.IO.FileMode dans la bibliothèque de classes du .NET contient un ensemble d’entiers constants nommés qui spécifient comment un fichier doit être ouvert. Il est défini comme indiqué dans l’exemple suivant :

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

La constante System.IO.FileMode.Create a la valeur 2. Toutefois, le nom est bien plus explicite pour les utilisateurs qui lisent le code source. pour cette raison, il est préférable d’utiliser des énumérations au lieu de nombres littéraux constants. Pour plus d’informations, consultez System.IO.FileMode.

Toutes les énumérations héritent de System.Enum, qui hérite de System.ValueType. Toutes les règles qui s’appliquent aux structs s’appliquent également aux enums. Pour plus d’informations sur les enums, consultez Types énumération.

Types référence

Type défini sous la forme class, record, delegate, d’un tableau, ou interface est un reference type.

Lors de la déclaration d’une variable de reference type, elle contient la valeur null jusqu’à ce que vous l’affectiez avec une instance de ce type ou en créiez une à l’aide de l’opérateur new. La création et l’affectation d’une classe sont illustrées dans l’exemple suivant :

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

interface ne peut pas être instancié directement à l’aide de l’opérateur new. Au lieu de cela, créez et affectez une instance d’une classe qui implémente l’interface. Prenons l’exemple suivant :

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

Lorsque l’objet est créé, la mémoire est allouée sur le tas managé. La variable contient uniquement une référence à l’emplacement de l’objet. Les types sur le tas managé nécessitent une surcharge à la fois quand ils sont alloués et quand ils sont récupérés. Garbage collection est la fonctionnalité de gestion automatique de la mémoire du CLR, qui effectue la récupération. Toutefois, garbage collection est également hautement optimisé et, dans la plupart des scénarios, il ne crée pas de problème de performances. Pour plus d’informations sur le garbage collection, consultez Gestion automatique de la mémoire.

Tous les tableaux sont des types référence, même si leurs éléments sont des types valeur. Les tableaux dérivent implicitement de la classe System.Array. Vous les déclarez et les utilisez avec la syntaxe simplifiée fournie par C#, comme illustré dans l’exemple suivant :

// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.
int len = nums.Length;

Les types référence prennent en charge l’héritage. Lorsque vous créez une classe, vous pouvez hériter de toute autre interface ou classe qui n’est pas définie comme sealed. D’autres classes peuvent hériter de votre classe et remplacer vos méthodes virtuelles. Pour plus d’informations sur la création de vos propres classes, consultez Classes, structs et enregistrements. Pour plus d’informations sur l’héritage et les méthodes virtuelles, consultez Héritage.

Types de valeurs littérales

Dans C#, les valeurs littérales reçoivent un type du compilateur. Vous pouvez spécifier la façon dont un littéral numérique doit être typé en ajoutant une lettre à la fin du nombre. Par exemple, pour spécifier que la valeur 4.56 doit être traitée comme une valeur float, ajoutez « f » ou « F » après le nombre : 4.56f. Si aucune lettre n’est ajoutée, le compilateur déduit un type pour le littéral. Pour plus d’informations sur les types qui peuvent être spécifiés avec des suffixes de lettre, consultez Types numériques intégraux et Types numériques à virgule flottante.

Comme les littéraux sont typés et que tous les types dérivent en fin de compte de System.Object, vous pouvez écrire et compiler du code, comme le code suivant :

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

Types génériques

Un type peut être déclaré avec un ou plusieurs paramètres de type qui servent d’espace réservé pour le type réel (le type concret). Le code client fournit le type concret lorsqu’il crée une instance du type. Ces types sont appelés types génériques. Par exemple, le type .NET System.Collections.Generic.List<T> a un paramètre de type qui, par convention, porte le nom T. Lorsque vous créez une instance du type, vous spécifiez le type des objets contenus dans la liste, par exemple, string :

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

L’utilisation du paramètre de type rend possible la réutilisation de la même classe pour contenir tout type d’élément, sans avoir à convertir chaque élément en object. Les classes de collections génériques sont appelées collections fortement typées, car le compilateur connaît le type spécifique des éléments de chaque collection et il peut déclencher une erreur au moment de la compilation. C’est le cas, par exemple, si vous essayez d’ajouter un entier à l’objet stringList dans l’exemple précédent. Pour plus d’informations, consultez Génériques.

Types implicites, types anonymes et types valeur pouvant accepter la valeur Null

Vous pouvez attribuer implicitement un type à une variable locale (mais pas les membres de la classe) à l’aide du mot clé var. La variable reçoit toujours un type au moment de la compilation, mais le type est fourni par le compilateur. Pour plus d’informations, consultez Variables locales implicitement typées.

Il peut être difficile de créer un type nommé pour des ensembles simples de valeurs associées que vous ne souhaitez pas stocker ou transférer en dehors des limites de la méthode. Vous pouvez alors créer des types anonymes. Pour plus d’informations, consultez Types anonymes.

Les types valeur ordinaires ne peuvent pas avoir la valeur null. Toutefois, vous pouvez créer des types valeur pouvant accepter la valeur Null en apposant un ? après le type. Par exemple, int? est un type int qui peut également avoir la valeur null. Les types valeur pouvant accepter la valeur Null sont des instances du type struct générique System.Nullable<T>. Les types valeur pouvant accepter la valeur Null sont particulièrement utiles lorsque vous transmettez des données vers et à partir de bases de données dans lesquelles les valeurs numériques peuvent être null. Pour plus d’informations, consultez Types valeur pouvant accepter la valeur Null.

Type de compilation et type d’exécution

Une variable peut avoir différents types de compilation et d’exécution. Le type au moment de la compilation est le type déclaré ou différé de la variable dans le code source. Le type d’exécution est le type de l’instance référencée par cette variable. Souvent, ces deux types sont les mêmes, comme dans l’exemple suivant :

string message = "This is a string of characters";

Dans d’autres cas, le type de compilation est différent, comme illustré dans les deux exemples suivants :

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

Dans les deux exemples précédents, le type d’exécution est un string. Le type au moment de la compilation est object dans la première ligne et IEnumerable<char> dans la deuxième.

Si les deux types sont différents pour une variable, il est important de comprendre quand le type de compilation et le type d’exécution s’appliquent. Le type de compilation détermine toutes les actions effectuées par le compilateur. Ces actions du compilateur incluent la résolution des appels de méthode, la résolution de surcharge et les casts implicites et explicites disponibles. Le type d’exécution détermine toutes les actions qui sont résolues au moment de l’exécution. Ces actions d’exécution incluent la distribution d’appels de méthode virtuelle, l’évaluation des expressions is et switch, ainsi que d’autres API de test de type. Pour mieux comprendre comment votre code interagit avec les types, identifiez l’action qui s’applique à quel type.

Pour plus d’informations, consultez les articles suivants :

spécification du langage C#

Pour plus d'informations, voir la spécification du langage C#. La spécification du langage est la source de référence pour la syntaxe C# et son utilisation.