Partager via


Système de type C#

C# est un langage fortement typé. Chaque variable et constante a un type, tout comme chaque expression qui se résout à une valeur. Chaque déclaration de méthode spécifie un nom, le type et la nature (valeur, référence ou sortie) pour chaque paramètre 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 une grande variété de constructions. Il s’agit notamment du système de fichiers, des connexions réseau, des collections et des tableaux d’objets et de 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 spécifiques au domaine de problème du programme.

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

  • Espace de stockage requis par une variable du type.
  • Valeurs maximales et minimales qu’elle peut représenter.
  • Les membres (méthodes, champs, événements, et ainsi de suite) qu’il contient.
  • Type de base dont le type est hérité.
  • Interfaces qu’il implémente.
  • Opérations autorisées.

Le compilateur utilise des informations de type pour vous assurer que toutes les opérations effectuées dans votre code sont sécurisées de type. Par exemple, si vous déclarez une variable de type int, le compilateur vous permet d’utiliser la variable en plus et en soustraction. Si vous essayez d’effectuer ces mêmes opérations sur 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;

Remarque

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 en tant que métadonnées. Le Common Language Runtime (CLR) utilise ces métadonnées au moment de l’exécution pour garantir davantage la sécurité du type lorsqu’elle alloue et récupère de la mémoire.

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

Lorsque vous déclarez une variable ou une constante dans un programme, vous devez spécifier son type ou utiliser le var mot clé pour permettre au compilateur de déduire le type. L’exemple suivant montre certaines déclarations de variables qui utilisent à la fois 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 de retour sont spécifiés dans la déclaration de méthode. La signature suivante montre une méthode qui nécessite un int 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 dé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 un int et ensuite lui attribuer une valeur booléenne de true. Toutefois, les valeurs peuvent être converties en d’autres types, par exemple lorsqu’elles sont affectées à de nouvelles variables ou transmises en tant qu’arguments de méthode. Une conversion de type qui n’entraîne pas de perte de données est effectuée automatiquement par le compilateur. Une conversion susceptible d’entraîner une perte de données nécessite un 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. Ces valeurs 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 intégrés string et object. Ces types sont disponibles pour vous permettre d’utiliser dans n’importe quel programme C#. Pour obtenir la liste complète des types intégrés, consultez les types intégrés.

Types personnalisés

Vous utilisez les constructions struct, class, interface, enumet record pour créer vos propres types personnalisés. La bibliothèque de classes .NET elle-même 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 n’importe quel programme C#. D’autres deviennent disponibles uniquement lorsque 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 la bibliothèque de classes .NET.

L’une des premières décisions que vous prenez lors de la définition d’un type consiste à décider quelle construction utiliser pour votre type. La liste suivante permet de prendre cette décision initiale. Il y a un chevauchement dans les choix. Dans la plupart des scénarios, plusieurs options sont un choix raisonnable.

  • Si la taille de stockage des données est petite, pas plus de 64 octets, choisissez un struct ou record struct.
  • Si le type est immuable, ou si vous souhaitez une mutation non destructeur, choisissez un struct ou record struct.
  • Si votre type doit avoir une sémantique de valeur pour l’égalité, choisissez un record class ou record struct.
  • Si le type est principalement utilisé pour stocker des données, et non un comportement, choisissez un record class ou record struct.
  • Si le type fait partie d’une hiérarchie d’héritage, choisissez un record class ou un class.
  • Si le type utilise le polymorphisme, choisissez un class.
  • Si l’objectif principal est le comportement, choisissez un class.

Le système de type commun

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

  • Il prend en charge le principe de l’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 d’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 finalement d’un seul type de base, qui est System.Object (mot clé C# : object). Cette hiérarchie de types unifié est appelée Common Type System (CTS). Pour plus d’informations sur l’héritage en C#, consultez Héritage.
  • Chaque type du CTS est défini comme un type valeur ou un type référence. Ces types incluent tous les types personnalisés dans la bibliothèque de classes .NET et vos propres types définis par l’utilisateur. Les types que vous définissez à l’aide du struct mot clé sont des types valeur ; tous les types numériques intégrés sont structs. Les types que vous définissez en utilisant les mots-clés class ou record sont des types de référence. Les types de référence et les types valeur ont des règles de compilation et un comportement d’exécution différent.

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

Capture d’écran montrant les types de valeurs CTS et les types de référence.

Remarque

Vous pouvez voir que les types les plus couramment utilisés sont tous organisés dans l’espace System de noms. Toutefois, l’espace de noms dans lequel un type est contenu n’a aucune relation avec s’il s’agit 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. Chacun est essentiellement une structure de données qui encapsule un ensemble de données et de comportements qui appartiennent ensemble en tant qu’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, ses propriétés, ses événements, et ainsi de suite, comme indiqué plus loin dans cet article.

Une déclaration de classe, de struct ou d’enregistrement est semblable à un blueprint 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 considéré comme un objet ou une 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 ses champs.

Une classe est un type référence. Lorsqu’un objet du type est créé, la variable à laquelle l’objet est affecté contient uniquement une référence à cette mémoire. Lorsque la référence d’objet est affectée à une nouvelle variable, la nouvelle variable fait référence à l’objet d’origine. Les modifications apportées par le biais d’une variable sont reflétées dans l’autre variable, car elles font tous les deux 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 affecté 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 donc 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). Les types d’enregistrements contiennent des méthodes qui prennent en charge l’égalité des valeurs.

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 les 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 les données qui ne sont pas destinées à être modifiées une fois l’objet créé.

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'une structure est allouée directement dans le contexte où la variable est déclarée. Il n'existe aucune surcharge distincte d'allocation de tas ou de gestion de la mémoire 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;

Toutefois, vous déclarez et affectez des valeurs comme si elles sont des types non agrégés simples :

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

Les types valeur sont scellés . Vous ne pouvez pas dériver un type à partir d’un type valeur, par exemple System.Int32. Vous ne pouvez pas définir de struct pour hériter d’une classe ou d’un struct défini par l’utilisateur, car un struct ne peut hériter que 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 Boxing et Unboxing.

Vous utilisez le mot clé struct pour créer vos propres types de valeurs personnalisés. En règle générale, un struct est utilisé comme conteneur pour un petit ensemble de variables associées, comme illustré dans 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. Une énumération définit un ensemble de constantes intégrales nommées. Par exemple, l’énumération System.IO.FileMode dans la bibliothèque de classes .NET contient un ensemble d’entiers de constantes nommées qui spécifient comment un fichier doit être ouvert. Elle est définie 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 beaucoup plus significatif pour les humains lisant le code source, et pour cette raison, il est préférable d’utiliser des énumérations plutôt que des 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 énumérations. Pour plus d’informations sur les énumérations, consultez Types d’énumération.

Types de référence

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

Lorsque vous déclarez une variable d’un reference type, elle contient la valeur null jusqu’à ce que vous l’affectiez avec une instance de ce type ou créez-en 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;

Il est impossible d'instancier directement un interface en utilisant 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, le ramasse-miettes est également hautement optimisé et, dans la plupart des scénarios, il ne pose pas de problème de performance. Pour plus d’informations sur le ramasse-miettes, consultez Gestion automatique de la mémoire.

Tous les tableaux sont des types de référence, même si leurs éléments sont des types de valeur. Les tableaux dérivent implicitement de la System.Array classe. Vous déclarez et utilisez-les avec la syntaxe simplifiée fournie par C#, comme indiqué 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 de référence prennent entièrement 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 scellée. 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

En 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 tapé en ajoutant une lettre à la fin du nombre. Par exemple, pour spécifier que la valeur 4.56 doit être traitée comme un float, ajoutez un « 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 (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 System.Collections.Generic.List<T> .NET a un paramètre de type qui, par convention, reçoit le nom T. Lorsque vous créez une instance du type, vous spécifiez le type des objets que la liste contient, 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 permet de réutiliser la même classe pour contenir n’importe quel type d’élément, sans avoir à convertir chaque élément en objet. Les classes de collection génériques sont appelées collections fortement typées , car le compilateur connaît le type spécifique des éléments de la collection et peut déclencher une erreur au moment de la compilation si, par exemple, vous essayez d’ajouter un entier à l’objet dans l’exemple stringList précédent. Pour plus d’informations, consultez Génériques.

Types implicites, types anonymes et types de valeur annulable

Vous pouvez taper implicitement une variable locale (mais pas des membres de classe) à l’aide du var mot clé. 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 gênant de créer un type nommé pour des ensembles simples de valeurs associées que vous n’avez pas l’intention de stocker ou de passer en dehors des limites de méthode. Vous pouvez créer des types anonymes à cet effet. 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 de valeurs nullables en ajoutant un ? après le type. Par exemple, int? est un int type qui peut également avoir la valeur null. Les types valeur nullable sont des instances du type de struct générique System.Nullable<T>. Les types de valeurs nullables sont particulièrement utiles lorsque vous transmettez des données vers et depuis des bases de données dans lesquelles des valeurs numériques peuvent être null. Pour plus d'informations, consultez types de valeur nullables.

Type de compilation et type d’exécution

Une variable peut avoir différents types de temps de compilation et de temps d'exécution. Le type de 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 identiques, 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 résolues au moment de l’exécution. Ces actions d’exécution incluent l'envoi des 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, reconnaissez 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.