3. Les bases du langage C#
3.1. Introduction
Nous traitons C# d'abord comme un langage de programmation classique. Nous aborderons les classes dans le chapitre suivant. Dans un programme, on trouve deux choses :
- des données ;
- les instructions qui les manipulent.
On s'efforce généralement de séparer les données des instructions :

Tous les exemples de ce chapitre sont des applications fichier unique : un seul fichier [.cs], sans fichier projet [.csproj], que l'on exécute avec la commande [dotnet run bases-01.cs]. Cette possibilité, apparue avec .NET 10, a été présentée au chapitre précédent. Les exemples se trouvent dans le dossier [03-bases] du code du cours. Ils s'exécutent en culture française (fr-FR), comme sur un poste Windows français : les nombres réels sont affichés avec une virgule décimale.
3.2. Le programme minimal
3.2.1. Les instructions de haut niveau
Le programme [bases-01.cs] est le suivant :
Résultats :
Commentaires :
- le programme ne contient ni classe ni méthode : les instructions sont écrites directement dans le fichier. On appelle cela des instructions de haut niveau (C# 9, 2020). Un seul fichier d'un programme peut en contenir ;
- ligne 3 : la méthode [WriteLine] de la classe [Console] écrit une ligne à l'écran ;
- ligne 5 : la classe [Console] appartient à l'espace de noms [System]. Son nom complet est [System.Console]. Un espace de noms regroupe des types apparentés, comme un dossier regroupe des fichiers ;
- ligne 5 : le programme dispose d'un tableau [args] contenant les arguments de la ligne de commande. Nous y reviendrons dans la section « Arguments du programme principal ». Le nombre d'éléments du tableau est [args.Length]. L'opérateur + concatène ici une chaîne et un nombre : le nombre est d'abord converti en chaîne.
Avec deux arguments sur la ligne de commande (tout ce qui suit [--] est passé au programme) :
3.2.2. La forme classique avec une méthode Main
Le cours de 2008 (C# 3.0) ne connaissait pas les instructions de haut niveau. Le même programme s'écrivait de la façon suivante [bases-02.cs] :
Résultats :
Commentaires :
- ligne 2 : la directive [using] importe l'espace de noms [System]. On peut alors écrire [Console] au lieu de [System.Console] ;
- ligne 4 : la classe est placée dans l'espace de noms [Bases]. Depuis C# 10, on peut écrire [namespace Bases;] (espace de noms de portée fichier) : tout le fichier appartient alors à cet espace de noms, sans accolades ni niveau d'indentation supplémentaire. C'est la forme que nous utiliserons désormais ;
- ligne 6 : en C#, tout code appartient à une classe. La classe s'appelle ici [Program] ;
- ligne 9 : la méthode statique [Main] est le point d'entrée du programme : c'est elle que le système exécute au lancement. Son paramètre [args] reçoit les arguments de la ligne de commande.
Les deux programmes sont équivalents. Lorsqu'il rencontre des instructions de haut niveau, le compilateur génère lui-même une classe et une méthode [Main] qui contiennent ces instructions, ainsi que le paramètre [string[] args]. Les instructions de haut niveau ne sont donc qu'une écriture abrégée de la forme classique. Elles conviennent bien aux petits programmes de ce chapitre ; la forme classique reste utile lorsqu'on veut contrôler la classe qui contient [Main].
3.2.3. Les using implicites
Le programme [bases-01.cs] utilise la classe [Console] sans directive [using System;]. C'est possible parce que le SDK .NET ajoute automatiquement à tout programme console un fichier généré contenant des directives [global using] (C# 10), valables pour tous les fichiers du programme. Pour [bases-01.cs], ce fichier contient :
Ce sont les using implicites : les espaces de noms les plus utilisés sont importés d'office. Un espace de noms absent de cette liste, comme [System.Globalization], doit toujours être importé par une directive [using] en tête de fichier. Dans le programme [bases-02.cs], la directive [using System;] est donc redondante : nous l'avons laissée pour montrer la forme de 2008.
3.3. Les données de C#
C# utilise les types de données suivants :
- les nombres entiers ;
- les nombres réels ;
- les nombres décimaux ;
- les caractères et chaînes de caractères ;
- les booléens ;
- les objets.
3.3.1. Les types de données prédéfinis
Type C# | Type .NET | Donnée représentée | Suffixe des littéraux | Codage | Domaine de valeurs |
[char] | [Char] (S) | caractère | 2 octets | caractère Unicode (UTF-16) | |
[string] | [String] (C) | chaîne de caractères | référence sur une séquence de caractères Unicode | ||
[int] | [Int32] (S) | nombre entier | 4 octets | [-2³¹, 2³¹-1] soit [-2147483648, 2147483647] | |
[uint] | [UInt32] (S) | entier non signé | U | 4 octets | [0, 2³²-1] soit [0, 4294967295] |
[long] | [Int64] (S) | nombre entier | L | 8 octets | [-2⁶³, 2⁶³-1] soit [-9223372036854775808, 9223372036854775807] |
[ulong] | [UInt64] (S) | entier non signé | UL | 8 octets | [0, 2⁶⁴-1] soit [0, 18446744073709551615] |
[sbyte] | [SByte] (S) | nombre entier | 1 octet | [-2⁷, 2⁷-1] soit [-128, 127] | |
[byte] | [Byte] (S) | entier non signé | 1 octet | [0, 2⁸-1] soit [0, 255] | |
[short] | [Int16] (S) | nombre entier | 2 octets | [-2¹⁵, 2¹⁵-1] soit [-32768, 32767] | |
[ushort] | [UInt16] (S) | entier non signé | 2 octets | [0, 2¹⁶-1] soit [0, 65535] | |
[float] | [Single] (S) | nombre réel | F | 4 octets | [1,5 10⁻⁴⁵, 3,4 10³⁸] en valeur absolue, 7 chiffres significatifs |
[double] | [Double] (S) | nombre réel | D | 8 octets | [5,0 10⁻³²⁴, 1,7 10³⁰⁸] en valeur absolue, 15 à 16 chiffres significatifs |
[decimal] | [Decimal] (S) | nombre décimal | M | 16 octets | [1,0 10⁻²⁸, 7,9 10²⁸] en valeur absolue, 28 à 29 chiffres significatifs |
[bool] | [Boolean] (S) | booléen | 1 octet | [true], [false] | |
[object] | [Object] (C) | référence d'objet | référence sur un objet de n'importe quel type |
Ci-dessus, on a mis en face des types C# leur type .NET équivalent, avec la mention (S) si ce type est une structure et (C) si c'est une classe. On découvre qu'il y a deux noms possibles pour un entier sur 32 bits : [int] et [Int32]. Le type [int] est un mot clé du langage C#. [Int32] est une structure appartenant à l'espace de noms [System] : son nom complet est [System.Int32]. Le type [int] est un alias C# qui désigne la structure .NET [System.Int32]. De même, le type C# [string] est un alias du type .NET [System.String]. [System.String] est une classe et non une structure. Les deux notions sont proches, avec cependant la différence fondamentale suivante :
- une variable de type structure contient directement sa valeur : on parle de type valeur ;
- une variable de type classe contient l'adresse de l'objet (sa référence) : on parle de type référence.
S'y ajoutent deux types moins courants :
- [nint] et [nuint] (C# 9) sont des entiers signé et non signé dont la taille est celle d'une adresse mémoire : 4 octets sur un système 32 bits, 8 octets sur un système 64 bits. Ils servent surtout à dialoguer avec du code natif ;
- [System.Half] (.NET 5) est un réel codé sur 2 octets, de faible précision, utilisé en calcul graphique et en apprentissage automatique. Ce n'est pas un type prédéfini du langage : il n'a pas de mot clé C#.
Une structure, comme une classe, est un type complexe ayant des attributs et des méthodes. Ainsi, on peut écrire :
Le littéral 3 est par défaut de type C# [int], donc de type .NET [System.Int32]. Cette structure a une méthode [GetType()] qui rend un objet décrivant le type de la donnée. Parmi les propriétés de cet objet, [FullName] est le nom complet du type. Le littéral 3 est donc un objet plus complexe qu'il n'y paraît à première vue. Le point d'interrogation de [string?] sera expliqué un peu plus loin.
Le programme [bases-03.cs] illustre ces différents points :
Résultats :
Commentaires :
- ligne 2 : déclaration d'un entier [ent] initialisé à 2 ;
- ligne 16 : l'instruction [Console.WriteLine("... {0} ... {1} ...", param0, param1, ...)] écrit à l'écran le texte qui est son premier paramètre (le format), en remplaçant chaque notation {i} par la valeur du paramètre n° i qui suit le format. Les paramètres sont numérotés à partir de 0 ;
- ligne 17 : le type d'une variable [v] est obtenu par [v.GetType().FullName]. La taille en octets d'un type valeur prédéfini [T] est obtenue par [sizeof(T)] ;
- ligne 20 : le type [string] désignant une classe et non une structure, on ne peut pas utiliser l'opérateur [sizeof] ;
- ligne 33 : [sizeof(nint)] n'est pas autorisé dans du code ordinaire car la taille de [nint] n'est connue qu'à l'exécution. La propriété [nint.Size] la donne : 8 octets sur la machine 64 bits utilisée ;
- ligne 35 : on ne peut pas écrire [Half h = 1.5;] : il n'existe pas de conversion implicite d'un [double] vers un [Half]. La notation [(Half)] est une conversion explicite (en anglais, un « cast »), que nous étudierons plus loin ;
- lignes 40-42 : les propriétés [MinValue] et [MaxValue] des types numériques donnent les bornes de leur domaine de valeurs.
L'affichage produit les noms des types .NET et non les alias C#. On remarquera la virgule décimale des réels : les nombres sont convertis en chaînes selon la culture du système, ici le français.
3.3.2. Notation des données littérales
Donnée | Exemples de littéraux |
entier [int] (32 bits) | 145, -7, 0xFF (hexadécimal), 0b1010_1010 (binaire) |
entier [long] (64 bits), suffixe L | 100000L, 1_000_000_000_000L |
entier non signé, suffixe U ou UL | 4000000000U, 10UL |
réel [double] | 134.789, -45E-18 (-45 10⁻¹⁸) |
réel [float], suffixe F | 134.789F, -45E-18F |
réel [decimal], suffixe M | 100000.50M |
caractère [char] | 'A', 'b', '\n', '\u00e9' |
chaîne [string] | "aujourd'hui", "c:\\chap1", @"c:\chap1", $"{nom} a {age} ans", """texte brut""" |
booléen [bool] | [true], [false] |
date | [new DateTime(1954, 10, 13)] (année, mois, jour) pour le 13/10/1954 |
Dans le code source, les réels s'écrivent toujours avec un point décimal, quelle que soit la culture du système. Depuis C# 7, on peut écrire les entiers en binaire (préfixe [0b]) et séparer les groupes de chiffres par le caractère _ pour améliorer la lisibilité : [1_000_000] est l'entier un million.
Une chaîne littérale classique peut contenir des séquences d'échappement introduites par le caractère \ : "\n" représente la marque de fin de ligne et non la succession des deux caractères \ et n, "\t" une tabulation, "\"" un guillemet, "\\" le caractère \ lui-même. Pour écrire le chemin Windows c:\chap1\paragraph3, il faut donc écrire "c:\\chap1\\paragraph3". On peut aussi écrire @"c:\chap1\paragraph3" : la syntaxe @"texte" demande que le texte soit pris exactement comme il est écrit, sans interpréter le caractère \. On appelle cela une chaîne verbatim.
C# offre aujourd'hui deux autres formes de chaînes :
- la chaîne interpolée (C# 6) $"..." : les expressions placées entre accolades sont évaluées et leur valeur est insérée dans la chaîne. Une expression peut être suivie d'un format après le caractère deux-points : [$"{d1:F2}"] affiche le réel [d1] avec deux décimales. C'est la façon la plus lisible de construire une chaîne et nous l'utiliserons désormais à la place des formats {0}, {1}, ... ;
- la chaîne brute (C# 11) """...""" : elle commence et se termine par au moins trois guillemets. Elle peut contenir des guillemets et des \ sans échappement, et s'étendre sur plusieurs lignes. Les espaces qui précèdent les guillemets fermants sont retirés de chaque ligne, ce qui permet d'indenter le texte avec le code. Elle est très pratique pour écrire du JSON, du SQL ou du HTML.
Le programme [bases-04.cs] montre ces différentes notations :
Résultats :
Commentaires :
- ligne 3 : [0xFF] vaut 255 et [0b1010_1010] vaut 170 ;
- ligne 7 : -45E-18 est affiché -4,5E-17 : c'est la même valeur, normalisée ;
- ligne 12 : la séquence \u00e9 désigne le caractère Unicode de code hexadécimal 00E9, c'est-à-dire é ;
- ligne 16 : une date. La méthode [ToShortDateString] l'affiche au format court de la culture française. Pour une date sans heure, .NET 6 a ajouté le type [DateOnly] ; nous le retrouverons au chapitre 5 ;
- lignes 20-22 : les deux chaînes sont identiques, comme le montre l'affichage ;
- ligne 30 : une chaîne interpolée. Les expressions [nom], [age] et [age + 10] sont évaluées puis insérées dans la chaîne ;
- ligne 32 : quelques formats : F1 (une décimale), X (hexadécimal), B8 (binaire sur 8 chiffres, .NET 8), N0 (séparateur des milliers, aucune décimale) ;
- ligne 33 : un format de date personnalisé : jour de la semaine, jour, mois en lettres, année ;
- lignes 36-41 : une chaîne brute sur plusieurs lignes. Les guillemets et le caractère \ y sont écrits tels quels. Les quatre espaces qui précèdent les guillemets fermants ont été retirés de chaque ligne ;
- ligne 44 : une chaîne brute interpolée. Avec deux caractères $, les expressions s'écrivent entre doubles accolades {{ et }} : les accolades simples, nombreuses en JSON, restent alors du texte ordinaire.
3.3.3. Déclaration des données
3.3.3.1. Rôle des déclarations
Un programme manipule des données caractérisées par un nom et un type. Ces données sont stockées en mémoire. Au moment de la traduction du programme, le compilateur affecte à chaque donnée un emplacement en mémoire caractérisé par une adresse et une taille. Il le fait en s'aidant des déclarations faites par le programmeur. Par ailleurs, celles-ci permettent au compilateur de détecter des erreurs de programmation. Ainsi l'opération [x = x * 2;] sera déclarée erronée si [x] est une chaîne de caractères.
3.3.3.2. Déclaration des constantes
La syntaxe de déclaration d'une constante est la suivante :
Par exemple :
Pourquoi déclarer des constantes ?
- la lecture du programme est plus aisée si l'on a donné à la constante un nom significatif ;
- la modification du programme est plus aisée si la « constante » vient à changer. Ainsi, si le taux de TVA passe à 0,25, la seule modification à faire sera celle de l'instruction qui définit sa valeur : [const double TauxTva = 0.25;]. Si l'on avait utilisé 0.2 explicitement dans le programme, ce serait de nombreuses instructions qu'il faudrait modifier.
La valeur d'une constante doit être connue à la compilation : un nombre, une chaîne, un booléen ou une expression calculable à partir d'autres constantes. Par convention, en .NET, les noms des constantes commencent par une majuscule.
3.3.3.3. Déclaration des variables
Une variable est identifiée par un nom et se rapporte à un type de données. C# fait la différence entre majuscules et minuscules : les variables [FIN] et [fin] sont différentes.
Les variables peuvent être initialisées lors de leur déclaration. La syntaxe de déclaration d'une ou plusieurs variables est :
où [type] est un type prédéfini ou un type défini par le programmeur. L'initialisation est facultative ; en revanche, C# refuse qu'on utilise la valeur d'une variable locale qui n'a pas encore reçu de valeur.
On peut également ne pas préciser le type d'une variable en utilisant le mot clé [var] à la place du type :
Le mot clé [var] ne veut pas dire que la variable n'a pas de type précis : elle a le type de la valeur qui lui est affectée, déterminé par le compilateur. L'initialisation est ici obligatoire, afin que le compilateur puisse en déduire le type. On utilise [var] lorsque le type est évident à la lecture de la valeur.
Le programme [bases-05.cs] est le suivant :
Résultats :
Commentaires :
- lignes 2-3 : deux constantes ;
- ligne 8 : une donnée typée explicitement ;
- ligne 9 : [GetType().Name] est le nom court du type, [GetType().FullName] son nom complet ;
- ligne 11 : une donnée typée implicitement. Parce que 3 est de type [int], [j] sera de type [int] ;
- ligne 13 : parce que [DateTime.Now] (la date et l'heure courantes) est de type [DateTime], [aujourdhui] sera de type [DateTime] ;
- lignes 15-18 : le type déduit dépend de l'écriture du littéral : 3.5 est un [double], 3.5F un [float], 10L un [long] ;
- ligne 21 : trois variables déclarées sur une même ligne ; seules [a] et [c] sont initialisées. [b] reçoit sa valeur à la ligne suivante.
Une variable typée implicitement par le mot clé [var] ne peut pas changer de type ensuite. Ainsi, après la ligne 13, on ne pourrait pas écrire :
car [aujourdhui] est de type [DateTime]. Nous verrons au chapitre 5, avec LINQ, qu'il est possible de créer un type « à la volée » dans une expression : c'est un type anonyme, auquel le programmeur n'a pas donné de nom. Si une donnée de type anonyme doit être affectée à une variable, la seule façon de déclarer celle-ci est d'utiliser le mot clé [var].
3.3.4. Les types nullables
Une variable d'un type valeur, comme [int], contient toujours une valeur. Il est parfois utile de pouvoir exprimer l'absence de valeur : un âge inconnu, un champ non renseigné dans une base de données. Le type [int?] (C# 2) est le type [int] auquel on ajoute la valeur [null], qui signifie « pas de valeur ». On parle de type valeur nullable. Le principe vaut pour tous les types valeur : [double?], [bool?], [DateTime?], ...
Une variable d'un type référence, comme [string], peut depuis toujours valoir [null] : cela signifie qu'elle ne référence aucun objet. Utiliser un membre d'une telle variable, par exemple sa propriété [Length], provoque alors une erreur à l'exécution ([NullReferenceException]), l'une des plus fréquentes en C#. Depuis C# 8, le compilateur peut aider à l'éviter : avec les types référence nullables, activés par défaut dans les projets créés par la commande [dotnet new] et dans les applications fichier unique,
- le type [string] désigne une chaîne qui ne vaut jamais [null] ;
- le type [string?] désigne une chaîne qui peut valoir [null].
Le compilateur suit les valeurs possibles des variables et émet un avertissement lorsqu'on risque d'utiliser une valeur [null] : affectation de [null] à une variable [string], utilisation de [Length] sur une variable [string?] qui n'a pas été testée, etc. Contrairement à [int?], [string?] ne crée pas un nouveau type à l'exécution : c'est une indication destinée au compilateur.
Nous rencontrerons ces types dès les premiers exemples, car la méthode [Console.ReadLine()], qui lit une ligne tapée au clavier, rend un [string?] : elle rend [null] lorsqu'il n'y a plus rien à lire (fin du flux d'entrée). Le programme [bases-06.cs] est le suivant :
Résultats avec une saisie au clavier :
Résultats lorsque l'utilisateur tape Ctrl-D (Ctrl-Z puis Entrée sous Windows), qui ferme le flux d'entrée :
Commentaires :
- ligne 3 : [age] ne contient pas de valeur ;
- ligne 4 : la propriété [HasValue] indique si la variable contient une valeur, la propriété [Value] donne cette valeur ;
- ligne 8 : un [int?] ne peut pas être affecté directement à un [int] puisqu'il peut valoir [null]. Il faut une conversion explicite, qui lance une exception si [age] vaut [null]. Nous verrons plus loin l'opérateur [??], plus sûr ;
- ligne 12 : une chaîne qui peut valoir [null] ;
- ligne 15 : une chaîne qui ne vaut jamais [null]. Écrire [nom = null;] provoquerait un avertissement du compilateur ;
- ligne 20 : [Console.ReadLine()] rend un [string?]. Déclarer [ligne] de type [string] provoquerait un avertissement ;
- ligne 21 : on teste la valeur [null] ;
- ligne 29 : dans la branche [else], le compilateur sait que [ligne] ne vaut pas [null] : il n'émet aucun avertissement pour l'utilisation de [ligne.Length].
3.3.5. Les conversions entre nombres et chaînes de caractères
Conversion | Méthode |
nombre -> chaîne | [nombre.ToString()] ou une chaîne interpolée [$"{nombre}"] |
chaîne -> int | [int.Parse(chaine)] ou [int.TryParse(chaine, out int nombre)] |
chaîne -> long | [long.Parse(chaine)] ou [long.TryParse(chaine, out long nombre)] |
chaîne -> double | [double.Parse(chaine)] ou [double.TryParse(chaine, out double nombre)] |
chaîne -> float | [float.Parse(chaine)] ou [float.TryParse(chaine, out float nombre)] |
chaîne -> decimal | [decimal.Parse(chaine)] ou [decimal.TryParse(chaine, out decimal nombre)] |
La conversion d'une chaîne vers un nombre peut échouer si la chaîne ne représente pas un nombre valide. La méthode [Parse] génère alors une erreur fatale appelée exception. Cette erreur peut être gérée par la clause [try / catch] suivante :
Si la fonction ne génère pas d'exception, on passe à l'instruction suivante ; sinon, on passe dans le corps de la clause [catch], puis à l'instruction suivante. Nous reviendrons sur la gestion des exceptions dans la section qui lui est consacrée.
La méthode [TryParse] (.NET 2.0) évite l'exception : elle rend le booléen [true] si la conversion a réussi, [false] sinon, et dépose le nombre obtenu dans une variable passée avec le mot clé [out]. Depuis C# 7, cette variable peut être déclarée dans l'appel lui-même : [int.TryParse(chaine, out var nombre)]. Le mode de passage [out] sera expliqué dans la section sur le passage des paramètres. Lorsqu'on s'attend à des saisies erronées, par exemple au clavier, [TryParse] est préférable à [Parse] : une erreur de saisie n'est pas une situation exceptionnelle.
Le programme [bases-07.cs] présente quelques techniques de conversion entre nombres et chaînes de caractères. La fonction [Affiche] y écrit à l'écran la valeur de son paramètre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
Résultats :
Commentaires :
- ligne 2 : l'espace de noms [System.Globalization], qui contient la classe [CultureInfo], ne fait pas partie des using implicites ;
- lignes 5-8 : les données à convertir ;
- lignes 11-14 : tout objet a une méthode [ToString()] qui le convertit en chaîne ;
- lignes 25-30 : on gère l'exception que peut lancer [int.Parse]. [e.Message] est le message d'erreur lié à l'exception [e]. Les messages des exceptions .NET sont en anglais : .NET 10 ne fournit pas de traduction française ;
- ligne 47 : la chaîne utilise la virgule décimale, conformément à la culture française du système ;
- ligne 73 : [TryParse] réussit et dépose 123 dans la variable [i2], déclarée dans l'appel ;
- ligne 77 : [TryParse] échoue et rend [false] ; la variable [i3] reçoit alors la valeur 0 ;
- ligne 83 : la culture courante du programme est fr-FR ;
- ligne 86 : en culture française, le point n'est pas un séparateur décimal : la conversion échoue ;
- ligne 94 : la culture invariante [CultureInfo.InvariantCulture] utilise le point décimal, quelle que soit la machine. On l'utilise pour lire et écrire des nombres dans des fichiers ou des échanges entre programmes, qui ne doivent pas dépendre de la langue de l'utilisateur ;
- lignes 95-96 : le même nombre converti en chaîne selon la culture courante (100,87) et selon la culture invariante (100.87) ;
- ligne 97 : [TryParse] accepte lui aussi une culture ;
- lignes 103-105 : une fonction locale. Dans un programme à instructions de haut niveau, on ne peut pas écrire de méthode, puisqu'il n'y a pas de classe ; on écrit à la place des fonctions locales, que l'on peut appeler avant leur définition. Elles sont présentées dans la section « Les fonctions locales et les tuples ».
Le cours de 2008 indiquait que les nombres réels sous forme de chaîne doivent utiliser la virgule : c'est vrai en culture française seulement. Un programme qui lit des nombres doit toujours savoir dans quelle culture ils ont été écrits.
3.3.6. Les tableaux de données
Un tableau C# est un objet permettant de rassembler sous un même identificateur des données de même type. Sa déclaration est la suivante :
[n] est le nombre de données que peut contenir le tableau. La syntaxe [tableau[i]] désigne la donnée n° i, où i appartient à l'intervalle [0, n-1]. Toute référence à la donnée [tableau[i]] où i n'appartient pas à l'intervalle [0, n-1] provoque une exception. À sa création, chaque élément du tableau reçoit la valeur par défaut de son type : 0 pour les nombres, [false] pour les booléens, [null] pour les types référence. Un tableau peut être initialisé en même temps que déclaré :
ou plus simplement :
ou encore, depuis C# 12, avec une expression de collection :
C'est cette dernière forme que nous utiliserons désormais. Les tableaux ont une propriété [Length] qui est leur nombre d'éléments.
Un tableau à deux dimensions est déclaré comme suit :
où [n] est le nombre de lignes et [m] le nombre de colonnes. La syntaxe [tableau[i, j]] désigne l'élément j de la ligne i. Le tableau à deux dimensions peut lui aussi être initialisé en même temps qu'il est déclaré :
Le nombre d'éléments de chacune des dimensions est obtenu par la méthode [GetLength(i)], où i=0 représente la dimension correspondant au 1er indice, i=1 la dimension correspondant au 2e indice, etc. Le nombre de dimensions est obtenu avec la propriété [Rank], le nombre total d'éléments avec la propriété [Length].
Un tableau de tableaux (on dit aussi tableau en escalier) est déclaré comme suit :
La déclaration ci-dessus crée un tableau de n éléments. Chaque élément [tableau[i]] est une référence de tableau à une dimension. Ces références ne sont pas initialisées par la déclaration ci-dessus : elles ont la valeur [null]. Contrairement à un tableau à deux dimensions, chaque ligne peut avoir une longueur différente.
C# 8 a ajouté deux notations commodes :
- l'index depuis la fin : [tableau[^1]] est le dernier élément, [tableau[^2]] l'avant-dernier, etc. [tableau[^1]] équivaut à [tableau[tableau.Length - 1]] ;
- la plage : [tableau[1..3]] est un nouveau tableau formé des éléments d'indices 1 et 2. La borne de début est incluse, la borne de fin exclue. Les bornes peuvent être omises : [tableau[2..]] va de l'indice 2 jusqu'à la fin, [tableau[..^1]] du début jusqu'à l'avant-dernier élément inclus.
Le programme [bases-08.cs] regroupe ces différents tableaux :
Résultats :
Commentaires :
- ligne 3 : un tableau à une dimension initialisé ;
- ligne 4 : une boucle [for] parcourt les indices de 0 à [entiers.Length - 1]. La boucle [for] est présentée plus loin ;
- ligne 10 : un tableau à deux dimensions (2 lignes, 2 colonnes) ;
- lignes 11-13 : [GetLength(0)] est le nombre de lignes, [GetLength(1)] le nombre de colonnes ;
- ligne 21 : un tableau [noms] de 3 éléments. Chaque élément est une référence sur un tableau de type [string[]], pour l'instant [null] ;
- ligne 24 : les 3 éléments du tableau [noms] sont initialisés. [noms[i]] référence désormais un tableau de i+1 chaînes ;
- ligne 31 : [noms[i][j]] est l'élément j du tableau référencé par [noms[i]]. Comme [noms[i]] est un tableau, [noms[i].Length] est son nombre d'éléments. L'expression "nom" + i + j est évaluée de gauche à droite : "nom" + i donne une chaîne, à laquelle on concatène j ;
- lignes 44-45 : des tableaux créés sans valeurs initiales. Le type [string?[]] est un tableau dont les éléments peuvent valoir [null] ;
- ligne 49 : une expression de collection ;
- ligne 51 : la méthode [string.Join] construit une chaîne avec les éléments du tableau, séparés par la chaîne passée en premier paramètre ;
- ligne 53 : le dernier et l'avant-dernier élément ;
- ligne 55 : les éléments d'indices 1 et 2 ;
- lignes 60-61 : une plage crée une copie : modifier [extrait] ne modifie pas [valeurs].
3.4. Les instructions élémentaires de C#
On distingue :
- les instructions élémentaires exécutées par l'ordinateur ;
- les instructions de contrôle du déroulement du programme.
Les instructions élémentaires apparaissent clairement lorsqu'on considère la structure d'un micro-ordinateur et de ses périphériques :

- 1 : lecture d'informations provenant du clavier ;
- 2 : traitement d'informations ;
- 3 : écriture d'informations à l'écran.
3.4.1. Écriture sur écran
Il existe différentes instructions d'écriture à l'écran :
où [expression] est toute donnée qui puisse être convertie en chaîne de caractères pour être affichée à l'écran. Tous les objets de C# ou .NET ont une méthode [ToString()] qui est utilisée pour faire cette conversion. La méthode [Write] fait la même chose que [WriteLine] sans passer à la ligne.
La classe [System.Console] donne accès aux opérations d'écriture à l'écran ([Write], [WriteLine]). Elle a deux propriétés [Out] et [Error] qui sont des flux d'écriture de type [TextWriter] :
- [Console.WriteLine()] est équivalent à [Console.Out.WriteLine()] et écrit sur le flux [Out], associé habituellement à l'écran ;
- [Console.Error.WriteLine()] écrit sur le flux [Error], habituellement associé lui aussi à l'écran.
Les flux [Out] et [Error] peuvent être redirigés vers des fichiers au moment de l'exécution du programme, comme nous le verrons prochainement.
3.4.2. Lecture de données tapées au clavier
Le flux de données provenant du clavier est désigné par l'objet [Console.In] de type [TextReader]. Ce type d'objet permet de lire une ligne de texte avec la méthode [ReadLine] :
La classe [Console] offre une méthode [ReadLine] associée par défaut au flux [In]. On peut donc écrire :
La ligne tapée au clavier, sans la marque de fin de ligne, est rangée dans la variable [ligne] et peut ensuite être exploitée par le programme. Comme nous l'avons vu, [ReadLine] rend [null] lorsque le flux d'entrée est épuisé : c'est pourquoi la variable est de type [string?]. Le flux [In] peut être redirigé vers un fichier, comme les flux [Out] et [Error].
3.4.3. Exemple d'entrées-sorties
Le programme [bases-09.cs] illustre les opérations d'entrées-sorties clavier / écran :
Résultats :
Commentaires :
- ligne 4 : [obj] est une référence d'objet ;
- ligne 5 : [obj] est écrit à l'écran. C'est la méthode [obj.ToString()] qui est appelée pour obtenir le texte à afficher ;
- ligne 9 : on écrit sur le flux [Error] ;
- ligne 10 : la même chose avec un format. Le premier paramètre "i={0}" est le format d'affichage, les autres paramètres sont les expressions à afficher. Les éléments {n} sont des paramètres positionnels : à l'exécution, {n} est remplacé par la valeur de l'expression n° n ;
- lignes 13-14 : on affiche une invite sans passer à la ligne, puis on lit la ligne tapée au clavier ;
- ligne 15 : dans une chaîne interpolée, une valeur [null] produirait une chaîne vide : aucun avertissement n'est émis.
Dans les résultats :
- ligne 2 : l'affichage produit par la ligne 5 du code. La méthode [obj.ToString()] a affiché le nom du type de la variable [obj] : [System.Object]. Le type [object] est un alias C# du type .NET [System.Object] ;
- lignes 3-4 : les deux écritures sur le flux [Error] apparaissent à l'écran comme celles du flux [Out].
3.4.4. Redirection des entrées-sorties
Windows, Linux et macOS connaissent trois périphériques standard :
- le périphérique d'entrée standard, qui désigne par défaut le clavier et porte le n° 0 ;
- le périphérique de sortie standard, qui désigne par défaut l'écran et porte le n° 1 ;
- le périphérique d'erreur standard, qui désigne par défaut l'écran et porte le n° 2.
En C#, le flux d'écriture [Console.Out] écrit sur le périphérique 1, le flux d'écriture [Console.Error] écrit sur le périphérique 2 et le flux de lecture [Console.In] lit les données provenant du périphérique 0.
Lorsqu'on lance un programme dans une fenêtre de commandes, on peut fixer quels seront les périphériques 0, 1 et 2 pour le programme exécuté. Derrière les arguments du programme, on peut rediriger les périphériques d'entrées-sorties standard vers des fichiers :
Redirection | Signification |
0<in.txt ou <in.txt | le flux d'entrée standard n° 0 est redirigé vers le fichier in.txt. Dans le programme, le flux [Console.In] prendra donc ses données dans le fichier in.txt |
1>out.txt ou >out.txt | redirige la sortie n° 1 vers le fichier out.txt. Dans le programme, le flux [Console.Out] écrira donc ses données dans le fichier out.txt |
1>>out.txt ou >>out.txt | idem, mais les données écrites sont ajoutées au contenu actuel du fichier out.txt |
2>err.txt | redirige la sortie n° 2 vers le fichier err.txt. Dans le programme, le flux [Console.Error] écrira donc ses données dans le fichier err.txt |
2>>err.txt | idem, mais les données écrites sont ajoutées au contenu actuel du fichier err.txt |
1>out.txt 2>err.txt | les périphériques 1 et 2 sont tous les deux redirigés vers des fichiers |
On notera que pour rediriger les flux d'entrées-sorties d'un programme vers des fichiers, le programme n'a pas besoin d'être modifié : c'est le système d'exploitation qui fixe la nature des périphériques 0, 1 et 2. Considérons le programme [bases-10.cs] suivant :
Le cours de 2008 générait un exécutable avec Visual Studio puis l'exécutait dans une fenêtre DOS. Aujourd'hui, la commande [dotnet run] suffit : les redirections s'appliquent au programme lancé. Voici une session dans un terminal Linux (bash) :
Commentaires :
- ligne 1 : on met la chaîne test dans le fichier in.txt ;
- ligne 2 : on affiche le contenu du fichier in.txt pour vérification ;
- ligne 4 : exécution du programme. Le flux [In] est redirigé vers le fichier in.txt, le flux [Out] vers le fichier out.txt, le flux [Error] vers le fichier err.txt. L'exécution ne provoque aucun affichage ;
- lignes 5-6 : le contenu du fichier out.txt montre que le fichier in.txt a été lu et que l'écriture sur le flux [Out] a été redirigée vers out.txt ;
- lignes 7-8 : vérification analogue pour le fichier err.txt ;
- lignes 9-10 : seul le flux [Error] est redirigé : l'écran n'affiche que l'écriture sur le flux [Out] ;
- lignes 11-12 : seul le flux [Out] est redirigé : l'écran n'affiche que l'écriture sur le flux [Error].
On voit clairement que les flux [Out] et [Error] n'écrivent pas sur les mêmes périphériques puisqu'on a pu les rediriger séparément. Le flux [Error] est fait pour les messages d'erreur : on peut ainsi rediriger les résultats d'un programme vers un fichier tout en continuant à voir ses messages d'erreur à l'écran.
Les opérateurs >, >> et 2> s'écrivent de la même façon dans l'invite de commandes Windows ([cmd]), dans PowerShell et dans bash. L'opérateur < est reconnu par [cmd] et bash, mais pas par PowerShell, qui le réserve pour un usage futur : on y envoie un fichier sur l'entrée standard d'un programme avec un tube (|). Les commandes suivantes, données à titre d'illustration (elles n'ont pas été exécutées pour ce cours), font sous Windows ce que fait la session bash ci-dessus :
3.4.5. Affectation de la valeur d'une expression à une variable
On s'intéresse ici à l'opération [variable = expression;]. L'expression peut être de type arithmétique, relationnelle, booléenne, caractères, etc.
3.4.5.1. Interprétation de l'opération d'affectation
L'opération [variable = expression;] est elle-même une expression dont l'évaluation se déroule de la façon suivante :
- la partie droite de l'affectation est évaluée : le résultat est une valeur V ;
- la valeur V est affectée à la variable ;
- la valeur V est aussi la valeur de l'affectation, vue cette fois en tant qu'expression.
C'est ainsi que l'opération [V1 = V2 = expression] est légale. L'opérateur = étant évalué de droite à gauche, on a [V1 = (V2 = expression)]. L'expression [V2 = expression] est évaluée et a pour valeur V. Son évaluation a provoqué l'affectation de V à V2. L'opérateur = suivant est alors évalué sous la forme [V1 = V]. La valeur de cette expression est encore V ; son évaluation provoque l'affectation de V à V1. Ainsi donc, l'opération [V1 = V2 = expression] est une expression dont l'évaluation :
- provoque l'affectation de la valeur de [expression] aux variables V1 et V2 ;
- rend comme résultat la valeur de [expression].
On peut généraliser à une expression du type [V1 = V2 = ... = Vn = expression].
3.4.5.2. Expressions arithmétiques
Les opérateurs des expressions arithmétiques sont les suivants :
Opérateur | Rôle |
+ | addition |
- | soustraction |
* | multiplication |
/ | division : le résultat est le quotient exact si l'un au moins des opérandes est réel. Si les deux opérandes sont entiers, le résultat est le quotient entier. Ainsi 5/2 vaut 2 et 5.0/2 vaut 2.5 |
% | reste de la division (modulo), quelle que soit la nature des opérandes. Le reste a le signe du dividende : -5 % 2 vaut -1 |
Il existe diverses fonctions mathématiques. En voici quelques-unes :
Fonction | Rôle |
[double Sqrt(double x)] | racine carrée |
[double Cos(double x)], [Sin], [Tan] | cosinus, sinus, tangente (angle en radians) |
[double Pow(double x, double y)] | x à la puissance y |
[double Exp(double x)] | exponentielle |
[double Log(double x)], [Log10], [Log2] | logarithme népérien, décimal, en base 2 |
[Abs(x)], [Max(x, y)], [Min(x, y)] | valeur absolue, maximum, minimum (pour tous les types numériques) |
[Floor(x)], [Ceiling(x)], [Truncate(x)] | partie entière inférieure, supérieure, troncature |
[Round(x)], [Round(x, n)] | arrondi à l'entier, à n décimales |
[Math.PI], [Math.E] | les constantes π et e |
Toutes ces fonctions sont définies dans une classe .NET appelée [Math]. Lorsqu'on les utilise, il faut les préfixer avec le nom de la classe où elles sont définies. Ainsi on écrira :
Le cours de 2008 reproduisait ici la définition complète de la classe [Math] : on la trouve aujourd'hui dans la documentation en ligne de .NET, learn.microsoft.com/dotnet/api/system.math.
Le programme [bases-11.cs] est le suivant :
Résultats :
Commentaires :
- ligne 3 : la division de deux entiers est une division entière ;
- ligne 5 : le reste a le signe du dividende ; l'opérateur % s'applique aussi aux réels ;
- ligne 8 : la division d'un réel par zéro ne provoque pas d'erreur : elle donne l'infini (affiché ∞) ou la valeur spéciale NaN (« Not a Number ») pour 0/0. En revanche, la division d'un entier par zéro lance une exception [DivideByZeroException] ;
- lignes 11-12 : l'exemple ci-dessus ;
- ligne 18 : [Floor] arrondit vers le bas, [Ceiling] vers le haut, [Truncate] vers zéro ;
- ligne 20 : par défaut, [Math.Round] arrondit une valeur située exactement à mi-chemin vers le nombre pair le plus proche (arrondi « bancaire ») : 2,5 donne 2 et 3,5 donne 4 ;
- ligne 21 : pour l'arrondi enseigné à l'école (2,5 donne 3), on précise le mode d'arrondi ;
- ligne 25 : la priorité des opérateurs, présentée ci-dessous ;
- ligne 29 : une affectation multiple.
3.4.5.3. Priorités dans l'évaluation des expressions arithmétiques
La priorité des opérateurs lors de l'évaluation d'une expression arithmétique est la suivante (du plus prioritaire au moins prioritaire) :
Les opérateurs d'un même bloc [ ] ont même priorité. À priorité égale, l'évaluation se fait de gauche à droite : 10 - 4 - 3 vaut (10 - 4) - 3 = 3.
3.4.5.4. Expressions relationnelles
Les opérateurs sont les suivants : <, <=, ==, !=, >, >=. Leurs priorités :
- >, >=, <, <= ;
- ==, !=.
Le résultat d'une expression relationnelle est le booléen [false] si l'expression est fausse, [true] sinon.
Comparaison de deux caractères
Soient deux caractères C1 et C2. Il est possible de les comparer avec les opérateurs <, <=, ==, !=, >, >=. Ce sont alors leurs codes Unicode, qui sont des nombres, qui sont comparés. Selon l'ordre Unicode, on a les relations suivantes :
Comparaison de deux chaînes de caractères
Elles sont comparées caractère par caractère. La première inégalité rencontrée entre deux caractères induit une inégalité de même sens sur les chaînes. Soit à comparer les chaînes "Chat" et "Chien" :
Cette dernière inégalité permet de dire que "Chat" < "Chien". Soit à comparer les chaînes "Chat" et "Chaton". Il y a égalité jusqu'à épuisement de la chaîne "Chat". Dans ce cas, la chaîne épuisée est déclarée la plus « petite ». On a donc la relation "Chat" < "Chaton".
Fonctions de comparaison de deux chaînes
On peut utiliser les opérateurs relationnels == et != pour tester l'égalité ou non de deux chaînes, ou bien la méthode [Equals] de la classe [System.String]. Pour les relations < <= > >=, les opérateurs ne s'appliquent pas aux chaînes : il faut utiliser la méthode [CompareTo] de la classe [System.String] ou la méthode statique [string.Compare]. Elles rendent un entier :
- nul si les deux chaînes sont égales ;
- positif si la chaîne n° 1 est supérieure à la chaîne n° 2 ;
- négatif si la chaîne n° 1 est inférieure à la chaîne n° 2.
Il y a une subtilité : [CompareTo] et [string.Compare] ne comparent pas simplement les codes Unicode. Ils suivent les règles de tri de la culture courante, celles d'un dictionnaire : en français, "élan" se range avant "face", bien que le code de 'é' (233) soit supérieur à celui de 'f' (102). Pour choisir le mode de comparaison, [string.Compare] et [Equals] acceptent un paramètre de type [StringComparison] :
- [StringComparison.Ordinal] : comparaison des codes Unicode, caractère par caractère. C'est la comparaison la plus rapide, à utiliser pour les identifiants, les mots clés, les noms de fichiers ;
- [StringComparison.OrdinalIgnoreCase] : idem, sans tenir compte de la différence entre majuscules et minuscules ;
- [StringComparison.CurrentCulture] (le défaut de [CompareTo]) : selon les règles de la culture courante, pour trier des textes destinés à l'utilisateur.
L'opérateur == et la méthode [Equals] sans paramètre font une comparaison ordinale.
Le programme [bases-12.cs] est le suivant :
Résultats :
Commentaires :
- ligne 3 : une expression relationnelle ;
- ligne 7 : la comparaison de caractères ;
- ligne 8 : la conversion [(int)] d'un caractère donne son code Unicode ;
- ligne 12 : la variable [n] reçoit un entier négatif puisque "chat" < "chien" ;
- ligne 13 : la variable [egal] a la valeur [true] si les deux chaînes sont égales, [false] sinon ;
- ligne 15 : on utilise les opérateurs == et != pour vérifier l'égalité ou non de deux chaînes. Dans une chaîne interpolée, \" désigne un guillemet ;
- ligne 16 : la chaîne épuisée la première est la plus petite ;
- lignes 19-20 : comparaisons selon la culture française : "élan" précède "face", et "Chat" suit "chat" (à lettres égales, la minuscule passe avant la majuscule) ;
- lignes 22-23 : comparaisons ordinales : le résultat est ici la différence des codes des premiers caractères différents : 233 - 102 = 131 et 67 - 99 = -32 ;
- lignes 25-27 : en ignorant la casse, "Chat" et "chat" sont égales.
3.4.5.5. Expressions booléennes
Les opérateurs utilisables sont ET (&&), OU (||) et NON (!). Le résultat d'une expression booléenne est un booléen. Priorités des opérateurs :
- ! ;
- && ;
- ||.
Les opérateurs relationnels ont priorité sur les opérateurs && et ||. Ces deux opérateurs n'évaluent leur opérande droit que si c'est nécessaire : dans [a && b], b n'est pas évalué si a est faux ; dans [a || b], b n'est pas évalué si a est vrai. On dit qu'ils sont « court-circuités ». On peut ainsi écrire sans risque [texte != null && texte.Length > 0].
3.4.5.6. Traitement de bits
Soient [i] et [j] deux entiers :
Opération | Rôle |
i << n | décale i de n bits sur la gauche. Les bits entrants sont des zéros |
i >> n | décale i de n bits sur la droite. Si i est un entier signé ([sbyte], [short], [int], [long]), le bit de signe est préservé |
i >>> n | décalage à droite non signé (C# 11) : les bits entrants sont des zéros, même si i est signé |
i & j | fait le ET logique de i et j bit à bit |
i | j | fait le OU logique de i et j bit à bit |
~i | complémente i à 1 |
i ^ j | fait le OU EXCLUSIF de i et j bit à bit |
Le programme [bases-13.cs] est le suivant :
Résultats :
Commentaires :
- ligne 4 : le format {0:x4} affiche le paramètre n° 0 en hexadécimal (x) avec 4 chiffres (4) ;
- ligne 7 : les opérations sur les bits d'un [short] donnent un [int] : on les reconvertit en [short] pour afficher 4 chiffres hexadécimaux. Sans conversion, ~i serait affiché sur 8 chiffres (ffffff9b) ;
- ligne 10 : le format B16 affiche un entier en binaire sur 16 chiffres. On vérifie bit à bit le ET logique de i et j ;
- ligne 13 : le décalage [>>] préserve le bit de signe de m, qui est négatif ; le décalage [>>>] fait entrer des zéros.
3.4.5.7. Combinaison d'opérateurs
[a = a + b] peut s'écrire [a += b] ; [a = a - b] peut s'écrire [a -= b]. Il en est de même avec les opérateurs /, %, *, <<, >>, >>>, &, |, ^. Ainsi [a = a / 2;] peut s'écrire [a /= 2;].
3.4.5.8. Opérateurs d'incrémentation et de décrémentation
La notation [variable++] signifie [variable = variable + 1] ou encore [variable += 1]. La notation [variable--] signifie [variable = variable - 1] ou encore [variable -= 1]. Utilisés dans une expression, ces opérateurs ont deux formes :
- [i++] (postfixée) : la valeur de l'expression est la valeur de i avant l'incrémentation ;
- [++i] (préfixée) : la valeur de l'expression est la valeur de i après l'incrémentation.
3.4.5.9. L'opérateur ternaire ?
L'expression [expr_cond ? expr1 : expr2] est évaluée de la façon suivante :
- l'expression [expr_cond] est évaluée. C'est une expression booléenne ;
- si elle est vraie, la valeur de l'expression est celle de [expr1], et [expr2] n'est pas évaluée ;
- si elle est fausse, c'est l'inverse qui se produit : la valeur de l'expression est celle de [expr2], et [expr1] n'est pas évaluée.
L'opération [i = (j > 4 ? j + 1 : j - 1);] affectera à la variable i : j+1 si j>4, j-1 sinon. C'est la même chose que d'écrire [if (j > 4) i = j + 1; else i = j - 1;], mais c'est plus concis.
3.4.5.10. Les opérateurs ??, ??= et ?.
Ces opérateurs facilitent le travail avec les valeurs [null] :
- [a ?? b] (C# 2) vaut a si a ne vaut pas [null], b sinon. On l'utilise pour donner une valeur de remplacement : [string nom = Console.ReadLine() ?? "";]. Le résultat n'est plus nullable ;
- [a ??= b] (C# 8) affecte b à a seulement si a vaut [null] ;
- [a?.Membre] (C# 6) vaut [null] si a vaut [null], [a.Membre] sinon. Ainsi [texte?.Length] ne provoque pas d'erreur lorsque [texte] vaut [null] ; le résultat est de type [int?].
Le programme [bases-14.cs] réunit ces différents opérateurs :
Résultats :
Commentaires :
- ligne 4 : une expression booléenne ;
- lignes 9-17 : les opérateurs combinés : 10+5=15, 15*2=30, 30/4=7 (division entière), 7%4=3, 3<<2=12 ;
- ligne 22 : [j] reçoit 5, puis [i] passe à 6 ;
- ligne 23 : [i] passe à 7, puis [k] reçoit 7 ;
- lignes 30-31 : deux opérateurs ternaires ;
- ligne 36 : [saisie] vaut [null] : [nom] reçoit "inconnu". Le type de [nom] est [string] et non [string?] ;
- ligne 38 : l'opérateur ?? permet de passer d'un [int?] à un [int] sans risque d'exception ;
- lignes 42-43 : la première affectation a lieu car [titre] vaut [null] ; la seconde n'a pas lieu ;
- ligne 47 : [texte] vaut [null], donc [longueur] aussi ;
- ligne 48 : les opérateurs se combinent : si [longueur] vaut [null], on affiche la chaîne "null".
3.4.5.11. Priorité générale des opérateurs
Le tableau suivant donne la priorité des opérateurs, du plus prioritaire au moins prioritaire :
Opérateurs | Associativité |
[x.y] [x?.y] [f(x)] [a[i]] [x++] [x--] [new] [typeof] [checked] [nameof] | gd |
[+x] [-x] [!x] [~x] [++x] [--x] [^x] [(T)x] (changement de type) | dg |
[x..y] (plage) | gd |
[switch] (expression switch) | gd |
[*] [/] [%] | gd |
[+] [-] | gd |
[<<] [>>] [>>>] | gd |
[<] [<=] [>] [>=] [is] [as] | gd |
[==] [!=] | gd |
[&] | gd |
[^] | gd |
[|] | gd |
[&&] | gd |
[||] | gd |
[??] | dg |
[? :] (ternaire) | dg |
[=] [+=] [-=] [??=] etc., [=>] | dg |
gd indique qu'à priorité égale, c'est la priorité gauche-droite qui est observée : lorsque dans une expression on a des opérateurs de même priorité, c'est l'opérateur le plus à gauche qui est évalué en premier. dg indique une priorité droite-gauche. En cas de doute, on n'hésitera pas à mettre des parenthèses : elles rendent aussi l'expression plus lisible.
3.4.5.12. Les changements de type
Il est possible, dans une expression, de changer momentanément le codage d'une valeur. On appelle cela changer le type d'une donnée, ou en anglais « type casting ». La syntaxe du changement du type d'une valeur dans une expression est la suivante :
La valeur prend alors le type indiqué. Cela entraîne un changement de codage de la valeur. On distingue :
- les conversions implicites, faites automatiquement par le compilateur lorsqu'elles ne font pas perdre d'information : d'un [int] vers un [long], d'un [int] vers un [double], d'un [char] vers un [int], etc. La conversion d'un [long] vers un [double] est elle aussi implicite, bien qu'elle puisse perdre en précision au-delà de 2⁵³ ;
- les conversions explicites, qui peuvent perdre de l'information et que le programmeur doit demander avec la notation (type) : d'un [double] vers un [int] (la partie décimale est perdue), d'un [long] vers un [int] (les bits de poids fort sont perdus), etc.
Par défaut, un dépassement de capacité dans un calcul entier ou une conversion entre entiers n'est pas signalé : le résultat est simplement faux. Le mot clé [checked] demande au contraire qu'un dépassement lance une exception [OverflowException]. Il s'applique à une expression, [checked(expression)], ou à un bloc d'instructions, [checked { ... }].
Le programme [bases-15.cs] est le suivant :
Résultats :
Commentaires :
- ligne 3 : [f1] aura la valeur 0. La division 3/4 est une division entière puisque les deux opérandes sont de type [int] ; son résultat 0 est ensuite converti en [float] ;
- ligne 4 : [(float)i] est la valeur de [i] transformée en [float]. On a maintenant une division entre un réel de type [float] et un entier de type [int] : la valeur de [j] est elle aussi transformée en [float], puis la division des deux réels est faite. [f2] a la valeur 0,75. Dans l'opération [(float)i], [i] est une valeur codée de façon exacte sur 4 octets, [(float)i] est la même valeur codée de façon approchée en réel sur 4 octets. Ce transcodage n'a lieu que le temps du calcul : la variable [i] conserve toujours son type [int] ;
- lignes 8-9 : des conversions implicites ;
- ligne 14 : la conversion d'un réel en entier tronque la partie décimale ;
- ligne 15 : pour arrondir, on utilise [Math.Round], qui rend un [double], puis on convertit ;
- ligne 18 : 3 milliards ne tient pas dans un [int] : seuls les 32 bits de poids faible sont conservés et le résultat n'a plus de sens ;
- lignes 22-23 : conversions entre caractères et codes Unicode ; la seconde est implicite ;
- ligne 28 : le dépassement de capacité passe inaperçu : on obtient le plus petit [int] ;
- ligne 33 : avec [checked], le dépassement lance une exception [OverflowException] ;
- lignes 42-45 : un bloc [checked] : la conversion de [grand] en [int] lance elle aussi une exception.
Si le dépassement a lieu dans une expression formée uniquement de constantes, comme [int.MaxValue + 1], c'est le compilateur qui le détecte et refuse le programme.
3.5. Les instructions de contrôle du déroulement du programme
3.5.1. Arrêt
La méthode [Exit] de la classe [Environment] permet d'arrêter l'exécution d'un programme :
[Exit] provoque la fin du processus en cours et rend la main au processus appelant, en général l'interpréteur de commandes. La valeur de [status], appelée code de retour, peut être utilisée par celui-ci. Par convention, 0 signifie que le programme s'est bien déroulé et une autre valeur signale une erreur. Sous Windows, le code de retour est disponible dans la variable [%ERRORLEVEL%] de l'invite de commandes et dans la variable [$LASTEXITCODE] de PowerShell ; sous Linux et macOS, dans la variable [$?] du shell. Ainsi :
arrêtera l'exécution du programme avec un code de retour égal à 0. Avec les instructions de haut niveau, une instruction [return] arrête aussi le programme (code de retour 0), sauf si elle se trouve dans une fonction.
Le programme [bases-16.cs] reçoit son code de retour en argument :
Résultats dans un terminal bash :
Commentaires :
- ligne 4 : on vérifie qu'un argument a été passé et qu'il s'agit d'un entier. Les arguments du programme sont présentés plus loin ;
- ligne 7 : si ce n'est pas le cas, on arrête le programme avec le code de retour 1 ;
- ligne 8 : cette instruction ne sera jamais exécutée, mais le compilateur ne sait pas que [Environment.Exit] ne rend jamais la main. Sans elle, il considérerait que l'on peut arriver à la ligne suivante avec une variable [code] non initialisée, et refuserait le programme ;
- ligne 11 : arrêt avec le code de retour passé en argument. La ligne qui suit n'est pas exécutée ;
- lignes 4-9 : la commande [echo $?] affiche le code de retour du dernier programme exécuté : 3 puis 1.
3.5.2. Structure de choix simple
Notes :
- la condition est entourée de parenthèses ;
- chaque action est terminée par un point-virgule ;
- les accolades ne sont pas suivies d'un point-virgule ;
- les accolades ne sont nécessaires que s'il y a plus d'une action ; on les met cependant souvent pour une seule action, par lisibilité et pour éviter les erreurs lors de l'ajout d'une action ;
- la clause [else] peut être absente ;
- il n'y a pas de mot clé [then].
L'équivalent algorithmique de cette structure est la structure si .. alors .. sinon :
Exemple :
On peut imbriquer les structures de choix. Se pose alors parfois le problème illustré par le programme [bases-17.cs] :
Résultats :
Commentaires :
- lignes 4-8 : le [else] de la ligne 7 se rapporte à quel [if] ? La règle est qu'un [else] se rapporte toujours au [if] le plus proche : [if (n > 6)], ligne 5. L'indentation du code, qui n'a aucun effet sur le compilateur, a été choisie pour le montrer ;
- lignes 12-21 : ici, nous voulions mettre un [else] au [if (n2 > 1)] et pas de [else] au [if (n2 > 6)]. À cause de la règle précédente, nous sommes obligés de mettre des accolades au [if (n2 > 1) { ... }] ;
- lignes 27-41 : une suite de choix exclusifs s'écrit avec des [else if] successifs. Les conditions sont testées dans l'ordre ; la première vraie détermine l'action exécutée.
3.5.3. Structure de cas
3.5.3.1. L'instruction switch
La syntaxe est la suivante :
Notes :
- la valeur de l'expression de contrôle du [switch] peut être un entier, un caractère, une chaîne de caractères, une valeur d'énumération, etc. ;
- l'expression de contrôle est entourée de parenthèses ;
- la clause [default] peut être absente ;
- les valeurs vi sont des valeurs possibles de l'expression. Si l'expression a pour valeur vi, les actions derrière la clause [case vi] sont exécutées ;
- l'instruction [break] fait sortir de la structure de cas ;
- chaque bloc d'instructions lié à une valeur vi doit se terminer par une instruction de branchement ([break], [return], [throw], ...), sinon le compilateur signale une erreur. Plusieurs clauses [case] peuvent en revanche se suivre sans instruction entre elles : elles partagent alors les mêmes actions.
Exemple en algorithmique :
3.5.3.2. L'expression switch
Très souvent, une structure de cas ne sert qu'à choisir une valeur. C# 8 a introduit pour cela l'expression switch :
L'expression est comparée successivement aux motifs ; la valeur associée au premier motif qui convient est le résultat. Le motif _ convient à toutes les valeurs : il joue le rôle de [default]. Un motif peut être :
- une constante : [0], ["samedi"] ;
- un motif relationnel (C# 9) : [< 0], [>= 18] ;
- une combinaison de motifs avec [and], [or] et [not] (C# 9) : [> 0 and <= 10], ["samedi" or "dimanche"].
Ces mêmes motifs s'utilisent avec l'opérateur [is] : [jour is "samedi" or "dimanche"] est vrai si [jour] vaut "samedi" ou "dimanche". Nous en verrons d'autres formes dans les chapitres suivants.
Le programme [bases-18.cs] est le suivant :
Résultats :
Commentaires :
- lignes 4-15 : la traduction en C# de l'algorithme ci-dessus ;
- ligne 7 : dans les instructions de haut niveau, [return] termine le programme ;
- lignes 24-25 : deux valeurs pour les mêmes actions ;
- lignes 34-38 : une expression switch sur une chaîne. Les motifs sont combinés avec [or] ;
- ligne 49 : le motif [not] avec l'opérateur [is] ;
- lignes 53-60 : deux fonctions locales ;
- lignes 63-69 : une fonction locale dont le corps est une expression switch. La notation => remplace le corps entre accolades et l'instruction [return] : on parle de corps d'expression. Les motifs relationnels classent l'entier [n].
3.5.4. Structures de répétition
3.5.4.1. Nombre de répétitions connu
Structure for
La syntaxe est la suivante :
Notes :
- les 3 arguments du [for] sont à l'intérieur d'une parenthèse et séparés par des points-virgules ;
- chaque action du [for] est terminée par un point-virgule ;
- l'accolade n'est nécessaire que s'il y a plus d'une action ;
- l'accolade n'est pas suivie d'un point-virgule.
L'équivalent algorithmique est la structure pour :
qu'on peut traduire par une structure tantque :
Structure foreach
La syntaxe est la suivante :
Notes :
- [collection] est une collection d'objets énumérable. La collection énumérable que nous connaissons déjà est le tableau ; nous en verrons d'autres ([List<T>], [Dictionary<K, V>], ...) au chapitre 5 ;
- [Type] est le type des objets de la collection. Pour un tableau, ce serait le type des éléments du tableau. On peut écrire [var] à la place ;
- [variable] est une variable locale à la boucle qui va prendre successivement pour valeur toutes les valeurs de la collection. Elle ne peut pas être modifiée dans la boucle.
3.5.4.2. Nombre de répétitions inconnu
Structure tantque (while)
On boucle tant que la condition est vérifiée. La boucle peut ne jamais être exécutée. Les notes sont les mêmes que pour le [for]. La structure algorithmique correspondante est la structure tantque :
Structure répéter jusqu'à (do while)
La syntaxe est la suivante :
On boucle jusqu'à ce que la condition devienne fausse. Ici, la boucle est faite au moins une fois. La structure algorithmique correspondante est la structure répéter ... jusqu'à :
Structure pour générale (for)
La syntaxe est la suivante :
On boucle tant que la condition est vraie (évaluée avant chaque tour de boucle). Les [instructions_depart] sont effectuées avant d'entrer dans la boucle pour la première fois. Les [instructions_fin_boucle] sont exécutées après chaque tour de boucle. Les différentes instructions de [instructions_depart] et de [instructions_fin_boucle] sont séparées par des virgules. La structure algorithmique correspondante est la suivante :
3.5.4.3. Instructions de gestion de boucle
Instruction | Rôle |
[break] | fait sortir de la boucle [for], [foreach], [while], [do ... while] |
[continue] | fait passer à l'itération suivante des boucles [for], [foreach], [while], [do ... while] |
3.5.4.4. Exemples
Le programme [bases-19.cs] est le suivant :
Résultats :
Commentaires :
- lignes 3-30 : quatre façons de calculer la somme des 10 premiers nombres entiers ;
- lignes 4-8 : un [for] classique ;
- lignes 10-13 : tout le travail est fait dans les instructions de fin de boucle : le corps de la boucle est vide. Le cours de 2008 écrivait [for (...) ;] : C# accepte cette instruction vide mais le compilateur émet un avertissement (« possible instruction vide erronée »), car un point-virgule placé par erreur derrière un [for] est une faute classique. Un bloc vide { } exprime la même chose sans ambiguïté ;
- ligne 28 : [i++] rend la valeur de [i] avant son incrémentation : on ajoute [i] à [somme], puis on incrémente [i] ;
- ligne 34 : la forme la plus courante du [for] : la variable de boucle est déclarée dans le [for] et n'existe que dans la boucle ;
- lignes 41-42 : la boucle [foreach] parcourt les éléments du tableau [amis] ;
- lignes 51-58 : [continue] passe à l'itération suivante pour les nombres pairs, [break] arrête la boucle dès que [k] dépasse 7 ;
- lignes 66-74 : une boucle infinie [while (true)] dont on sort par [break]. Cette forme est commode lorsque la condition d'arrêt se calcule au milieu de la boucle. Nous l'utiliserons pour les saisies au clavier.
3.6. La gestion des exceptions
De nombreuses méthodes .NET sont susceptibles de générer des exceptions, c'est-à-dire des erreurs. Lorsqu'une méthode est susceptible de générer une exception, le programmeur devrait la gérer dans le but d'obtenir des programmes plus résistants aux erreurs : il faut toujours éviter l'arrêt brutal d'une application.
3.6.1. La structure try / catch / finally
La gestion d'une exception se fait selon le schéma suivant :
Si le code de la clause [try] ne génère pas d'exception, on passe à l'instruction suivante ; sinon, on passe dans le corps de la clause [catch], puis à l'instruction suivante. Notons les points suivants :
- [e] est un objet de type [Exception] ou d'un type dérivé. Il existe de nombreux types d'exceptions : [IndexOutOfRangeException], [FormatException], [OverflowException], [ArgumentException], etc. En écrivant [catch (Exception e)], on indique qu'on veut gérer tous les types d'exceptions. Si le code de la clause [try] est susceptible de générer plusieurs types d'exceptions, on peut vouloir être plus précis en gérant l'exception avec plusieurs clauses [catch] :
Les clauses [catch] sont examinées dans l'ordre ; la première dont le type correspond à l'exception est exécutée. Une clause [catch (Exception e)] doit donc être placée en dernier ;
- on peut ajouter aux clauses [try / catch] une clause [finally] :
Qu'il y ait exception ou pas, le code de la clause [finally] est toujours exécuté. On y place par exemple la fermeture d'un fichier ;
- dans la clause [catch], on peut ne pas vouloir utiliser l'objet [Exception] disponible. Au lieu d'écrire [catch (Exception e) { ... }], on écrit alors [catch (Exception) { ... }] ou plus simplement [catch { ... }] ;
- la classe [Exception] a une propriété [Message] qui détaille l'erreur qui s'est produite. Si on veut l'afficher, on écrira :
- la classe [Exception] a une méthode [ToString] qui rend une chaîne de caractères indiquant le type de l'exception, la valeur de la propriété [Message] et la pile des appels, c'est-à-dire la liste des méthodes en cours d'exécution au moment de l'erreur, avec le nom du fichier source et le numéro de ligne. C'est l'information la plus utile pour le développeur, mais elle n'est pas destinée à l'utilisateur. Dans ce cours, nous afficherons le plus souvent le nom du type de l'exception, [ex.GetType().Name], et son message.
3.6.2. Un premier exemple
Le programme [bases-20.cs] montre une exception générée par l'utilisation d'un élément de tableau inexistant :
Résultats :
Commentaires :
- ligne 16 : cette instruction génère une exception parce que le tableau [tab] n'a pas d'élément n° 100 ;
- ligne 20 : l'exception est de type [System.IndexOutOfRangeException] (ligne 10 des résultats) ;
- ligne 21 : on quitte le programme ;
- lignes 23-25 : la clause [finally] a été exécutée (ligne 11 des résultats), alors même que la clause [catch] contenait une instruction [return]. On retiendra que la clause [finally] est toujours exécutée ;
- ligne 27 : à cause du [return], cette ligne n'est pas exécutée.
3.6.3. Une saisie contrôlée
Voici un autre exemple, [bases-21.cs], où l'on gère l'exception provoquée par la conversion en entier d'une chaîne qui ne représente pas un nombre entier :
Résultats :
Commentaires :
- ligne 4 : si le flux d'entrée est fermé, [ReadLine] rend [null] : on le remplace par une chaîne vide. La variable [nom] peut ainsi être de type [string] ;
- lignes 8-23 : la boucle de saisie de l'âge ;
- ligne 15 : la ligne tapée au clavier est transformée en nombre entier par la méthode [int.Parse]. Cette méthode lance une exception si la conversion n'est pas possible. C'est pourquoi l'opération a été placée dans un [try / catch] ;
- ligne 16 : si on arrive à cette ligne, c'est que la conversion a réussi. On vérifie cependant que l'entier obtenu est bien supérieur ou égal à 1 ;
- ligne 18 : si une exception [FormatException] est lancée, on va dans le [catch] où rien n'est fait. Ainsi, le booléen [ageOK], initialisé à [false], reste à [false]. Le cours de 2008 écrivait [catch { }], qui intercepte toutes les exceptions : c'est déconseillé, car on masque alors aussi les erreurs que l'on n'a pas prévues ;
- lignes 21-23 : un message d'erreur est émis si l'âge est incorrect.
Ce programme a une faiblesse : un âge comme 99999999999 provoque une exception [OverflowException], non interceptée, qui arrête le programme. On pourrait ajouter une clause [catch] ; il est plus simple d'utiliser [int.TryParse], comme le fera l'application exemple ci-dessous. On réservera les exceptions aux situations réellement exceptionnelles.
3.6.4. Plusieurs clauses catch, filtres et lancement d'exceptions
Le programme peut lui-même lancer une exception avec l'instruction [throw] :
L'exécution de la méthode en cours s'arrête et l'exception remonte vers les méthodes appelantes jusqu'à ce qu'une clause [catch] la gère ; si aucune ne le fait, le programme s'arrête en affichant l'exception. Dans une clause [catch], l'instruction [throw;] (sans paramètre) relance l'exception en cours, par exemple après l'avoir journalisée.
Depuis C# 6, une clause [catch] peut être suivie d'un filtre [when (condition)] : la clause n'est retenue que si la condition est vraie. Depuis C# 7, [throw] peut aussi être utilisé comme une expression, à droite de l'opérateur ?? ou dans un opérateur ternaire.
Le programme [bases-22.cs] illustre ces possibilités :
Résultats :
Commentaires :
- ligne 2 : des saisies simulées, valides ou non ;
- ligne 7 : la fonction locale [LireAge] peut lancer plusieurs types d'exceptions ;
- ligne 11 : cette clause ne gère une [FormatException] que si la saisie est vide ;
- ligne 15 : les autres [FormatException] ;
- ligne 19 : un nombre trop grand pour un [int] ;
- ligne 23 : l'exception lancée par [LireAge] elle-même ;
- ligne 35 : une expression [throw] : si [nom] vaut [null], une exception [ArgumentNullException] est lancée. L'opérateur [nameof] (C# 6) donne le nom d'une variable sous forme de chaîne ; [ArgumentNullException] l'ajoute à son message (ligne 9 des résultats) ;
- ligne 52 : la fonction [LireAge] lance une exception si l'âge est hors limites.
3.7. Application exemple - V1
On se propose d'écrire un programme permettant de calculer l'impôt d'un contribuable. On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer. Les chiffres sont ceux du barème 2004 sur les revenus 2003, conservés tels quels : c'est un exercice de programmation, pas un calcul de l'impôt actuel. Cette application nous servira de fil rouge : nous la reprendrons dans les chapitres suivants avec des classes, des fichiers, une base de données, puis sur le réseau.
- on calcule le nombre de parts du salarié : nbParts = nbEnfants/2 + 1 s'il n'est pas marié, nbEnfants/2 + 2 s'il est marié, où nbEnfants est son nombre d'enfants ;
- s'il a au moins trois enfants, il a une demi-part de plus ;
- on calcule son revenu imposable R = 0,72 * S, où S est son salaire annuel ;
- on calcule son quotient familial QF = R / nbParts ;
- on calcule son impôt I à l'aide du tableau suivant :
limite | coeffR | coeffN |
4962 | 0 | 0 |
8382 | 0,068 | 291,09 |
14753 | 0,191 | 1322,92 |
23888 | 0,283 | 2668,39 |
38868 | 0,374 | 4846,98 |
47932 | 0,426 | 6883,66 |
0 | 0,481 | 9505,54 |
Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne telle que QF <= limite. Par exemple, si QF = 5000, on trouvera la ligne :
L'impôt I est alors égal à 0,068 * R - 291,09 * nbParts, tronqué à l'euro. Si QF est tel que la relation QF <= limite n'est jamais vérifiée, ce sont les coefficients de la dernière ligne qui sont utilisés : I = 0,481 * R - 9505,54 * nbParts. La limite 0 de la dernière ligne n'est donc jamais utilisée.
Le programme C# correspondant [impots-v1.cs] est le suivant :
Commentaires :
- lignes 5-7 : les valeurs numériques sont suffixées par M pour être de type [decimal]. Le type [decimal] code exactement les nombres décimaux comme 0,068 ; c'est le type des calculs financiers. Avec un [double], 0,068 ne serait qu'une approximation binaire ;
- lignes 10-21 : la saisie du statut marital, dans une boucle [while (true)] dont on sort par [break] lorsque la réponse est correcte ;
- ligne 14 : [Console.ReadLine()] rend la chaîne C1 tapée au clavier, ou [null] (remplacé par "") ; [C1.Trim()] enlève les espaces de début et de fin de C1 et rend une chaîne C2 ; [C2.ToLower()] rend la chaîne C2 transformée en minuscules. On accepte ainsi O et o ;
- ligne 15 : un motif combiné avec [or] ;
- ligne 19 : les messages d'erreur sont écrits sur le flux [Error] ;
- ligne 21 : le booléen [marie] reçoit la valeur [true] ou [false] de la relation [reponse == "o"] ;
- ligne 28 : [TryParse] rend [false] si la saisie n'est pas un entier ; la condition [nbEnfants >= 0] n'est alors pas évaluée (opérateur && court-circuité). [TryParse] accepte une chaîne [null] : il rend alors [false]. Contrairement à la version de 2008, aucune exception n'est utilisée ;
- ligne 40 : même chose pour le salaire ;
- ligne 48 : on ne peut pas écrire simplement nbEnfants / 2 : si nbEnfants vaut 3, on aurait 3/2, une division entière qui donnerait 1 et non 1,5. Le diviseur 2M étant de type [decimal], la division est faite en [decimal]. Le cours de 2008 écrivait [(decimal)nbEnfants / 2], ce qui revient au même ;
- lignes 62-65 : la recherche de la tranche. On s'arrête à la première limite supérieure ou égale au quotient familial, et au plus tard sur la dernière tranche. La version de 2008 écrivait d'abord QF dans la dernière case du tableau des limites pour être sûre de s'arrêter : cela modifiait les données du barème ; la condition [i < limites.Length - 1] évite cet effet de bord ;
- ligne 69 : l'impôt est tronqué à l'euro par la conversion en [int].
Voici des exemples d'exécution :
On retrouve les résultats du cours de 2008 : 4282 euros pour un contribuable marié avec 2 enfants et 2959 euros avec 3 enfants, pour un salaire annuel de 60000 euros. Un célibataire sans enfant ayant le même salaire paie 11519 euros. Dans la deuxième exécution, la saisie « 60000 euros » est refusée : [int.TryParse] n'accepte que les chaînes représentant exactement un entier (des espaces au début et à la fin sont tolérés).
3.8. Arguments du programme principal
La fonction principale [Main] peut admettre comme paramètre un tableau de chaînes [string[]]. Ce tableau contient les arguments de la ligne de commande utilisée pour lancer l'application. Ainsi, si on lance le programme P avec la commande :
et si la fonction [Main] est déclarée comme suit :
on aura args[0]="arg0", args[1]="arg1", etc. Avec les instructions de haut niveau, il n'y a pas de méthode [Main] écrite par le programmeur, mais le tableau [args] est disponible de la même façon : c'est le paramètre de la méthode [Main] générée par le compilateur.
Avec [dotnet run], les arguments destinés au programme se placent après deux tirets [--], qui les séparent des options de la commande [dotnet run] elle-même :
Le cours de 2008 indiquait les arguments dans les propriétés du projet Visual Studio ; la ligne de commande suffit aujourd'hui. Voici un exemple, [bases-23.cs] :
Résultats :
Un argument qui contient des espaces doit être entouré de guillemets :
Le même programme écrit avec une méthode [Main] classique [bases-24.cs] :
Résultats :
Commentaires :
- ligne 2 : un espace de noms de portée fichier (C# 10) ;
- ligne 4 : une classe statique ne contient que des membres statiques ;
- ligne 6 : le point d'entrée du programme. Comme ce fichier ne contient pas d'instructions de haut niveau, le compilateur cherche une méthode statique nommée [Main] ;
- ligne 8 : le reste est identique à [bases-23.cs].
La signature [static void Main()] est valide si la fonction [Main] n'attend pas de paramètres. [Main] peut aussi rendre un [int], qui est alors le code de retour du programme : [static int Main(string[] args)].
3.9. Les énumérations
Une énumération est un type de données dont le domaine de valeurs est un ensemble de constantes entières nommées. Considérons un programme qui a à gérer des mentions à un examen. Il y en aurait cinq : Passable, AssezBien, Bien, TresBien, Excellent. On peut alors définir une énumération pour ces cinq constantes :
De façon interne, ces cinq constantes sont codées par des entiers consécutifs commençant par 0 pour la première constante, 1 pour la suivante, etc. On peut aussi fixer ces valeurs : [enum Mentions { Passable = 10, AssezBien = 12, ... }]. Une variable peut être déclarée comme prenant ses valeurs dans l'énumération :
On peut comparer une variable aux différentes valeurs possibles de l'énumération :
De la même façon que le type simple [int] est équivalent à la structure [System.Int32], toute énumération dérive de la classe [System.Enum]. Celle-ci a des méthodes statiques qui permettent d'obtenir les valeurs et les noms d'un type énuméré :
- [Enum.GetValues<Mentions>()] (.NET 5) rend le tableau des valeurs de l'énumération [Mentions] ;
- [Enum.GetNames<Mentions>()] rend le tableau de leurs noms ;
- [Enum.TryParse(chaine, out Mentions m)] convertit une chaîne en valeur de l'énumération.
Le cours de 2008 écrivait [Enum.GetValues(typeof(Mentions))] : cette forme, toujours valide, rend un tableau non typé qu'il faut ensuite convertir. La forme générique [GetValues<Mentions>()] rend directement un tableau [Mentions[]].
Le programme [bases-25.cs] met en lumière ce qui vient d'être écrit :
Résultats :
Commentaires :
- ligne 3 : une variable de type [Mentions] ;
- ligne 5 : une valeur d'énumération est affichée par son nom ;
- lignes 12-14 : la liste des valeurs. La conversion [(int)m] donne la valeur entière de la constante ;
- ligne 17 : la liste des noms ;
- ligne 19 : la conversion inverse, d'un entier vers l'énumération ;
- ligne 22 : la conversion d'une chaîne en valeur de l'énumération. Les valeurs d'une énumération se comparent comme les entiers qui les codent ;
- lignes 27-31 : les énumérations se prêtent bien aux expressions switch ;
- ligne 34 : le type entier sous-jacent est [int] par défaut ;
- ligne 37 : la déclaration de l'énumération. Dans un fichier à instructions de haut niveau, les déclarations de types (énumérations, classes, ...) se placent après les instructions.
Une énumération dont les valeurs sont des puissances de 2 (1, 2, 4, 8, ...) peut recevoir l'attribut [Flags] : ses valeurs se combinent alors avec l'opérateur | (OU bit à bit) pour représenter un ensemble d'options, par exemple les droits d'accès à un fichier (lecture | écriture).
3.10. Passage de paramètres à une fonction
Nous nous intéressons ici au mode de passage des paramètres d'une fonction. Considérons la fonction suivante :
Dans la définition de la fonction, ligne 1, [a] est appelé un paramètre formel. Il n'est là que pour les besoins de la définition de la fonction [ChangeInt]. Il aurait tout aussi bien pu s'appeler [b]. Considérons maintenant une utilisation de cette fonction :
Ici, dans l'instruction de la ligne 2, [age] est le paramètre effectif qui va transmettre sa valeur au paramètre formel [a]. Nous nous intéressons à la façon dont un paramètre formel récupère la valeur d'un paramètre effectif.
3.10.1. Les modes de passage
C# propose les modes de passage suivants :
- par valeur (le mode par défaut) : la valeur du paramètre effectif est recopiée dans le paramètre formel correspondant. On a deux entités distinctes : si la fonction modifie le paramètre formel, le paramètre effectif n'est en rien modifié. Ce mode convient aux paramètres d'entrée d'une fonction ;
- par référence avec [ref] : le paramètre effectif et le paramètre formel sont une seule et même entité. Si la fonction modifie le paramètre formel, le paramètre effectif est lui aussi modifié. Le mot clé [ref] doit figurer à la fois dans la définition de la fonction et dans l'appel. Le paramètre effectif doit avoir une valeur avant l'appel. Ce mode convient aux paramètres d'entrée-sortie ;
- par référence avec [out] : comme [ref], mais le paramètre est uniquement un paramètre de sortie : le paramètre effectif n'a pas besoin de valeur initiale, et la fonction doit affecter une valeur au paramètre formel. C'est le mode utilisé par les méthodes [TryParse]. Depuis C# 7, la variable peut être déclarée dans l'appel : [out var x] ou [out int x] ;
- par référence en lecture seule avec [in] (C# 7.2) : le paramètre est passé par référence, ce qui évite la copie, mais la fonction ne peut pas le modifier. Ce mode n'a d'intérêt que pour des structures volumineuses ; pour les types prédéfinis, le passage par valeur est aussi efficace.
Le programme [bases-26.cs] illustre ces différents modes :
Résultats :
Commentaires :
- lignes 3-5 : la valeur 20 du paramètre effectif [age] a été recopiée dans le paramètre formel [a] (ligne 35). Celui-ci a ensuite été modifié (ligne 37). Le paramètre effectif est resté inchangé ;
- lignes 8-10 : avec [ref], le paramètre effectif [age2] a suivi la modification du paramètre formel [a2] ;
- lignes 13-15 : la variable [age3] n'est pas initialisée avant l'appel : c'est possible avec [out] ;
- ligne 17 : la variable [age4] est déclarée dans l'appel ;
- lignes 21-22 : l'échange de deux valeurs nécessite le passage par référence : avec un passage par valeur, la fonction échangerait des copies ;
- ligne 27 : un passage en lecture seule. Le mot clé [in] est facultatif dans l'appel. Le résultat 120,0 a une décimale parce que le produit de deux [decimal] conserve les décimales de ses opérandes : 100 × 1,2 = 120,0 ;
- lignes 30-32 : un tableau est un type référence : ce qui est passé par valeur, c'est la référence du tableau. La fonction [ModifierTableau] travaille donc sur le même tableau que l'appelant : la modification de [t[0]] (ligne 69) est visible de l'appelant. En revanche, faire pointer [t] sur un autre tableau (ligne 71) ne modifie que la copie locale de la référence.
Si l'on remplace la ligne 8 par [int age2;], le compilateur refuse le programme :
On peut contourner l'obstacle en affectant une valeur initiale à [age2], ou en remplaçant le mot clé [ref] par le mot clé [out] : on exprime alors que le paramètre est uniquement un paramètre de sortie. De même, si la fonction [Ttc] essaie de modifier son paramètre [in] :
3.10.2. Paramètres optionnels, arguments nommés, nombre variable de paramètres
C# 4 a ajouté les paramètres optionnels : un paramètre peut recevoir une valeur par défaut dans la définition de la fonction ; l'appelant peut alors l'omettre. Les paramètres optionnels se placent après les paramètres obligatoires. Il a ajouté aussi les arguments nommés : à l'appel, on peut faire précéder un paramètre effectif du nom du paramètre formel suivi de deux-points. On peut alors omettre un paramètre optionnel qui en précède d'autres, ou donner les paramètres dans un ordre quelconque.
Le mot clé [params] permet d'écrire une fonction à nombre variable de paramètres : le dernier paramètre formel est un tableau, et l'appelant peut passer soit un tableau, soit une liste de valeurs (éventuellement vide), que le compilateur range dans un tableau. Depuis C# 13, le paramètre [params] peut aussi être d'un autre type de collection, comme [List<T>] ou [ReadOnlySpan<T>].
Le programme [bases-27.cs] est le suivant :
Résultats :
Commentaires :
- ligne 17 : la fonction [Saluer] a un paramètre obligatoire [nom] et deux paramètres optionnels [formule] et [majuscules] ;
- ligne 3 : les deux paramètres optionnels prennent leur valeur par défaut ;
- ligne 4 : seul [majuscules] prend sa valeur par défaut ;
- ligne 6 : grâce à l'argument nommé, on passe [majuscules] sans passer [formule] ;
- ligne 7 : avec des arguments nommés, l'ordre est libre ;
- ligne 23 : [Somme] accepte un nombre quelconque d'entiers ;
- lignes 10-13 : on appelle [Somme] sans paramètre, avec trois entiers, puis avec un tableau ;
- ligne 15 : de nombreuses méthodes .NET, comme [string.Join] ou [Console.WriteLine] avec un format, ont un paramètre [params].
3.11. Les fonctions locales et les tuples
3.11.1. Les fonctions locales
Une fonction locale (C# 7) est une fonction définie à l'intérieur d'une méthode, ou, comme dans nos exemples, parmi les instructions de haut niveau. Elle n'est utilisable qu'à cet endroit. Puisque nos programmes n'ont plus de classe, les fonctions locales y remplacent les méthodes statiques des exemples de 2008. Une fonction locale :
- peut être appelée avant sa définition ;
- peut utiliser les variables locales qui l'entourent, sauf si elle est déclarée [static] : le mot clé [static] (C# 8) garantit qu'elle ne dépend que de ses paramètres, ce qui la rend plus facile à comprendre. Nous déclarons nos fonctions locales [static] chaque fois que c'est possible ;
- peut avoir un corps d'expression : [static int Carre(int x) => x * x;] est équivalent à un corps [{ return x * x; }].
Dans un programme plus important, on écrit des classes et des méthodes : c'est l'objet du chapitre suivant.
3.11.2. Les tuples
Un tuple (C# 7) regroupe plusieurs valeurs, éventuellement de types différents, en une seule. Le type [(int, string)] désigne un couple formé d'un entier et d'une chaîne ; ses éléments s'appellent [Item1] et [Item2]. On peut les nommer : [(int Numero, string Nom)]. Un tuple peut être déconstruit : [var (numero, nom) = t;] range ses éléments dans deux variables.
Les tuples permettent à une fonction de rendre plusieurs valeurs sans utiliser de paramètres [out] : c'est souvent plus lisible.
Le programme [bases-28.cs] est le suivant :
Résultats :
Commentaires :
- ligne 3 : une fonction locale statique à corps d'expression ;
- lignes 7-8 : une fonction locale non statique : elle modifie la variable [compteur] du programme ;
- ligne 14 : un tuple dont les éléments ne sont pas nommés. L'affichage d'un tuple donne ses éléments entre parenthèses ;
- lignes 17-18 : deux tuples aux éléments nommés : le premier par son type, le second par les noms donnés dans l'expression ;
- ligne 21 : la déconstruction du tuple [t3] ;
- ligne 25 : la fonction [Diviser] rend le quotient et le reste de la division entière ; on déconstruit directement le résultat ;
- ligne 27 : on garde ici le tuple et l'on utilise les noms de ses éléments [Min] et [Max] ;
- ligne 30 : depuis .NET 6, [Math.DivRem] rend lui aussi un tuple (quotient, reste) ;
- ligne 35 : l'échange de deux variables s'écrit en une ligne avec un tuple, sans variable temporaire ;
- ligne 38 : le type rendu par la fonction est un tuple nommé ;
- lignes 40-48 : une fonction qui calcule en une passe le minimum et le maximum d'un tableau.