Apprenez A Developper en C
Apprenez A Developper en C
Apprenez A Developper en C
Éric Mittelette
Responsable des relations techniques avec les développeurs chez Microsoft France
1. Most Valuable Professional, expert en technologies Microsoft.
i
ii
Avant-propos
iii
CHAPITRE 0. AVANT-PROPOS
On parle du C# ?
J’y viens ! C’est dans cette optique que j’ai commencé à écrire. Pouvoir partager mes
connaissances souvent durement acquises et aider ceux qui ont du mal à se lancer. Et
c’est vrai que ce n’est pas facile, malgré toute la bonne volonté du monde. Sans une
méthodologie simple et des explications claires, il n’est pas aisé de se lancer sans se sentir
perdu. C’est là où j’espère pouvoir faire quelque chose à travers la collection des Livres
du Zéro. Après tous mes essais de jeunesse, mes études et mon entrée dans le monde du
travail, j’ai acquis une certaine expérience des différents langages de programmation.
J’ai pris goût à l’écriture en commençant à rédiger des articles avec mon langage préféré
de l’époque, le C++. Aujourd’hui, c’est le C# qui occupe cette place prestigieuse dans
mon classement ultra-personnel des langages de programmation ! C’est donc l’occasion
de pouvoir mettre à profit cette volonté de partage de connaissances et ce goût pour
la rédaction, dans un ouvrage permettant d’apprendre le C# et qui est destiné aux
débutants.
iv
COMMENT LIRE CE LIVRE ?
À la fin de cet ouvrage, vous aurez acquis toutes les bases vous permettant de vous lan-
cer sans appréhension dans le monde du développement d’applications professionnelles
avec le C#. Vous découvrirez en bonus un aperçu des différentes applications que l’on
peut réaliser avec le C#.
Esprit du livre
Oui, oui, vous avez bien lu, ce livre est pour les débutants. Pas besoin d’avoir fait du
développement auparavant pour pouvoir lire cet ouvrage ! Je vais donc faire de mon
mieux pour détailler au maximum mes explications, c’est promis. Bien sûr, il y en a
peut-être parmi vous qui ont déjà fait du C, du C++, du Java. . . Évidemment, si vous
avez déjà fait du développement informatique, ce sera plus facile pour vous. Attention
néanmoins de ne pas aller trop vite : le C# ressemble à d’autres langages mais il a
quand même ses spécificités ! Nous allons découvrir ensemble de nombreuses choses
en apprenant à développer en C#. Il y aura bien entendu des TP pour vous faire
pratiquer, afin que vous puissiez vous rendre compte de ce que vous êtes capables de
faire après avoir lu plusieurs chapitres plus théoriques. Néanmoins, je veux que vous
soyez actifs ! Ne vous contentez pas de lire passivement mes explications, même lorsque
les chapitres sont plutôt théoriques ! Testez les codes et les manipulations au fur et à
mesure. Essayez les petites idées que vous avez pour améliorer ou adapter légèrement
le code. Sortez un peu des sentiers battus du tutoriel : cela vous fera pratiquer et
vous permettra de découvrir rapidement si vous avez compris ou non le chapitre. Pas
d’inquiétude, si jamais vous bloquez sur quoi que ce soit qui n’est pas expliqué dans ce
cours, la communauté qui sillonne les forums du Site du Zéro saura vous apporter son
aide précieuse.
Afin de tirer parti du Site du Zéro dont ce livre est issu, celui-ci vous propose ce qu’on
appelle des « codes web ». Ce sont des codes à six chiffres à saisir sur une page du Site
du Zéro pour être automatiquement redirigé vers un site web sans avoir à en recopier
l’adresse.
v
CHAPITRE 0. AVANT-PROPOS
Remerciements
Je souhaite remercier un certain nombre de personnes qui, de près ou de loin, ont
contribué à la naissance de cet ouvrage :
– ma femme Delphine qui me soutient au quotidien et m’offre chaque jour une raison
d’avancer dans la vie à ses côtés ;
2. Vous pouvez aussi utiliser le formulaire de recherche du Site du Zéro, section « Code web ».
vi
REMERCIEMENTS
vii
CHAPITRE 0. AVANT-PROPOS
viii
Sommaire
Avant-propos iii
On parle du C# ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv
Qu’allez-vous apprendre en lisant ce livre ? . . . . . . . . . . . . . . . . . . . . iv
Comment lire ce livre ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v
Ce livre est issu du Site du Zéro . . . . . . . . . . . . . . . . . . . . . . . . . vi
Remerciements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi
1 Introduction au C# 3
Qu’est-ce que le C# ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Comment sont créées les applications informatiques ? . . . . . . . . . . . . . . 4
Exécutables ou assemblages ? . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Qu’est-ce que le framework .NET ? . . . . . . . . . . . . . . . . . . . . . . . . 8
ix
SOMMAIRE
3 La syntaxe générale du C# 27
Écrire une ligne de code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Le caractère de terminaison de ligne . . . . . . . . . . . . . . . . . . . . . . . 29
Les commentaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
La complétion automatique . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4 Les variables 35
Qu’est-ce qu’une variable ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Les différents types de variables . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Affectations, opérations, concaténation . . . . . . . . . . . . . . . . . . . . . . 38
Les caractères spéciaux dans les chaînes de caractères . . . . . . . . . . . . . 41
7 Les méthodes 57
Créer une méthode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
La méthode spéciale Main() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Paramètres d’une méthode . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Retour d’une méthode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
x
SOMMAIRE
11 Les boucles 91
La boucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
La boucle foreach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
La boucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Les instructions break et continue . . . . . . . . . . . . . . . . . . . . . . . . 100
xi
SOMMAIRE
xii
SOMMAIRE
22 La POO et le C# 191
Des types, des objets, type valeur et type référence . . . . . . . . . . . . . . . 192
Héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Substitution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
La conversion entre les objets avec le casting . . . . . . . . . . . . . . . . . . 214
xiii
SOMMAIRE
xiv
SOMMAIRE
IV C# avancé 359
xv
SOMMAIRE
xvi
Première partie
1
Chapitre 1
Introduction au C#
Difficulté :
D
ans ce tout premier chapitre, nous allons découvrir ce qu’est le C#, son histoire
et son rapport avec le framework .NET. D’ailleurs, vous ne savez pas ce qu’est un
framework ? Ce n’est pas grave, tout ceci sera expliqué !
Nous verrons dans ce chapitre ce que sont les applications informatiques et comment des
langages de programmation évolués comme le C# nous permettent de réaliser de telles
applications.
Et ce n’est que le début. . . alors ouvrez grands vos yeux, chaussez vos lunettes et explorons
ce monde merveilleux !
3
CHAPITRE 1. INTRODUCTION AU C#
Qu’est-ce que le C# ?
Le C# est un langage de programmation créé par Microsoft en 2002.
Ces applications informatiques peuvent être de beaucoup de sortes, par exemple une
application Windows, comme un logiciel de traitement de texte, une calculatrice ou
encore un jeu de cartes. On les appelle également des clients lourds. Il est égale-
ment possible de développer des applications web, comme un site d’e-commerce, un
intranet, etc. Nous pourrons accéder à ces applications grâce à un navigateur inter-
net que l’on appelle un client léger. Toujours grâce à un navigateur internet, nous
pourrons développer des clients riches. Ce sont des applications qui se rapprochent
d’une application Windows mais qui fonctionnent dans un navigateur. Bien d’autres
types d’applications peuvent être écrites avec le C#, citons encore le développement
d’applications mobiles sous Windows phone 7, de jeux ou encore de web services. . .
Nous verrons un peu plus en détail à la fin de cet ouvrage comment réaliser de telles
applications !
Le C# est un langage dont la syntaxe ressemble un peu au C++ ou au Java qui sont
d’autres langages de programmation très populaires. Le C# est le langage phare de
Microsoft. Il fait partie d’un ensemble plus important : il est en fait une brique de ce
qu’on appelle le « Framework .NET ». Gardons encore un peu de suspens sur ce
qu’est le framework .NET, nous découvrirons ce que c’est un peu plus loin dans ce
livre.
4
COMMENT SONT CRÉÉES LES APPLICATIONS INFORMATIQUES ?
Avec des langages traditionnels comme le C et le C++, on écrit des instructions sim-
plifiées, lisibles par un humain comme :
1 printf ( " Bonjour " ) ;
Ce n’est pas vraiment du français, mais c’est quand même beaucoup plus simple que
le binaire et on comprend globalement avec cet exemple que l’on va afficher le mot
« Bonjour ».
Bien entendu, l’ordinateur ne comprend pas ces instructions. Lui, il veut du binaire, du
vrai ! Pour obtenir du binaire à partir d’un code écrit en C ou C++, on doit effectuer
ce qu’on appelle une compilation. Le compilateur est un programme qui traduit le
code source en binaire exécutable. Ce n’est pas clair ? Observez donc la figure 1.2 !
Cette méthode est efficace et a fait ses preuves. De nombreuses personnes développent
toujours en C et C++ aujourd’hui. Néanmoins, ces langages ont aussi un certain nombre
de défauts dus à leur ancienneté. Par exemple, un programme compilé (binaire) ne
fonctionne que sur la plateforme pour laquelle il a été compilé. Cela veut dire que si
5
CHAPITRE 1. INTRODUCTION AU C#
vous compilez sous Windows, vous obtenez un programme qui fonctionne sous Windows
uniquement (et sur un type de processeur particulier). Impossible de le faire tourner
sous Mac OS X ou Linux, à moins de le recompiler sous ces systèmes d’exploitation et
d’effectuer au passage quelques modifications (voir figure 1.3).
Les programmes binaires ont ce défaut : ils ne fonctionnent que pour un type de ma-
chine. Pour les développeurs qui écrivent le code, c’est assez fastidieux à gérer.
6
COMMENT SONT CRÉÉES LES APPLICATIONS INFORMATIQUES ?
Lorsqu’on exécute le programme CIL, celui-ci est lu par un autre programme (une
machine à analyser les programmes, appelée CLR) qui le compile cette fois en vrai
programme binaire. Cette fois, le programme peut s’exécuter, ouf !
Le CLR vérifie aussi la sécurité du code ; ainsi en C du code mal pensé (par
exemple une mauvaise utilisation des pointeurs) peut entraîner des problèmes
pour votre PC, ce que vous risquez beaucoup moins avec le C#. De plus, le
CLR dispose du JIT debugger qui permet de lancer Visual Studio si une erreur
survient dans un programme .NET pour voir ce qui a causé cette erreur. On
parle de code « managé ».
Cette complexité ralentit légèrement la vitesse d’exécution des programmes (par rap-
port au C ou au C++), mais la différence est aujourd’hui vraiment négligeable par
rapport aux gains que cela apporte.
Donc, en théorie, il est possible d’utiliser n’importe quelle application compilée en
langage intermédiaire à partir du moment où il y a une implémentation du CLR dispo-
nible. En réalité, il n’y a que sous Windows qu’il existe une implémentation complète
du CLR. Il existe cependant une implémentation partielle sous Linux : Mono. Cela
veut dire que si votre programme utilise des fonctionnalités qui ne sont pas couvertes
par Mono, il ne fonctionnera pas.
En conclusion, dans la pratique, le .NET est totalement exploitable sous Windows et
ailleurs, non.
7
CHAPITRE 1. INTRODUCTION AU C#
Exécutables ou assemblages ?
J’ai dit juste au dessus que le C# était compilé en langage intermédiaire et qu’on le
retrouve sous la forme d’un .exe comme les programmes habituels.
Il est à noter qu’un raccourci est souvent fait avec le terme assembly. On a
tendance à croire que le mot assembly sert à désigner uniquement les biblio-
thèques dont l’extension est .dll.
8
QU’EST-CE QUE LE FRAMEWORK .NET ?
Pour simplifier, on peut dire qu’un framework est une espèce de grosse boîte à fonc-
tionnalités qui va nous permettre de réaliser des applications informatiques de toutes
sortes.
Le framework .NET a été créé par Microsoft en 2002, en même temps que le C#, qui est
principalement dédié à la réalisation d’applications fonctionnant dans des environne-
ments Microsoft. Nous pourrons par exemple réaliser des programmes qui fonctionnent
sous Windows, ou bien des sites web ou encore des applications qui fonctionnent sur
téléphone mobile, etc.
Disons que la réalisation d’une application informatique, c’est un peu comme un chan-
tier (je ne dis pas ça parce qu’il y a toujours du retard, même si c’est vrai !). Il est
possible de construire différentes choses, comme une maison, une piscine, une terrasse,
etc. Pour réaliser ces constructions, nous allons avoir besoin de matériaux, comme des
briques, de la ferraille, etc. Certains matériaux sont communs à toutes les construc-
tions (fer, vis,. . .) et d’autres sont spécifiques à certains domaines (pour construire une
piscine, je vais avoir besoin d’un liner par exemple).
On peut voir le framework .NET comme ces matériaux ; c’est un ensemble de compo-
sants que l’on devra assembler pour réaliser notre application. Certains sont spécifiques
pour la réalisation d’applications web, d’autres pour la réalisation d’applications Win-
dows, etc.
Pour réaliser un chantier, nous allons avoir besoin d’outils afin de manipuler les ma-
tériaux. Qui envisagerait de tourner une vis avec les doigts ou de poser des parpaings
sans les coller avec du mortier ? C’est la même chose pour une application informatique,
pour assembler notre application, nous allons utiliser un langage de programmation :
le C#.
À l’heure où j’écris ces lignes, le C# est en version 4 ainsi que le framework .NET.
Ce sont des versions stables et utilisées par beaucoup de personnes. Chaque version
intermédiaire a apporté son lot d’évolutions. Le framework .NET et le C# sont en
perpétuelle évolution, preuve de la dynamique apportée par Microsoft.
C’est tout ce qu’il y a à savoir pour l’instant ! Nous reviendrons un peu plus en détail
sur le framework .NET dans les chapitres suivants. Pour l’heure, il est important de
retenir que c’est grâce au langage de programmation C# et grâce aux composants du
framework .NET que nous allons pouvoir développer des applications informatiques.
En résumé
– Le C# est un langage de programmation permettant d’utiliser le framework .NET.
C’est le langage phare de Microsoft.
– Le framework .NET est une énorme boîte à fonctionnalités permettant la création
9
CHAPITRE 1. INTRODUCTION AU C#
d’applications.
– Le C# permet de développer des applications de toutes sortes, exécutables par le
CLR qui traduit le MSIL en binaire.
– Il est possible de créer des assemblys de deux sortes : des assemblys de processus
exécutables par le CLR et des assemblys de bibliothèques.
10
Chapitre 2
Créer un projet avec Visual C# 2010
Express
Difficulté :
D
ans ce chapitre nous allons faire nos premiers pas avec le C#. Nous allons dans un
premier temps installer et découvrir les outils qui nous seront nécessaires pour réaliser
des applications informatiques avec le C#. Nous verrons comment démarrer avec ces
outils et à la fin de ce chapitre, nous serons capables de créer un petit programme qui
affiche du texte simple et nous aurons commencé à nous familiariser avec l’environnement
de développement.
Il faut bien commencer par les bases, mais vous verrez comme cela peut être gratifiant
d’arriver enfin à faire un petit quelque chose. Allez, c’est parti !
11
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
12
INSTALLER VISUAL C# 2010 EXPRESS
soft propose gratuitement Visual Studio dans sa version Express. C’est une version
allégée de l’environnement de développement qui permet de faire plein de choses, mais
avec des outils en moins par rapport aux versions payantes. Rassurez-vous, ces ver-
sions gratuites sont très fournies et permettent de faire tout ce dont on a besoin pour
apprendre le C# et suivre ce cours.
Pour réaliser des applications d’envergure, il pourra cependant être judicieux d’in-
vestir dans l’outil complet et ainsi bénéficier de fonctionnalités complémentaires qui
permettent d’améliorer, de faciliter et d’industrialiser les développements.
Pour développer en C# gratuitement et créer des applications Windows, nous allons
avoir besoin de Microsoft Visual C# 2010 Express que vous pouvez télécharger
en suivant ce code web :
☛ ✟
Visual C# 2010 Express
⊲
✡Code web : 389622 ✠
Pour résumer :
– Visual Studio est la version payante de l’outil de développement.
– Microsoft Visual C# 2010 Express est une version allégée et gratuite de Visual
Studio, dédiée au développement en C#. Exactement ce qu’il nous faut !
Cliquez sur Visual C# 2010 Express et choisissez la langue qui vous convient. Puis
cliquez sur Téléchargez (voir les figures 2.1 et 2.2).
Une fois l’exécutable téléchargé, il ne reste plus qu’à le lancer et l’installation démarre.
Cliquez sur Suivant pour démarrer l’installation, ainsi qu’indiqué à la figure 2.3.
Vous devez ensuite lire la licence d’utilisation du logiciel et l’accepter pour pouvoir
continuer l’installation.
Une application sans données, c’est plutôt rare. C’est un peu comme un site d’e-
13
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
14
INSTALLER VISUAL C# 2010 EXPRESS
commerce sans produits, un traitement de texte sans fichiers ou le site du zéro sans
tutoriel. On risque de vite s’ennuyer !
Heureusement, le programme d’installation nous propose d’installer « Microsoft SQL
Server 2008 Express Service Pack 1 ». Microsoft propose en version gratuite un serveur
de base de données allégé. Il va nous permettre de créer facilement une base de données
et de l’utiliser depuis nos applications en C#.
Nous l’avons déjà évoqué et nous y reviendrons plus en détail dans un chapitre
ultérieur mais une base de données est un énorme endroit où sont stockées
les données de notre application.
Nous avons également évoqué dans l’introduction qu’il était possible de réaliser des
applications qui ressemblent à des applications Windows mais dans un navigateur, que
nous avons appelé client riche. Silverlight va justement permettre de créer ce genre
d’application.
Cochez donc tout pour installer Silverlight et Sql Server et cliquez sur Suivant (voir
figure 2.4).
15
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
☛ ✟
Service pack 1
⊲
Code web : 974305
✡ ✠
Vous voilà avec votre copie de Visual C# Express qui va vous permettre de créer des
programmes en C# gratuitement et facilement. L’installation de l’outil de développe-
ment est terminée.
Notez que, bien que gratuite, vous aurez besoin d’enregistrer votre copie de
Visual C# Express avant 30 jours. C’est une opération rapide et nécessitant
un compte Windows Live. Après cela, vous pourrez utiliser Visual C# Express
sans retenue.
16
CRÉER UN PROJET
Créer un projet
Commençons par créer un nouveau projet en cliquant dans la zone rouge. Cette com-
mande est également accessible via le menu Fichier > Nouveau > Projet.
Un projet va contenir les éléments de ce que l’on souhaite réaliser. Cela peut être par
exemple une application web, une application Windows, etc.
Le projet est aussi un container de fichiers et notamment dans notre cas de fichiers en
langage C# qui vont permettre de construire ce que l’on souhaite réaliser. Le projet
est en fait représenté par un fichier dont l’extension est .csproj. Son contenu décrit les
paramètres de configuration correspondant à ce que l’on souhaite réaliser et les fichiers
qui composent le projet.
Créons donc un nouveau projet. La fenêtre de création de nouveau projet s’ouvre et
nous avons plusieurs possibilités de choix. Nous allons dans un premier temps aller
dans Visual C# pour choisir de créer une Application console, comme indiqué à la
figure 2.6.
17
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
À noter que si vous n’avez installé que Visual C# Express, vous aurez la même
fenêtre que moi. Si vous disposez de la version payante de Visual Studio, alors
la fenêtre sera certainement plus garnie. De même, il y aura plus de choses
si vous avez installé d’autres outils de la gamme Express.
Ce que nous faisons ici, c’est utiliser ce qu’on appelle un « modèle »de création de
projet, plus couramment appelé par son équivalent anglais template.
Si vous naviguez à l’intérieur des différents modèles, vous pourrez constater que Visual
C# nous propose des modèles de projets plus ou moins compliqués. Ces modèles sont
très utiles pour démarrer un projet car toute la configuration du projet est déjà faite.
Le nombre de modèles peut être différent en fonction de votre version de Visual Studio
ou du nombre de versions Express installées.
L’application console est la forme de projet pouvant produire une application exé-
cutable la plus simple. Elle permet de réaliser un programme qui va s’exécuter dans la
console noire qui ressemble à une fenêtre ms-dos, pour les dinosaures comme moi qui
ont connu cette époque. . . À noter que les projets de type « Bibliothèque de classes »
permettent de générer des assemblys de bibliothèques (.dll).
Dans cette console, nous allons notamment pouvoir afficher du texte simple. Ce type
de projet est parfait pour démarrer l’apprentissage du C# car il y a seulement besoin
de savoir comment afficher du texte pour commencer.
En bas de la fenêtre de création de projet, nous avons la possibilité de choisir un nom
pour le projet, ici ConsoleApplication1. Changeons le nom de notre application en,
par exemple, MaPremiereApplication, dans la zone correspondante.
Cliquons sur OK pour valider la création de notre projet.
Visual C# Express crée alors pour nous les fichiers composant une application console
vide, qui utilise le C# comme langage et que nous avons nommée « MaPremiereAppli-
cation ».
Avant toute chose, nous allons enregistrer le projet.
✄ Allons✄ dans le menu Fichier >
Enregistrer (ou utiliser le raccourci bien connu ✂Ctrl ✁+ ✂S ✁).
Visual C# Express nous ouvre la fenêtre d’enregistrement de projet (voir figure 2.7).
Nous pouvons donner un nom, préciser un emplacement où nous souhaitons que les
18
ANALYSE DE L’ENVIRONNEMENT DE DÉVELOPPEMENT ET DU CODE
GÉNÉRÉ
fichiers soient enregistrés et un nom de solution. Une case pré-cochée nous propose de
créer un répertoire pour la solution. C’est ce que nous allons faire ! Cliquons maintenant
sur Enregistrer.
19
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
20
ÉCRIRE DU TEXTE DANS NOTRE APPLICATION
de manière à avoir :
1 static void Main ( string [] args )
2 {
3 Console . WriteLine ( " Hello World !! " ) ;
4 }
Nous venons d’écrire une instruction qui va afficher la phrase « Hello World ! ! ». Pour
l’instant vous avez juste besoin de savoir ça. Nous étudierons ultérieurement à quoi
cela correspond exactement.
L’exécution du projet
Ça y est ! Nous avons écrit notre premier code qui affiche un message très populaire.
Mais pour le moment, ça ne fait rien. . . On veut voir ce que ça donne !
Comme je vous comprends !
La première chose à faire est de transformer le langage C# que nous venons d’écrire
en programme exécutable. Cette phase s’appelle la « génération de la solution » sous
Visual C#. On l’appelle souvent la « compilation » ou en anglais le build .
Allez dans le menu Déboguer et cliquez sur Générer la solution (voir figure 2.9).
Visual C# lance alors la génération de la solution et on voit dans la barre des tâches
en bas à gauche qu’il travaille jusqu’à nous indiquer que la génération a réussi, ainsi
que vous pouvez le voir sur la figure 2.10.
Si nous allons dans le répertoire contenant la solution, nous pouvons voir dans le
répertoire MaPremiereApplication\MaPremiereApplication\bin\Release qu’il y a
deux fichiers :
– MaPremiereApplication.exe
– MaPremiereApplication.pdb
Le premier est le fichier exécutable, possédant l’extension .exe, qui est le résultat du
processus de génération. Il s’agit bien de notre application.
Le second est un fichier particulier qu’il n’est pas utile de connaître pour l’instant, nous
allons l’ignorer !
21
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
22
L’EXÉCUTION DU PROJET
23
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
24
L’EXÉCUTION DU PROJET
25
CHAPITRE 2. CRÉER UN PROJET AVEC VISUAL C# 2010 EXPRESS
Dans la suite du livre, nous utiliserons la syntaxe suivante pour remplacer l’image de
console du dessus :
Hello World !!
En résumé
– Visual C# Express est l’outil de développement gratuit de Microsoft permettant de
démarrer avec le C#.
– Visual Studio est l’outil de développement payant de Microsoft permettant d’être
efficace dans le développement d’applications .NET.
– Microsoft SQL Server Express est le moteur de base de données utilisable facilement
avec Visual C# Express.
– L’environnement de développement nous permet de créer du code C# qui sera
contenu dans des projets, qui peuvent être réunis dans une solution.
26
Chapitre 3
La syntaxe générale du C#
Difficulté :
N
ous allons aborder ici la syntaxe générale du C# dans le cadre d’une application
console. Nous allons utiliser très souvent l’instruction Console.WriteLine("...");
que nous avons vue au chapitre précédent et qui est dédiée à l’affichage sur la console.
C’est une instruction qui va s’avérer très pratique pour notre apprentissage car nous pour-
rons avoir une représentation visuelle de ce que nous allons apprendre.
Préparez-vous, nous plongeons petit à petit dans l’univers du C#. Dans ce chapitre, nous
allons nous attaquer à la syntaxe générale du C# et nous serons capables de reconnaître
les lignes de code et de quoi elles se composent.
27
CHAPITRE 3. LA SYNTAXE GÉNÉRALE DU C#
Attention, chaque ligne de code doit être correcte d’un point de vue syn-
taxique sinon le compilateur ne saura pas la traduire en langage exécutable.
28
LE CARACTÈRE DE TERMINAISON DE LIGNE
✄ ✄
Si je tente de lancer mon application (avec le raccourci ✂Ctrl ✁+✂F5 ✁), Visual C# Ex-
press va tenter de compiler et d’exécuter l’application. Ceci n’étant pas possible, il
m’affichera un message indiquant qu’il y a des erreurs (voir figure 3.2).
Ce sont des erreurs de compilation qu’il va falloir résoudre si l’on souhaite que l’appli-
cation console puisse s’exécuter.
Nous allons voir dans les chapitres suivants comment écrire correctement des instruc-
tions en C#. Mais il est important de noter à l’heure actuelle que le C# est sensible
à la casse, ce qui veut dire que les majuscules comptent ! Ainsi le mot WriteLine et le
mot WriTEline sont deux mots bien distincts et peuvent potentiellement représenter
deux instructions différentes. Ici, le deuxième mot est incorrect car il n’existe pas.
Rappelez-vous bien que la casse est déterminante pour que l’application puisse
compiler.
par :
1 class Program { static void Main ( string [] args ) { Console .
WriteLine ( " Hello World !! " ) ;}}
29
CHAPITRE 3. LA SYNTAXE GÉNÉRALE DU C#
ou encore :
1 class Program
2 {
3 static void Main ( string [] args )
4 {
5 Console
6
7
8 . WriteLine ( " Hello World !! "
9
10 );
11 }
12 }
En général, pour que le code soit le plus lisible possible, on écrit une instruction par
ligne et on indente le code de façon à ce que les blocs soient lisibles.
Indenter signifie que chaque ligne de code qui fait partie d’un même bloc de
code commence avec le même retrait sur l’éditeur. Ce sont soit des tabula-
tions, soit des espaces qui permettent de faire ce retrait.
Visual C# Express nous aide pour faire correctement cette indentation quand nous
écrivons du code.
✄ Il peut
✄ également
✄ remettre
✄ toute la page en forme avec la combinaison
de touche : ✂Ctrl ✁+ ✂K ✁+ ✂Ctrl ✁+ ✂D ✁.
Décortiquons à présent cette ligne de code :
1 Console . WriteLine ( " Hello World !! " ) ;
Pour simplifier, nous dirons que nous appelons la méthode WriteLine qui permet
d’écrire une chaîne de caractères sur la console.
Une méthode représente une fonctionnalité, écrite avec du code, qui est utili-
sable par d’autres bouts de code (par exemple, calculer la racine carrée d’un
nombre ou afficher du texte. . .).
L’instruction "Hello World ! !" représente une chaîne de caractères et est passée en
paramètre de la méthode Console.WriteLine à l’aide des parenthèses. La chaîne de
caractères est délimitée par les guillemets. Enfin, le point-virgule permet d’indiquer
que l’instruction est terminée et qu’on peut enchaîner sur la suivante.
Certains points ne sont peut-être pas encore tout à fait clairs, comme ce qu’est vraiment
une méthode, ou comment utiliser des chaînes de caractères, mais ne vous inquiétez
pas, nous allons y revenir plus en détail dans les chapitres suivants et découvrir au fur
30
LES COMMENTAIRES
Les commentaires
Pour faciliter la compréhension du code ou pour se rappeler un point précis, il est
possible de mettre des commentaires dans son code. Les commentaires sont ignorés par
le compilateur et n’ont qu’une valeur informative pour le développeur. Dans un fichier
de code C# (.cs), on peut écrire des commentaires de deux façons différentes :
– en commençant son commentaire par /* et en le terminant par */ ce qui permet
d’écrire un commentaire sur plusieurs lignes ;
– en utilisant // et tout ce qui se trouve après sur la même ligne est alors un commen-
taire.
Visual C# Express colore les commentaires en vert pour faciliter leur identification.
1 /* permet d ' afficher du texte
2 sur la console */
3 Console . WriteLine ( " Hello World !! " ) ; // ne pas oublier le point
virgule
La complétion automatique
Visual C# Express est un formidable outil qui nous facilite à tout moment la tâche,
notamment grâce à la complétion automatique. La complétion automatique est le
fait de proposer de compléter automatiquement ce que nous sommes en train d’écrire
en se basant sur ce que nous avons le droit de faire. Par exemple, si vous avez cherché
à écrire l’instruction :
1 Console . WriteLine ( " Hello World !! " ) ;
✄
vous avez pu constater que lors de l’appui sur la touche ✂C ✁, Visual C# Express nous
affiche une fenêtre avec tout ce qui commence par « C », ainsi que vous pouvez l’observer
à la figure 3.3.
Au fur et à mesure de la saisie, il affine les propositions pour se positionner sur la
plus
✄ pertinente. Il est possible de valider la proposition en appuyant sur la touche
✂Entrée ✁. Non seulement cela nous économise des appuis de touches, paresseux comme
nous sommes, mais cela nous permet également de vérifier la syntaxe de ce que nous
écrivons et d’obtenir également une mini-aide sur ce que nous essayons d’utiliser (voir
figure 3.4).
31
CHAPITRE 3. LA SYNTAXE GÉNÉRALE DU C#
32
LA COMPLÉTION AUTOMATIQUE
Ainsi, fini les fautes de frappe qui entraînent une erreur de compilation ou les listes de
mots-clés dont il faut absolument retenir l’orthographe !
De la même façon, une fois que vous avez fini de saisir « Console » vous allez saisir le
point « . » et Visual C# Express va nous proposer toute une série d’instructions en
rapport avec le début de l’instruction (voir figure 3.5).
Nous pourrons ainsi facilement finir de saisir WriteLine et ceci sans erreur d’écriture,
ni problème de majuscule.
En résumé
– Le code C# est composé d’une suite d’instructions qui se terminent par un point-
virgule.
– La syntaxe d’un code C# doit être correcte sinon nous aurons des erreurs de com-
pilation.
– Il est possible de commenter son code grâce aux caractères « // », « /* » et « */ ».
– Visual C# Express dispose d’un outil puissant qui permet d’aider à compléter ses
instructions : la complétion automatique.
33
CHAPITRE 3. LA SYNTAXE GÉNÉRALE DU C#
34
Chapitre 4
Les variables
Difficulté :
D
ans ce chapitre nous allons apprendre ce que sont les variables et comment ces
éléments indispensables vont nous rendre bien des services pour traiter de l’informa-
tion susceptible de changer dans nos programmes informatiques. Nous continuerons
en découvrant les différents types de variables et nous ferons nos premières manipulations
avec elles.
Soyez attentifs à ce chapitre, il est vraiment fondamental de bien comprendre à quoi servent
les variables lors du développement d’une application informatique.
35
CHAPITRE 4. LES VARIABLES
Par exemple, l’âge d’une personne pourrait être stocké sous la forme d’un entier et
accessible par la variable age, ce qui s’écrit en C# :
1 int age ;
Pour déclarer une variable en C#, on commence toujours par indiquer son type (int,
ici un entier) et son nom (ici, age). Il faudra impérativement affecter une valeur à cette
variable avec l’opérateur « = », soit sur la même instruction que la déclaration, soit un
peu plus loin dans le code, mais dans tous les cas, avant l’utilisation de cette variable.
36
QU’EST-CE QU’UNE VARIABLE ?
Nous pouvons à tout moment demander la valeur contenue dans la variable age, par
exemple :
1 int age = 30 ;
2 Console . WriteLine ( age ) ; // affiche 30
1 int age = 30 ;
2 Console . WriteLine ( age ) ; // affiche 30
3 age = 20 ;
4 Console . WriteLine ( age ) ; // affiche 20
Vous pouvez nommer vos variables à peu près n’importe comment, à quelques détails
près. Les noms de variables ne peuvent pas avoir le même nom qu’un type. Il sera
alors impossible d’appeler une variable int. Il est également impossible d’utiliser des
caractères spéciaux, comme des espaces ou des caractères de ponctuation. De même,
on ne pourra pas nommer une variable en commençant par des chiffres.
Il est par contre possible d’utiliser des accents dans les noms de variables,
cependant ceci n’est pas recommandé et ne fait pas partie des bonnes pra-
tiques de développement. En effet, il est souvent recommandé de nommer ses
variables en anglais (langue qui ne contient pas d’accents). Vous aurez noté
que, volontairement, je ne le fais pas dans ce livre afin de ne pas ajouter une
contrainte supplémentaire lors de la lecture du code. Mais libre à vous de le
faire !
En général, une variable commence par une minuscule et, si son nom représente plu-
sieurs mots, on démarrera un nouveau mot par une majuscule. Par exemple :
1 int ageDuVisiteur ;
1 int ageduvisiteur = 30 ;
2 int ageDuVisiteur = 20 ;
3 Console . WriteLine ( ageduvisiteur ) ; // affiche 30
4 Console . WriteLine ( ageDuVisiteur ) ; // affiche 20
37
CHAPITRE 4. LES VARIABLES
À noter un détail qui peut paraître évident, mais toutes les variables sont
réinitialisées à chaque nouvelle exécution du programme. Dès qu’on démarre
le programme, les classeurs sont vidés, comme si on emménageait dans de
nouveaux locaux à chaque fois. Il est donc impossible de faire persister une
information entre deux exécutions du programme en utilisant des variables.
Pour ceci, on utilisera d’autres solutions, comme enregistrer des valeurs dans
un fichier ou dans une base de données. Nous y reviendrons ultérieurement.
Il est important de stocker des données dans des variables ayant le bon type.
38
AFFECTATIONS, OPÉRATIONS, CONCATÉNATION
Type Description
byte Entier de 0 à 255
short Entier de -32768 à 32767
int Entier de -2147483648 à 2147483647
long Entier de -9223372036854775808 à 9223372036854775807
float Nombre simple précision de -3,402823e38 à 3,402823e38
double Nombre double précision de -1,79769313486232e308 à
1,79769313486232e308
decimal Nombre décimal convenant particulièrement aux calculs financiers
(en raison de ses nombres significatifs après la virgule)
char Représente un caractère
string Une chaîne de caractère
bool Une valeur booléenne (vrai ou faux)
En plus de la simple affectation, nous pouvons également faire des opérations, par
exemple :
1 int resultat = 2 * 3 ;
ou encore
1 int age1 = 20 ;
2 int age2 = 30 ;
3 int moyenne = ( age1 + age2 ) / 2 ;
39
CHAPITRE 4. LES VARIABLES
L’opérateur + peut également servir à concaténer des chaînes de caractères, par exemple :
1 string codePostal = " 33000 " ;
2 string ville = " Bordeaux " ;
3 string adresse = codePostal + " " + ville ;
4 Console . WriteLine ( adresse ) ; // affiche : 33000 Bordeaux
D’autres opérateurs particuliers existent que nous ne trouvons pas dans les cours de
mathématiques. Par exemple, l’opérateur ++ qui permet de réaliser une incrémentation
de 1, ou l’opérateur -- qui permet de faire une décrémentation de 1. De même, les
opérateurs que nous avons déjà vus peuvent se cumuler à l’opérateur = pour simplifier
une opération qui prend une variable comme opérande et cette même variable comme
résultat. Par exemple :
1 int age = 20 ;
2 age = age + 10 ; // age contient 30 ( addition )
3 age = age ++; // age contient 31 ( incr é mentation de 1 )
4 age = age - -; // age contient 30 ( d é cr é mentation de 1 )
5 age += 10 ; // é quivalent à age = age + 10 ( age contient 40 )
6 age /= 2 ; // é quivalent à age = age / 2 = > ( age contient 20 )
Comme nous avons pu le voir dans nos cours de mathématiques, il est possible de
grouper des opérations avec des parenthèses pour agir sur leurs priorités.
Ainsi, l’instruction précédemment vue :
1 int moyenne = ( age1 + age2 ) / 2 ;
effectue bien la somme des deux âges avant de les diviser par 2, car les parenthèses sont
prioritaires.
Cependant, l’instruction suivante :
1 int moyenne = age1 + age2 / 2 ;
aurait commencé par diviser age2 par 2 et aurait ajouté age1, ce qui n’aurait plus rien
à voir avec une moyenne. En effet, la division est prioritaire par rapport à l’addition.
40
LES CARACTÈRES SPÉCIAUX DANS LES CHAÎNES DE CARACTÈRES
1 int moyenne = 5 / 2 ;
2 Console . WriteLine ( moyenne ) ;
Oui et non. Si nous divisions 5 par 2, nous obtenons bien 2.5. Par contre, ici nous
divisons l’entier 5 par l’entier 2 et nous stockons le résultat dans l’entier moyenne. Le
C# réalise en fait une division entière, c’est-à-dire qu’il prend la partie entière de 2.5,
c’est-à-dire 2.
De plus, l’entier moyenne est incapable de stocker une valeur contenant des chiffres
après la virgule. Il ne prendrait que la partie entière.
Pour avoir 2.5, il faudrait utiliser le code suivant :
1 double moyenne = 5 . 0 / 2 . 0 ;
2 Console . WriteLine ( moyenne ) ;
Ici, nous divisons deux doubles entre eux et nous stockons le résultat dans un double.
(Rappelez-vous, le type de données double permet de stocker des nombres à virgule.)
affichera :
41
CHAPITRE 4. LES VARIABLES
la ligne à chaque fois. C’est le rôle de l’instruction WriteLine qui affiche la chaîne de
caractères et passe à la ligne à la fin de la chaîne de caractères.
Nous pouvons faire la même chose en utilisant le caractère spécial « \n ». Il permet de
passer à la ligne à chaque fois qu’il est rencontré. Ainsi, le code suivant :
1 string phrase = " Mon pr é nom est \" Nicolas \" " ;
2 Console . WriteLine ( phrase ) ;
3 Console . WriteLine ( " Passe \ n à \ nla \ nligne \ n \ n \ n " ) ;
Vous me diriez qu’on pourrait enchaîner les Console.WriteLine et vous auriez raison.
Mais les caractères spéciaux nous permettent de faire d’autres choses comme une ta-
bulation par exemple grâce au caractère spécial « \t ». Le code suivant :
1 Console . WriteLine ( " Choses à faire : " ) ;
2 Console . WriteLine ( " \ t - Arroser les plantes " ) ;
3 Console . WriteLine ( " \ t - Laver la voiture " ) ;
Choses à faire :
- Arroser les plantes
- Laver la voiture
Nous avons vu que le caractère \ était un caractère spécial et qu’il permettait de dire
au compilateur que nous voulions l’utiliser combiné à la valeur qui le suit, pour avoir
une tabulation ou un retour à la ligne. Comment pourrons-nous avoir une chaîne de
caractères qui contienne ce fameux caractère ?
Le principe est le même, il suffira de faire suivre ce fameux caractère spécial de lui-
même :
1 string fichier = " c :\\ repertoire \\ fichier . cs " ;
2 Console . WriteLine ( fichier ) ;
c :\ repertoire \ fichier . cs
Pour ce cas particulier, il est également possible d’utiliser la syntaxe suivante en utili-
sant le caractère spécial @ devant la chaîne de caractères :
42
LES CARACTÈRES SPÉCIAUX DANS LES CHAÎNES DE CARACTÈRES
Bien sûr, nous pouvons stocker des caractères spéciaux dans des variables pour faire
par exemple :
1 string sautDeLigne = " \ n " ;
2 Console . WriteLine ( " Passer " + sautDeLigne + " à " +
3 sautDeLigne + " la " + sautDeLigne + " ligne " ) ;
qui affichera :
Passer
à
la
ligne
Notez qu’il est possible de passer à la ligne lors de l’écriture d’une instruction
C# comme je l’ai fait dans le dernier bout de code afin d’améliorer la lisibilité.
N’oubliez pas que c’est le point-virgule qui termine l’instruction.
En fait, je triche un peu sur les mots. Pour faciliter la compréhension, on peut considérer
que Environment.NewLine est une variable, au même titre que la variable sautDeLigne
que nous avons définie. En réalité, c’est un peu plus complexe qu’une variable. Nous
découvrirons plus loin de quoi il s’agit vraiment.
En résumé
– Une variable est une zone mémoire permettant de stocker une valeur d’un type
particulier.
– Le C# possède plein de types prédéfinis, comme les entiers (int), les chaînes de
caractères (string), etc.
43
CHAPITRE 4. LES VARIABLES
44
Chapitre 5
Les instructions conditionnelles
Difficulté :
D
ans nos programmes C#, nous allons régulièrement avoir besoin de faire des opé-
rations en fonction d’un résultat précédent. Par exemple, lors d’un processus de
connexion à une application, si le login et le mot de passe sont bons, alors nous
pouvons nous connecter, sinon nous afficherons une erreur.
Il s’agit de ce que l’on appelle une condition. Elle est évaluée lors de l’exécution et en
fonction de son résultat (vrai ou faux) nous ferons telle ou telle chose.
Bien que relativement court, ce chapitre est très important. N’hésitez pas à le relire et à
vous entraîner.
45
CHAPITRE 5. LES INSTRUCTIONS CONDITIONNELLES
Opérateur Description
== Égalité
!= Différence
> Supérieur à
< Inférieur à
>= Supérieur ou égal
<= Inférieur ou égal
&& ET logique
|| OU logique
! Négation
Nous allons voir comment les utiliser en combinaison avec les instructions condition-
nelles.
L’instruction if
L’instruction if permet d’exécuter du code si une condition est vraie 1 . Par exemple :
1 decimal compteEnBanque = 300 ;
2 if ( compteEnBanque >= 0 )
3 Console . WriteLine ( " Votre compte est cr é diteur " ) ;
Ici, nous avons une variable contenant le solde de notre compte en banque. Si notre
solde est supérieur ou égal à 0 alors nous affichons que le compte est créditeur.
Pour afficher que le compte est débiteur, on pourrait tester si la valeur de la variable
est inférieure à 0 et afficher que le compte est débiteur :
1 decimal compteEnBanque = 300 ;
2 if ( compteEnBanque >= 0 )
3 Console . WriteLine ( " Votre compte est cr é diteur " ) ;
4 if ( compteEnBanque < 0 )
5 Console . WriteLine ( " Votre compte est d é biteur " ) ;
Une autre solution est d’utiliser le mot-clé else, qui veut dire « sinon » en anglais.
« Si la valeur est vraie, alors on fait quelque chose, sinon, on fait autre chose », ce qui
se traduit en C# par :
1 decimal compteEnBanque = 300 ;
2 if ( compteEnBanque >= 0 )
3 Console . WriteLine ( " Votre compte est cr é diteur " ) ;
46
L’INSTRUCTION IF
4 else
5 Console . WriteLine ( " Votre compte est d é biteur " ) ;
Il faut bien se rendre compte que l’instruction if teste si une valeur est vraie (dans
l’exemple précédent la comparaison compteEnBanque >= 0).
On a vu rapidement dans les chapitres précédents qu’il existait un type de variable
qui permettait de stocker une valeur vraie ou fausse : le type bool, autrement appelé
booléen (boolean en anglais). Ainsi, il sera également possible de tester la valeur d’un
booléen. L’exemple précédent peut aussi s’écrire :
1 decimal compteEnBanque = 300 ;
2 bool estCrediteur = ( compteEnBanque >= 0 ) ;
3 if ( estCrediteur )
4 Console . WriteLine ( " Votre compte est cr é diteur " ) ;
5 else
6 Console . WriteLine ( " Votre compte est d é biteur " ) ;
Un type bool peut prendre deux valeurs, vrai ou faux, qui s’écrivent avec les mots-clés
true et false.
1 bool estVrai = true ;
2 if ( estVrai )
3 Console . WriteLine ( "C ' est vrai ! " ) ;
4 else
5 Console . WriteLine ( "C ' est faux ! " ) ;
Il est également possible de combiner les tests grâce aux opérateurs de logique condi-
tionnelle, par exemple && qui correspond à l’opérateur « ET ».
Dans l’exemple qui suit, nous affichons le message de bienvenue uniquement si le login
est « Nicolas » ET que le mot de passe est « test ». Si l’un des deux ne correspond pas,
nous irons dans l’instruction else.
1 string login = " Nicolas " ;
2 string motDePasse = " test " ;
3 if ( login == " Nicolas " && motDePasse == " test " )
4 Console . WriteLine ( " Bienvenue Nicolas " ) ;
47
CHAPITRE 5. LES INSTRUCTIONS CONDITIONNELLES
5 else
6 Console . WriteLine ( " Login incorrect " ) ;
Remarquons ici que nous avons utilisé le test d’égalité ==, à ne pas confondre
avec l’opérateur d’affection =. C’est une erreur classique de débutant !
D’autres opérateurs de logique existent, nous avons notamment l’opérateur || qui cor-
respond au « OU » logique :
1 if ( civilite == " Mme " || civilite == " Mlle " )
2 Console . WriteLine ( " Vous ê tes une femme " ) ;
3 else
4 Console . WriteLine ( " Vous ê tes un homme " ) ;
L’exemple parle de lui-même ; si la civilité de la personne est Mme ou Mlle, alors nous
avons à faire à une femme.
Ici, si la première condition du if est vraie alors la deuxième ne sera pas évaluée. C’est
un détail ici, mais cela peut s’avérer important dans certaines situations dont une que
nous verrons un peu plus loin.
Un autre opérateur très courant est la négation que l’on utilise avec l’opérateur « ! ».
Par exemple :
1 bool estVrai = true ;
2 if (! estVrai )
3 Console . WriteLine ( "C ' est faux ! " ) ;
4 else
5 Console . WriteLine ( "C ' est vrai ! " ) ;
Ce test pourrait se lire ainsi : si la négation de la variable estVrai est vraie, alors
on écrira « C’est faux ! » La variable « estVrai » étant égale à true, sa négation vaut
false. Dans cet exemple, le programme nous affichera donc l’instruction correspondant
au else, à savoir « C’est vrai ! »
Rappelez-vous, nous avons dit qu’une instruction se finissait en général par un point-
virgule. Comment cela se fait-il alors qu’il n’y ait pas de point-virgule à la fin du if
ou du else ? Et si nous écrivions l’exemple précédent de cette façon ?
1 bool estVrai = true ;
2 if (! estVrai ) Console . WriteLine ( "C ' est faux ! " ) ;
3 else Console . WriteLine ( "C ' est vrai ! " ) ;
Ceci est tout à fait valable et permet de voir où s’arrête vraiment l’instruction grâce
au point-virgule. Cependant, nous écrivons en général ces instructions de la première
façon afin que celles-ci soient plus lisibles. Vous aurez l’occasion de rencontrer dans les
chapitres suivants d’autres instructions qui ne se terminent pas obligatoirement par un
point-virgule.
48
L’INSTRUCTION SWITCH
Remarquons enfin qu’il est possible d’enchaîner les tests de manière à traiter plusieurs
conditions en utilisant la combinaison else if. Cela donne :
1 if ( civilite == " Mme " )
2 Console . WriteLine ( " Vous ê tes une femme " ) ;
3 else if ( civilite == " Mlle " )
4 Console . WriteLine ( " Vous ê tes une femme non mari é e " ) ;
5 else if ( civilite == " M . " )
6 Console . WriteLine ( " Vous ê tes un homme " ) ;
7 else
8 Console . WriteLine ( " Je n ' ai pas pu d é terminer votre civilit é
");
L’instruction switch
L’instruction switch peut être utilisée lorsqu’une variable prend beaucoup de valeurs.
Elle permet de simplifier l’écriture. Ainsi, l’instruction suivante :
1 string civilite = " M . " ;
2 if ( civilite == " M . " )
3 Console . WriteLine ( " Bonjour monsieur " ) ;
4 if ( civilite == " Mme " )
5 Console . WriteLine ( " Bonjour madame " ) ;
6 if ( civilite == " Mlle " )
7 Console . WriteLine ( " Bonjour mademoiselle " ) ;
pourra s’écrire :
1 string civilite = " M . " ;
2 switch ( civilite )
3 {
4 case " M . " :
5 Console . WriteLine ( " Bonjour monsieur " ) ;
6 break ;
7 case " Mme " :
8 Console . WriteLine ( " Bonjour madame " ) ;
9 break ;
10 case " Mlle " :
11 Console . WriteLine ( " Bonjour mademoiselle " ) ;
12 break ;
13 }
switch commence par évaluer la variable qui lui est passée entre parenthèses. Avec
le mot-clé case on énumère les différents cas possibles pour la variable et on exécute
49
CHAPITRE 5. LES INSTRUCTIONS CONDITIONNELLES
les instructions correspondantes jusqu’au mot-clé break qui signifie que l’on sort du
switch.
Nous pouvons également indiquer une valeur par défaut en utilisant le mot-clé default,
ainsi dans l’exemple suivant tout ce qui n’est pas « M. », « Mme » ou « Mlle » donnera
l’affichage d’un « Bonjour inconnu » :
1 switch ( civilite )
2 {
3 case " M . " :
4 Console . WriteLine ( " Bonjour monsieur " ) ;
5 break ;
6 case " Mme " :
7 Console . WriteLine ( " Bonjour madame " ) ;
8 break ;
9 case " Mlle " :
10 Console . WriteLine ( " Bonjour mademoiselle " ) ;
11 break ;
12 default :
13 Console . WriteLine ( " Bonjour inconnu " ) ;
14 break ;
15 }
Nous pouvons également enchaîner plusieurs cas pour qu’ils fassent la même chose, ce
qui reproduit le fonctionnement de l’opérateur logique « OU » (« || »). Par exemple,
on pourra remplacer l’exemple suivant :
1 string mois = " Janvier " ;
2 if ( mois == " Mars " || mois == " Avril " || mois == " Mai " )
3 Console . WriteLine ( "C ' est le printemps " ) ;
4 if ( mois == " Juin " || mois == " Juillet " || mois == " Ao û t " )
5 Console . WriteLine ( "C ' est l ' é t é " ) ;
6 if ( mois == " Septembre " || mois == " Octobre " || mois == "
Novembre " )
7 Console . WriteLine ( "C ' est l ' automne " ) ;
8 if ( mois == " D é cembre " || mois == " Janvier " || mois == " F é vrier
")
9 Console . WriteLine ( "C ' est l ' hiver " ) ;
par :
1 switch ( mois )
2 {
3 case " Mars " :
4 case " Avril " :
5 case " Mai " :
6 Console . WriteLine ( "C ' est le printemps " ) ;
7 break ;
8 case " Juin " :
9 case " Juillet " :
10 case " Ao û t " :
11 Console . WriteLine ( "C ' est l ' é t é " ) ;
50
L’INSTRUCTION SWITCH
12 break ;
13 case " Septembre " :
14 case " Octobre " :
15 case " Novembre " :
16 Console . WriteLine ( "C ' est l ' automne " ) ;
17 break ;
18 case " D é cembre " :
19 case " Janvier " :
20 case " F é vrier " :
21 Console . WriteLine ( "C ' est l ' hiver " ) ;
22 break ;
23 }
En résumé
– Les instructions conditionnelles permettent d’exécuter des instructions seulement si
une condition est vérifiée.
– On utilise en général le résultat d’une comparaison dans une instruction condition-
nelle.
– Le C# possède beaucoup d’opérateurs de comparaison, comme l’opérateur d’égalité
==, l’opérateur de supériorité >, d’infériorité <, etc.
51
CHAPITRE 5. LES INSTRUCTIONS CONDITIONNELLES
52
Chapitre 6
Les blocs de code et la portée d’une
variable
Difficulté :
N
ous avons régulièrement utilisé dans le chapitre précédent les accolades ouvrantes et
fermantes : { et }. Nous avons rapidement dit que ces accolades servaient à créer des
blocs de code.
L’utilisation d’accolades implique également une autre subtilité. Vous l’avez vu dans le titre
du chapitre, il s’agit de la portée d’une variable.
Regardons à présent comment cela fonctionne.
53
CHAPITRE 6. LES BLOCS DE CODE ET LA PORTÉE D’UNE VARIABLE
54
LA PORTÉE D’UNE VARIABLE
est incorrect et provoquera une erreur de compilation. En effet, nous essayons d’accéder
à la variable age en dehors du bloc de code où elle est définie. Nous voyons que cette
variable est définie dans le bloc qui est exécuté lorsque le test d’égalité du prénom avec
la chaîne « Nicolas » est vrai alors que nous essayons de la comparer à « 18 » dans un
endroit où elle n’existe plus.
Nous pouvons utiliser age dans tout le premier if, et dans les sous-blocs de code,
comme c’est le cas dans le sous-bloc du switch, mais pas en dehors du bloc de code
dans lequel la variable est définie. Ainsi, la variable prenom est accessible dans le dernier
if car elle a été définie dans un bloc père.
Vous noterez qu’ici, la complétion nous est utile. En effet, Visual C# Express propose
de compléter le nom de la variable dans un bloc où elle est accessible. Dans un bloc où
elle ne l’est pas, la complétion ne nous la propose pas. Comme Visual C# Express est
malin, si la complétion ne propose pas ce que vous souhaitez, c’est que vous n’y avez
pas le droit ! Cela peut être parce que la portée ne vous l’autorise pas. Pour corriger
l’exemple précédent, il faut déclarer la variable age au même niveau que la variable
prenom.
Ok, mais alors, pourquoi on ne déclarerait pas tout au début une bonne fois
pour toutes ? Cela éviterait ces erreurs. . . non ?
En résumé
– Un bloc de code permet de regrouper des instructions qui commencent par { et qui
finissent par }.
– Une variable définie à l’intérieur d’un bloc de code aura pour portée ce bloc de code.
55
CHAPITRE 6. LES BLOCS DE CODE ET LA PORTÉE D’UNE VARIABLE
56
Chapitre 7
Les méthodes
Difficulté :
É
lément indispensable de tout programme informatique, une méthode regroupe un
ensemble d’instructions, pouvant prendre des paramètres et pouvant renvoyer une
valeur. Lors de vos développements, vous allez avoir besoin de créer beaucoup de
méthodes.
Nous allons découvrir les méthodes dans ce chapitre mais nous y reviendrons petit à petit
tout au long de l’ouvrage et vous aurez ainsi l’occasion d’approfondir vos connaissances.
Vous pourrez trouver de temps en temps le mot « fonction » à la place du mot « mé-
thode ». Cela signifie la même chose. C’est une relique du passé correspondant à un ancien
mode de développement qui s’utilise de moins en moins, de même que le terme « procé-
dure » qui est encore plus vieux !
57
CHAPITRE 7. LES MÉTHODES
Si plus tard on veut réafficher le message de bienvenue, il faudra réécrire ces quatre
lignes de code à moins que nous utilisions une méthode :
1 static void Aff ic ha geB ie nv enu e ()
2 {
3 Console . WriteLine ( " Bonjour Nicolas " ) ;
4 Console . WriteLine ( " ------- " + Environment . NewLine ) ;
5 Console . WriteLine ( " \ tBienvenue dans le monde merveilleux du
C#");
6 }
est ce qu’on appelle la signature de la méthode. Elle nous renseigne sur les paramètres
de la méthode et sur ce qu’elle va renvoyer. Le mot-clé void signifie que la méthode
ne renvoie rien. Les parenthèses vides à la fin de la signature indiquent que la méthode
n’a pas de paramètre.
58
LA MÉTHODE SPÉCIALE MAIN()
Le mot-clé static ne nous intéresse pas pour l’instant, mais sachez qu’il sert à indiquer
que la méthode est toujours disponible et prête à être utilisée. Dans ce contexte, il est
obligatoire. Nous y reviendrons.
En dessous de la signature de la méthode, nous retrouvons les accolades. Elles per-
mettent de délimiter la méthode. Le bloc de code ainsi formé constitue ce qu’on appelle
le « corps de la méthode ».
En résumé, pour déclarer une méthode, nous aurons :
1 Signature de la m é thode
2 {
3 Corps de la m é thode
4 }
Nous pouvons désormais appeler cette méthode (c’est-à-dire l’exécuter), dans notre
programme grâce à son nom. Par exemple, ici je l’appelle très facilement deux fois de
suite :
1 static void Main ( string [] args )
2 {
3 Af fi ch age Bi en ven ue () ;
4 Af fi ch age Bi en ven ue () ;
5 }
6
7 static void Af fic ha ge Bie nv en ue ()
8 {
9 Console . WriteLine ( " Bonjour Nicolas " ) ;
10 Console . WriteLine ( " ------- " + Environment . NewLine ) ;
11 Console . WriteLine ( " \ tBienvenue dans le monde merveilleux du
C#");
12 }
Et tout ça, sans effort ! C’est quand même plus simple et plus clair, non ?
Elle a été générée par Visual C# Express lorsque nous avons créé le projet Console.
59
CHAPITRE 7. LES MÉTHODES
Cette méthode est en fait le point d’entrée de l’application, c’est-à-dire que quand le
CLR tente d’exécuter notre application, il recherche cette méthode afin de pouvoir
commencer à exécuter des instructions à partir d’elle. S’il ne la trouve pas, alors, il
ne pourra pas exécuter notre application. C’est pour cela qu’il est important que cette
méthode soit accessible de partout ; rappelez-vous, c’est le rôle du mot-clé static
que nous aurons l’occasion d’étudier plus en détail ultérieurement. Visual C# Express
nous garde bien de cette erreur. En effet, si vous supprimez cette méthode (ou que
vous enlevez le mot-clé static) et que vous tentez de compiler notre application, vous
aurez le message d’erreur suivant :
Le message d’erreur est clair. Il a besoin d’une méthode Main() pour démarrer.
Voilà pour cette méthode spéciale Main(). Elle est indispensable dans tout
programme exécutable et c’est par-là que le programme démarre.
Les lecteurs attentifs auront remarqué que cette méthode possède certains éléments
dans la signature, entre les parenthèses. . . Des paramètres ! Découvrons-les dans la
prochaine section. . .
60
PARAMÈTRES D’UNE MÉTHODE
Finalement, ce n’est pas si pratique que ça. . . Alors que nous venions juste d’évoquer
le principe DRY, nous nous retrouvons avec deux méthodes quasiment identiques qui
ne diffèrent que d’un tout petit détail.
C’est là qu’interviennent les paramètres de méthode. Nous l’avons évoqué au para-
graphe précédent, il est possible de passer des paramètres à une méthode. Ainsi, nous
pourrons utiliser les valeurs de ces paramètres dans le corps de nos méthodes, les mé-
thodes en deviendront d’autant plus génériques.
Dans notre exemple d’affichage de message de bienvenue, il est évident que le nom de
l’utilisateur sera un paramètre de la méthode.
Les paramètres s’écrivent à l’intérieur des parenthèses qui suivent le nom de la méthode.
Nous devons indiquer le type du paramètre ainsi que le nom de la variable qui le
représentera au sein de la méthode.
Il est possible de passer plusieurs paramètres à une méthode, on les séparera avec une
virgule. Par exemple :
1 static void DireBonjour ( string prenom , int age )
2 {
3 Console . WriteLine ( " Bonjour " + prenom ) ;
4 Console . WriteLine ( " Vous avez " + age + " ans " ) ;
5 }
Bonjour Nicolas
Vous avez 30 ans
Bonjour J é r é mie
Vous avez 20 ans
Bien sûr, il est obligatoire de fournir en paramètre d’une méthode une variable de même
type que le paramètre attendu. Dans le cas contraire, le compilateur est incapable de
mettre la donnée qui a été passée dans le paramètre. D’ailleurs, si vous ne fournissez
pas le bon paramètre, vous avez droit à une erreur de compilation.
61
CHAPITRE 7. LES MÉTHODES
Il est évidemment possible de passer des variables à une méthode, cela fonctionne de
la même façon :
1 string prenom = " Nicolas " ;
2 DireBonjour ( prenom , 30 ) ;
Nous allons revenir plus en détail sur ce qu’il se passe exactement lorsque nous abor-
derons le chapitre sur le mode de passage des paramètres.
Vous voyez, cela ressemble beaucoup à ce que nous avons déjà fait avec la méthode
Console.WriteLine(). Facile, non ?
La méthode Console.WriteLine fait partie de la bibliothèque du framework .NET.
Elle est utilisée pour écrire des chaînes de caractères, des nombres ou plein d’autres
choses sur la console. Le framework .NET contient énormément de méthodes utilitaires
de toutes sortes. Nous y reviendrons un peu plus tard.
Vous aurez peut-être remarqué un détail : nous avons préfixé toutes nos méthodes du
mot-clé static. J’ai dit que c’était obligatoire dans notre contexte, pour être plus
précis, c’est parce que la méthode Main() est statique que nous sommes obligés de
créer des méthodes statiques. On a dit que la méthode Main() était obligatoirement
statique parce qu’elle devait être accessible de partout afin que le CLR puisse trouver le
point d’entrée de notre programme. Or, une méthode statique ne peut appeler que des
méthodes statiques, c’est pour cela que nous sommes obligés (pour l’instant) de préfixer
nos méthodes par le mot-clé static. Nous décrirons ce que recouvre exactement le
mot-clé static dans la partie suivante.
62
RETOUR D’UNE MÉTHODE
5 return resultat ;
6 }
Vous aurez remarqué que la signature de la méthode commence par le mot-clé double,
qui indique qu’elle va nous renvoyer une valeur de type double. Comme on l’a vu,
double a et double b sont deux paramètres de la méthode et sont du type double.
La méthode Math.Sqrt est une méthode du framework .NET, au même titre que la
méthode Console.WriteLine, qui permet de renvoyer la racine carrée d’un nombre.
Elle prend en paramètre un double et nous retourne une valeur de type double éga-
lement qui correspond à la racine carrée du paramètre. C’est tout naturellement que
nous stockons ce résultat dans une variable grâce à l’opérateur d’affectation =.
À la fin de la méthode, le mot-clé return indique que la méthode renvoie la valeur à
la méthode qui l’a appelée. Ici, nous renvoyons le résultat.
Cette méthode pourra s’utiliser ainsi :
1 static void Main ( string [] args )
2 {
3 double valeur = L on gu eur Hy po ten us e (1 , 3 ) ;
4 Console . WriteLine ( valeur ) ;
5 valeur = Lo ng ueu rH yp ote nu se ( 10 , 10 ) ;
6 Console . WriteLine ( valeur ) ;
7 }
Comme précédemment, nous utilisons une variable pour stocker le résultat de l’exécu-
tion de la méthode :
3 ,16227766016838
14 ,142135623731
Notez qu’il est également possible de se passer d’une variable intermédiaire pour stocker
le résultat. Ainsi, nous pourrons par exemple écrire :
1 Console . WriteLine ( " Le r é sultat est : " + Lo ng ue urH yp ot enu se (1 ,
3));
Avec cette écriture le résultat renvoyé par la méthode LongueurHypotenuse est direc-
tement concaténé à la chaîne « Le résultat est : » et passé en paramètre à la méthode
Console.WriteLine.
Remarquez qu’on a fait l’opération : « a*a » pour mettre « a » au carré. On aurait
également pu faire Math.Pow(a, 2) qui permet de faire la même chose à la différence
que Pow permet de mettre à la puissance que l’on souhaite. Ainsi, Math.Pow(a, 3)
permet de mettre « a » au cube.
Il faut savoir que le mot-clé return peut apparaître à n’importe quel endroit de la
méthode. Il interrompt alors l’exécution de celle-ci et renvoie la valeur passée. Ce mot-
clé est obligatoire, sans cela la méthode ne compile pas. Il est également primordial que
tous les chemins possibles d’une méthode renvoient quelque chose. Les chemins sont
déterminés par les instructions conditionnelles que nous avons vues précédemment.
63
CHAPITRE 7. LES MÉTHODES
car peu importe la valeur de la variable genre, la méthode renverra une chaîne.
Alors que celui-ci :
1 static string Conjugaison ( string genre )
2 {
3 if ( genre == " homme " )
4 return " é " ;
5 else
6 {
7 if ( genre == " femme " )
8 return " é e " ;
9 }
10 }
est incorrect. En effet, que renvoie la méthode si la variable genre contient autre chose
que « homme » ou « femme » ?
En général, Visual C# Express nous indiquera qu’il détecte un problème avec une
erreur de compilation.
Nous pourrons corriger ceci avec, par exemple :
1 static string Conjugaison ( string genre )
2 {
3 if ( genre == " homme " )
4 return " é " ;
5 else
6 {
7 if ( genre == " femme " )
8 return " é e " ;
9 }
10 return " " ;
11 }
À noter que "" correspond à une chaîne vide et peut également s’écrire
string.Empty.
Nous avons vu dans le début du chapitre qu’il était possible de créer des méthodes qui
ne retournent rien. Dans ce cas, on peut utiliser le mot-clé return sans valeur qui le
suit pour stopper l’exécution de la méthode. Par exemple :
64
RETOUR D’UNE MÉTHODE
Ainsi, si la variable prenom vaut « inconnu », alors nous quittons la méthode Bonjour
et l’instruction Console.WriteLine ne sera pas exécutée.
En résumé
– Une méthode regroupe un ensemble d’instructions pouvant prendre des paramètres
et pouvant renvoyer une valeur.
– Les paramètres d’une méthode doivent être utilisés avec le bon type.
– Une méthode qui ne renvoie rien est préfixée du mot-clé void.
– Le point d’entrée d’un programme est la méthode statique Main().
– Le mot-clé return permet de renvoyer une valeur du type de retour de la méthode,
à l’appelant de cette méthode.
65
CHAPITRE 7. LES MÉTHODES
66
Chapitre 8
Tableaux, listes et énumérations
Difficulté :
D
ans les chapitres précédents, nous avons pu utiliser les types de base du framework
.NET, comme int, string, double, etc. Nous allons découvrir ici d’autres types qui
vont s’avérer très utiles dans la construction de nos applications informatiques.
Une fois bien maîtrisés, vous ne pourrez plus vous en passer !
67
CHAPITRE 8. TABLEAUX, LISTES ET ÉNUMÉRATIONS
Les tableaux
Voici le premier nouveau type que nous allons étudier, le type tableau. En déclarant
une variable de type tableau, nous allons en fait utiliser une variable qui contient une
suite de variables du même type. Prenons cet exemple :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
Nous parcourons ici les éléments de 0 à taille-1 et nous affichons l’élément du tableau
correspondant à l’indice en cours. Ce qui nous donne :
Lundi
Mardi
Mercredi
Jeudi
Vendredi
Samedi
Dimanche
68
LES TABLEAUX
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
Cette écriture permet de créer un tableau qui contient 7 éléments et d’affecter une
valeur à chaque élément du tableau. Il s’agit en fait ici d’une écriture simplifiée. Cette
écriture est équivalente à celle-ci :
1 string [] jours = new string [ 7 ];
2 jours [ 0 ] = " Lundi " ;
3 jours [ 1 ] = " Mardi " ;
4 jours [ 2 ] = " Mercredi " ;
5 jours [ 3 ] = " Jeudi " ;
6 jours [ 4 ] = " Vendredi " ;
7 jours [ 5 ] = " Samedi " ;
8 jours [ 6 ] = " Dimanche " ;
qui est beaucoup plus verbeuse, mais d’un autre côté, plus explicite.
La première instruction crée un tableau qui peut contenir 7 éléments. C’est la taille
du tableau, elle ne peut pas changer. Chaque instruction suivante affecte une valeur à
un indice du tableau. Rappelez-vous, un tableau commence à l’indice 0 et va jusqu’à
l’indice taille – 1.
Il est possible facilement de faire des opérations sur un tableau, comme un tri. On
pourra utiliser la méthode Array.Sort(). Par exemple :
1 Array . Sort ( jours ) ;
Avec cette instruction, le tableau sera classé par ordre alphabétique. Vous aurez l’oc-
casion de voir d’autres méthodes dans des chapitres ultérieurs. Ainsi, le code suivant :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 Array . Sort ( jours ) ;
3 for ( int i = 0 ; i < jours . Length ; i ++)
4 {
5 Console . WriteLine ( jours [ i ]) ;
6 }
Dimanche
Jeudi
Lundi
Mardi
Mercredi
Samedi
Vendredi
69
CHAPITRE 8. TABLEAUX, LISTES ET ÉNUMÉRATIONS
Les listes
Un autre type que nous allons beaucoup utiliser est la liste. Nous allons voir comment
ce type fonctionne mais sans en faire une étude exhaustive car elle pourrait être bien
longue et ennuyeuse. Regardons cet exemple :
1 List < int > chiffres = new List < int >() ; // cr é ation de la liste
2 chiffres . Add ( 8 ) ; // chiffres contient 8
3 chiffres . Add ( 9 ) ; // chiffres contient 8, 9
4 chiffres . Add ( 4 ) ; // chiffres contient 8, 9, 4
5
6 chiffres . RemoveAt ( 1 ) ; // chiffres contient 8 , 4
7
8 foreach ( int chiffre in chiffres )
9 {
10 Console . WriteLine ( chiffre ) ;
11 }
La première ligne permet de créer la liste. Nous reviendrons sur cette instruction un
peu plus loin dans ce chapitre. Il s’agit d’une liste d’entiers.
Nous ajoutons des entiers à la liste grâce à la méthode Add(). Nous ajoutons en l’oc-
currence les entiers 8, 9 et 4.
La méthode RemoveAt() permet de supprimer un élément en utilisant son indice, ici
nous supprimons le deuxième entier, c’est-à-dire 9.
8
4
Les lecteurs assidus auront remarqué que la construction de la liste est un peu particu-
lière. On observe en effet l’utilisation de chevrons <> pour indiquer le type de la liste.
70
LES LISTES
Pour avoir une liste d’entier, il suffit d’indiquer le type int à l’intérieur des chevrons.
Ainsi, il ne sera pas possible d’ajouter autre chose qu’un entier dans cette liste. Par
exemple, l’instruction suivante provoque une erreur de compilation :
1 List < int > chiffres = new List < int >() ; // cr é ation de la liste
2 chiffres . Add ( " cha î ne " ) ; // ne compile pas
Les listes possèdent des méthodes bien pratiques qui permettent toutes sortes d’opé-
rations. Par exemple, la méthode IndexOf() permet de rechercher un élément dans la
liste et de renvoyer son indice.
1 List < string > jours = new List < string >() ;
2 jours . Add ( " Lundi " ) ;
3 jours . Add ( " Mardi " ) ;
4 jours . Add ( " Mercredi " ) ;
5 jours . Add ( " Jeudi " ) ;
6 jours . Add ( " Vendredi " ) ;
7 jours . Add ( " Samedi " ) ;
8 jours . Add ( " Dimanche " ) ;
9
10 int indice = jours . IndexOf ( " Mercredi " ) ; // indice vaut 2
Nous aurons l’occasion de voir d’autres utilisations de méthodes de la liste dans les
chapitres suivants.
La liste que nous venons de voir (List<>) est en fait ce que l’on appelle un type
générique. Nous n’allons pas rentrer dans le détail de ce qu’est un type générique
pour l’instant, mais il faut juste savoir qu’un type générique permet d’être spécialisé
par un type concret. Pour notre liste, cette généricité permet d’indiquer de quel type
est la liste, une liste d’entiers ou une liste de chaînes de caractères, etc.
Ne vous inquiétez pas si tout ceci n’est pas parfaitement clair, nous reviendrons plus
en détail sur les génériques dans un chapitre ultérieur. Le but ici est de commencer à
se familiariser avec le type List<> que nous utiliserons régulièrement et les exemples
que nous verrons permettront d’appréhender les subtilités de ce type.
À noter qu’il existe également une écriture simplifiée des listes. En effet, il est possible
de remplacer :
1 List < string > jours = new List < string >() ;
2 jours . Add ( " Lundi " ) ;
3 jours . Add ( " Mardi " ) ;
71
CHAPITRE 8. TABLEAUX, LISTES ET ÉNUMÉRATIONS
par
1 List < string > jours = new List < string > { " Lundi " , " Mardi " , "
Mercredi " , " Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
Liste ou tableau ?
Vous aurez remarqué que les deux types, tableau et liste, se ressemblent beaucoup.
Essayons de voir ce qui les différencie afin de savoir quel type choisir entre les deux.
En fait, une des grosses différences est que le tableau peut être multidimensionnel. C’est
un point important mais dans le cadre de vos premières applications, il est relativement
rare d’avoir à s’en servir. Cela sert le plus souvent dans le développement de jeux ou
lorsque l’on souhaite faire des calculs 3D.
Par contre, le plus important pour nous est que le type tableau est de taille fixe alors
que la liste est de taille variable. On peut ajouter sans problème un nouvel élément à
une liste grâce à la méthode Add(). De même, on peut supprimer des éléments avec
la méthode Remove alors qu’avec un tableau, on peut seulement remplacer les valeurs
existantes et il n’est pas possible d’augmenter sa capacité.
Globalement, vous verrez que dans vos applications, vous utiliserez plutôt les listes,
par exemple pour afficher une liste de produits, une liste de clients, etc.
Gardez quand même dans un coin de l’esprit les tableaux, ils pourront vous aider dans
des situations précises.
Les énumérations
Un type particulier que nous allons également utiliser est l’énumération. Cela corres-
pond comme son nom l’indique à une énumération de valeurs.
Par exemple, il pourrait être très facile de représenter les jours de la semaine dans une
énumération plutôt que dans un tableau.
On définit l’énumération de cette façon, grâce au mot-clé enum :
1 enum Jours
2 {
3 Lundi ,
4 Mardi ,
5 Mercredi ,
72
LES ÉNUMÉRATIONS
6 Jeudi ,
7 Vendredi ,
8 Samedi ,
9 Dimanche
10 }
À noter qu’on ne peut pas définir cette énumération n’importe où, pour l’instant,
contentons-nous de la définir en dehors de la méthode Main().
Pour être tout à fait précis, une énumération est un type dont toutes les valeurs dé-
finies sont des entiers. La première vaut 0, et chaque valeur suivante prend la valeur
précédente augmentée de 1. C’est-à-dire que Lundi vaut 0, Mardi vaut 1, etc.
Il est possible de forcer des valeurs à toutes ou certaines valeurs de l’énumération, les
valeurs non forcées prendront la valeur précédente augmentée de 1 :
1 enum Jours
2 {
3 Lundi = 5 , // lundi vaut 5
4 Mardi , // mardi vaut 6
5 Mercredi = 9 , // mercredi vaut 9
6 Jeudi = 10 , // jeudi vaut 10
7 Vendredi , // vendredi vaut 11
8 Samedi , // samedi vaut 12
9 Dimanche = 20 // dimanche vaut 20
10 }
Mais, à part pour enregistrer une valeur dans une base de données, il est rare de
manipuler les énumérations comme des entiers car le but de l’énumération est justement
d’avoir une liste exhaustive et fixée de valeurs constantes. Le code s’en trouve plus clair,
plus simple et plus lisible.
Le fait de définir une telle énumération revient en fait à enrichir les types que nous avons
à notre disposition, comme les entiers ou les chaînes de caractères (int ou string). Ce
nouveau type s’appelle Jours.
Nous pourrons définir une variable du type Jours de la même façon qu’avec un autre
type :
1 Jours jourDeLaSemaine ;
La seule différence c’est que les valeurs qu’il est possible d’affecter à notre variable sont
figées et font partie des valeurs définies dans l’énumération.
Pour pouvoir accéder à un élément de l’énumération, il faudra utiliser le nom de l’énu-
mération suivi de l’opérateur point « . », suivi encore de la valeur de l’énumération
choisie. Par exemple :
1 Jours lundi = Jours . Lundi ;
2 Console . WriteLine ( lundi ) ;
73
CHAPITRE 8. TABLEAUX, LISTES ET ÉNUMÉRATIONS
2 {
3 enum Jours
4 {
5 Lundi ,
6 Mardi ,
7 Mercredi ,
8 Jeudi ,
9 Vendredi ,
10 Samedi ,
11 Dimanche
12 }
13
14 static void Main ( string [] args )
15 {
16 Jours lundi = Jours . Lundi ;
17 Console . WriteLine ( lundi ) ;
18 }
19 }
Nous pourrons également nous servir des énumérations pour faire des tests de compa-
raison, comme par exemple :
1 if ( jourDeLaSemaine == Jours . Dimanche || jourDeLaSemaine ==
Jours . Samedi )
2 {
3 Console . WriteLine ( " Bon week - end " ) ;
4 }
Sachez que le framework .NET utilise beaucoup les énumérations. Il est important de
savoir les manipuler.
Vous aurez peut-être remarqué que lorsqu’on affiche la valeur d’une énumé-
ration, la console nous affiche le nom de l’énumération et non pas sa valeur
entière. Ça peut paraître étrange, mais c’est parce que le C# fait un traite-
ment particulier dans le cadre d’une énumération à l’affichage.
En résumé
– Un tableau est un type évolué pouvant contenir une séquence d’autres types, comme
un tableau d’entiers ou un tableau de chaînes de caractères.
– Une liste est un type complexe un peu plus souple que le tableau permettant d’avoir
une liste de n’importe quel type.
– Une énumération s’utilise lorsque l’on veut créer un type possédant plusieurs valeurs
fixes, comme les jours de la semaine.
74
Chapitre 9
Utiliser le framework .NET
Difficulté :
C
omme on l’a déjà évoqué, le framework .NET est une énorme boîte à outils qui
contient beaucoup de méthodes permettant de construire toutes sortes d’applications.
Nous allons avoir besoin régulièrement d’utiliser les éléments du framework .NET pour
réaliser nos applications. Il est donc grand temps d’apprendre à savoir le manipuler !
Rentrons tout de suite dans le vif du sujet.
75
CHAPITRE 9. UTILISER LE FRAMEWORK .NET
L’instruction using
Nous allons sans arrêt solliciter la puissance du framework .NET. Par exemple, nous
pouvons lui demander de nous donner la date courante.
Pour ce faire, on utilisera l’instruction :
1 Console . WriteLine ( DateTime . Now ) ;
Ce qui se passe ici, c’est que nous demandons à notre application l’affichage de la
propriété Now de l’objet DateTime. Nous allons revenir en détail sur ce que sont des
propriétés et des objets, considérez pour l’instant qu’ils correspondent simplement à
une instruction qui nous fournit la date du moment :
09/08/2011 23:47:58
En effet, les objets DateTime et Console se situent dans l’espace de nom System.
Un espace de nom 1 correspond à un endroit où l’on range des méthodes et des objets.
Il est caractérisé par des mots séparés par des points « . ».
C’est un peu comme des répertoires, nous pouvons dire que le fichier DateTime est
rangé dans le répertoire « System » et, quand nous souhaitons y accéder, nous devons
fournir l’emplacement complet du fichier, à savoir System.DateTime.
Cependant, plutôt que d’écrire le chemin complet à chaque fois, il est possible de dire :
« OK, maintenant, à chaque fois que je vais avoir besoin d’accéder à une fonctionnalité,
va la chercher dans l’espace de nom System. Si elle s’y trouve, utilise-la ».
C’est ce qui se passe grâce à l’instruction suivante :
1 using System ;
et
1. En anglais, namespace
76
LA BIBLIOTHÈQUE DE CLASSES .NET
Sachant qu’elles ne s’écrivent pas côte à côte. En général, on met l’instruction using
en en-tête du fichier .cs, comme ce qu’a fait Visual C# Express lorsqu’il a généré le
fichier. L’autre instruction est à positionner à l’endroit où nous souhaitons qu’elle soit
exécutée.
À noter que dans ce cas-là, si Visual C# Express reconnaît l’instruction mais que
l’espace de nom n’est pas inclus, il le propose en soulignant le début du mot DateTime
d’un petit trait bleu et blanc, comme l’illustre la figure 9.1.
Figure 9.1 – Le trait bleu permet d’indiquer que l’IDE sait résoudre l’import de
l’espace de nom
Un clic droit sur le mot permettra d’ouvrir un menu déroulant, de choisir Résoudre et
d’importer le using correspondant automatiquement (voir figure 9.2).
77
CHAPITRE 9. UTILISER LE FRAMEWORK .NET
Nico
et
1 Console . WriteLine ( Environment . UserName ) ;
Mais d’ailleurs, comment se fait-il que nous puissions accéder aux méthodes
du framework .NET sans nous poser de question ? Il est magique ce frame-
work ? Où se trouve le code qui permet de récupérer la date du jour ?
78
RÉFÉRENCER UNE ASSEMBLY
Dans des assemblys bien sûr ! Comme on l’a vu, les assemblys possèdent des fragments
de code compilés en langage intermédiaire. S’ils sont réutilisables, ils se trouvent dans
des fichiers dont l’extension est .dll.
Le framework .NET est composé d’une multitude d’assemblys qui sont installées sur
votre système d’exploitation, dans le « GAC » 2 qui est un endroit où sont stockées ces
assemblys afin de les rendre accessibles depuis nos programmes. Pour pouvoir utiliser
ces assemblys dans notre programme, nous devons indiquer que nous voulons les utiliser.
Pour ça, il faut les référencer dans le projet. Si l’on déplie les références dans notre
explorateur de solutions, nous pouvons voir que nous avons déjà pas mal de références
qui ont été ajoutées par Visual C# Express (voir figure 9.3).
Ce sont des assemblys qui sont très souvent utilisées, c’est pour ça que Visual C#
Express nous les a automatiquement référencées. Toujours ce souci de nous simplifier
le travail. Qu’il est sympa !
Prenons par exemple la référence System.Xml. Son nom nous suggère que dedans
est réuni tout ce qu’il nous faut pour manipuler le XML. Commençons par taper
System.Xml., la complétion automatique nous propose plusieurs choses (voir figure
9.4).
On ne sait pas du tout à quoi elle sert, mais déclarons par exemple une variable de
l’énumération ConformanceLevel :
2. GAC est l’acronyme de Global Assembly Cache.
79
CHAPITRE 9. UTILISER LE FRAMEWORK .NET
Figure 9.4 – L’espace de nom System.Xml est accessible si la référence est présente
Loin d’être bête, Visual C# Express nous affiche un message d’erreur plutôt explicite.
En effet, cette énumération est introuvable car elle est définie dans une assembly qui
n’est pas référencée. C’est comme si on nous demandait de prendre le marteau pour
enfoncer un clou, mais que le marteau n’est pas à côté de nous, mais au garage bien
rangé ! Il faut donc référencer le marteau afin de pouvoir l’utiliser. Pour ce faire, faisons
un clic sur Références et ajoutons une référence (voir figure 9.6).
Ici, nous avons plusieurs onglets (voir figure 9.7). Selon la version de Visual Studio que
vous possédez, vous aurez peut-être une présentation légèrement différente.
80
RÉFÉRENCER UNE ASSEMBLY
Figure 9.7 – Liste des assemblys présentes dans le GAC pouvant être référencées
81
CHAPITRE 9. UTILISER LE FRAMEWORK .NET
Eh oui, il existe une assembly qui n’apparaît pas dans les références et qui contient
tout le cœur du framework .NET. Cette assembly doit obligatoirement être référencée,
il s’agit de mscorlib.dll. Comme elle est obligatoire, elle est référencée par défaut dès
que l’on crée un projet.
D’autres exemples
Il arrivera souvent que vous ayez besoin d’une fonctionnalité trouvée dans la docu-
mentation ou sur internet et qu’il faille ajouter une référence. Ce sera peut-être une
référence au framework .NET, à des bibliothèques tierces ou à vous. Nous verrons plus
loin comment créer nos propres bibliothèques.
Dans la documentation MSDN, il est toujours indiqué quelle assembly il faut référencer
pour utiliser une fonctionnalité. Prenons par exemple la propriété DateTime.Now, la
documentation nous dit :
82
D’AUTRES EXEMPLES
Cela veut donc dire qu’on utilise la date du jour en utilisant l’assembly obligatoire
mscorlib et la fonctionnalité se trouve dans l’espace de nom System, comme déjà vu.
Il n’y a donc rien à faire pour pouvoir utiliser DateTime.Now. En imaginant que nous
ayons besoin de faire un programme qui utilise des nombres complexes, vous allez
sûrement avoir besoin du type Complex, trouvé dans la documentation adéquate, via
le code web suivant :
☛ ✟
Doc complex
⊲
✡Code web : 836961 ✠
Pour l’utiliser, comme indiqué, il va falloir référencer l’assembly System.Numerics.dll.
Rien de plus simple, répétons la procédure pour référencer une assembly et allons
trouver System.Numerics.dll, comme indiqué à la figure 9.8.
83
CHAPITRE 9. UTILISER LE FRAMEWORK .NET
permettant d’éviter de préfixer Complex par System.Numerics. Mais ça, vous l’aviez
trouvé tout seul, n’est-ce pas ?
(1 , 0)
Partie r é elle : 1
Partie imaginaire : 0
(0 ,707106781186548 , -0 ,707106781186547)
En résumé
– Le framework .NET est un ensemble d’assemblys qu’il faut référencer dans son projet
afin d’avoir accès à leurs fonctionnalités.
– On utilise le mot clé using pour inclure un espace de nom comme raccourci dans
son programme, ce qui permet de ne pas avoir à préfixer les types de leurs espaces
de noms complets.
84
Chapitre 10
TP : bonjour c’est le week-end !
Difficulté :
B
ienvenue dans ce premier TP ! Vous avez pu découvrir dans les chapitres précédents
les premières bases du langage C# permettant la construction d’applications. Il est
grand temps de mettre en pratique ce que nous avons appris. C’est ici l’occasion pour
vous de tester vos connaissances et de valider ce que vous appris en réalisant cet exercice.
85
CHAPITRE 10. TP : BONJOUR C’EST LE WEEK-END !
Valeur Traduction
Sunday Dimanche
Monday Lundi
Tuesday Mardi
Wednesday Mercredi
Thursday Jeudi
Friday Vendredi
Saturday Samedi
Il ne restera plus qu’à comparer deux valeurs d’énumération, comme on l’a vu dans le
chapitre sur les énumérations.
Voilà, vous avez tous les outils en main pour réaliser ce premier TP ! N’hésitez pas
à revenir sur les chapitres précédents si vous avez un doute sur la syntaxe ou sur les
instructions à réaliser. On ne peut pas apprendre un langage par cœur du premier coup.
86
CORRECTION
À vous de jouer !
Correction
Vous êtes autorisés à lire cette correction uniquement si vous vous êtes arraché les
cheveux sur ce TP ! Je vois qu’il vous en reste, encore un effort ! Si vous avez réussi
avec brio le TP, vous pourrez également comparer votre travail au mien.
Quoi qu’il en soit, voici la correction que je propose. Bien évidemment, il peut y en
avoir plusieurs, mais elle contient les informations nécessaires pour la réalisation de ce
TP.
Première chose à faire : créer un projet de type console. J’ai ensuite ajouté le code
suivant :
1 static void Main ( string [] args )
2 {
3 if ( DateTime . Now . DayOfWeek == DayOfWeek . Saturday ||
DateTime . Now . DayOfWeek == DayOfWeek . Sunday )
4 {
5 // nous sommes le week - end
6 Af fi ch erB on We ekE nd () ;
7 }
8 else
9 {
10 // nous sommes en semaine
11
12 if ( DateTime . Now . DayOfWeek == DayOfWeek . Monday &&
DateTime . Now . Hour < 9 )
13 {
14 // nous sommes le lundi matin
15 Af fi ch erB on We ekE nd () ;
16 }
17 else
18 {
19 if ( DateTime . Now . Hour >= 9 && DateTime . Now . Hour <
18 )
20 {
21 // nous sommes dans la journ é e
22 AfficherBonjour () ;
23 }
24 else
25 {
26 // nous sommes en soir é e
27
28 if ( DateTime . Now . DayOfWeek == DayOfWeek . Friday
&& DateTime . Now . Hour >= 18 )
29 {
30 // nous sommes le vendredi soir
31 Af fi ch erB on We ekE nd () ;
87
CHAPITRE 10. TP : BONJOUR C’EST LE WEEK-END !
32 }
33 else
34 {
35 AfficherBonsoir () ;
36 }
37 }
38 }
39 }
40 }
41
42 static void Af fic he rBo nW ee kEn d ()
43 {
44 Console . WriteLine ( " Bon week - end " + Environment . UserName ) ;
45 }
46
47 static void AfficherBonjour ()
48 {
49 Console . WriteLine ( " Bonjour " + Environment . UserName ) ;
50 }
51
52 static void AfficherBonsoir ()
53 {
54 Console . WriteLine ( " Bonsoir " + Environment . UserName ) ;
55 }
88
ALLER PLUS LOIN
Le premier test permet de vérifier que nous sommes soit samedi, soit dimanche, soit
que nous sommes lundi et que l’heure est inférieure à 9, soit que nous sommes vendredi
et que l’heure est supérieure à 18. Nous avons, pour ce faire, combiné les tests avec
l’opérateur logique « OU » : ||. Remarquons que les parenthèses nous permettent d’agir
sur l’ordre d’évaluation des conditions. Pour que ce soit le week-end, il faut bien sûr être
« vendredi et que l’heure soit supérieure à 18 » ou « lundi et que l’heure soit inférieure
à 9 » ou samedi ou dimanche.
On pourrait encore simplifier en évitant de solliciter à chaque fois le framework .NET
pour obtenir la date courante. Il suffit de stocker la date courante dans une variable de
type DateTime. Ce qui donnerait :
1 DateTime dateCourante = DateTime . Now ;
2
3 if ( dateCourante . DayOfWeek == DayOfWeek . Saturday ||
4 dateCourante . DayOfWeek == DayOfWeek . Sunday ||
5 ( dateCourante . DayOfWeek == DayOfWeek . Monday && dateCourante
. Hour < 9 ) ||
6 ( dateCourante . DayOfWeek == DayOfWeek . Friday && dateCourante
. Hour >= 18 ) )
7 {
8 // nous sommes le week - end
9 Af fi ch erB on We ekE nd () ;
10 }
89
CHAPITRE 10. TP : BONJOUR C’EST LE WEEK-END !
11 else
12 {
13 // nous sommes en semaine
14 if ( dateCourante . Hour >= 9 && dateCourante . Hour < 18 )
15 {
16 // nous sommes dans la journ é e
17 AfficherBonjour () ;
18 }
19 else
20 {
21 AfficherBonsoir () ;
22 }
23 }
On utilise ici le type DateTime comme le type string ou int. Il sert à gérer les dates
et l’heure. Il est légèrement différent des types que nous avons vus pour l’instant, nous
ne nous attarderons pas dessus. Nous aurons l’occasion de découvrir de quoi il s’agit
dans la partie suivante. Cette optimisation n’a rien d’extraordinaire, mais cela nous
évite un appel répété au framework .NET.
Voilà pour ce TP. J’espère que vous aurez réussi avec brio l’exercice. Vous avez pu
remarquer que ce n’était pas trop difficile. Il a simplement fallu réfléchir à comment
imbriquer correctement nos conditions.
N’hésitez pas à pratiquer et à vous entraîner avec d’autres problèmes de votre cru. Si
vous avez la moindre hésitation, vous pouvez relire les chapitres précédents. Vous verrez
que nous aurons l’occasion d’énormément utiliser ces instructions conditionnelles dans
tous les programmes que nous allons écrire !
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 106683 ✠
90
Chapitre 11
Les boucles
Difficulté :
N
ous les avons évoquées rapidement un peu plus tôt en parlant des tableaux et des
listes. Dans ce chapitre nous allons décrire plus précisément les boucles.
Elles sont souvent utilisées pour parcourir des éléments énumérables, comme le sont les
tableaux ou les listes. Elles peuvent également être utilisées pour effectuer la même action
tant qu’une condition n’est pas réalisée.
91
CHAPITRE 11. LES BOUCLES
La boucle for
La première instruction que nous avons aperçue est la boucle for. Elle permet de
répéter un bout de code tant qu’une condition est vraie. Souvent cette condition est un
compteur. Nous pouvons par exemple afficher un message 50 fois avec le code suivant :
1 int compteur ;
2 for ( compteur = 0 ; compteur < 50 ; compteur ++)
3 {
4 Console . WriteLine ( " Bonjour C # " ) ;
5 }
Ce qui affichera :
Bonjour C#
Bonjour C#
Bonjour C#
[... 50 fois en tout , mais abr é g é pour é conomiser du papier ...
!]
Bonjour C#
Bonjour C#
Bonjour C#
Nous définissons ici un entier compteur. Il est initialisé à 0 en début de boucle (compteur
= 0). Après chaque exécution du bloc de code du for, c’est-à-dire à chaque itération,
il va afficher « Bonjour C# ». À la fin de chaque itération, la variable compteur est
incrémentée (compteur++) et nous recommençons l’opération tant que la condition
« compteur < 50 » est vraie.
Bien sûr, la variable compteur est accessible dans la boucle et change de valeur à chaque
itération.
1 int compteur ;
2 for ( compteur = 0 ; compteur < 50 ; compteur ++)
3 {
4 Console . WriteLine ( " Bonjour C # " + compteur ) ;
5 }
Bonjour C # 0
Bonjour C # 1
Bonjour C # 2
Bonjour C # 3
[... abr é g é é galement ...]
Bonjour C # 48
Bonjour C # 49
92
LA BOUCLE FOR
En général, on utilise la boucle for pour parcourir un tableau. Ainsi, nous pourrons
utiliser le compteur comme indice pour accéder aux éléments du tableau :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 int indice ;
3 for ( indice = 0 ; indice < 7 ; indice ++)
4 {
5 Console . WriteLine ( jours [ indice ]) ;
6 }
Dans cette boucle, vu qu’à la première itération indice vaut 0, nous afficherons l’élé-
ment du tableau à la position 0, à savoir « Lundi ». À l’itération suivante, indice passe
à 1, nous affichons l’élément du tableau à la position 1, à savoir « Mardi », et ainsi de
suite :
Lundi
Mardi
Mercredi
Jeudi
Vendredi
Samedi
Dimanche
Attention à ce que l’indice ne dépasse pas la taille du tableau, sinon l’accès à un indice
en dehors des limites du tableau provoquera une erreur à l’exécution. Pour éviter ceci,
on utilise en général la taille du tableau comme condition de fin :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 int indice ;
3 for ( indice = 0 ; indice < jours . Length ; indice ++)
4 {
5 Console . WriteLine ( jours [ indice ]) ;
6 }
93
CHAPITRE 11. LES BOUCLES
9
7
5
3
1
Vous avez pu voir que dans cet exemple, nous avons défini l’entier i directement dans
l’instruction for. C’est une commodité qui permet que la variable i n’existe qu’à
l’intérieur de la boucle, car sa portée correspond aux accolades qui délimitent le for.
Attention, il est possible de faire un peu tout et n’importe quoi dans ces boucles. Aussi
il peut arriver que l’on se retrouve avec des bugs, comme des boucles infinies.
Par exemple, le code suivant :
1 for ( int indice = 0 ; indice < 7 ; indice - -)
2 {
3 Console . WriteLine ( " Test " + indice ) ;
4 }
est une boucle infinie. En effet, on modifie la variable indice en la décrémentant. Sauf
que la condition de sortie de la boucle est valable pour un indice qui dépasse ou égale
la valeur 7, ce qui n’arrivera jamais.
Si on exécute l’application avec ce code, la console va afficher à l’infini le mot « Test »
avec son indice. La seule solution pour quitter le programme sera de fermer brutalement
l’application.
L’autre solution est d’attendre que le programme se termine. . .
Oui c’est vrai, mais en fait, ici on se heurte à un cas limite du C#. C’est à cause de
la variable indice, qui est un entier que l’on décrémente. Au début il vaut zéro, puis
-1, puis -2, etc. Lorsque la variable indice arrive à la limite inférieure que le type
int est capable de gérer, c’est-à-dire -2147483648, se produit alors ce qu’on appelle
un dépassement de capacité. Sans rentrer dans les détails, il ne peut pas stocker
un entier plus petit et donc il boucle et repart à l’entier le plus grand, c’est-à-dire
2147483647.
Donc pour résumer, l’indice fait :
– 0
– -1
– -2
– ...
– -2147483647
– -2147483648
94
LA BOUCLE FOREACH
– +2147483647
Et comme là, il se retrouve supérieur à 7, la boucle se termine !
Test 0
Test -1
Test -2
[... abr é g é ...]
Test -2147483647
Test -2147483648
Donc, techniquement, ce n’est pas une boucle infinie, mais on a attendu tellement
longtemps pour atteindre ce cas limite que c’est tout comme.
Et surtout, nous tombons sur un cas imprévu. Ici, ça se termine plutôt bien, mais ça
aurait pu finir en crash de l’application.
Dans tous les cas, il faut absolument maîtriser ses conditions de sortie de
boucle pour éviter la boucle infinie, un des cauchemars du développeur !
La boucle foreach
Comme il est très courant d’utiliser les boucles pour parcourir un tableau ou une liste,
le C# dispose d’un opérateur particulier : foreach que l’on peut traduire par « pour
chaque » ; pour chaque élément du tableau faire ceci. . .
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 foreach ( string jour in jours )
3 {
4 Console . WriteLine ( jour ) ;
5 }
Cette boucle va nous permettre de parcourir tous les éléments du tableau jours. À
chaque itération, la boucle va créer une chaîne de caractères jour qui contiendra l’élé-
ment courant du tableau. À noter que la variable jour aura une portée égale au bloc
foreach. Nous pourrons ainsi utiliser cette valeur dans le corps de la boucle et pourquoi
pas l’afficher comme dans l’exemple précédent :
Lundi
Mardi
Mercredi
Jeudi
Vendredi
Samedi
Dimanche
95
CHAPITRE 11. LES BOUCLES
L’instruction foreach fonctionne aussi avec les listes. Par exemple le code suivant :
1 List < string > jours = new List < string > { " Lundi " , " Mardi " , "
Mercredi " , " Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 foreach ( string jour in jours )
3 {
4 Console . WriteLine ( jour ) ;
5 }
nous permettra d’afficher tous les jours de la semaine contenus dans la liste des jours.
Attention, la boucle foreach est une boucle en lecture seule. Cela veut dire qu’il n’est
pas possible de modifier l’élément de l’itération en cours.
Par exemple, le code suivant :
1 List < string > jours = new List < string > { " Lundi " , " Mardi " , "
Mercredi " , " Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 foreach ( string jour in jours )
3 {
4 jour = " pas de jour ! " ;
5 }
Il vous arrivera aussi sûrement un jour (ça arrive à tous les développeurs) de vouloir
modifier une liste en elle-même lors d’une boucle foreach. C’est-à-dire lui rajouter
un élément, en supprimer un autre, etc. C’est également impossible, car à partir du
moment où l’on rentre dans la boucle foreach, la liste devient non-modifiable.
Prenons l’exemple, ô combien classique, de la recherche d’une valeur dans une liste
pour la supprimer. Nous serions tentés de faire :
1 List < string > jours = new List < string > { " Lundi " , " Mardi " , "
Mercredi " , " Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 foreach ( string jour in jours )
3 {
96
LA BOUCLE FOREACH
ce qui semblerait tout à fait correct et en plus, ne provoque pas d’erreur de compilation.
Sauf que si vous exécutez l’application, vous aurez un message d’erreur :
Le programme nous indique alors que « la collection a été modifiée » et que « l’opération
d’énumération peut ne pas s’exécuter ».
Il est donc impossible de faire notre suppression ainsi.
Eh bien, plusieurs solutions existent. Celle qui vient en premier à l’esprit est d’utiliser
une boucle for par exemple :
1 for ( int i = 0 ; i < jours . Count ; i ++)
2 {
3 if ( jours [ i ]== " Mercredi " )
4 jours . Remove ( jours [ i ]) ;
5 }
Cette solution est intéressante ici, mais elle peut poser un problème dans d’autres
situations. En effet, vu que nous supprimons un élément de la liste, nous allons nous
retrouver avec une incohérence entre l’indice en cours et l’élément que nous essayons
d’atteindre. En effet, lorsque le jour courant est mercredi, l’indice i vaut 2. Si l’on
supprime cet élément, c’est jeudi qui va se retrouver en position 2. Et nous allons rater
son analyse car la boucle va continuer à l’indice 3, qui sera vendredi. On peut éviter ce
problème en parcourant la boucle à l’envers :
1 for ( int i = jours . Count - 1 ; i >= 0 ; i - -)
2 {
3 if ( jours [ i ] == " Mercredi " )
4 jours . Remove ( jours [ i ]) ;
5 }
97
CHAPITRE 11. LES BOUCLES
D’une manière générale, il ne faut pas modifier les collections que nous
sommes en train de parcourir.
Nous n’étudierons pas les autres solutions car elles font appel à des notions que nous
verrons en détail plus tard.
Après avoir lu ceci, vous pourriez avoir l’impression que la boucle foreach n’est pas
souple et difficilement exploitable : autant utiliser autre chose. . . Vous verrez à l’utili-
sation que non, elle est en fait très pratique. Il faut simplement connaître ses limites.
Voilà qui est chose faite !
Nous avons vu que l’instruction foreach permettait de boucler sur tous les éléments
d’un tableau ou d’une liste. En fait, il est possible de parcourir tous les types qui sont
énumérables. Nous verrons plus loin ce qui caractérise un type énumérable, car pour
l’instant, c’est un secret ! Chuuut. . .
La boucle while
La boucle while est en général moins utilisée que for ou foreach. C’est la boucle qui
va nous permettre de faire quelque chose tant qu’une condition n’est pas vérifiée. Elle
ressemble de loin à la boucle for, mais la boucle for se spécialise dans le parcours de
tableau tandis que la boucle while est plus générique.
Par exemple :
1 int i = 0 ;
2 while ( i < 50 )
3 {
4 Console . WriteLine ( " Bonjour C # " ) ;
5 i ++;
6 }
Ce code permet d’écrire « Bonjour C# » 50 fois de suite. Ici, la condition du while est
évaluée en début de boucle.
Dans l’exemple suivant :
1 int i = 0 ;
2 do
3 {
4 Console . WriteLine ( " Bonjour C # " ) ;
5 i ++;
6 }
7 while ( i < 50 ) ;
98
LA BOUCLE WHILE
contrario, on passera au moins une fois dans le corps de la boucle du second exemple,
même si l’entier i est initialisé à 50 car la condition de sortie de boucle est évaluée à
la fin.
Les conditions de sortie de boucles ne portent pas forcément sur un compteur ou un
indice, n’importe quelle expression peut être évaluée. Par exemple :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 int i = 0 ;
3 bool trouve = false ;
4 while (! trouve )
5 {
6 string valeur = jours [ i ];
7 if ( valeur == " Jeudi " )
8 {
9 trouve = true ;
10 }
11 else
12 {
13 i ++;
14 }
15 }
16 Console . WriteLine ( " Trouv é à l ' indice " + i ) ;
Le code précédent va répéter les instructions contenues dans la boucle while tant
que le booléen trouve sera faux (c’est-à-dire qu’on va s’arrêter dès que le booléen sera
vrai). Nous analysons la valeur pour l’indice i. Si la valeur est celle cherchée, alors nous
passons le booléen à vrai et nous pourrons sortir de la boucle. Sinon, nous incrémentons
l’indice pour passer au suivant.
Attention encore aux valeurs de sortie de la boucle. Si nous ne trouvons pas la chaîne
recherchée, alors i continuera à s’incrémenter ; le booléen ne passera jamais à vrai, nous
resterons bloqués dans la boucle et nous risquons d’atteindre un indice qui n’existe pas
dans le tableau. Il serait plus prudent que la condition porte également sur la taille du
tableau, par exemple :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 int i = 0 ;
3 bool trouve = false ;
4 while ( i < jours . Length && ! trouve )
5 {
6 string valeur = jours [ i ];
7 if ( valeur == " Jeudi " )
8 {
9 trouve = true ;
10 }
11 else
12 {
13 i ++;
99
CHAPITRE 11. LES BOUCLES
14 }
15 }
16 if (! trouve )
17 Console . WriteLine ( " Valeur non trouv é e " ) ;
18 else
19 Console . WriteLine ( " Trouv é à l ' indice " + i ) ;
Ainsi, si l’indice est supérieur à la taille du tableau, nous sortons de la boucle et nous
éliminons le risque de boucle infinie.
Une erreur classique est que la condition ne devienne jamais vraie à cause d’une erreur
de programmation. Par exemple, si j’oublie d’incrémenter la variable i, alors à chaque
passage de la boucle j’analyserai toujours la première valeur du tableau et je n’atteindrai
jamais la taille maximale du tableau ; condition qui me permettrait de sortir de la
boucle.
100
LES INSTRUCTIONS BREAK ET CONTINUE
1 List < string > jours = new List < string > { " Lundi " , " Mardi " , "
Mercredi " , " Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 bool trouve = false ;
3 foreach ( string jour in jours )
4 {
5 if ( jour == " Jeudi " )
6 {
7 trouve = true ;
8 break ;
9 }
10 }
En résumé
– On utilise la boucle for pour répéter des instructions tant qu’une condition n’est
pas vérifiée, les éléments de la condition changeant à chaque itération.
101
CHAPITRE 11. LES BOUCLES
– On utilise en général la boucle for pour parcourir un tableau, avec un indice qui
s’incrémente à chaque itération.
– La boucle foreach est utilisée pour simplifier le parcours des tableaux ou des listes.
– La boucle while permet de répéter des instructions tant qu’une condition n’est pas
vérifiée. C’est la boucle la plus souple.
– Il faut faire attention aux conditions de sortie d’une boucle afin d’éviter les boucles
infinies qui font planter l’application.
102
Chapitre 12
TP : calculs en boucle
Difficulté :
Ç
a y est, grâce au chapitre précédent, vous devez avoir une bonne idée de ce que sont
les boucles. Par contre, vous ne voyez peut-être pas encore complètement qu’elles
vont vous servir tout le temps. C’est un élément qu’il est primordial de maîtriser. Il
vous faut donc vous entraîner pour être bien sûrs d’avoir tout compris. C’est justement
l’objectif de ce deuxième TP. Finie la théorie des boucles, place à la pratique en boucle !
Notre but est de réaliser des calculs qui vont avoir besoin des for et des foreach. Vous
êtes prêts ? Alors, c’est parti !
103
CHAPITRE 12. TP : CALCULS EN BOUCLE
Enfin, pour la dernière méthode, vous allez devoir trouver tous les multiples de 3 et
de 5. Le plus simple, à mon avis, pour calculer tous les multiples de 3, est de faire une
boucle qui démarre à 3 et d’avancer de 3 en 3 jusqu’à la valeur souhaitée. On fait de
même pour les multiples de 5.
104
CORRECTION
Ensuite, il sera nécessaire de faire deux boucles imbriquées afin de déterminer les in-
tersections. C’est-à-dire parcourir la liste des multiples de 3 et à l’intérieur de cette
boucle, parcourir la liste des multiples de 5. On compare les deux éléments ; s’ils sont
égaux, c’est qu’ils sont communs aux deux listes.
Voilà, vous devriez avoir tous les éléments en main pour réussir ce TP, c’est à vous de
jouer !
Correction
J’imagine que vous avez réussi ce TP, non pas sans difficultés, mais à force de tâton-
nements, vous avez sans doute réussi. Bravo ! Quoi qu’il en soit, voici la correction que
je propose.
– Première méthode :
1 static int Ca lcu lS om meE nt ie rs ( int borneMin , int borneMax )
2 {
3 int resulat = 0 ;
4 for ( int i = borneMin ; i <= borneMax ; i ++)
5 {
6 resulat += i ;
7 }
8 return resulat ;
9 }
Ce n’était pas très compliqué, il faut dans un premier temps construire une méthode qui
renvoie un entier et qui accepte deux entiers en paramètres. Ensuite, on boucle grâce
à l’instruction for de la borne inférieure à la borne supérieure (incluse, donc il faut
utiliser l’opérateur de comparaison <= ) en incrémentant un compteur de 1 à chaque
itération. Nous ajoutons la valeur du compteur à un résultat, que nous retournons en
fin de boucle.
– Seconde méthode :
1 static double CalculMoyenne ( List < double > liste )
2 {
3 double somme = 0 ;
4 foreach ( double valeur in liste )
5 {
6 somme += valeur ;
7 }
8 return somme / liste . Count ;
9 }
Ici, le principe est grosso modo le même, la différence est que la méthode retourne
un double et accepte une liste de double en paramètres. Ici, nous utilisons la boucle
foreach pour parcourir tous les éléments que nous ajoutons à un résultat. Enfin, nous
retournons ce résultat divisé par la taille de la liste.
105
CHAPITRE 12. TP : CALCULS EN BOUCLE
– Troisième méthode :
1 static int C a l c u l S o m m e I n t e r s e c t i o n ()
2 {
3 List < int > multiplesDe3 = new List < int >() ;
4 List < int > multiplesDe5 = new List < int >() ;
5
6 for ( int i = 3 ; i <= 100 ; i += 3 )
7 {
8 multiplesDe3 . Add ( i ) ;
9 }
10 for ( int i = 5 ; i <= 100 ; i += 5 )
11 {
12 multiplesDe5 . Add ( i ) ;
13 }
14
15 int somme = 0 ;
16 foreach ( int m3 in multiplesDe3 )
17 {
18 foreach ( int m5 in multiplesDe5 )
19 {
20 if ( m3 == m5 )
21 somme += m3 ;
22 }
23 }
24 return somme ;
25 }
106
ALLER PLUS LOIN
– m5 vaut 10 ;
– on compare 3 à 10, ils sont différents ;
– de même jusqu’à ce qu’on ait fini de parcourir la liste des multiplesDe5 ;
– on passe à l’itération suivante de la liste multiplesDe3 ;
– m3 vaut 6 ;
– on rentre dans la boucle qui parcoure la liste multiplesDe5 ;
– m5 vaut 5 ;
– on compare 6 à 5, ils sont différents ;
– on passe à l’itération suivante de la liste multiplesDe5 ;
– etc.
Il est aussi possible de parcourir les listes avec un for et d’accéder aux éléments de la
liste avec un indice, comme on le fait pour un tableau. Ce qui donnerait :
1 static double CalculMoyenne ( List < double > liste )
2 {
3 double somme = 0 ;
4 for ( int i = 0 ; i < liste . Count ; i ++)
5 {
6 somme += liste [ i ];
7 }
8 return somme / liste . Count ;
9 }
Notez qu’on se sert de liste.Count pour obtenir la taille de la liste et qu’on accède à
l’élément courant avec liste[i]. Je reconnais que la boucle foreach est plus explicite,
mais cela peut être utile de connaître le parcours d’une liste avec la boucle for. À vous
de voir.
Vous aurez également noté ma technique pour avoir des multiples de 3 et de 5. Il y en
a plein d’autres. Je pense à une en particulier et qui fait appel à des notions que nous
avons vues auparavant : la division entière et la division de double.
107
CHAPITRE 12. TP : CALCULS EN BOUCLE
Pour savoir si un nombre est un multiple d’un autre, il suffit de les diviser entre eux et
de voir s’il y a un reste à la division. On peut faire cela de deux façons. La première,
en comparant la division entière avec la division double et en vérifiant que le résultat
est le même. Si le résultat est le même, c’est qu’il n’y a pas de chiffres après la virgule :
1 for ( int i = 1 ; i <= 100 ; i ++)
2 {
3 if ( i / 3 == i / 3 . 0 )
4 multiplesDe3 . Add ( i ) ;
5 if ( i / 5 == i / 5 . 0 )
6 multiplesDe5 . Add ( i ) ;
7 }
Cette technique fait appel à des notions que nous n’avons pas encore vues :
comment se fait-il qu’on puisse comparer un entier à un double ? Nous le
verrons un peu plus tard.
L’autre solution est d’utiliser l’opérateur modulo que nous avons vu précédemment qui
fait justement ça :
1 for ( int i = 1 ; i <= 100 ; i ++)
2 {
3 if ( i % 3 == 0 )
4 multiplesDe3 . Add ( i ) ;
5 if ( i % 5 == 0 )
6 multiplesDe5 . Add ( i ) ;
7 }
L’avantage de ces deux techniques est qu’on peut construire les deux listes de multiples
en une seule boucle.
Voilà, c’est fini pour ce TP. N’hésitez pas à vous faire la main sur ces boucles car il est
fondamental que vous les maîtrisiez. Faites-vous plaisir, tentez de les repousser dans
leurs limites, essayez de résoudre d’autres problèmes. . . L’important, c’est de pratiquer.
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 764808 ✠
108
Deuxième partie
109
Chapitre 13
Les conversions entre les types
Difficulté :
N
ous avons vu que le C# est un langage qui possède plein de types de données
différents : entier, décimal, chaîne de caractères, etc.
Dans nos programmes, nous allons très souvent avoir besoin de manipuler des données
entre elles alors qu’elles ne sont pas forcément du même type. Lorsque cela sera possible,
nous aurons besoin de convertir un type de données en un autre.
111
CHAPITRE 13. LES CONVERSIONS ENTRE LES TYPES
Plus précisément, cela fonctionne pour les types qui sont compatibles entre eux, enten-
dez par là « qui se ressemblent ».
Par exemple, l’entier et le petit entier se ressemblent. Pour rappel, nous avons :
Type Description
short Entier de -32768 à 32767
int Entier de -2147483648 à 2147483647
Ils ne diffèrent que par leur capacité. Le short ne pourra pas contenir le nombre 100000
par exemple, alors que l’int le pourra.
Nous ne l’avons pas encore fait, mais le C# nous autorise à faire :
1 short s = 200 ;
2 int i = s ;
En effet, il est toujours possible de stocker un petit entier dans un grand. Peu importe
la valeur de s, i sera toujours à même de contenir sa valeur. C’est comme dans une
voiture. Si nous arrivons à tenir à 5 dans un monospace, nous pourrons facilement tenir
à 5 dans un bus.
L’inverse n’est pas vrai bien sûr. Si nous tenons à 100 dans un bus, ce n’est pas certain
que nous pourrons tenir dans le monospace, même en se serrant bien.
Ce qui fait que si nous faisons :
1 int i = 100000 ;
2 short s = i ;
Eh oui, comment pouvons-nous faire rentrer 100000 dans un type qui ne peut aller que
jusqu’à 32767 ? Le compilateur le sait bien.
Vous remarquerez que nous aurons cependant la même erreur si nous tentons de faire :
1 int i = 200 ;
2 short s = i ;
112
ENTRE LES TYPES COMPATIBLES : LE CASTING
Oui tout à fait, mais le compilateur nous avertit quand même. Il nous dit :
« Attention, vous essayez de faire rentrer les personnes du bus dans un mo-
nospace, êtes-vous bien sûr ? »
« Oui, oui, je sais qu’il y a très peu de personnes dans le bus et qu’ils pourront
rentrer sans aucun problème dans le monospace. Fais-moi confiance ! »
Pour faire un tel cast, il suffit de faire précéder la variable à convertir du nom du
type souhaité entre parenthèses. Cela permet d’indiquer à notre compilateur que nous
savons ce que nous faisons, et que l’entier tiendra correctement dans le petit entier.
Mais attention, le compilateur nous fait confiance. Nous sommes le boss ! Cela implique
une certaine responsabilité, il ne faut pas faire n’importe quoi.
Si nous faisons :
1 int i = 40000 ;
2 short s = ( short ) i ;
3 Console . WriteLine ( s ) ;
4 Console . WriteLine ( i ) ;
nous tentons de faire rentrer un trop gros entier dans ce qu’est capable de stocker le
petit entier.
Si nous exécutons notre application, nous aurons un résultat surprenant :
-25536
40000
113
CHAPITRE 13. LES CONVERSIONS ENTRE LES TYPES
Le résultat n’est pas du tout celui attendu. Nous avons fait n’importe quoi, le C# nous
a punis !
En fait, plus précisément, il s’est passé ce qu’on appelle un dépassement de capacité.
Nous l’avons déjà vu lors du chapitre sur les boucles. Ici, il s’est produit la même chose.
Le petit entier est allé à sa valeur maximale puis a bouclé en repartant de sa valeur
minimale.
Bref, tout ça pour dire que nous obtenons un résultat inattendu. Il faut donc faire
attention à ce que l’on fait et honorer la confiance que nous porte le compilateur en
faisant bien attention à ce que l’on caste. 1 .
D’une façon similaire à l’entier et au petit entier, l’entier et le double se ressemblent un
peu. Ce sont tous les deux des types permettant de contenir des nombres. Le premier
permet de contenir des nombres entiers, le deuxième peut contenir des nombres à
virgules.
Ainsi, nous pouvons écrire le code C# suivant :
1 int i = 20 ;
2 double d = i ;
En effet, un double est plus précis qu’un entier. Il est capable d’avoir des chiffres après
la virgule alors que l’entier ne le peut pas. Ce qui fait que le double est entièrement
capable de stocker toute la valeur d’un entier sans perdre d’information dans cette
affectation.
Par contre, comme on peut s’y attendre, l’inverse n’est pas possible. Le code suivant :
1 double prix = 125 . 55 ;
2 int valeur = prix ;
En effet, un double étant plus précis qu’un entier, si nous mettons ce double dans
l’entier nous allons perdre notamment les chiffres après la virgule.
Il restera encore une fois à demander au compilateur de nous faire confiance : « OK,
ceci est un double, mais ce n’est pas grave, je veux l’utiliser comme un entier ! Oui, oui,
même si je sais que je vais perdre les chiffres après la virgule ».
Ce qui s’écrit en C# de cette manière, comme nous l’avons vu :
1 double prix = 125 . 55 ;
2 int valeur = ( int ) prix ; // valeur vaut 125
Nous faisons précéder la variable prix du type dans lequel nous voulons la convertir
en utilisant les parenthèses.
1. Nous utiliserons souvent cet anglicisme, qui signifiera bien sûr que nous convertissons des types
qui se ressemblent entre eux, pour le plus grand malheur des professeurs de français.
114
ENTRE LES TYPES COMPATIBLES : LE CASTING
C’est plutôt sympa comme instruction. Mais n’oubliez pas que cette opération est
possible uniquement avec les types qui se ressemblent entre eux.
Par exemple, le cast suivant est impossible :
1 string nombre = " 123 " ;
2 int valeur = ( int ) nombre ;
car les deux types sont trop différents et sont incompatibles entre eux. Même si la
chaîne de caractères représente un nombre.
Nous verrons plus loin comment convertir une chaîne en entier. Pour l’instant, Visual
C# Express nous génère une erreur de compilation :
Le message d’erreur est plutôt explicite. Il ne nous propose même pas d’utiliser
de cast, il considère que les types sont vraiment trop différents !
Nous avons vu précédemment que les énumérations représentaient des valeurs entières.
Il est donc possible de récupérer l’entier correspondant à une valeur de l’énumération
grâce à un cast.
Par exemple, en considérant l’énumération suivante :
1 enum Jours
2 {
3 Lundi = 5 , // lundi vaut 5
4 Mardi , // mardi vaut 6
5 Mercredi = 9 , // mercredi vaut 9
6 Jeudi = 10 , // jeudi vaut 10
7 Vendredi , // vendredi vaut 11
8 Samedi , // samedi vaut 12
9 Dimanche = 20 // dimanche vaut 20
10 }
Le code suivant :
1 int valeur = ( int ) Jours . Lundi ; // valeur vaut 5
115
CHAPITRE 13. LES CONVERSIONS ENTRE LES TYPES
Cette méthode, bien que simple d’utilisation, présente un inconvénient dans le cas où
la chaîne ne représente pas un entier. À ce moment-là, la conversion va échouer et le
C# va renvoyer une erreur au moment de l’exécution.
Par exemple :
1 string chaineAge = " vingt ans " ;
2 int age = Convert . ToInt32 ( chaineAge ) ;
Si vous exécutez ce bout de code, vous verrez que la console nous affiche ce que l’on
appelle une exception :
Il s’agit tout simplement d’une erreur. Nous aurons l’occasion d’étudier plus en détail
les exceptions dans les chapitres ultérieurs. Pour l’instant, on a juste besoin de voir que
ceci ne nous convient pas. L’erreur est explicite : « Le format de la chaîne d’entrée est
incorrect », mais cela se passe au moment de l’exécution et notre programme plante
lamentablement. Nous verrons dans le chapitre sur les exceptions comment gérer les
erreurs.
116
ENTRE LES TYPES INCOMPATIBLES
Bien sûr, il se passera exactement la même chose que précédemment quand la chaîne
ne représentera pas un entier correct.
Il existe par contre une autre façon de convertir une chaîne en entier qui ne provoque
pas d’erreur quand le format n’est pas correct et qui nous informe si la conversion s’est
bien passée ou non, c’est la méthode int.TryParse() qui s’utilise ainsi :
1 string chaineAge = " ab20cd " ;
2 int age ;
3 if ( int . TryParse ( chaineAge , out age ) )
4 {
5 Console . WriteLine ( " La conversion est possible , age vaut " +
age ) ;
6 }
7 else
8 {
9 Console . WriteLine ( " Conversion impossible " ) ;
10 }
Conversion impossible
La méthode int.TryParse nous renvoie vrai si la conversion est bonne, et faux sinon.
Nous pouvons donc effectuer une action en fonction du résultat grâce aux if/else.
On passe en paramètres la chaîne à convertir et la variable où l’on veut stocker le
résultat de la conversion. Le mot-clé out signifie juste que la variable est modifiée par
la méthode.
Les méthodes que nous venons de voir Convert.ToString() ou int.TryParse() se
déclinent en général pour tous les types de base, par exemple double.TryParse() ou
Convert.ToDecimal(), etc.
En résumé
– Il est possible, avec le casting, de convertir la valeur d’un type dans un autre lorsqu’ils
sont compatibles entre eux.
– Le casting explicite s’utilise en préfixant une variable par un type précisé entre pa-
renthèses.
– Le framework .NET possède des méthodes permettant de convertir des types incom-
patibles entre eux s’ils sont sémantiquement proches.
117
CHAPITRE 13. LES CONVERSIONS ENTRE LES TYPES
118
Chapitre 14
Lire le clavier dans la console
Difficulté :
N
ous avons beaucoup écrit avec Console.WriteLine(), maintenant il est temps de
savoir lire. En l’occurrence, il faut être capable de récupérer ce que l’utilisateur a saisi
au clavier. En effet, un programme qui n’a pas d’interaction avec l’utilisateur, ce n’est
pas vraiment intéressant. Vous imaginez un jeu où on ne fait que regarder ? Ça s’appelle
une vidéo, c’est intéressant aussi, mais ce n’est pas pareil !
Aujourd’hui, il existe beaucoup d’interfaces de communication que l’on peut utiliser sur
un ordinateur (clavier, souris, doigts, manettes,. . .). Dans ce chapitre nous allons regarder
comment savoir ce que l’utilisateur a saisi au clavier dans une application console.
119
CHAPITRE 14. LIRE LE CLAVIER DANS LA CONSOLE
Lorsque notre application rencontre cette instruction, elle se met en pause et attend
une saisie de la part de✄ l’utilisateur. La saisie s’arrête lorsque l’utilisateur valide ce qu’il
a écrit avec la touche ✂Entrée ✁. Ainsi les instructions suivantes :
1 Console . WriteLine ( " Veuillez saisir une phrase et valider avec
la touche \" Entr é e \" " ) ;
2 string saisie = Console . ReadLine () ;
3 Console . WriteLine ( " Vous avez saisi : " + saisie ) ;
Veuillez saisir une phrase et valider avec la touche " Entr é e"
Bonjour à tous
Vous avez saisi : Bonjour à tous
Vous aurez remarqué que nous stockons le résultat de la saisie dans une variable de
type chaîne de caractères. C’est bien souvent le seul type que nous aurons à notre
disposition lors des saisies utilisateurs. Cela veut bien sûr dire que si vous avez besoin
de manipuler l’âge de la personne, il faudra la convertir en entier. Par exemple :
1 bool ageIsValid = false ;
2 int age = -1 ;
3 while (! ageIsValid )
4 {
5 Console . WriteLine ( " Veuillez saisir votre â ge " ) ;
6 string saisie = Console . ReadLine () ;
7 if ( int . TryParse ( saisie , out age ) )
8 ageIsValid = true ;
9 else
10 {
11 ageIsValid = false ;
12 Console . WriteLine ( "L ' â ge que vous avez saisi est
incorrect ... " ) ;
13 }
14 }
15 Console . WriteLine ( " Votre â ge est de : " + age ) ;
120
LIRE UN CARACTÈRE
Ce code est facile à comprendre maintenant que vous maîtrisez les boucles et les condi-
tions. Nous commençons par initialiser nos variables. Ensuite, nous demandons à l’uti-
lisateur de saisir son âge. Nous tentons de le convertir en entier. Si cela fonctionne, on
pourra passer à la suite, sinon, tant que l’âge n’est pas valide, nous recommençons la
boucle.
Il est primordial de toujours vérifier ce que saisit l’utilisateur, cela vous évi-
tera beaucoup d’erreurs et de plantages intempestifs. On dit souvent qu’en
informatique, il ne faut jamais faire confiance à l’utilisateur !
Lire un caractère
Il peut arriver que nous ayons besoin de ne saisir qu’un seul caractère. Pour cela, nous
allons pouvoir utiliser la méthode Console.ReadKey(). Nous pourrons nous en servir
par exemple pour faire une pause dans notre application. Le code suivant réclame
l’attention de l’utilisateur avant de démarrer un calcul hautement important :
1 Console . WriteLine ( " Veuillez appuyer sur une touche pour d é
marrer le calcul ... " ) ;
2 Console . ReadKey ( true ) ;
3 int somme = 0 ;
4 for ( int i = 0 ; i < 100 ; i ++)
5 {
6 somme += i ;
7 }
8 Console . WriteLine ( somme ) ;
Notez que nous avons passé true en paramètre de la méthode afin d’indiquer au C#
que nous ne souhaitions pas que notre saisie apparaisse
✄ à l’écran. Si le paramètre avait
été false et que j’avais par exemple appuyé sur ✂H ✁pour démarrer le calcul, le résultat
se serait vu précédé d’un disgracieux « H ». . .
121
CHAPITRE 14. LIRE LE CLAVIER DANS LA CONSOLE
tester la valeur Key de cette variable qui est une énumération du type ConsoleKey. Par
exemple :
✄
Nous comparons donc à la valeur ConsoleKey.O qui représente la touche ✂O ✁.
Notez que le type ConsoleKeyInfo est ce qu’on appelle une structure et que Key est
une propriété de la structure. Nous reviendrons dans la partie suivante sur ce que cela
veut vraiment dire. Pour l’instant, considérez juste qu’il s’agit d’une variable évoluée
qui permet de contenir plusieurs choses. . .
Quand même, je trouve que c’est super de pouvoir lire les saisies de l’utilisateur !
C’est vital pour toute application qui se respecte. Aujourd’hui, dans une application
moderne, il est primordial de savoir lire la position de la souris, réagir à des clics sur
des boutons, ouvrir des fenêtres, etc. C’est quelque chose qu’il n’est pas possible à faire
avec les programmes en mode console. N’oubliez pas que ces applications sont parfaites
pour apprendre le C#, cependant il est inimaginable de réaliser une application digne
de ce nom avec une application console.
Pour ce faire, on utilisera des systèmes graphiques adaptés, qui sortent du cadre d’étude
de ce livre.
122
LIRE UN CARACTÈRE
En résumé
– La méthode Console.ReadLine nous permet de lire des informations saisies par
l’utilisateur au clavier. ✄
– Il est possible de lire toute une phrase terminée par la touche ✂Entrée ✁ou seulement
un unique caractère.
123
CHAPITRE 14. LIRE LE CLAVIER DANS LA CONSOLE
124
Chapitre 15
Utiliser le débogueur
Difficulté :
N
ous allons maintenant faire une petite pause. Le C# c’est bien, mais notre envi-
ronnement de développement, Visual C# Express, peut faire beaucoup plus que sa
fonction basique d’éditeur de fichiers. Il possède un outil formidable qui va nous per-
mettre d’être très efficaces dans le débogage de nos applications et dans la compréhension
de leur fonctionnement.
Il s’agit du débogueur. Découvrons vite à quoi il sert et comment il fonctionne !
125
CHAPITRE 15. UTILISER LE DÉBOGUEUR
À quoi ça sert ?
Fidèle à son habitude de nous simplifier la vie, Visual C# Express possède un débo-
gueur. C’est un outil très pratique qui va permettre d’obtenir plein d’informations sur
le déroulement d’un programme.
Il va permettre d’exécuter les instructions les unes après les autres, de pouvoir observer
le contenu des variables, de revenir en arrière, bref, de savoir exactement ce qui se passe
et surtout pourquoi tel bout de code ne fonctionne pas.
Pour l’utiliser, il faut que Visual C# Express soit en mode « débogage ». Je✄n’en ai
pas encore parlé. Pour ce faire, il faut exécuter l’application en appuyant sur ✂F5 ✁, ou
alors passer par le menu Déboguer > Démarrer le débogage, ou encore cliquer sur le
petit triangle vert dont l’icône rappelle un bouton de lecture (voir figure 15.1).
126
METTRE UN POINT D’ARRÊT ET AVANCER PAS À PAS
25 somme += m3 ;
26 }
27 }
28 return somme ;
29 }
Il est possible de mettre autant de points d’arrêt que nous le souhaitons, à n’importe
quel endroit de notre code.
✄
Lançons l’application avec ✂F5 ✁. Nous pouvons voir que Visual C# Express s’arrête sur
la ligne prévue en la surlignant en jaune (voir figure 15.3).
Nous en profitons pour remarquer au niveau du carré rouge, que le débogueur est en
pause et qu’il est possible, soit de continuer l’exécution (triangle), soit de l’arrêter
(carré), soit enfin de redémarrer le programme depuis le début (carré avec une flèche
blanche dedans). Nous pouvons désormais exécuter notre code pas à pas, en nous
servant des icônes à côté ou des raccourcis clavier indiqués à la figure 15.4.
127
CHAPITRE 15. UTILISER LE DÉBOGUEUR
Figure 15.3 – Visual C# Express est arrêté sur une ligne du code
Figure 15.4 – Les boutons permettant d’avancer pas à pas dans l’application
128
OBSERVER DES VARIABLES
✄
Utilisons la touche ✂F10 ✁pour continuer l’exécution du code pas à pas.✄ La ligne suivante
se trouve surlignée à son tour. Appuyons à nouveau sur la touche ✂F10 ✁ pour aller à
l’instruction suivante ; il n’y en a plus : le programme se termine.
Vous me direz : « c’est bien beau, mais nous ne sommes pas passés dans la méthode
CalculSommeIntersection()
✄ ». Eh oui, c’est parce que nous avons utilisé la touche
✂F10 ✁qui est
✄ le pas à pas principal. Pour rentrer dans la méthode, il aurait fallu utiliser
la touche ✂F11 ✁qui est le pas à pas détaillé.
Si nous souhaitons que le programme se poursuive pour✄ aller jusqu’au prochain point
d’arrêt, il suffit d’appuyer sur le triangle (play) ou sur ✂F5 ✁.
Relançons notre programme en mode débogage, ✄ et cette fois-ci, lorsque le débogueur
s’arrête sur notre point d’arrêt, appuyons sur ✂F11 ✁pour rentrer dans le corps de la mé-
thode. Voilà, nous sommes dans✄ la méthode CalculSommeIntersection(). Continuons
à appuyer plusieurs fois sur ✂F10 ✁afin de rentrer dans le corps de la boucle for.
Il nous indique que i vaut 1, ce qui est normal, car nous sommes dans ✄ la première
itération de la boucle. Si nous continuons le parcours en appuyant sur ✂F10 ✁plusieurs
fois, nous voyons que la valeur de i augmente, conformément
✄ à ce qui est attendu.
Maintenant, mettons un point d’arrêt (avec la touche ✂F9 ✁) sur la ligne :
1 multiplesDe3 . Add ( i ) ;
✄
et poursuivons✄ l’exécution en appuyant sur ✂F5 ✁. Il s’arrête au moment où i vaut 3.
Appuyons sur ✂F10 ✁pour exécuter l’ajout de i à la liste et passons la souris sur la liste
(voir figure 15.6).
Visual C# Express nous indique que la liste multiplesDe3 a son Count qui vaut 1. Si
nous cliquons sur le + pour déplier la liste, nous pouvons voir que 3 a été ajouté dans
le premier emplacement de la liste (indice 0). Si nous continuons l’exécution plusieurs
fois, nous voyons que les listes se remplissent au fur et à mesure (voir figure 15.7).
129
CHAPITRE 15. UTILISER LE DÉBOGUEUR
✄
Enlevez le point d’arrêt sur la ligne en appuyant à nouveau sur ✂F9 ✁ et mettez un
nouveau point d’arrêt sur la ligne :
1 int somme = 0 ;
✄
Poursuivez l’exécution avec ✂F5 ✁, la boucle est terminée, nous pouvons voir à la figure
15.8 que les listes sont complètement remplies !
Grâce au débogueur, nous pouvons voir vraiment tout ce qui nous intéresse, c’est une
des grandes forces du débogueur et c’est un atout vraiment très utile pour comprendre
ce qui se passe dans un programme (en général, ça se passe mal !).
Il est également possible de voir les variables locales en regardant en bas dans la fenêtre
Variables locales. Dans cette fenêtre, vous pourrez observer les variables qui sont
130
REVENIR EN ARRIÈRE
couramment accessibles. Il existe également une fenêtre Espion qui permet, de la même
façon, de surveiller une ou plusieurs variables précises (voir figure 15.9).
Figure 15.9 – La fenêtre des variables locales permet de voir le contenu des variables
qui sont dans la portée
Revenir en arrière
C’est presque ça, Doc ! Si vous souhaitez réexécuter un bout de code, parce que vous
n’avez pas bien vu ou que vous avez raté l’information qu’il vous fallait, vous pouvez
forcer le débogueur à revenir en arrière dans le programme. Pour cela, vous avez deux
solutions. Soit vous faites un clic droit à l’endroit souhaité et vous choisissez l’élément
de menu Définir l’instruction suivante, comme indiqué à la figure 15.10 ; soit
vous déplacez avec la souris la petite flèche jaune sur la gauche qui indique l’endroit
où nous en sommes, comme l’illustre la figure 15.11.
Il faut faire attention, car ce retour en arrière n’en est pas complètement un. En effet,
si vous revenez au début de la boucle qui calcule les multiples et que vous continuez
l’exécution, vous verrez que la liste continue à se remplir. À la fin de la boucle, au lieu
de contenir 33 éléments, la liste des multiples de 3 en contiendra 66. En effet, à aucun
moment nous n’avons vidé la liste et donc le fait de réexécuter cette partie de code
risque de provoquer des comportements inattendus. Ici, il vaudrait mieux revenir au
début de la méthode afin que la liste soit de nouveau créée.
Même si ça ne vous saute pas aux yeux pour l’instant, vous verrez à l’usage que cette
possibilité est bien pratique. D’autant plus quand vous aurez bien assimilé toutes les
notions de portée de variable.
131
CHAPITRE 15. UTILISER LE DÉBOGUEUR
Mais rassurez-vous, celui-ci est quand même déjà bien avancé et permet de faire beau-
coup de choses.
La pile des appels est également affichée lorsqu’on rencontre une erreur, elle
permettra d’identifier plus facilement où est le problème. Vous l’avez vu dans
le chapitre sur les conversions entre des types incompatibles.
132
LA PILE DES APPELS
133
CHAPITRE 15. UTILISER LE DÉBOGUEUR
En tout cas, le débogueur est vraiment un outil à forte valeur ajoutée. Je ne vous ai
présenté que le strict minimum nécessaire et indispensable. Mais croyez-moi, vous aurez
l’occasion d’y revenir !
En résumé
– Le débogueur est un outil très puissant permettant d’inspecter le contenu des va-
riables lors de l’exécution d’un programme.
– On peut s’arrêter à un endroit de notre application grâce à un point d’arrêt.
– Le débogueur permet d’exécuter son application pas à pas et de suivre son évolution.
134
Chapitre 16
TP : le jeu du plus ou du moins
Difficulté :
W
aouh , nous augmentons régulièrement le nombre de choses que nous savons. C’est
super. Nous commençons à être capables d’écrire des applications qui ont un peu
plus de panache !
Enfin. . . moi, j’y arrive ! Et vous ? C’est ce que nous allons vérifier avec ce TP.
Savoir interagir avec son utilisateur est important. Voici donc un petit TP sous forme de
création d’un jeu simple qui va vous permettre de vous entraîner. L’idée est de réaliser le
jeu classique du plus ou du moins. . .
135
CHAPITRE 16. TP : LE JEU DU PLUS OU DU MOINS
Le principe est grosso modo le suivant : tant qu’on n’a pas trouvé la bonne valeur, nous
devons en saisir une nouvelle. Dans ce cas, la console nous indique si la valeur est trop
grande ou trop petite. Il faudra bien sûr incrémenter un compteur de coups à chaque
essai.
N’oubliez pas de gérer le cas où l’utilisateur saisit n’importe quoi. Nous ne voudrions
pas que notre premier jeu ait un bug qui fasse planter l’application !
Allez, je vous en ai trop dit. C’est à vous de jouer. Bon courage.
Correction
Voici ma correction de ce TP.
Bien sûr, il existe beaucoup de façons de réaliser ce petit jeu. S’il fonctionne, c’est que
votre solution est bonne. Ma solution fonctionne, la voici :
1 static void Main ( string [] args )
2 {
3 int valeurATrouver = new Random () . Next (0 , 100 ) ;
4 int nombreDeCoups = 0 ;
5 bool trouve = false ;
6 Console . WriteLine ( " Veuillez saisir un nombre compris entre
0 et 100 ( exclu ) " ) ;
7 while (! trouve )
8 {
9 string saisie = Console . ReadLine () ;
10 int valeurSaisie ;
11 if ( int . TryParse ( saisie , out valeurSaisie ) )
12 {
13 if ( valeurSaisie == valeurATrouver )
14 trouve = true ;
15 else
16 {
17 if ( valeurSaisie < valeurATrouver )
18 Console . WriteLine ( " Trop petit ... " ) ;
136
CORRECTION
19 else
20 Console . WriteLine ( " Trop grand ... " ) ;
21 }
22 nombreDeCoups ++;
23 }
24 else
25 Console . WriteLine ( " La valeur saisie est incorrecte ,
veuillez recommencer ... " ) ;
26 }
27 Console . WriteLine ( " Vous avez trouv é en " + nombreDeCoups +
" coup ( s ) " ) ;
28 }
On commence par obtenir un nombre aléatoire avec l’instruction que j’ai fournie dans
l’énoncé. Nous avons ensuite les initialisations de variables. L’entier nombreDeCoups va
permettre de stocker le nombre d’essai et le booléen trouve va permettre d’avoir une
condition de sortie de boucle.
Notre boucle démarre et ne se terminera qu’une fois que le booléen trouve sera passé
à vrai (true). Dans le corps de la boucle, nous demandons à l’utilisateur de saisir une
valeur que nous essayons de convertir en entier. Si la conversion échoue, nous l’indiquons
à l’utilisateur et nous recommençons notre boucle. Notez ici que je n’incrémente pas le
nombre de coups, jugeant qu’il n’y a pas lieu de pénaliser le joueur parce qu’il a mal
saisi ou qu’il a renversé quelque chose sur son clavier juste avant de valider la saisie.
Si en revanche, la conversion se passe bien, nous pouvons commencer à comparer la
valeur saisie avec la valeur à trouver. Si la valeur est la bonne, nous passons le booléen
à vrai, ce qui nous permettra de sortir de la boucle et de passer à la suite. Sinon,
nous afficherons un message pour indiquer si la saisie est trop grande ou trop petite
en fonction du résultat de la comparaison. Dans tous les cas, nous incrémenterons le
nombre de coups. Enfin, en sortie de boucle, nous indiquerons sa victoire au joueur
ainsi que le nombre de coups utilisés pour trouver le nombre secret :
137
CHAPITRE 16. TP : LE JEU DU PLUS OU DU MOINS
138
ALLER PLUS LOIN
Tout ceci est une question de goût. Je préfère personnellement la version précédente
n’aimant pas trop les break et les continue. Mais après tout, chacun fait comme il
préfère, l’important est que nous nous amusions à écrire le programme et à y jouer !
Voilà, ce TP est terminé. Vous avez pu voir finalement que nous étions tout à fait
capables de réaliser de petites applications récréatives. Personnellement, j’ai commencé
à m’amuser à faire de la programmation en réalisant toute sorte de petits programmes
de ce genre.
Je vous encourage fortement à essayer de créer d’autres programmes par vous-mêmes.
Plus vous vous entraînerez à faire des petits programmes simples et plus vous réussirez
à appréhender les subtilités de ce que nous avons appris.
Bien sûr, plus tard, nous serons capables de réaliser des applications plus compliquées. . .
Cela vous tente ? Alors continuons la lecture !
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 694204 ✠
139
CHAPITRE 16. TP : LE JEU DU PLUS OU DU MOINS
140
Chapitre 17
La ligne de commande
Difficulté :
S
ous ce nom un peu barbare se cache une fonctionnalité très présente dans nos usages
quotidiens mais qui a tendance à être masquée à l’utilisateur lambda. Nous allons,
dans un premier temps, voir ce qu’est exactement la ligne de commande et à quoi elle
sert. Ensuite, nous verrons comment l’exploiter dans notre application avec le C#.
Notez que ce chapitre n’est pas essentiel mais qu’il pourra sûrement vous servir plus tard
dans la création de vos applications.
141
CHAPITRE 17. LA LIGNE DE COMMANDE
On peut également le faire depuis une invite de commande, en allant dans : Menu
Démarrer > Accessoires > Invite de commande.
Ceci nous ouvre une console noire, comme celle que l’on connaît bien et dans laquelle
nous pouvons taper des instructions (voir figure 17.2).
Mais nous, ce qui nous intéresse surtout, c’est de pouvoir le faire depuis Visual C#
Express afin de pouvoir passer des arguments à notre programme. Pour ce faire, on va
142
PASSER DES PARAMÈTRES EN LIGNE DE COMMANDE
aller dans les propriétés de notre projet (clic droit sur le projet, Propriétés) puis nous
allons dans l’onglet Déboguer et nous voyons une zone de texte permettant de mettre
des arguments à la ligne de commande. Rajoutons par exemple « Bonjour Nico » (voir
figure 17.3).
Voilà, maintenant lorsque nous exécuterons notre application, Visual C# Express lui
passera les arguments que nous avons définis en paramètre de la ligne de commande.
À noter que dans ce cas, les arguments Bonjour Nico ne seront valables que lorsque
nous exécuterons l’application à travers Visual C# Express. Évidemment, si nous exé-
cutons notre application par l’invite de commande, nous aurons besoin de repasser les
arguments au programme pour qu’il puisse les exploiter.
143
CHAPITRE 17. LA LIGNE DE COMMANDE
C’est bien ! Sauf que pour l’instant, ça ne nous change pas la vie ! Il faut apprendre à
traiter ces paramètres. . .
La variable args est un tableau de chaînes de caractères. Sachant que chaque paramètre
est délimité par des espaces, nous retrouverons chacun des paramètres à un indice du
tableau différent.
Ce qui fait que si j’utilise le code suivant :
1 foreach ( string parametre in args )
2 {
3 Console . WriteLine ( parametre ) ;
4 }
et que je lance mon application avec les paramètres définis précédemment, je vais
obtenir :
Bonjour
Nico
144
LIRE LA LIGNE DE COMMANDE
Ce qui donne :
En résumé
– On peut passer des paramètres à une application en utilisant la ligne de commande.
– Le programme C# peut lire et interpréter ces paramètres.
– Ces paramètres sont séparés par des espaces.
145
CHAPITRE 17. LA LIGNE DE COMMANDE
146
Chapitre 18
TP : calculs en ligne de commande
Difficulté :
E
t voici un nouveau TP qui va nous permettre de récapituler un peu tout ce que nous
avons vu. Au programme, des if, des switch, des méthodes, des boucles et. . . de la
ligne de commande bien sûr.
Grâce à nos connaissances grandissantes, nous arrivons à augmenter la difficulté de nos
exercices et c’est une bonne chose. Vous verrez que dans le moindre petit programme, vous
aurez besoin de toutes les notions que nous avons apprises.
Allez, ne traînons pas trop et à vous de travailler !
147
CHAPITRE 18. TP : CALCULS EN LIGNE DE COMMANDE
Correction
STOP ! Je relève les copies.
J’ai dit qu’il n’y avait pas de subtilité, mais j’ai un peu menti en fait. Je l’avoue, c’était
pour que vous alliez au bout du développement sans aide.
Pour vérifier que vous avez trouvé la subtilité, essayez votre programme avec les para-
mètres suivants :
MonProgramme addition 5 ,5 2 ,5
Obtenez-vous 8 ?
C’était à peu près la seule subtilité, il fallait juste savoir qu’un nombre à virgule s’écri-
148
CORRECTION
vait avec une virgule plutôt qu’avec un point. Si nous écrivons 2.5, alors le C# ne sait
pas faire la conversion.
149
CHAPITRE 18. TP : CALCULS EN LIGNE DE COMMANDE
150
ALLER PLUS LOIN
151
CHAPITRE 18. TP : CALCULS EN LIGNE DE COMMANDE
7 switch ( operateur )
8 {
9 case " addition " :
10 ok = Addition ( args ) ;
11 break ;
12 case " multiplication " :
13 ok = Multiplication ( args ) ;
14 break ;
15 case " moyenne " :
16 ok = Moyenne ( args ) ;
17 break ;
18 }
19 }
20
21 if (! ok )
22 AfficherAide () ;
23 }
24
25 static void AfficherAide ()
26 {
27 Console . WriteLine ( " Utilisez l ' application de la mani è re
suivante : " ) ;
28 Console . WriteLine ( " MonProgamme addition 2 5 " ) ;
29 Console . WriteLine ( " MonProgamme multiplication 2 5 " ) ;
30 Console . WriteLine ( " MonProgamme moyenne 2 5 10 11 " ) ;
31 }
32
33 static bool Addition ( string [] args )
34 {
35 if ( args . Length != 3 )
36 return false ;
37
38 double somme = 0 ;
39 for ( int i = 1 ; i < args . Length ; i ++)
40 {
41 double valeur ;
42 if (! double . TryParse ( args [ i ] , out valeur ) )
43 return false ;
44 somme += valeur ;
45 }
46
47 Console . WriteLine ( " R é sultat de l ' addition : " + somme ) ;
48 return true ;
49 }
50
51 static bool Multiplication ( string [] args )
52 {
53 if ( args . Length != 3 )
54 return false ;
55 double resultat = 1 ;
152
ALLER PLUS LOIN
Nous n’affichons l’aide qu’à un seul endroit en utilisant le fait que les méthodes renvoient
un booléen indiquant si l’opération a été possible ou pas.
D’une manière générale, il est important d’essayer de rendre son code le plus mainte-
nable possible, comme nous l’avons fait ici.
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 846482 ✠
153
CHAPITRE 18. TP : CALCULS EN LIGNE DE COMMANDE
154
Troisième partie
155
Chapitre 19
Introduction à la programmation
orientée objet
Difficulté :
D
ans ce chapitre, nous allons essayer de décrire ce qu’est la programmation orientée
objet (abrégée souvent en POO). Je dis bien « essayer », car pour être complètement
traitée, la POO nécessiterait qu’on lui dédie un ouvrage entier !
Nous allons tâcher d’aller à l’essentiel et de rester proche de la pratique. Ce n’est pas très
grave si tous les concepts abstraits ne sont pas parfaitement appréhendés : il est impos-
sible d’apprendre la POO en deux heures. Cela nécessite une longue pratique, beaucoup
d’empirisme et des approfondissements théoriques.
Ce qui est important ici, c’est de comprendre les notions de base et de pouvoir les utili-
ser dans de petits programmes. Après cette mise au point, attaquons sans plus tarder la
programmation orientée objet !
157
CHAPITRE 19. INTRODUCTION À LA PROGRAMMATION ORIENTÉE
OBJET
Qu’est-ce qu’un objet ?
Vous avez pu voir précédemment que j’ai utilisé de temps en temps le mot « objet » et
que le mot-clé new est apparu comme par magie. . . Il a été difficile de ne pas trop en
parler et il est temps d’en savoir un peu plus.
Si on prend le monde réel (si, si, vous allez voir, vous connaissez. . .), nous sommes
entourés d’objets : une chaise, une table, une voiture, etc. Ces objets forment un tout.
– Ils possèdent des propriétés (la chaise possède quatre pieds, elle est de couleur bleue,
etc.).
– Ces objets peuvent faire des actions (la voiture peut rouler, klaxonner, etc.).
– Ils peuvent également interagir entre eux (l’objet conducteur démarre la voiture,
l’objet voiture fait tourner l’objet volant, etc.).
Il faut bien faire attention à distinguer ce qu’est l’objet et ce qu’est la définition d’un
objet.
La définition de l’objet (ou structure de l’objet) permet d’indiquer ce qui compose un
objet, c’est-à-dire quelles sont ses propriétés, ses actions etc. Comme par exemple, le
fait qu’une chaise ait des pieds ou qu’on puisse s’asseoir dessus. Par contre, l’objet
chaise est bien concret. On peut donc avoir plusieurs objets chaise : on parle également
d’instances. Les objets chaise, ce sont bien ceux, concrets, que l’on voit devant nous
autour de l’objet table, pour démarrer une partie de belote. On peut faire l’analogie
avec notre dictionnaire qui nous décrit ce qu’est une chaise. Le dictionnaire décrit
en quoi consiste l’objet, et l’instance de l’objet représente le concret associé à cette
définition. Chaque objet a sa propre vie et diffère d’un autre. Nous pouvons avoir une
chaise bleue, une autre rouge, une autre avec des roulettes, une cassée. . .
Vous voyez, finalement, la notion d’objet est plutôt simple quand on la ramène à ce
qu’on connaît déjà !
Sachant qu’un objet en programmation, c’est comme un objet du monde réel, mais ce
n’est pas forcément restreint au matériel. Un chien est un objet. Des concepts comme
l’amour ou une idée sont également des objets, tandis qu’on ne dirait pas cela dans le
monde réel.
En conclusion :
– La définition (ou structure) d’un objet est un concept abstrait, comme une définition
dans le dictionnaire. Cette définition décrit les caractéristiques d’un objet (la chaise
a des pieds, l’homme a des jambes, etc.). Cette définition est unique, comme une
définition dans le dictionnaire.
– Un objet ou une instance est la réalisation concrète de la structure de l’objet. On
peut avoir de multiples instances, comme les cent voitures sur le parking devant
chez moi. Elles peuvent avoir des caractéristiques différentes (une voiture bleue, une
158
L’ENCAPSULATION
L’encapsulation
Le fait de concevoir une application comme un système d’objets interagissant entre eux
apporte une certaine souplesse et une forte abstraction.
Prenons un exemple tout simple : la machine à café du bureau. Nous insérons nos pièces
dans le monnayeur, choisissons la boisson et nous nous retrouvons avec un gobelet de
la boisson commandée. Nous nous moquons complètement de savoir comment cela
fonctionne à l’intérieur et nous pouvons complètement ignorer si le café est en poudre,
en grain, comment l’eau est ajoutée, chauffée, comment le sucre est distribué, etc. Tout
ce qui nous importe, c’est que le fait de mettre des sous dans la machine nous donne
un café qui va nous permettre d’attaquer la journée.
Voilà un bel exemple de programmation orientée objet. Nous manipulons un objet
MachineACafe qui a des propriétés (allumée/éteinte, présence de café, présence de
gobelet,. . .) et qui sait faire des actions (AccepterMonnaie, DonnerCafe,. . .). Et c’est
tout ce que nous avons besoin de savoir : on se moque du fonctionnement interne, peu
importe ce qui se passe à l’intérieur, notre objet nous donne du café, point !
C’est ce qu’on appelle l’encapsulation. Cela permet de protéger l’information conte-
nue dans notre objet et de le rendre manipulable uniquement par ses actions ou pro-
priétés. Ainsi, l’utilisateur ne peut accéder ni au café ni au sucre et encore moins à la
monnaie. Notre objet est ainsi protégé et fonctionne un peu comme une boîte noire.
L’intérêt est que si la personne qui entretient la machine met du café en grain à la
place du café soluble, c’est invisible pour l’utilisateur qui n’a juste qu’à se soucier de
mettre ses pièces dans la machine.
L’encapsulation protège donc les données de l’objet et son fonctionnement interne.
Héritage
Un autre élément important dans la programmation orientée objet, que nous allons
aborder maintenant, est l’héritage.
Eh bien c’est presque comme en droit, à part que l’objet ne meurt pas et qu’il n’y a pas
de taxe sur l’héritage. C’est-à-dire qu’un objet dit « père » peut transmettre certaines
de ses caractéristiques à un autre objet dit « fils ».
Pour cela, on pourra définir une relation d’héritage entre eux.
S’il y a une relation d’héritage entre un objet père et un objet fils, alors l’objet fils
159
CHAPITRE 19. INTRODUCTION À LA PROGRAMMATION ORIENTÉE
OBJET
hérite de l’objet père. On dit également que l’objet fils est une spécialisation de
l’objet père ou qu’il dérive de l’objet père.
En langage plus courant, on peut également dire que l’objet fils est « une sorte » d’objet
père.
Alors, prenons par exemple l’objet chien et imaginons ses caractéristiques tirées du
monde réel en utilisant l’héritage :
– l’objet chien est une sorte d’objet mammifere ;
– l’objet mammifere est une sorte d’objet animal ;
– l’objet animal est une sorte d’objet etreVivant.
Chaque père est un peu plus général que son fils. Et inversement, chaque fils est un
peu plus spécialisé que son père. Avec l’exemple du dessus, un mammifère est un peu
plus général qu’un chien, l’être vivant étant encore plus général qu’un mammifère.
Il est possible pour un père d’avoir plusieurs fils, par contre, l’inverse est impossible,
un fils ne peut pas avoir plusieurs pères. Eh oui, c’est triste mais c’est comme ça, c’est
le règne du père célibataire avec plusieurs enfants à charge !
Ainsi, un objet chat peut également être un fils de l’objet mammifere. Un objet vegetal
peut également être fils de l’objet etreVivant.
Ce qu’on peut reproduire sur le schéma suivant (voir figure 19.1) où chaque bulle
représente un objet et chaque flèche représente l’héritage entre les objets.
On peut définir une sorte de hiérarchie entre les objets, un peu comme on le ferait avec
un arbre généalogique. La différence est qu’un objet héritant d’un autre peut obtenir
certains ou tous les comportements de l’objet qu’il spécialise, alors qu’un enfant n’hérite
160
POLYMORPHISME - SUBSTITUTION
pas forcément des yeux bleus de sa mère ou du côté bougon de son grand-père, le hasard
de la nature faisant le reste.
Pour bien comprendre cet héritage de comportement, empruntons à nouveau les exemples
au monde réel.
– L’être vivant peut, par exemple, faire l’action « vivre ».
– Le mammifère possède des yeux.
– Le chien, qui est une sorte d’être vivant et une sorte de mammifère, peut également
faire l’action « vivre » et aura des yeux.
– Le chat, qui est une autre sorte d’être vivant, peut lui aussi faire l’action « vivre »
et aura également des yeux.
On voit bien ici que le chat et le chien héritent des comportements de leurs parents et
grands-parents en étant capables de vivre et d’avoir des yeux.
Par contre, l’action « aboyer » est spécifique au chien. Ce qui veut dire que ni le chat
ni le dauphin ne seront capables d’aboyer. Il n’y a que dans les dessins animés de Tex
Avery que ceci est possible ! Évidemment, il n’y a pas de notion d’héritage entre le
chien et le chat et l’action d’aboyer est définie au niveau du comportement du chien.
Ceci implique également que seul un objet qui est une sorte de chien, par exemple
l’objet labrador ou l’objet chihuahua, pourra hériter du comportement « aboyer », car
il y a une relation d’héritage entre eux.
Finalement, c’est plutôt logique !
Rappelons juste avant de terminer ce paragraphe qu’un objet ne peut pas hériter de plu-
sieurs objets. Il ne peut hériter que d’un seul objet. Le C# ne permet pas ce qu’on ap-
pelle l’héritage multiple, a contrario d’autres langages comme le C++ par exemple.
Voilà globalement pour la notion d’héritage.
Je dis globalement, car il y a certaines subtilités que je n’ai pas abordées mais ce
n’est pas trop grave ; vous verrez dans les chapitres suivants comment le C# utilise la
notion d’héritage et ce qu’il y a vraiment besoin de savoir. Ne vous inquiétez pas si
certaines notions sont encore un peu floues, vous comprendrez sûrement mieux grâce à
la pratique.
Polymorphisme - Substitution
Le mot polymorphisme suggère qu’une chose peut prendre plusieurs formes. Sous ce
terme un peu barbare se cachent plusieurs notions de l’orienté objet qui sont souvent
source d’erreurs.
Je vais volontairement passer rapidement sur certains points qui ne vont pas nous servir
pour me concentrer sur ceux qui sont importants pour ce livre.
En fait, on peut dire qu’une manifestation du polymorphisme est la capacité pour un
objet de faire une même action avec différents types d’intervenants. C’est ce qu’on
appelle le polymorphisme ad hoc ou le polymorphisme « paramétré ». Par exemple,
notre objet voiture peut rouler sur la route, rouler sur l’autoroute, rouler sur la terre
161
CHAPITRE 19. INTRODUCTION À LA PROGRAMMATION ORIENTÉE
OBJET
si elle est équipée de pneus adéquats, rouler au fond de l’eau si elle est amphibie, etc.
Concrètement ici, je fais interagir un objet voiture avec un objet autoroute ou un
objet terre. . . par l’action qui consiste à rouler. Cela peut paraître anodin décrit
ainsi, mais nous verrons ce que cela implique avec le C#.
La substitution est une autre manifestation du polymorphisme. Il s’agit de la capacité
d’un objet fils à redéfinir des caractéristiques ou des actions d’un objet père.
Prenons par exemple un objet mammifère qui sait faire l’action « se déplacer ». Les
objets qui dérivent du mammifère peuvent potentiellement avoir à se déplacer d’une
manière différente. Par exemple, l’objet homme va se déplacer sur ses deux jambes et
donc différemment de l’objet dauphin qui se déplacera grâce à ses nageoires ou bien
encore différemment de l’objet « homme accidenté » qui va avoir besoin de béquilles
pour s’aider dans son déplacement.
Tous ces mammifères sont capables de se déplacer, mais chacun va le faire d’une manière
différente. Ceci est donc possible grâce à la substitution qui permet de redéfinir un
comportement hérité.
Ainsi, chaque fils sera libre de réécrire son propre comportement, si celui de son père
ne lui convient pas.
Interfaces
Un autre concept important de la programmation orientée objet est la notion d’inter-
face.
Prenons un exemple dans notre monde réel et connu : les prises de courant.
Elles fournissent de l’électricité à 220 V avec deux trous et (souvent) une prise de
terre. Peu importe ce qu’il y a derrière, du courant alternatif de la centrale du coin,
un transformateur, quelqu’un qui pédale. . . nous saurons à coup sûr que nous pouvons
brancher nos appareils électriques car ces prises s’engagent à nous fournir du courant
alternatif avec le branchement adéquat.
Elles respectent le contrat ; elles sont « branchables ».
Ce dernier terme est un peu barbare et peut faire mal aux oreilles. Mais vous verrez
que suffixer des interfaces par un « able » est très courant et permet d’être plus précis
sur la sémantique de l’interface. C’est également un suffixe qui fonctionne en anglais
et les interfaces du framework .NET finissent pour la plupart par « able ».
À noter que les interfaces ne fournissent qu’un contrat, elles ne fournissent pas d’im-
plémentation, c’est-à-dire pas de code C#. Ce n’est pas clair ? Les interfaces indiquent
que les objets qui choisissent de respecter ce contrat auront forcément telle action
ou telle caractéristique mais elles n’indiquent pas comment faire, c’est-à-dire qu’elles
162
À QUOI SERT LA PROGRAMMATION ORIENTÉE OBJET ?
n’ont pas de code C# associé. Chaque objet respectant cette interface (on parle d’objet
implémentant une interface) sera responsable de coder la fonctionnalité associée au
contrat.
Pour manipuler ces prises, nous pourrons utiliser cette interface en disant : « Allez
hop, tous les branchables, venez par ici, on a besoin de votre courant ! » Peu importe
que l’objet implémentant cette interface soit une prise murale ou une prise reliée à
une dynamo, ou autre, nous pourrons manipuler ces objets par leur interface et donc
brancher nos prises permettant d’alimenter nos appareils.
Contrairement à l’héritage, un objet est capable d’implémenter plusieurs interfaces.
Par exemple, une pompe à chaleur peut être « chauffante » et « refroidissante » 1 .
Nous en avons terminé avec la théorie sur les interfaces. Il est fort probable que vous ne
saisissiez pas encore tout l’intérêt des interfaces ou ce qu’elles sont exactement. Nous
allons y revenir avec des exemples concrets et vous verrez des utilisations d’interfaces
dans le cadre du framework .NET qui vous éclaireront davantage.
163
CHAPITRE 19. INTRODUCTION À LA PROGRAMMATION ORIENTÉE
OBJET
dans lequel nous pourrons mettre un objet bouton et un objet zone de texte. Ces
objets héritent tous des mêmes comportements, comme le fait d’être cliquable ou sé-
lectionnable, etc.
De même, des composants tout faits et prêts à l’emploi peuvent être vendus par des
entreprises tierces (systèmes de log, contrôles utilisateur améliorés, etc.).
Il faut savoir que la POO, c’est beaucoup plus que ça et nous en verrons des subtilités
plus loin, mais comprendre ce qu’est un objet est globalement suffisant pour une grande
partie de l’ouvrage.
En résumé
– L’approche orientée objet permet de modéliser son application sous la forme d’inter-
actions entre objets.
– Les objets ont des propriétés et peuvent faire des actions.
– Ils masquent la complexité d’une implémentation grâce à l’encapsulation.
– Les objets peuvent hériter de fonctionnalités d’autres objets s’il y a une relation
d’héritage entre eux.
164
Chapitre 20
Créer son premier objet
Difficulté :
A
h , enfin un peu de concret et surtout de code. Dans ce chapitre, nous allons appliquer
les notions que nous avons vues sur la programmation orientée objet pour continuer
notre apprentissage du C#.
Même si vous n’avez pas encore appréhendé exactement où la POO pouvait nous mener,
ce n’est pas grave. Les notions s’affineront au fur et à mesure de la lecture du livre. Il est
temps pour nous de commencer à créer des objets, à les faire hériter entre eux, etc. Bref,
à jouer, grâce à ces concepts, avec le C#.
Vous verrez qu’avec un peu de pratique tout s’éclaircira ; vous saisirez l’intérêt de la POO
et comment être efficace avec le C#.
165
CHAPITRE 20. CRÉER SON PREMIER OBJET
Ça y est, il a fini avec son baratin qui donne mal à la tête ? Pourquoi tout ce
blabla sur les objets ?
Parce que tout dans le C# est un objet. Comme déjà dit, une fenêtre Windows est un
objet. Une chaîne de caractères est un objet. La liste que nous avons vue plus haut est
un objet.
Nous avons vu que les objets possédaient des caractéristiques ; il s’agit de propriétés.
Un objet peut également faire des actions ; ce sont des méthodes.
Suivant ce principe, une chaîne de caractères est un objet et possède des propriétés (par
exemple sa longueur). De la même façon, il sera possible que les chaînes de caractères
fassent des actions (par exemple se mettre en majuscules).
Nous allons voir plus bas qu’il est évidemment possible de créer nos propres objets
(chat, chien, etc.) et que grâce à eux, nous allons enrichir les types qui sont à notre
disposition. Un peu comme nous avons déjà fait avec les énumérations.
Voyons dès à présent comment faire, grâce aux classes.
Les classes
Dans le chapitre précédent, nous avons parlé des objets mais nous avons également
parlé de la définition de l’objet, de sa structure. Eh bien, c’est exactement ce qu’est
une classe.
Nous avons déjà pu voir une classe dans le code que nous avons utilisé précédemment
et qui a été généré par Visual C# Express, la classe Program. Nous n’y avons pas fait
trop attention, mais voilà à peu près à quoi elle ressemblait :
1 class Program
2 {
3 static void Main ( string [] args )
4 {
5 }
6 }
C’est elle qui contenait la méthode spéciale Main() qui sert de point d’entrée à l’appli-
cation. Nous pouvons découvrir avec des yeux neufs le mot-clé class qui, comme son
nom le suggère, permet de définir une classe, c’est-à-dire la structure d’un objet.
166
LES CLASSES
Rappelez-vous, les objets peuvent avoir des caractéristiques et faire des actions.
Ici, c’est exactement ce qui se passe. Nous avons défini la structure d’un objet Program
qui contient une action : la méthode Main(). Vous aurez remarqué au passage que pour
définir une classe, nous utilisons à nouveau les accolades permettant de créer un bloc
de code qui délimite la classe.
Passons sur cette classe particulière et lançons-nous dans la création d’une classe qui
nous servira à créer des objets. Par exemple, une classe Voiture. À noter qu’il est
possible de créer une classe à plusieurs endroits dans le code, mais en général, nous
utiliserons un nouveau fichier, du même nom que la classe, qui lui sera entièrement
dédié. Une règle d’écriture commune à beaucoup de langages de programmation est
que chaque fichier doit avoir une seule classe.
Faisons un clic droit sur notre projet pour ajouter une nouvelle classe, comme indiqué
à la figure 20.1.
Visual C# Express nous ouvre sa fenêtre permettant de faire l’ajout d’un élément en
se positionnant sur l’élément Classe. Nous pourrons donner un nom à cette classe :
Voiture (voir figure 20.2).
Vous remarquerez que les classes commencent en général par une majuscule. Visual
C# Express nous génère le code suivant :
1 using System ;
2 using System . Collections . Generic ;
3 using System . Linq ;
4 using System . Text ;
5
6 namespace M a P r e m i e r e A p p l i c a t i o n
7 {
8 class Voiture
9 {
10 }
11 }
167
CHAPITRE 20. CRÉER SON PREMIER OBJET
Figure 20.2 – Choix d’un modèle de fichier Classe dans la fenêtre de sélection des
modèles
Nous retrouvons le mot-clé class suivi du nom de notre classe Voiture et les accolades
ouvrantes et fermantes permettant de délimiter notre classe. Notons également que
cette classe fait partie de l’espace de nom MaPremiereApplication qui est l’espace
de nom par défaut de notre projet. Pour nous simplifier le travail, Visual C# Express
nous a également inclus quelques espaces de noms souvent utilisés.
Mais pour l’instant cette classe ne fait pas grand-chose.
Comme cette classe est vide, elle ne possède ni propriétés ni actions. Nous ne pouvons
absolument rien faire avec, à part en créer une instance, c’est-à-dire un objet. Cela se
fait grâce à l’utilisation du mot-clé new. Nous y reviendrons plus en détail plus tard,
mais cela donne :
1 static void Main ( string [] args )
2 {
3 Voiture voitureNicolas = new Voiture () ;
4 Voiture voitureJeremie = new Voiture () ;
5 }
Nous avons créé deux instances de l’objet Voiture et nous les stockons dans les variables
voitureNicolas et voitureJeremie.
Si vous vous rappelez bien, nous aurions logiquement dû écrire :
1 M a P r e m i e r e A p p l i c a t i o n . Voiture voitureNicolas = new
M a P r e m i e r e A p p l i c a t i o n . Voiture () ;
Ou alors positionner le using qui allait bien, permettant d’inclure l’espace de nom
MaPremiereApplication.
168
LES MÉTHODES
En fait, ici c’est superflu vu que nous créons les objets depuis la méthode Main(), qui
fait partie de la classe Program, faisant partie du même espace de nom que notre classe.
Les méthodes
Nous venons de créer notre objet Voiture mais nous ne pouvons pas en faire grand-
chose. Ce qui est bien dommage. Ça serait bien que notre voiture puisse klaxonner par
exemple si nous sommes bloqués dans des embouteillages. Bref, que notre voiture soit
capable de faire des actions.
Qui dit « action » dit « méthode ».
Nous allons pouvoir définir des méthodes faisant partie de notre objet Voiture. Pour
ce faire, il suffit de créer une méthode, comme nous l’avons déjà vu, directement dans
le corps de la classe :
1 class Voiture
2 {
3 void Klaxonner ()
4 {
5 Console . WriteLine ( " Pouet ! " ) ;
6 }
7 }
Notez quand même l’absence du mot-clé static que nous étions obligés de mettre
avant. Je vous expliquerai un peu plus loin pourquoi.
Ce qui fait que si nous voulons faire klaxonner notre voiture, nous aurons juste besoin
d’invoquer la méthode Klaxonner() depuis l’objet Voiture, ce qui s’écrit :
1 Voiture voitureNicolas = new Voiture () ;
2 voitureNicolas . Klaxonner () ;
Cela ressemble beaucoup à ce que nous avons déjà fait. En fait, nous avons déjà uti-
lisé des méthodes sur des objets. Rappelez-vous, nous avons utilisé la méthode Add()
permettant d’ajouter une valeur à une liste :
1 List < int > maListe = new List < int >() ;
2 maListe . Add ( 1 ) ;
Comme nous avons un peu plus de notions désormais, nous pouvons remarquer que
nous instancions un objet List (plus précisément, une liste d’entier) grâce au mot-clé
new et que nous invoquons la méthode Add de notre liste pour lui ajouter l’entier 1.
Nous avons fait pareil pour obtenir un nombre aléatoire, dans une écriture un peu plus
concise. Nous avions écrit :
1 int valeurATrouver = new Random () . Next (0 , 100 ) ;
169
CHAPITRE 20. CRÉER SON PREMIER OBJET
Nous créons un objet du type Random grâce à new puis nous appelons la méthode
Next() qui prend en paramètres les bornes du nombre aléatoire que nous souhaitons
obtenir (0 étant inclus et 100 exclu). Puis nous stockons le résultat dans un entier.
Eh oui, nous avons manipulé quelques objets sans le savoir. . . !
Revenons à notre embouteillage et compilons le code nous permettant de faire klaxonner
notre voiture :
1 Voiture voitureNicolas = new Voiture () ;
2 voitureNicolas . Klaxonner () ;
Nous allons y revenir juste après, mais le mot-clé public permet d’indiquer que la
méthode est accessible depuis n’importe où.
Exécutons notre application et nous obtenons :
Pouet !
170
LES MÉTHODES
Cette méthode accepte une vitesse en paramètre et si elle est supérieure à 90, alors la
vitesse n’est pas autorisée. Cette méthode pourrait également s’écrire :
1 class Voiture
2 {
3 public bool VitesseAutorisee ( int vitesse )
4 {
5 return vitesse <= 90 ;
6 }
7 }
1 class Voiture
2 {
3 public bool VitesseAutorisee ( int vitesse )
4 {
5 bool e s t Vi t es s eA u t or i se e = vitesse <= 90 ;
6 return e s tV i t es s eA u to r i se e ;
7 }
8 }
Ce que nous pouvons écrire finalement en une seule ligne comme précédemment.
Il est bien sûr possible d’avoir plusieurs méthodes dans une même classe et elles peuvent
s’appeler entre elles, par exemple :
1 class Voiture
2 {
3 public bool VitesseAutorisee ( int vitesse )
4 {
5 return vitesse <= 90 ;
6 }
7
8 public void Klaxonner ()
9 {
10 if (! VitesseAutorisee ( 180 ) )
11 Console . WriteLine ( " Pouet ! " ) ;
12 }
13 }
171
CHAPITRE 20. CRÉER SON PREMIER OBJET
Notion de visibilité
Oui, mais le mot-clé public. . . il nous a bien sauvé la vie, mais. . . qu’est-ce
donc ?
En fait, je l’ai rapidement évoqué et nous nous sommes bien rendu compte que sans ce
mot-clé, impossible de compiler car la méthode n’était pas accessible.
Le mot-clé public sert à indiquer que notre méthode pouvait être accessible depuis
d’autres classes ; en l’occurrence dans notre exemple depuis la classe Program. C’est-à-
dire que sans ce mot-clé, il est impossible à d’autres objets d’utiliser cette méthode.
Pour faire en sorte qu’une méthode soit inaccessible, nous pouvons utiliser le mot-clé
private. Ce mot-clé permet d’avoir une méthode qui n’est accessible que depuis la
classe dans laquelle elle est définie. Prenons l’exemple suivant :
1 class Voiture
2 {
3 public bool Demarrer ()
4 {
5 if ( ClesSurLeContact () )
6 {
7 DemarrerLeMoteur () ;
8 return true ;
9 }
10 return false ;
11 }
12
13 public void So rtirDeLa Voiture ()
14 {
15 if ( ClesSurLeContact () )
16 P r e v e n i r L U t il i s a t e u r () ;
17 }
18
19 private bool ClesSurLeContact ()
20 {
21 // faire quelque chose pour v é rifier
22 return true ;
23 }
24
25 private void DemarrerLeMoteur ()
26 {
27 // faire quelque chose pour d é marrer le moteur
28 }
29
30 private void P r e v e n i r L U t i l i s a t eu r ()
31 {
32 Console . WriteLine ( " Bip bip bip " ) ;
33 }
172
NOTION DE VISIBILITÉ
34 }
Notez d’ailleurs que la complétion automatique n’est pas proposée pour les
méthodes inaccessibles.
Visibilité Description
public Accès non restreint
protected Accès depuis la même classe ou depuis une classe
dérivée
private Accès uniquement depuis la même classe
internal Accès restreint à la même assembly
protected internal Accès restreint à la même assembly ou depuis une
classe dérivée
Les visibilités qui vont le plus vous servir sont représentées par les mots clés public et
private. Nous verrons que le mot-clé protected va servir un peu plus tard quand nous
parlerons d’héritage. Notez qu’internal pourra être utilisé une fois que nous aurons
bien maîtrisé toutes les notions.
Ces mots-clés sont utilisables avec beaucoup d’autres concepts. Nous avons utilisé les
méthodes pour les illustrer mais ceci est également valable pour les classes ou les
propriétés que nous allons découvrir juste après.
Oui, il existe des visibilités par défaut suivant les types déclarés. Vous aurez compris
par exemple que la visibilité par défaut d’une méthode est privée si l’on ne spécifie pas
le mot-clé.
Pour éviter tout risque et toute ambiguïté, il est recommandé de toujours indiquer la
visibilité ; ce que nous ferons désormais dans ce livre, maintenant que nous savons de
quoi il s’agit.
173
CHAPITRE 20. CRÉER SON PREMIER OBJET
Les propriétés
Des objets c’est bien. Des actions sur ces objets, c’est encore mieux. Il nous manque
encore les caractéristiques des objets. C’est là qu’interviennent les propriétés.
Sans le savoir, vous avez déjà utilisé des propriétés, par exemple dans le code suivant :
1 string [] jours = new string [] { " Lundi " , " Mardi " , " Mercredi " , "
Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 for ( int i = 0 ; i < jours . Length ; i ++)
3 {
4 Console . WriteLine ( jours [ i ]) ;
5 }
Dans la boucle, nous utilisons jours.Length. Nous utilisons en fait la propriété Length
du tableau « jours », un tableau étant bien sûr un objet.
Nous avons pu utiliser d’autres propriétés, par exemple dans l’instruction suivante :
1 List < string > jours = new List < string > { " Lundi " , " Mardi " , "
Mercredi " , " Jeudi " , " Vendredi " , " Samedi " , " Dimanche " };
2 for ( int i = 0 ; i < jours . Count ; i ++)
3 {
4 Console . WriteLine ( jours [ i ]) ;
5 }
Notez que j’ai rajouté les visibilités pour la classe et pour la variable.
Grâce à ce code, la chaîne de caractères Couleur est désormais une caractéristique de
la classe Voiture. Nous pourrons l’utiliser en faisant suivre notre objet de l’opérateur
174
LES PROPRIÉTÉS
Cela ressemble beaucoup à ce que nous avons déjà fait. Nous utilisons le « . » pour
accéder aux propriétés d’un objet. Comme d’habitude, les variables vont pouvoir stocker
des valeurs. Ainsi, nous pourrons avoir une voiture rouge pour Nicolas et une voiture
verte pour Jérémie.
Notez ici qu’il s’agit bien de variables membres et non de vraies propriétés.
En général, les variables d’une classe ne doivent jamais être publiques. Nous
utiliserons rarement cette construction.
Les propriétés :
Les propriétés sont en fait des variables évoluées. Elles sont à mi-chemin entre une
variable et une méthode.
Prenons cet exemple :
1 public class Voiture
2 {
3 private int vitessePrivee ;
4 public int Vitesse
5 {
6 get
7 {
8 return vitessePrivee ;
9 }
10 set
11 {
12 vitessePrivee = value ;
13 }
14 }
15 }
Nous pouvons voir que nous définissons dans un premier temps une variable privée,
vitessePrivee de type entier. Comme prévu, cette variable est masquée pour les
utilisateurs d’objets Voiture. Ainsi, le code suivant :
1 Voiture voitureNicolas = new Voiture () ;
2 voitureNicolas . vitessePrivee = 50 ;
175
CHAPITRE 20. CRÉER SON PREMIER OBJET
Par contre, nous en avons profité pour définir la propriété Vitesse, de type int.
Pour ceci, nous avons défini une partie de la propriété avec le mot-clé get suivi d’un
return vitessePrivee. Et juste en dessous, nous avons utilisé le mot-clé set, suivi
de vitessePrivee = value.
Ce que nous avons fait, c’est définir la possibilité de lire la propriété grâce au mot-
clé get. Ici, la lecture de la propriété nous renvoie la valeur de la variable privée. De
la même façon, nous avons défini la possibilité d’affecter une valeur à la propriété en
utilisant le mot-clé set et en affectant la valeur à la variable privée.
Les blocs de code délimités par get et set se comportent un peu comme des méthodes.
Ils ont un corps qui est délimité par des accolades et dans le cas du get, ils doivent
renvoyer une valeur du même type que la propriété.
À noter que dans le cas du set, « value » est un mot-clé qui permet de dire : « la
valeur que nous avons affectée à la propriété ».
Prenons l’exemple suivant :
1 Voiture voitureNicolas = new Voiture () ;
2 voitureNicolas . Vitesse = 50 ;
3 Console . WriteLine ( voitureNicolas . Vitesse ) ;
Ok, c’est super, mais dans ce cas, pourquoi passer par une propriété et pas
par une variable ? Même s’il paraît que les variables ne doivent jamais être
publiques. . .
Eh bien parce que dans ce cas-là, la propriété peut faire un peu plus que simplement
renvoyer une valeur. Et aussi parce que nous masquons la structure interne de notre
classe à ceux qui veulent l’utiliser. Nous pourrions très bien envisager d’aller lire la
vitesse dans les structures internes du moteur, ou en faisant un calcul poussé par
rapport au coefficient du vent et de l’âge du capitaine (ou plus vraisemblablement en
allant lire la valeur en base de données). Et ici, nous pouvons tirer parti de la puissance
des propriétés pour masquer tout ça à l’appelant qui, lui, n’a besoin que d’une vitesse.
Par exemple :
1 private int vitessePrivee ;
2 public int Vitesse
3 {
4 get
5 {
6 int v = vitesseDesRoues * rayon * coefficient ; // ce
calcul est compl è tement farfelu !
7 M e t t r e A J o u r L e C o m p t e u r () ;
176
LES PROPRIÉTÉS
8 A d a p t e r L a V i t e s s e D e s E s s u i e G l a c e s () ;
9 return v ;
10 }
11 set
12 {
13 // faire la mise à jour des variables internes
14 M e t t r e A J o u r L e C o m p t e u r () ;
15 A d a p t e r L a V i t e s s e D e s E s s u i e G l a c e s () ;
16 }
17 }
En faisant tout ça dans le bloc de code get, nous masquons les rouages de notre classe
à l’utilisateur. Lui, il n’a besoin que d’obtenir la vitesse, sans s’encombrer du compteur
ou des essuie-glaces.
Bien sûr, la même logique peut s’adapter au bloc de code set, qui permet d’affecter
une valeur à la propriété.
Il est également possible de rendre une propriété en lecture seule, c’est-à-dire non
modifiable par les autres objets. Il pourra par exemple sembler bizarre de positionner
une valeur à la vitesse alors qu’en fait, pour mettre à jour la vitesse, il faut appeler la
méthode AppuyerPedale(double forceAppui).
Pour empêcher les autres objets de pouvoir directement mettre à jour la propriété
Vitesse, il suffit de ne pas déclarer le bloc de code set et de ne garder qu’un get. Par
exemple :
1 public class Voiture
2 {
3 private int vitessePrivee ;
4 public int Vitesse
5 {
6 get
7 {
8 // faire des calculs ...
9 return vitessePrivee ;
10 }
11 }
12 }
Ce qui fait que si nous tentons d’affecter une valeur à la propriété Vitesse depuis une
autre classe, par exemple :
1 Voiture voitureNicolas = new Voiture () ;
2 voitureNicolas . Vitesse = 50 ;
177
CHAPITRE 20. CRÉER SON PREMIER OBJET
Le compilateur nous indique donc très justement qu’il est impossible de faire cette
affectation car la propriété est en lecture seule. Il devient donc impossible pour un
utilisateur de cette classe de modifier la vitesse de cette façon.
De même, il est possible de définir une propriété pour qu’elle soit accessible en écriture
seule. Il suffit de définir uniquement le bloc de code set :
1 private double acceleration ;
2 public double Acceleration
3 {
4 set
5 {
6 acceleration = value ;
7 }
8 }
Ce bridage d’accès à des propriétés prend tout son sens quand nous souhaitons exposer
nos objets à d’autres consommateurs qui n’ont aucun intérêt à connaître la structure
interne de notre classe. C’est un des principes de l’encapsulation.
178
LES PROPRIÉTÉS
est un cas très fréquent d’utilisation de propriétés. Nous exposons ici une variable privée
à travers les propriétés get et set. L’écriture du dessus est équivalente à la suivante :
1 public int Vitesse { get ; set ; }
Dans ce cas, le compilateur fait le boulot lui-même, il génère (dans le code compilé)
une variable membre qui va servir à stocker la valeur de la propriété.
C’est exactement pareil, sauf que cela nous simplifie grandement l’écriture.
En plus, nous pouvons encore accélérer son écriture en utilisant ce qu’on appelle des
« snippets » qui sont des extraits de code. Pour les utiliser, il suffit de commencer
à écrire un mot et Visual C# nous complète le reste. Un peu comme la complétion
automatique sauf que cela fonctionne pour des bouts de code très répétitifs et très
classiques à écrire.
Commencez par exemple à taper « prop » ; Visual C# nous propose plusieurs extraits
de code, comme vous pouvez le voir sur la figure 20.3.
179
CHAPITRE 20. CRÉER SON PREMIER OBJET
✄
la propriété, que vous pouvez à nouveau changer. Appuyez enfin sur ✂Entrée ✁ pour
terminer la saisie et vous aurez une belle propriété auto-implémentée (voir figure 20.5).
Vous avez pu remarquer qu’en commençant à taper « prop », Visual C# Express vous a
proposé d’autres extraits de code, par exemple « propfull » qui va générer la propriété
complète telle qu’on l’a vue un peu plus haut.
D’autres extraits de code existent, comme « propg » qui permet de définir une propriété
en lecture seule.
En effet, comme au chapitre précédent, il est possible de définir des propriétés auto-
implémentées en lecture seule ou en écriture seule avec une écriture simplifiée. Dans le
cas des propriétés auto-implémentées, il y a cependant une subtilité.
Pour avoir de la lecture seule, nous devons indiquer que l’affectation est privée, comme
on peut le voir en utilisant l’extrait de code « propg ». Visual C# Express nous génère
le bout de code suivant :
1 public int Vitesse { get ; private set ; }
Si c’est une propriété auto-implémentée, évidemment que si. Par contre, si nous n’uti-
lisons pas une propriété auto-implémentée et que nous utilisons une variable membre
pour sauvegarder la valeur de la propriété, nous pourrons éventuellement modifier ou
lire la valeur de la variable à partir d’une méthode ou d’une autre propriété.
Bref, maintenant que vous connaissez les deux syntaxes, vous pourrez utiliser la plus
adaptée à votre besoin.
180
LES PROPRIÉTÉS
Les accolades servent ici à initialiser les propriétés au même moment que l’instanciation
de l’objet.
Note : cela paraît évident, mais il est bien sûr possible d’accéder aux propriétés
d’une classe depuis une méthode de la même classe.
En résumé
– On utilise des classes pour représenter quasiment la plupart des objets.
– On utilise le mot-clé class pour définir une classe et le mot-clé new pour l’instancier.
– Une classe peut posséder des caractéristiques (les propriétés) et peut faire des actions
(les méthodes).
181
CHAPITRE 20. CRÉER SON PREMIER OBJET
182
Chapitre 21
Manipuler des objets
Difficulté :
M
aintenant que nous avons bien vu comment définir des objets, il est temps de
savoir les utiliser. Nous allons voir dans ce chapitre quelles sont les subtilités de
l’instanciation des objets. Vous verrez notamment ce qu’est un constructeur et qu’on
peut avoir des valeurs nulles pour des objets. N’hésitez pas à jouer avec tous ces concepts,
il est important d’être à l’aise avec les bases pour pouvoir être efficace avec les concepts
avancés !
183
CHAPITRE 21. MANIPULER DES OBJETS
Le constructeur
Le constructeur est une méthode particulière de l’objet qui permet de faire des choses
au moment de la création d’un objet, c’est-à-dire au moment où nous utilisons le mot-
clé new. Il est en général utilisé pour initialiser des valeurs par défaut d’un objet. Par
exemple, si nous voulons que lors de la création d’une voiture, elle ait automatiquement
une vitesse, nous pouvons faire :
1 public class Voiture
2 {
3 public int Vitesse { get ; set ; }
4
5 public Voiture ()
6 {
7 Vitesse = 5 ;
8 }
9 }
Le constructeur est en fait une méthode spéciale qui a le même nom que la classe et
qui ne possède pas de type de retour. Elle est appelée lors de la création de l’objet,
avec new.
Pour illustrer le comportement du constructeur, ajoutons une méthode Rouler à notre
classe, de cette façon :
1 public class Voiture
2 {
3 public int Vitesse { get ; set ; }
4
5 public Voiture ()
6 {
7 Vitesse = 5 ;
8 }
9
10 public void Rouler ()
11 {
12 Console . WriteLine ( " Je roule à " + Vitesse + " km / h " ) ;
13 }
14 }
184
LE CONSTRUCTEUR
Je roule à 5 km / h
Je roule à 50 km / h
Le constructeur est la première « méthode » à être appelée lors de la création d’un objet.
C’est l’endroit approprié pour faire des initialisations, ou pour charger des valeurs, etc.
Le constructeur que nous avons vu est ce qu’on appelle le constructeur par défaut,
car il ne possède pas de paramètres. Il est possible de passer des paramètres à un
constructeur, pour initialiser des variables de notre classe avec des valeurs. Pour ce
faire, nous devons déclarer un constructeur avec un paramètre. Par exemple :
1 public class Voiture
2 {
3 public int Vitesse { get ; set ; }
4
5 public Voiture ()
6 {
7 Vitesse = 5 ;
8 }
9
10 public Voiture ( int vitesse )
11 {
12 Vitesse = vitesse ;
13 }
14
15 public void Rouler ()
16 {
17 Console . WriteLine ( " Je roule à " + Vitesse + " km / h " ) ;
18 }
19 }
Ainsi, nous pourrons créer un objet voiture en lui précisant une vitesse par défaut de
cette façon :
1 Voiture voitureNicolas = new Voiture ( 20 ) ;
185
CHAPITRE 21. MANIPULER DES OBJETS
À noter qu’il est possible de cumuler les constructeurs tant qu’ils ont chacun des para-
mètres différents. Dans notre exemple, nous pourrons donc créer des voitures de deux
façons différentes :
1 Voiture voitureNicolas = new Voiture () ; // vitesse vaut 5
2 Voiture voitureJeremie = new Voiture ( 20 ) ; // vitesse vaut 20
Il est aussi possible de ne pas définir de constructeur par défaut et d’avoir unique-
ment un constructeur possédant des paramètres. Dans ce cas, il devient impossible
d’instancier un objet sans lui passer de paramètres.
Instancier un objet
Nous allons revenir à présent sur l’instanciation d’un objet. Comme nous venons de
le voir, nous utilisons le mot-clé new pour créer une instance d’un objet. C’est lui
qui permet la création d’un objet. Il appelle le constructeur correspondant. Si aucun
constructeur n’existe, il ne se passera rien de plus qu’une création de base. Par exemple :
1 Voiture voitureNicolas = new Voiture () ;
Pour rentrer un peu dans la technique, au moment de l’instanciation d’un objet, l’opé-
rateur new crée l’objet et le met à une place disponible en mémoire. Cette adresse
mémoire est conservée dans la variable voitureNicolas. On dit que voitureNicolas
contient une référence vers l’objet. Nous verrons un peu plus tard ce que cela implique.
Comme pour les types que nous avons vus plus haut, nous sommes obligés d’initialiser
un objet avant de l’utiliser. Sinon, Visual C# Express nous générera une erreur de
compilation. Par exemple, les instructions suivantes :
1 Voiture voitureNicolas ;
2 voitureNicolas . Vitesse = 5 ;
En effet, Visual C# Express est assez intelligent pour se rendre compte que l’on va
essayer d’accéder à un objet qui n’a pas été initialisé.
Il faut toujours initialiser une variable avant de pouvoir l’utiliser. Comme pour les types
précédents, il est possible de dissocier la déclaration d’un objet de son instanciation,
en écrivant les instructions sur plusieurs lignes, par exemple :
186
INSTANCIER UN OBJET
1 Voiture voitureNicolas ;
2 // des choses
3 voitureNicolas = new Voiture () ;
ceci est possible tant que nous n’utilisons pas la variable voitureNicolas avant de
l’avoir instanciée.
Les objets peuvent également avoir une valeur nulle. Ceci est différent de l’absence
d’initialisation car la variable est bien initialisée et sa valeur vaut « nul ». Ceci est
possible grâce à l’emploi du mot-clé null.
1 Voiture voitureNicolas = null ;
Attention, il est par contre impossible d’accéder à un objet qui vaut null. Eh oui,
comment voulez-vous vous asseoir sur une chaise qui n’existe pas ? Eh bien vous vous
retrouverez avec les fesses par terre, personne ne vous a indiqué que la chaise n’existait
pas.
C’est pareil pour notre application, si nous tentons d’utiliser une voiture qui n’existe
pas, nous aurons droit à un beau plantage.
Par exemple, avec le code suivant :
1 Voiture voitureNicolas = null ;
2 voitureNicolas . Vitesse = 5 ;
vous n’aurez pas d’erreur à la compilation, par contre vous aurez un message d’excep-
tion assez explicite :
Comme nous l’avons déjà vu, le programme nous affiche une exception ; nous avons
dit que c’était simplement une erreur qui faisait planter notre programme. Ce n’est
pas bien ! Surtout que cela se passe au moment de l’exécution. Nous perdons toute
crédibilité !
Ici, le programme nous dit que la référence d’un objet n’est pas définie à une instance
d’un objet. Concrètement, cela veut dire que nous essayons de travailler sur un objet
null.
Pour éviter ce genre d’erreur à l’exécution, il faut impérativement instancier ses objets,
en utilisant l’opérateur new, comme nous l’avons déjà vu. Il n’est cependant pas toujours
pertinent d’instancier un objet dont on pourrait ne pas avoir besoin. Le C# nous
offre donc la possibilité de tester la nullité d’un objet. Il suffit d’utiliser l’opérateur de
comparaison « == » en comparant un objet au mot-clé null, par exemple :
1 string prenom = " Nicolas " ;
2 Voiture voiture = null ;
187
CHAPITRE 21. MANIPULER DES OBJETS
Ainsi, seul Nicolas possédera une voiture et le test de nullité sur l’objet permet d’éviter
une erreur d’exécution si le prénom est différent.
Maintenant que vous connaissez le mot-clé null et que vous savez qu’un objet peut
prendre une valeur nulle, nous allons revenir sur un point que j’ai rapidement abordé
auparavant. Je ne sais pas si vous vous en rappelez, mais lors de l’étude des opérateurs
logiques j’ai parlé du fait que l’opérateur OU ( || ) évaluait la première condition et
si elle était vraie alors il n’évaluait pas la suivante, considérant que de toute façon, le
résultat allait être vrai.
Ce détail prend toute son importance dans le cas suivant :
1 if ( voiture == null || voiture . Couleur == " Bleue " )
2 {
3 // faire quelque chose
4 }
Dans ce cas, si la voiture est effectivement nulle, alors le fait d’évaluer la propriété
Couleur de la voiture devrait renvoyer une erreur. Heureusement, le C# avait prévu
le coup. Si la première condition est vraie alors la seconde ne sera pas évaluée, ce qui
évitera l’erreur. Ainsi, nous sommes sûrs de n’avoir aucune voiture bleue.
Il est par contre évident qu’une telle condition utilisant l’opérateur ET (&&) est une
hérésie car pour que la condition soit vraie, le C# a besoin d’évaluer les deux opérandes.
Et donc si la voiture est nulle, l’utilisation d’une propriété sur une valeur nulle renverra
une erreur.
Notons également que lorsque nous utilisons l’opérateur ET (&&), si la première opé-
rande est fausse, alors de la même façon, il n’évalue pas la seconde, car pour que la
condition soit vraie il faut que les deux le soient. Ce qui fait qu’il est également possible
d’écrire ce code :
1 if ( voiture != null && voiture . Couleur == " Rouge " )
2 {
3 // faire autre chose
4 }
qui ne provoquera pas d’erreur à l’exécution, même si voiture vaut null car dans ce
cas, le fait que le premier test soit faux évitera le test de l’autre partie de l’expression.
Vous verrez que vous aurez l’occasion d’utiliser le mot-clé null régulièrement.
188
LE MOT-CLÉ THIS
Le mot-clé this
Lorsque nous écrivons le code d’une classe, le mot-clé this représente l’objet dans
lequel nous nous trouvons. Il permet de clarifier éventuellement le code, mais il est gé-
néralement facultatif. Ainsi, pour accéder à une variable de la classe ou éventuellement
une méthode, nous pouvons les préfixer par « this. ». Par exemple, nous pourrions
écrire notre classe de cette façon :
1 public class Voiture
2 {
3 public int Vitesse { get ; set ; }
4 public string Couleur { get ; set ; }
5
6 public Voiture ()
7 {
8 this . Vitesse = 5 ;
9 }
10
11 public void Rouler ()
12 {
13 Console . WriteLine ( " Je roule à " + this . Vitesse + " km / h
");
14 }
15
16 public void Accelerer ( int acceleration )
17 {
18 this . Vitesse += acceleration ;
19 this . Rouler () ;
20 }
21 }
Ici, dans le constructeur, nous utilisons le mot-clé this pour accéder à la propriété
Vitesse. C’est la même chose dans la méthode Rouler. De la même façon, on peut uti-
liser this.Rouler() pour appeler la méthode Rouler depuis la méthode Accelerer().
C’est une façon pour la classe de dire : « Regardez, avec this, c’est "ma variable à
moi" ».
Notez bien sûr que sans le mot-clé this, notre classe compilera quand même et sera
tout à fait fonctionnelle. Ce mot-clé est facultatif mais il peut aider à bien faire la
différence entre ce qui appartient à la classe et ce qui fait partie des paramètres des
méthodes ou d’autres objets utilisés.
Suivant les personnes, le mot-clé this est soit systématiquement utilisé, soit jamais. Je
fais plutôt partie des personnes qui ne l’utilisent jamais. Il arrive par contre certaines
situations où il est absolument indispensable, comme celle-ci, mais en général, j’essaie
d’éviter ce genre de construction :
1 public void ChangerVitesse ( int Vitesse )
2 {
3 this . Vitesse = Vitesse ;
189
CHAPITRE 21. MANIPULER DES OBJETS
4 }
En résumé
– Les classes possèdent une méthode particulière, appelée à l’instanciation de l’objet :
le constructeur.
– Une instance d’une classe peut être initialisée avec une valeur nulle grâce au mot-clé
null.
– Le mot-clé this représente l’objet en cours de la classe.
190
Chapitre 22
La POO et le C#
Difficulté :
D
ans ce chapitre, vous allez vous immerger un peu plus dans les subtilités de la POO
en utilisant le C#. Il est temps un peu de tourmenter nos objets et de voir ce qu’ils
ont dans le ventre. Ainsi, nous allons voir comment les objets héritent les uns des
autres ou comment fonctionnent les différents polymorphismes.
Nous allons également voir comment tous ces concepts se retrouvent dans le quotidien d’un
développeur C#.
191
CHAPITRE 22. LA POO ET LE C#
Ok, je sais maintenant créer des objets, mais je me rappelle qu’au début du
livre, nous avons manipulé des int et des string et que tu as appelé ça des
« types » ; et après, tu nous dis que tout est objet. . . Tu serais pas en train
de raconter n’importe quoi par hasard ? !
Eh bien non, ô perspicace lecteur ! Précisons un peu, maintenant que vous avez de
meilleures connaissances. J’ai bien dit que tout était objet, je le maintiens, même sous
la torture ! C’est-à-dire que même les types simples comme les entiers int ou les chaînes
de caractères sont des objets.
J’en veux pour preuve ce simple exemple :
1 int a = 10 ;
2 string chaine = a . ToString () ;
3 chaine = " abc " + chaine ;
4 string chaineE nMajuscul e = chaine . ToUpper () ;
5 Console . WriteLine ( c haineEnM ajuscule ) ;
6 Console . WriteLine ( c haineEnM ajuscule . Length ) ;
La variable a est un entier. Nous appelons la méthode ToString() sur cet entier. Même
si nous n’avons pas encore vu à quoi elle servait, nous pouvons supposer qu’elle effectue
une action qui consiste à transformer l’entier en chaîne de caractères. Nous concaténons
ensuite la chaîne abc à cette chaîne et nous effectuons une action qui, à travers la mé-
thode ToUpper(), met la chaîne en majuscule. Enfin, la méthode Console.WriteLine
nous affiche « ABC10 » puis nous affiche la propriété Length de la chaîne de caractères
qui correspond bien sûr à sa taille.
Pour créer une chaîne de caractères, nous utilisons le mot-clé string. Sachez que ce
mot-clé est équivalent à la classe String (notez la différence de casse). En créant une
chaîne de caractères, nous avons instancié un objet défini par la classe String.
En fait, le mot-clé string est ce qu’on appelle un alias de la classe String qui se situe
dans l’espace de nom System. De même, le mot-clé int est un alias de la structure
Int32 qui se situe également dans l’espace de nom System (nous verrons un peu plus
loin ce qu’est vraiment une structure).
Ce qui fait que les instructions suivantes :
1 int a = 10 ;
2 string chaine = " abc " ;
192
DES TYPES, DES OBJETS, TYPE VALEUR ET TYPE RÉFÉRENCE
En pratique, comme on l’a déjà fait, on utilise plutôt les alias que les classes
qu’ils représentent.
Cependant, les entiers, les booléens et autres types « simples » sont ce qu’on appelle des
types intégrés. Et même si ce sont des objets à part entière (méthodes, propriétés,. . . ),
ils ont des particularités, notamment dans la façon dont ils sont gérés par le framework
.NET. On les appelle des types valeur, car les variables de ce type possèdent la vraie
valeur de ce qu’on leur affecte a contrario des classes qui sont des types référence
dont les variables possèdent simplement un lien vers un objet en mémoire.
Par exemple :
1 int entier = 5 ;
Ici, la variable contient vraiment l’entier 5. Alors que pour l’instanciation suivante :
1 Voiture voitureNicolas = new Voiture () ;
193
CHAPITRE 22. LA POO ET LE C#
5 Console . WriteLine ( b ) ;
affichera les valeurs 5 puis 6 ; ce qui est le résultat que l’on attend.
– En effet, la variable a a été initialisée à 5.
– On a ensuite affecté a à b. La valeur 5 s’est copiée (dupliquée) dans la variable b.
– Puis nous avons affecté 6 à b.
Ce qui paraît tout à fait logique ! Par contre, l’exemple suivant :
1 Voiture voitureNicolas = new Voiture () ;
2 voitureNicolas . Couleur = " Bleue " ;
3 Voiture voitureJeremie = voitureNicolas ;
4 voitureJeremie . Couleur = " Verte " ;
5 Console . WriteLine ( voitureNicolas . Couleur ) ;
6 Console . WriteLine ( voitureJeremie . Couleur ) ;
Quoi ? Nous indiquons que la voiture de Nicolas est bleue. Puis nous disons
que celle de Jérémie est verte et quand on demande d’afficher la couleur des
deux voitures, on nous dit qu’elles sont vertes toutes les deux alors qu’on
croyait que celle de Nicolas était bleue ? Tout à l’heure, le fait de changer b
n’avait pas changé la valeur de a. . .
Eh oui, ceci illustre le fait que les classes (comme Voiture) sont des types référence
et ne possèdent qu’une référence vers une instance de Voiture. Quand nous affectons
voitureNicolas à voitureJeremie, nous disons en fait que la voiture de Jérémie
référence la même chose que celle de Nicolas. Concrètement, le C# copie la référence
de l’objet Voiture qui est contenue dans la variable voitureNicolas dans la variable
voitureJeremie. Ce sont donc deux variables différentes qui possèdent toutes les deux
une référence vers l’objet Voiture, qui est la voiture de Nicolas. C’est-à-dire que les
deux variables référencent le même objet. Ainsi, la modification des propriétés de l’un
affectera forcément l’autre.
Inattendu au premier abord, mais finalement, c’est très logique. Comprendre cette
différence entre les types valeur et les types référence est important, nous verrons
dans les chapitres suivants quels sont les autres impacts de cette différence. À noter
également qu’il est impossible de dériver d’un type intégré alors que c’est possible, et
facile, de dériver d’une classe.
D’ailleurs, si nous parlions un peu d’héritage ?
Héritage
Nous avons vu pour l’instant la théorie de l’héritage. Que les objets chiens héritaient
des comportements des objets Animaux, que les labradors héritaient des comportements
des chiens, etc.
194
HÉRITAGE
Passons maintenant à la pratique et créons une classe Animal et une classe Chien qui
en hérite. Nous allons créer des classes relativement courtes et nous nous limiterons
dans le nombre d’actions ou de propriétés de celles-ci. Par exemple, nous pourrions
imaginer que la classe Animal possède une propriété NombreDePattes qui est un entier
et une méthode Respirer qui affiche le détail de l’action. Ce qui donne :
1 public class Animal
2 {
3 public int NombreDePattes { get ; set ; }
4
5 public void Respirer ()
6 {
7 Console . WriteLine ( " Je respire " ) ;
8 }
9 }
La classe Chien dérive de la classe Animal et peut donc hériter de certains de ses com-
portements. En l’occurrence, la classe Chien héritera de tout ce qui est public ou pro-
tégé, identifiés comme vous le savez désormais par les mots-clés public et protected.
Le chien sait également faire quelque chose qui lui est propre, à savoir aboyer. Il pos-
sédera donc une méthode supplémentaire. Ce qui donne :
1 public class Chien : Animal
2 {
3 public void Aboyer ()
4 {
5 Console . WriteLine ( " Wouaf ! " ) ;
6 }
7 }
Si nous exécutons ce code, nous nous rendons bien compte que l’objet Chien, bien que
n’ayant pas défini la propriété NombreDePattes ou la méthode Respirer() dans le
corps de sa classe, est capable d’avoir des pattes et de faire l’action respirer :
Je respire
Je respire
Wouaf !
195
CHAPITRE 22. LA POO ET LE C#
Il a hérité ces comportements de l’objet Animal, en tout cas, ceux qui sont publics.
Rajoutons deux variables membres de la classe Animal :
1 public class Animal
2 {
3 private bool estVivant ;
4 public int age ;
5
6 public int NombreDePattes { get ; set ; }
7
8 public void Respirer ()
9 {
10 Console . WriteLine ( " Je respire " ) ;
11 }
12 }
L’entier age est public alors que le booléen estVivant est privé. Si nous tentons de les
utiliser depuis la classe fille Chien, comme ci-dessous :
1 public class Chien : Animal
2 {
3 public void Aboyer ()
4 {
5 Console . WriteLine ( " Wouaf ! " ) ;
6 }
7
8 public void Vieillir ()
9 {
10 age ++;
11 }
12
13 public void Naissance ()
14 {
15 age = 0 ;
16 estVivant = true ; /* Erreur > '
M a P r e m i e r e A p p l i c a t i o n . Animal . estVivant ' est
inaccessible en raison de son niveau de protection
*/
17 }
18 }
nous voyons qu’il est tout à fait possible d’utiliser la variable age depuis la méthode
Vieillir() alors que l’utilisation du booléen estVivant provoque une erreur de com-
pilation. Vous avez bien compris que celui-ci était inaccessible car il est défini comme
membre privé. Pour l’utiliser, on pourra le rendre public par exemple.
Il existe par contre un autre mot-clé qui permet de rendre des variables, propriétés ou
méthodes inaccessibles depuis un autre objet tout en les rendant accessibles depuis des
classes filles. Il s’agit du mot-clé protected. Si nous l’utilisons à la place de private
pour définir la visibilité du booléen estVivant, nous pourrons nous rendre compte que
la classe Chien peut désormais compiler :
196
HÉRITAGE
Par contre, cette variable est toujours inaccessible depuis d’autres classes, comme l’est
également une variable privée. Dans notre classe Program, l’instruction suivante :
1 chien . estVivant = true ;
Le mot-clé protected prend tout son intérêt dès que nous avons à faire avec l’héritage.
Nous verrons un peu plus loin d’autres exemples de ce mot-clé.
Nous avons dit dans l’introduction qu’un objet B qui dérive de l’objet A est « une
sorte » d’objet A. Dans notre exemple du dessus, le Chien est une sorte d’Animal. Cela
veut dire que nous pouvons utiliser un chien en tant qu’animal. Par exemple, le code
suivant :
1 Animal animal = new Chien { NombreDePattes = 4 };
est tout à fait correct. Nous disons que notre variable animal, de type Animal est une
instance de Chien.
Avec cette façon d’écrire, nous avons réellement instancié un objet Chien mais celui-
ci sera traité en tant qu’Animal. Cela veut dire qu’il sera capable de Respirer() et
d’avoir des pattes. Par contre, même si en vrai, notre objet est capable d’aboyer, le fait
qu’il soit manipulé en tant qu’Animal nous empêche de pouvoir le faire Aboyer. Cela
veut dire que le code suivant :
1 Animal animal = new Chien { NombreDePattes = 4 };
2 animal . Respirer () ;
3 animal . Aboyer () ; // erreur de compilation
provoquera une erreur de compilation pour indiquer que la classe Animal ne contient
aucune définition pour la méthode Aboyer(). Ce qui est normal, car un animal ne sait
pas forcément aboyer. . .
197
CHAPITRE 22. LA POO ET LE C#
Bonne question.
Pour y répondre, nous allons enrichir notre classe Animal, garder notre classe Chien et
créer une classe Chat qui hérite également d’Animal. Ce pourrait être :
1 public class Animal
2 {
3 protected string prenom ;
4
5 public void Respirer ()
6 {
7 Console . WriteLine ( " Je suis " + prenom + " et je respire
");
8 }
9 }
10
11 public class Chien : Animal
12 {
13 public Chien ( string prenomDuChien )
14 {
15 prenom = prenomDuChien ;
16 }
17
18 public void Aboyer ()
19 {
20 Console . WriteLine ( " Wouaf ! " ) ;
21 }
22 }
23
24 public class Chat : Animal
25 {
26 public Chat ( string prenomDuChat )
27 {
28 prenom = prenomDuChat ;
29 }
30
31 public void Miauler ()
32 {
33 Console . WriteLine ( " Miaou " ) ;
34 }
35 }
Nous forçons les chiens et les chats à avoir un nom, hérité de la classe Animal, grâce au
constructeur afin de pouvoir les identifier facilement. Le chat garde le même principe
que le chien, sauf que nous avons une méthode Miauler() à la place de la méthode
Aboyer(). . . Ce qui est, somme toute, logique ! L’idée est de pouvoir utiliser nos chiens
et nos chats ensemble comme des animaux, par exemple en utilisant une liste.
198
HÉRITAGE
Nous avons dans un premier temps instancié une liste d’animaux à laquelle nous avons
rajouté 3 chiens et 2 chats, chacun étant considéré comme un animal puisqu’ils sont
tous des sortes d’animaux, grâce à l’héritage. Maintenant, nous n’avons plus que des
animaux dans la liste. Il sera donc possible de les faire tous respirer en une simple
boucle :
1 foreach ( Animal animal in animaux )
2 {
3 animal . Respirer () ;
4 }
Ce qui donne :
199
CHAPITRE 22. LA POO ET LE C#
6 {
7 prenom = prenomAnimal ;
8 }
9
10 public void Respirer ()
11 {
12 Console . WriteLine ( " Je suis " + prenom + " et je respire
");
13 }
14 }
15
16 public class Chien : Animal
17 {
18 public Chien ( string prenomDuChien ) : base ( prenomDuChien )
19 {
20 }
21
22 public void Aboyer ()
23 {
24 Console . WriteLine ( " Wouaf ! " ) ;
25 }
26 }
27
28 public class Chat : Animal
29 {
30 public Chat ( string prenomDuChat ) : base ( prenomDuChat )
31 {
32 }
33
34 public void Miauler ()
35 {
36 Console . WriteLine ( " Miaou " ) ;
37 }
38 }
200
HÉRITAGE
Ici, la classe Animal met un prénom par défaut dans son constructeur. Le chien n’a pas
de constructeur et le chat en a un qui accepte un paramètre.
Il est donc possible de créer un Chien sans qu’il ait de prénom mais il est obligatoire
d’en définir un pour le chat. Sauf que lorsque nous instancierons notre objet chien, il
appellera automatiquement le constructeur de la classe mère et tous nos chiens s’ap-
pelleront Marcel :
1 static void Main ( string [] args )
2 {
3 List < Animal > animaux = new List < Animal >() ;
4 Animal chien = new Chien () ;
5 Animal tom = new Chat ( " Tom " ) ;
201
CHAPITRE 22. LA POO ET LE C#
Ce qui affichera :
Si nous souhaitons rajouter un constructeur par défaut qui initialise la vitesse à 10 par
exemple, nous pourrons faire :
1 public class Voiture
2 {
3 private int vitesse ;
4
5 public Voiture ()
6 {
7 vitesse = 10 ;
8 }
9
10 public Voiture ( int vitesseVoiture )
11 {
12 vitesse = vitesseVoiture ;
13 }
14 }
Ou encore :
202
HÉRITAGE
Ici, l’utilisation du mot-clé this, suivi d’un entier permet d’appeler le constructeur
qui possède un paramètre entier au début du constructeur par défaut. Inversement,
nous pouvons appeler le constructeur par défaut d’une classe depuis un constructeur
possédant des paramètres afin de pouvoir bénéficier des initialisations de celui-ci :
1 public class Voiture
2 {
3 private int vitesse ;
4 private string couleur ;
5
6 public Voiture ()
7 {
8 vitesse = 10 ;
9 }
10
11 public Voiture ( string couleurVoiture ) : this ()
12 {
13 couleur = couleurVoiture ;
14 }
15 }
Puisque nous parlons d’héritage, il faut savoir que tous les objets que nous créons ou
qui sont disponibles dans le framework .NET héritent d’un objet de base. On parle
en général d’un « super-objet ». L’intérêt de dériver d’un tel objet est de permettre à
tous les objets d’avoir certains comportements en commun, mais également de pouvoir
éventuellement tous les traiter en tant qu’objet. Notre super-objet est représenté par
la classe Object qui définit plusieurs méthodes. Vous les avez déjà vues si vous avez
regardé dans la complétion automatique après avoir créé un objet. Prenons une classe
toute vide, par exemple :
1 public class ObjetVide
2 {
3 }
Si nous instancions cet objet et que nous souhaitons l’utiliser, nous verrons que la
complétion automatique nous propose des méthodes que nous n’avons jamais créées
(voir figure 22.1).
203
CHAPITRE 22. LA POO ET LE C#
Ce qui affiche :
M a P r e m i e r e A p p l i c a t i o n . ObjetVide
M a P r e m i e r e A p p l i c a t i o n . Chien
30
Nicolas
204
HÉRITAGE
J’en profite maintenant que vous connaissez la méthode ToString() pour parler d’un
point qui a peut-être titillé vos cerveaux. Dans la première partie, nous avions fait
quelque chose du genre :
1 int vitesse = 20 ;
2 string chaine = " La vitesse est " + vitesse + " km / h " ;
La variable vitesse est un entier. La chaîne La vitesse est est une chaîne de carac-
tères. Nous essayons d’ajouter un entier à une chaîne alors que j’ai dit qu’ils n’étaient
pas compatibles entre eux ! Et pourtant cela fonctionne. Effectivement, c’est bizarre !
Nous concaténons une chaîne à un entier avec l’opérateur + et nous concaténons encore
une chaîne.
Et si je fais l’inverse :
1 int vitesse = 20 + " 40 " ;
cela provoque une erreur de compilation. C’est logique, on ne peut pas ajouter un entier
et une chaîne de caractères. Alors pourquoi cela fonctionne dans l’autre sens ? Ce qui
se passe en fait dans l’instruction :
1 string chaine = " La vitesse est " + vitesse + " km / h " ;
c’est que le compilateur se rend compte que nous concaténons une chaîne avec un
autre objet, peu importe que ce soit un entier ou un objet complexe. Alors, pour que
ça fonctionne, il demande une représentation de l’objet sous la forme d’une chaîne de
caractères. Nous avons vu que ceci se faisait en appelant la méthode ToString() qui
est héritée de l’objet racine Object.
L’instruction est donc équivalente à :
1 string chaine = " La vitesse est " + vitesse . ToString () + " km / h
";
205
CHAPITRE 22. LA POO ET LE C#
Dans le cas d’un type valeur comme un entier, la méthode ToString() renvoie la
représentation interne de la valeur, à savoir « 20 ». Dans le cas d’un objet complexe,
elle aurait renvoyé le nom du type de l’objet.
Avant de terminer, il est important d’indiquer que le C# n’autorise pas l’héritage
multiple. Ainsi, si nous possédons une classe Carnivore et une classe EtreVivant, il
ne sera pas possible de faire hériter directement un objet Homme de l’objet Carnivore
et de l’objet EtreVivant. Ainsi, le code suivant :
En revanche, et c’est pertinent, nous pourrons faire un héritage en cascade afin que
Carnivore dérive de EtreVivant et que Homme dérive de Carnivore :
Cependant, il n’est pas toujours pertinent d’opérer de la sorte. Notre Homme pourrait
être à la fois Carnivore et Frugivore, cependant cela n’a pas de sens qu’un carnivore
soit également frugivore, ou l’inverse.
206
SUBSTITUTION
Tu avais pourtant dit que chaque objet dérivait du super-objet Object, mais
s’il dérive d’une autre classe comme un chien dérive d’un animal, ça fait bien
deux classes dont il dérive. . .
Effectivement, mais dans ce cas-là, ce n’est pas pareil. Comme il est automatique de
dériver de object, c’est comme si on avait le chien qui hérite de animal qui hérite
lui-même de object. Le C# est assez malin pour ça !
Substitution
Nous avons vu juste avant l’utilisation de la méthode ToString() qui permet d’obtenir
la représentation d’un objet sous forme de chaîne de caractères. En l’occurrence, vous
conviendrez avec moi que la représentation de notre classe Chien n’est pas particuliè-
rement exploitable. Le nom du type c’est bien, mais ce n’est pas très parlant.
Ça serait pas mal que, quand nous demandons d’afficher un chien, nous obtenions
le nom du chien, vous ne trouvez pas ? C’est là qu’intervient la substitution. Nous
en avons parlé dans l’introduction à la POO, la substitution permet de redéfinir un
comportement dont l’objet a hérité afin qu’il corresponde aux besoins de l’objet fils.
Typiquement, ici, la méthode ToString() du super-objet ne nous convient pas et dans
le cas de notre chien, nous souhaitons la redéfinir, en écrire une nouvelle version.
Pour cet exemple, simplifions notre classe Chien afin qu’elle n’ait qu’une propriété pour
stocker son prénom :
1 public class Chien
2 {
3 public string Prenom { get ; set ; }
4 }
Pour redéfinir la méthode ToString() nous allons devoir utiliser le mot-clé override
qui signifie que nous souhaitons substituer la méthode existante afin de remplacer son
comportement, ce que nous pourrons écrire en C# avec :
1 public class Chien
2 {
3 public string Prenom { get ; set ; }
4
5 public override string ToString ()
6 {
7 return " Je suis un chien et je m ' appelle " + Prenom ;
8 }
9 }
207
CHAPITRE 22. LA POO ET LE C#
Nous pourrions imaginer de créer une classe ChienMuet qui dérive de la classe Chien
et qui hérite donc de ses comportements. Mais, que penser d’un chien muet qui serait
capable d’aboyer ? Cela n’a pas de sens ! Il faut donc redéfinir cette fichue méthode.
Utilisons alors le mot-clé override comme nous l’avons vu pour obtenir :
1 public class ChienMuet : Chien
2 {
3 public override void Aboyer ()
4 {
5 Console . WriteLine ( " ... " ) ;
6 }
7 }
Sauf que nous rencontrons un problème. Si nous tentons de compiler ce code, Visual
C# Express nous génère une erreur de compilation :
En réalité, pour pouvoir créer une méthode qui remplace une autre, il faut qu’une condi-
tion supplémentaire soit vérifiée : il faut que la méthode à remplacer s’annonce
comme candidate à la substitution. Cela veut dire que l’on ne peut pas substituer
n’importe quelle méthode, mais seulement celles qui acceptent de l’être. C’est le cas
pour la méthode ToString que nous avons vue précédemment. Les concepteurs du fra-
mework .NET ont autorisé cette éventualité. Heureusement, sinon, nous serions bien
embêtés !
208
SUBSTITUTION
Pour marquer notre méthode Aboyer de la classe Chien comme candidate éventuelle à
la substitution, il faut la préfixer du mot-clé virtual. Ainsi, elle annonce à ses futures
filles que si elles le souhaitent, elles peuvent redéfinir cette méthode.
Cela se traduit ainsi dans le code :
1 public class Chien
2 {
3 public virtual void Aboyer ()
4 {
5 Console . WriteLine ( " Wouaf ! " ) ;
6 }
7 }
8
9 public class ChienMuet : Chien
10 {
11 public override void Aboyer ()
12 {
13 Console . WriteLine ( " ... " ) ;
14 }
15 }
Désormais, l’instanciation de l’objet est possible et nous pourrons avoir notre code :
1 ChienMuet pauvreChien = new ChienMuet () ;
2 pauvreChien . Aboyer () ;
Ce code affichera :
...
Il y a encore un dernier point que nous n’avons pas abordé. Il s’agit de la capacité
pour une classe fille de redéfinir une méthode tout en conservant la fonctionnalité de
209
CHAPITRE 22. LA POO ET LE C#
la méthode de la classe mère. Imaginons notre classe Animal qui possède une méthode
Manger() :
1 public class Animal
2 {
3 public virtual void Manger ()
4 {
5 Console . WriteLine ( " Mettre les aliments dans la bouche " )
;
6 Console . WriteLine ( " Mastiquer " ) ;
7 Console . WriteLine ( " Avaler " ) ;
8 Console . WriteLine ( " ... " ) ;
9 }
10 }
Dans cet exemple, je fais quelque chose avant d’appeler la méthode de la classe mère,
puis je fais quelque chose d’autre après. Maintenant, si nous faisons manger notre chien :
1 Chien chien = new Chien () ;
2 chien . Manger () ;
s’affichera dans la console toute la série des actions définies comme étant « manger » :
R é clamer à manger au ma î tre
Mettre les aliments dans la bouche
Mastiquer
Avaler
...
Remuer la queue
Nous voyons bien avec cet exemple comment la classe fille peut réutiliser les méthodes
de sa classe mère.
210
POLYMORPHISME
Polymorphisme
Nous avons dit qu’une manifestation du polymorphisme était la capacité pour une classe
d’effectuer la même action sur différents types d’intervenants. Il s’agit de la surcharge,
appelée aussi polymorphisme ad hoc.
Concrètement, cela veut dire qu’il est possible de définir la même méthode avec des
paramètres en entrée différents. Si vous vous rappelez bien, c’est quelque chose que nous
avons déjà fait sans le savoir. Devinez . . . Oui, c’est ça, avec la méthode
Console.WriteLine.
Nous avons pu afficher des chaînes de caractères, mais aussi des entiers, même des types
double, et plus récemment des objets. Comment ceci est possible alors que nous avons
déjà vu qu’il était impossible de passer des types en paramètres d’une méthode qui ne
correspondent pas à sa signature ? !
Ainsi, l’exemple suivant :
1 public class Program
2 {
3 static void Main ( string [] args )
4 {
5 Math math = new Math () ;
6 int a = 5 ;
7 int b = 6 ;
8 int resultat = math . Addition (a , b ) ;
9
10 double c = 1 . 5 ;
11 double d = 5 . 0 ;
12 resultat = math . Addition (c , d ) ; /* erreur de
compilation */
13 }
14 }
15
16 public class Math
17 {
18 public int Addition ( int a , int b )
19 {
20 return a + b ;
21 }
22 }
provoquera une erreur de compilation lorsque nous allons essayer de passer des variables
du type double à notre méthode qui prend des entiers en paramètres. Pour que ceci
fonctionne, nous allons rendre polymorphe cette méthode en définissant à nouveau
cette même méthode mais en lui faisant prendre des paramètres d’entrée différents :
1 public class Program
2 {
3 static void Main ( string [] args )
4 {
5 Math math = new Math () ;
211
CHAPITRE 22. LA POO ET LE C#
6 int a = 5 ;
7 int b = 6 ;
8 int resultat = math . Addition (a , b ) ;
9
10 double c = 1 . 5 ;
11 double d = 5 . 0 ;
12 double resultatDouble = math . Addition (c , d ) ; /* ç a
compile , youpi */
13 }
14 }
15
16 public class Math
17 {
18 public int Addition ( int a , int b )
19 {
20 return a + b ;
21 }
22
23 public double Addition ( double a , double b )
24 {
25 return a + b ;
26 }
27 }
Nous avons ainsi écrit deux formes différentes de la même méthode. Une qui accepte des
entiers et l’autre qui accepte des double. Ce code fonctionne désormais correctement.
Il est bien sûr possible d’écrire cette méthode avec beaucoup de paramètres de types
différents, même une classe Chien, en imaginant que le fait d’additionner deux chiens
correspond au fait d’additionner leurs nombres de pattes :
1 public class Math
2 {
3 public int Addition ( int a , int b )
4 {
5 return a + b ;
6 }
7
8 public double Addition ( double a , double b )
9 {
10 return a + b ;
11 }
12
13 public int Addition ( Chien c1 , Chien c2 )
14 {
15 return c1 . NombreDePattes + c2 . NombreDePattes ;
16 }
17 }
Attention, j’ai toujours indiqué qu’il était possible d’ajouter une nouvelle forme à la
même méthode en changeant les paramètres d’entrées. Vous ne pourrez pas le faire en
212
POLYMORPHISME
changeant uniquement le paramètre de retour. Ce qui fait que cet exemple ne pourra
pas compiler :
1 public class Math
2 {
3 public int Addition ( int a , int b )
4 {
5 return a + b ;
6 }
7
8 public double Addition ( int a , int b )
9 {
10 return a + b ;
11 }
12 }
Les deux méthodes acceptent deux entiers en paramètres et renvoient soit un entier,
soit un double. Le compilateur ne sera pas capable de choisir quelle méthode utiliser
lorsque nous essayerons d’appeler cette méthode. Les méthodes doivent se différencier
avec les paramètres d’entrées. Lorsque nous avons plusieurs signatures possibles pour
la même méthode, vous remarquerez que la complétion automatique nous propose alors
plusieurs possibilités (voir figure 22.2).
Visual C# indique qu’il a trois méthodes possibles qui s’appellent Math.Addition. Pour
voir la signature des autres méthodes, il suffit de cliquer sur les flèches, ou d’utiliser les
flèches du clavier, comme indiqué à la figure 22.3.
C’est ce qui se passe dans la méthode Console.WriteLine (voir figure 22.4).
Nous voyons ici qu’il existe 19 écritures de la méthode WriteLine, la cinquième prenant
en paramètre un décimal. Notez que pour écrire plusieurs formes de cette méthode, nous
pouvons également jouer sur le nombre de paramètres. La méthode :
1 public int Addition ( int a , int b , int c )
2 {
3 return a + b + c ;
4 }
213
CHAPITRE 22. LA POO ET LE C#
nous pourrons alors convertir le chien en animal dans la mesure où le chien est une
214
LA CONVERSION ENTRE LES OBJETS AVEC LE CASTING
sorte d’animal :
1 Chien medor = new Chien () ;
2 Animal animal = ( Animal ) medor ;
Nous utilisons pour ce faire un cast, comme nous l’avons déjà fait pour les types intégrés
(int, bool, etc.). Il suffit de préfixer la variable à convertir du type entre parenthèses
dans lequel nous souhaitons le convertir. Ici, nous pouvons convertir facilement notre
Chien en Animal.
Par contre, il est impossible de convertir un chien en voiture, car il n’y a pas de relation
d’héritage entre les deux. Ainsi les instructions suivantes :
1 Chien medor = new Chien () ;
2 Voiture voiture = ( Voiture ) medor ;
Dans ce cas, nous créons un objet Chien et un objet Chat que nous mettons dans une
liste d’objets Animal grâce à une conversion utilisant un cast. En fait, ce cast est inutile
et nous pouvons simplement écrire :
1 List < Animal > animaux = new List < Animal >() ;
2 Chien chien = new Chien () ;
3 Chat chat = new Chat () ;
4
5 animaux . Add ( chien ) ;
6 animaux . Add ( chat ) ;
La conversion est implicite, comme lorsque nous avions utilisé un object en paramètres
d’une méthode et que nous pouvions lui passer tous les types qui dérivent d’object.
Nous avions également vu que nous pouvions traiter les chiens et les chats comme
des animaux à partir du moment où nous les mettions dans une liste. Avec les objets
suivants :
215
CHAPITRE 22. LA POO ET LE C#
Nous pouvions utiliser une boucle pour faire respirer tous nos animaux :
1 List < Animal > animaux = new List < Animal >() ;
2 Chien chien = new Chien () ;
3 Chat chat = new Chat () ;
4
5 animaux . Add ( chien ) ;
6 animaux . Add ( chat ) ;
7
8 foreach ( Animal animal in animaux )
9 {
10 animal . Respirer () ;
11 }
Mais impossible de faire aboyer le chien, ni miauler le chat. Si vous tentez de remplacer
dans la boucle Animal par Chien, avec :
1 foreach ( Chien c in animaux )
2 {
3 c . Aboyer () ;
4 }
Vous pourrez faire aboyer le premier élément de la liste qui est effectivement un chien,
par contre il y aura un plantage au deuxième élément de la liste car il s’agit d’un chat :
Waouf
216
LA CONVERSION ENTRE LES OBJETS AVEC LE CASTING
Lorsque notre programme a tenté de convertir un animal qui est un chat en chien, il
nous a fait comprendre qu’il n’appréciait que moyennement. Les chiens n’aiment pas
trop les chats d’une manière générale, alors en plus, un chat qui essaie de se faire passer
pour un chien : c’est une déclaration de guerre ! Voilà pourquoi notre programme a levé
une exception. Il lui était impossible de convertir un Chat en Chien.
Il est cependant possible de tester si une variable correspond à un objet grâce au mot-
clé is. Ce qui nous permettra de faire la conversion adéquate et de nous éviter une
erreur à l’exécution :
1 foreach ( Animal animal in animaux )
2 {
3 if ( animal is Chien )
4 {
5 Chien c = ( Chien ) animal ;
6 c . Aboyer () ;
7 }
8 if ( animal is Chat )
9 {
10 Chat c = ( Chat ) animal ;
11 c . Miauler () ;
12 }
13 }
Nous testons avec le mot-clé is si l’animal est une instance d’un chien ou d’un chat.
Le code du dessus nous permettra d’utiliser dans la boucle l’animal courant comme un
chien ou un chat en fonction de ce qu’il est vraiment, grâce au test :
Waouf
Miaou
Le fait de tester ce qu’est vraiment l’animal avant de le convertir est une sécurité
indispensable pour éviter ce genre d’erreur. C’est l’inconvénient du cast explicite. Il
convient très bien si nous sommes certains du type dans lequel nous souhaitons en
convertir un autre. Par contre, si la conversion n’est pas possible, alors nous aurons
une erreur. Lorsque nous ne sommes pas certains du résultat du cast, mieux vaut tester
si l’instance d’un objet correspond bien à l’objet lui-même.
Cela peut se faire comme nous l’avons vu avec le mot-clé is, mais également avec un
autre cast qui s’appelle le cast dynamique. Il se fait en employant le mot-clé as. Ce
cast dynamique vérifie que l’objet est bien convertible. Si c’est le cas, alors il fait un
217
CHAPITRE 22. LA POO ET LE C#
cast explicite pour renvoyer le résultat de la conversion, sinon, il renvoie une référence
nulle. Le code du dessus peut donc s’écrire :
1 foreach ( Animal animal in animaux )
2 {
3 Chien c1 = animal as Chien ;
4 if ( c1 != null )
5 {
6 c1 . Aboyer () ;
7 }
8 Chat c2 = animal as Chat ;
9 if ( c2 != null )
10 {
11 c2 . Miauler () ;
12 }
13 }
218
LA CONVERSION ENTRE LES OBJETS AVEC LE CASTING
À noter que ces opérations sont consommatrices de temps, elles sont donc à
faire le moins possible.
En résumé
– Les objets peuvent être des types valeur ou des types référence. Les variables de
type valeur possèdent la valeur de l’objet, comme un entier. Les variables de type
référence possèdent une référence vers l’objet en mémoire.
– Tous les objets dérivent de la classe de base Object.
– On peut substituer une méthode grâce au mot-clé override si elle s’est déclarée
candidate à la substitution grâce au mot-clé virtual.
– La surcharge est le polymorphisme permettant de faire varier les types des paramètres
d’une même méthode ou leurs nombres.
– Il est possible grâce au cast de convertir un type en un autre type, s’ils ont une
relation d’héritage.
219
CHAPITRE 22. LA POO ET LE C#
220
Chapitre 23
Notions avancées de POO en C#
Difficulté :
D
ans ce chapitre, nous allons continuer à découvrir comment nous pouvons faire de
l’orienté objet avec le C#. Nous allons pousser un peu plus loin en découvrant les
interfaces et en manipulant les classes statiques et abstraites.
À la fin de ce chapitre, vous serez capables de faire des objets encore plus évolués et vous
devriez être capables de créer un vrai petit programme orienté objet. . . !
221
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Ce code affichera la chaîne « Les objets référencent la même instance » car effectivement,
nous avons affecté la référence de voitureNicolas à voitureJeremie - ce qui implique
également que la modification de la voiture de Jérémie affecte également la voiture de
Nicolas, comme nous l’avons déjà vu.
Par contre, le code suivant :
1 Voiture voitureNicolas = new Voiture () ;
2 Voiture voitureJeremie = new Voiture () ;
3 if ( voitureJeremie == voitureNicolas )
4 {
5 Console . WriteLine ( " Les objets r é f é rencent la m ê me instance "
);
6 }
222
COMPARER DES OBJETS
S’il s’avère qu’il est vraiment pertinent de comparer deux voitures entre elles, il faut
savoir que c’est quand même possible. La première chose à faire est de définir les critères
de comparaison. Par exemple, nous n’avons qu’à dire que deux voitures sont identiques
quand la couleur, la marque et la vitesse sont égales. Je sais, c’est un peu irréel, mais
c’est pour l’exemple.
Ainsi, nous pourrons par exemple vérifier que deux voitures sont égales avec l’instruc-
tion suivante :
1 if ( voitureNicolas . Couleur == voitureJeremie . Couleur &&
voitureNicolas . Marque == voitureJeremie . Marque &&
voitureNicolas . Vitesse == voitureJeremie . Vitesse )
2 {
3 Console . WriteLine ( " Les deux voitures sont identiques " ) ;
4 }
La comparaison d’égalité entre deux objets, c’est en fait le rôle de la méthode Equals()
dont chaque objet hérite de la classe mère Object. À part pour les types valeur, le com-
portement par défaut de la méthode Equals() est de comparer les références des objets.
Seulement, il est possible de définir un comportement plus approprié pour notre classe
Voiture, grâce à la fameuse spécialisation. Pour plus d’informations sur la méthode
Equals(), je vous renvoie au code web suivant :
☛ ✟
Méthode Equals
⊲
✡Code web : 657987 ✠
Comme on l’a déjà vu, on utilise le mot-clé override. Ceci est possible dans la me-
sure où la classe Object a défini la méthode Equals comme virtuelle, avec le mot-clé
virtual. Ce qui donne :
1 public class Voiture
2 {
3 public string Couleur { get ; set ; }
4 public string Marque { get ; set ; }
5 public int Vitesse { get ; set ; }
6
7 public override bool Equals ( object obj )
8 {
9 Voiture v = obj as Voiture ;
10 if ( v == null )
11 return false ;
12 return Vitesse == v . Vitesse && Couleur == v . Couleur &&
Marque == v . Marque ;
13 }
14 }
223
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Nos deux voitures sont identiques car leurs marques, leurs couleurs et leurs vitesses
sont identiques :
C’est facile de comparer ! Sauf que vous aurez peut-être remarqué que la compilation
de ce code provoque un avertissement. Il ne s’agit pas d’une erreur, mais Visual C#
Express nous informe qu’il faut faire attention (voir la figure 23.1).
Il nous dit que nous avons substitué la méthode Equals() sans avoir redéfini la méthode
GetHashCode(). Nous n’avons pas besoin ici de savoir à quoi sert vraiment la méthode
GetHashCode(), mais si l’envie vous prend de vous instruire à ce sujet, vous pouvez
lire la documentation officielle disponible via le code web suivant :
☛ ✟
Méthode GetHashCode()
⊲
✡Code web : 785089 ✠
Toujours est-il que nous devons rajouter une spécialisation de la méthode GetHashCode(),
dont le but est de renvoyer un identifiant plus ou moins unique représentant l’objet, ce
qui donnera :
1 public class Voiture
2 {
3 public string Couleur { get ; set ; }
4 public string Marque { get ; set ; }
5 public int Vitesse { get ; set ; }
6
7 public override bool Equals ( object obj )
8 {
9 Voiture v = obj as Voiture ;
10 if ( v == null )
11 return false ;
224
LES INTERFACES
Nous nous servons du fait que chaque variable de la classe possède déjà un identifiant
obtenu avec la méthode GetHashCode(). En combinant chaque identifiant de chaque
propriété nous pouvons en créer un nouveau. Ici, la classe est complète et prête à
être comparée. Elle pourra donc fonctionner correctement avec tous les algorithmes
d’égalité du framework .NET, comme les clés de hachage. Notez quand même que
devoir substituer ces deux méthodes est une opération relativement rare.
Ce qu’il est important de retenir, c’est ce fameux warning. La conclusion à tirer est que
notre façon de comparer, bien que fonctionnelle pour notre voiture, n’est pas parfaite.
Pourquoi ? Parce qu’en ayant substitué la méthode Equals(), nous croyons que la
comparaison est bonne sauf que le compilateur nous apprend que ce n’est pas le cas.
Heureusement qu’il était là, ce compilateur ! Comme c’est une erreur classique, il est
capable de la détecter. Mais si c’est autre chose et qu’il ne le détecte pas ?
Tout ça manque d’uniformisation, vous ne trouvez pas ? Il faudrait quelque chose qui
nous assure que la classe est correctement comparable. Une espèce de contrat que l’objet
s’engagerait à respecter pour être sûr que toutes les comparaisons soient valides.
Un contrat ? Un truc qui finit par « able » ? Ça me rappelle quelque chose ça. . . mais
oui, les interfaces !
Les interfaces
Une fois n’est pas coutume. Plutôt que de commencer par étudier le plus simple, nous
allons étudier le plus logique puis nous reviendrons sur le plus simple. C’est-à-dire que
nous allons pousser un peu plus loin la comparaison en nous servant des interfaces et
nous reviendrons ensuite sur le moyen de créer une interface.
Nous avons donc dit qu’une interface était un contrat que s’engageait à respecter un
objet. C’est tout à fait ce dont on a besoin ici. Notre objet doit s’engager à fonctionner
pour tous les types de comparaison. Il doit être comparable. Mais comparable ne veut
pas forcément dire « égalité », nous devrions être aussi capables d’indiquer si un objet
est supérieur à un autre.
Pourquoi ? Imaginons que nous possédions un tableau de voitures et que nous souhai-
tions le trier comme on a vu dans un chapitre précédent. Pour les entiers c’était une
opération plutôt simple ; avec la méthode Array.Sort() ils étaient automatiquement
triés par ordre croissant. Mais dans notre cas, comment un tableau sera capable de
225
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Si vous tentez de compiler ce code, vous aurez un message d’erreur (voir figure 23.2).
Le compilateur nous rappelle à l’ordre : nous annonçons que nous souhaitons respecter
le contrat de comparaison, sauf que nous n’avons pas la méthode adéquate !
Eh oui, le contrat indique ce que nous nous engageons à faire mais pas la façon de le
faire. L’implémentation de la méthode est à notre charge.
Toujours dans l’optique de simplifier la tâche du développeur, Visual C# Express
nous aide pour implémenter les méthodes d’un contrat. Faites un clic droit sur l’inter-
face et choisissez dans le menu contextuel Implémenter l’interface et Implémenter
l’interface (voir figure 23.3).
226
LES INTERFACES
Il ne reste plus qu’à écrire le code de la méthode. Pour ce faire, il faut définir un critère
de tri. Trier des voitures n’a pas trop de sens, aussi nous dirons que nous souhaitons
les trier suivant leurs vitesses.
Pour respecter correctement le contrat, nous devons respecter la règle suivante qui se
trouve dans la documentation de la méthode de l’interface :
– si une voiture est inférieure à une autre, alors nous devons renvoyer une valeur
inférieure à 0, disons -1 ;
– si elle est égale, alors nous devons renvoyer 0 ;
– si elle est supérieure, nous devons renvoyer une valeur supérieure à 0, disons 1.
Ce qui donne :
1 public int CompareTo ( object obj )
2 {
3 Voiture voiture = ( Voiture ) obj ;
4 if ( this . Vitesse < voiture . Vitesse )
5 return -1 ;
6 if ( this . Vitesse > voiture . Vitesse )
7 return 1 ;
8 return 0 ;
9 }
227
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
La comparaison s’effectue entre l’objet courant et un objet qui lui est passé en para-
mètre. Pour que ce soit un peu plus clair, j’ai utilisé le mot-clé this qui permet de bien
identifier l’objet courant et l’objet passé en paramètre. Comme il est facultatif, nous
pouvons le supprimer.
Vous aurez également remarqué que j’utilise un cast explicite avant de comparer. Ceci
permet de renvoyer une erreur si jamais l’objet à comparer n’est pas du bon type. En
effet, que devrais-je renvoyer si jamais l’objet qu’on me passe n’est pas une voiture ?
Une erreur ! C’est très bien.
Le code est suffisamment explicite pour que nous comprenions facilement ce que l’on
doit faire : comparer les vitesses.
Il est possible de simplifier grandement le code, car pour comparer nos deux voitures,
nous effectuons la comparaison sur la valeur d’un entier, ce qui est plutôt trivial.
D’autant plus que l’entier, en bon objet comparable, possède également la méthode
CompareTo(). Ce qui fait qu’il est possible d’écrire notre méthode de comparaison de
cette façon :
1 public int CompareTo ( object obj )
2 {
3 Voiture voiture = ( Voiture ) obj ;
4 return Vitesse . CompareTo ( voiture . Vitesse ) ;
5 }
Ce qui affichera :
10
40
40
50
100
Voilà pour le tri, mais si je peux me permettre, je trouve que ce code-là n’est pas très
esthétique ! J’y reviendrai un peu plus tard. . .
228
LES INTERFACES
Nous avons donc implémenté notre première interface. Finalement, ce n’était pas si
compliqué. Voyons à présent comment créer nos propres interfaces. Une interface se
définit en C# comme une classe, sauf qu’on utilise le mot-clé interface à la place
de class. En tant que débutant, vous aurez rarement besoin de créer des interfaces.
Cependant, il est utile de savoir le faire. Par contre, il sera beaucoup plus fréquent que
vos classes implémentent des interfaces existantes du framework .NET, comme nous
venons de le faire.
Voyons à présent comment créer une interface et examinons le code suivant :
1 public interface IVolant
2 {
3 int Nomb rePropul seurs { get ; set ; }
4 void Voler () ;
5 }
Comme pour les classes, il est recommandé de créer les interfaces dans un
fichier à part. Rappelez-vous également de la convention qui fait que les
interfaces doivent commencer par un I majuscule.
Nous définissons ici une interface IVolant qui possède une propriété de type int et
une méthode Voler() qui ne renvoie rien. Voilà, c’est tout simple !
Nous avons créé une interface. Les objets qui choisiront d’implémenter cette inter-
face seront obligés d’avoir une propriété entière NombrePropulseurs et une méthode
Voler() qui ne renvoie rien. Rappelez-vous que l’interface ne contient que le contrat
et aucune implémentation. C’est-à-dire que nous ne verrons jamais de corps de mé-
thode dans une interface ni de variables membres ; uniquement des méthodes et des
propriétés. Un contrat.
Notez quand même qu’il ne faut pas définir de visibilité sur les membres d’une interface.
Nous serons obligés de définir les visibilités en public sur les objets implémentant
l’interface.
Créons désormais deux objets Avion et Oiseau qui implémentent cette interface, ce
qui donne :
1 public class Oiseau : IVolant
2 {
3 public int N ombrePro pulseurs { get ; set ; }
4
5 public void Voler ()
6 {
7 Console . WriteLine ( " Je vole gr â ce à " +
Nomb rePropuls eurs + " ailes " ) ;
8 }
9 }
10
11 public class Avion : IVolant
12 {
13 public int N ombrePro pulseurs { get ; set ; }
229
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Grâce à ce contrat, nous savons maintenant que n’importe lequel de ces objets saura
voler.
Il est possible de traiter ces objets comme des objets volants, un peu comme ce que nous
avions fait avec les classes mères, en utilisant l’interface comme type pour la variable.
Par exemple :
1 IVolant oiseau = new Oiseau { No mbrePropu lseurs = 2 };
2 oiseau . Voler () ;
Nous instancions vraiment un objet Oiseau, mais nous le manipulons en tant que
IVolant. Un des intérêts dans ce cas sera de pouvoir manipuler des objets qui partagent
un même comportement :
1 Oiseau oiseau = new Oiseau { Nomb rePropul seurs = 2 };
2 Avion avion = new Avion { Nombre Propulseu rs = 4 };
3
4 List < IVolant > volants = new List < IVolant > { oiseau , avion };
5 foreach ( IVolant volant in volants )
6 {
7 volant . Voler () ;
8 }
Ce qui produira :
Je vole gr â ce à 2 ailes
Je vole gr â ce à 4 moteurs
Grâce à l’interface, nous avons pu mettre dans une même liste des objets différents,
qui n’héritent pas entre eux mais qui partagent une même interface, c’est-à-dire un
même comportement : IVolant. Pour accéder à ces objets, nous devrons utiliser leurs
interfaces.
Il sera possible quand même de caster nos IVolant en Avion ou en Oiseau, si jamais
nous souhaitons rajouter une propriété propre à l’avion. Par exemple je rajoute une
propriété NomDuCommandant à mon avion mais qui ne fait pas partie de l’interface :
1 public class Avion : IVolant
2 {
3 public int N ombreProp ulseurs { get ; set ; }
4 public string NomDuCommandant { get ; set ; }
5 public void Voler ()
6 {
7 Console . WriteLine ( " Je vole gr â ce à " +
Nombr ePropuls eurs + " moteurs " ) ;
230
LES INTERFACES
8 }
9 }
Cela veut dire que l’objet Avion pourra affecter un nom de commandant mais qu’il ne
sera pas possible d’y accéder par l’interface :
1 IVolant avion = new Avion { No mbreProp ulseurs = 4 ,
NomDuCommandant = " Nico " };
2 Console . WriteLine ( avion . NomDuCommandant ) ; // erreur de
compilation
L’erreur de compilation nous indique que IVolant ne possède pas de définition pour
NomDuCommandant ; ce qui est vrai !
Pour accéder au nom du commandant, nous pourrons tenter de caster nos IVolant en
Avion. Si le cast est valide, alors nous pourrons accéder à notre propriété :
1 Oiseau oiseau = new Oiseau { Nom brePropul seurs = 2 };
2 Avion avion = new Avion { NombreP ropulseu rs = 4 ,
NomDuCommandant = " Nico " };
3
4 List < IVolant > volants = new List < IVolant > { oiseau , avion };
5 foreach ( IVolant volant in volants )
6 {
7 volant . Voler () ;
8 Avion a = volant as Avion ;
9 if ( a != null )
10 {
11 Console . WriteLine ( a . NomDuCommandant ) ;
12 }
13 }
Lorsque nous avons abordé la boucle foreach, j’ai dit qu’elle nous servait à
parcourir des éléments « énumérables ». En disant ça, je disais en fait que
la boucle foreach fonctionne avec tous les types qui implémentent l’inter-
face IEnumerable. Maintenant que vous savez ce qu’est une interface, vous
comprenez mieux ce à quoi je faisais vraiment référence.
Il faut également noter que les interfaces peuvent hériter entre elles, comme c’est le cas
avec les objets. C’est-à-dire que je vais pouvoir déclarer une interface IVolantMotorise
qui hérite de l’interface IVolant.
1 public interface IVolant
2 {
3 int Nomb rePropul seurs { get ; set ; }
4 void Voler () ;
5 }
6
7 public interface IVolantMotorise : IVolant
231
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
8 {
9 void DemarrerLeMoteur () ;
10 }
Enfin, et nous nous arrêterons là pour les interfaces, il est possible pour une classe
d’implémenter plusieurs interfaces. Il suffira pour cela de séparer les interfaces par une
virgule et d’implémenter bien sûr tout ce qu’il faut derrière. Par exemple :
232
LES CLASSES ET LES MÉTHODES ABSTRAITES
Pour déclarer une méthode comme étant abstraite, il faut utiliser le mot-clé abstract
et ne fournir aucune implémentation de la méthode, c’est-à-dire que la déclaration de
la méthode doit se terminer par un point-virgule :
233
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Ah oui, c’est vrai, on a dit qu’une classe qui contient au moins une méthode abstraite
était forcément abstraite. C’est le même principe que pour la méthode, il suffit d’utiliser
le mot-clé abstract devant le mot-clé class :
1 public abstract class Animal
2 {
3 public abstract void SeDeplacer () ;
4 }
234
LES CLASSES ET LES MÉTHODES ABSTRAITES
Comme prévu, il n’est pas possible d’instancier un objet Animal. Si nous tentons l’opé-
ration :
1 Animal animal = new Animal () ;
Par contre, il est possible de créer une classe Chien qui dérive de la classe Animal :
1 public class Chien : Animal
2 {
3 }
Cette classe ne pourra pas compiler dans cet état car il faut obligatoirement redéfinir la
méthode abstraite SeDeplacer(). Cela se fait en utilisant le mot-clé override, comme
on l’a déjà vu.
Vous aurez sûrement remarqué que la méthode abstraite n’utilise pas le mot-clé virtual
comme cela doit absolument être le cas lors de la substitution d’une méthode dans une
classe non-abstraite. Il est ici implicite et ne doit pas être utilisé, sinon nous aurons
une erreur de compilation. Et puis cela nous arrange ; un seul mot-clé, c’est largement
suffisant !
Nous devons donc spécialiser la méthode SeDeplacer, soit en écrivant la méthode à la
main, soit en utilisant encore une fois notre ami Visual C# Express.
Il suffit de faire un clic droit sur la classe dont hérite Chien (en l’occurrence Animal)
et de cliquer sur Implémenter une classe abstraite (voir la figure 23.4).
235
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
2 {
3 public override void SeDeplacer ()
4 {
5 throw new N o t I m p l e m e n t e d E x c e p t i o n () ;
6 }
7 }
Ainsi, nous pourrons créer un objet Chien et le faire se déplacer puis le faire mourir
car il hérite des comportements de la classe Animal. Paix à son âme.
1 Chien max = new Chien () ;
2 max . SeDeplacer () ;
3 max . Mourir () ;
Ce code affichera :
Vous pouvez désormais vous rendre compte de la véracité de la phrase que j’ai écrite
en introduction :
« [Les classes abstraites] pourront vous servir pour combiner la puissance des interfaces
à l’héritage. »
En effet, la classe abstraite peut fournir des implémentations alors que l’interface ne
propose qu’un contrat. Cependant, une classe concrète ne peut hériter que d’une seule
classe mais peut implémenter plusieurs interfaces.
La classe abstraite est un peu à mi-chemin entre l’héritage et l’interface.
236
LES CLASSES PARTIELLES
Nous pourrons compléter sa définition dans un autre fichier pour lui rajouter par
exemple des méthodes :
1 public partial class Voiture
2 {
3 public string Rouler ()
4 {
5 return " Je roule à " + Vitesse + " km / h " ;
6 }
7 }
À la compilation, Visual C# Express réunit les deux classes en une seule et l’objet
fonctionne comme toute autre classe qui ne serait pas forcément partielle.
À noter qu’il faut impérativement que les deux classes possèdent le mot-clé partial
pour que cela soit possible, sinon Visual C# Express générera une erreur de compila-
tion :
Vous allez me dire que ce n’est pas super utile comme fonctionnalité. Et je vous dirai
que vous avez raison. Sauf dans un cas particulier.
Les classes partielles prennent de l’intérêt quand une partie du code de la classe est
générée par Visual C# Express. C’est le cas pour la plupart des plateformes qui servent
à développer de vraies applications. Par exemple ASP.NET pour un site internet, WPF
pour une application Windows, Silverlight pour un client riche, etc. C’est aussi le cas
lorsque nous générons de quoi permettre l’accès à une base de données.
Dans ce cas-là, disons pour simplifier que Visual C# Express va nous générer tout
un tas d’instructions pour nous connecter à la base de données ou pour récupérer
des données. Toutes ces instructions seront mises dans une classe partielle que nous
pourrons enrichir avec nos besoins.
L’intérêt est que si nous générons une nouvelle version de notre classe, seul le fichier
généré sera impacté. Si nous avions modifié le fichier pour enrichir la classe avec nos
besoins, nous aurions perdu tout notre travail. Vu que, grâce aux classes partielles, ce
code est situé dans un autre fichier, il n’est donc pas perdu. Pour notre plus grand
bonheur !
À noter que le mot-clé partial peut se combiner sans problème avec d’autres mots-clés,
comme abstract par exemple que nous venons de voir.
237
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Il est fréquent aussi de voir des classes partielles utilisées quand plusieurs développeurs
travaillent sur la même classe. Le fait de séparer la classe en deux fichiers permet de
travailler sans se marcher dessus.
Nous aurons l’occasion de voir des classes partielles générées plus tard dans l’ouvrage.
Nous utilisons la méthode WriteLine de la classe Console sans avoir créé d’objet
Console. Étrange.
Il s’agit, vous l’aurez deviné, d’une méthode statique qui est accessible en dehors de
toute instance de Console. D’ailleurs, la classe entière est une classe statique. Si nous
essayons d’instancier la classe Console avec :
1 Console c = new Console () ;
Nous avons dit que la méthode spéciale Main() est obligatoirement statique. Cela
permet au CLR qui va exécuter notre application de ne pas avoir besoin d’instancier
238
CLASSES STATIQUES ET MÉTHODES STATIQUES
Ici, la méthode addition sert à additionner deux entiers. Elle est complètement indé-
pendante de la classe Math et donc des instances de l’objet Math.
Nous pouvons donc en faire une méthode statique, pour cela il suffira de préfixer du
mot-clé static son type de retour :
1 public class Math
2 {
3 public static int Addition ( int a , int b )
4 {
5 return a + b ;
6 }
7 }
Et nous pourrons alors utiliser l’addition sans créer d’instance de la classe Math, mais
simplement en utilisant le nom de la classe suivi du nom de la méthode statique :
1 int resultat = Math . Addition (5 , 6 ) ;
239
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
À noter que la classe Math est toujours instanciable mais qu’il n’est pas possible d’ap-
peler les méthodes qui sont statiques depuis un objet Math. Le code suivant :
1 Math math = new Math () ;
2 long resultat = math . Multiplication (5 , 6 ) ;
Ok, mais à quoi ça sert de pouvoir instancier un objet Math si nous ne pouvons
accéder à aucune de ses méthodes, vu qu’elles sont statiques ?
Absolument à rien ! Si une classe ne possède que des membres statiques, alors il est
possible de rendre cette classe statique grâce au même mot-clé.
Celle-ci deviendra non-instanciable, comme la classe Console :
1 public static class Math
2 {
3 public static int Addition ( int a , int b )
4 {
5 return a + b ;
6 }
7 }
Ainsi, si nous tentons d’instancier l’objet Math, nous aurons une erreur de compilation.
En général, les classes statiques servent à regrouper des méthodes utilitaires qui par-
tagent une même fonctionnalité. Ici, la classe Math permettrait de ranger toutes les
méthodes du style addition, multiplication, racine carrée, etc.
240
CLASSES STATIQUES ET MÉTHODES STATIQUES
Ah, on me fait signe que cette classe existe déjà dans le framework .NET et qu’elle
s’appelle également Math. Elle est rangée dans l’espace de nom System. Souvenez-vous,
nous l’avons utilisée pour calculer la racine carrée. Cette classe est statique, c’est la
version aboutie de la classe que nous avons commencé à écrire.
Par contre, ce n’est pas parce qu’une classe possède des méthodes statiques qu’elle est
obligatoirement statique. Il est aussi possible d’avoir des membres statiques dans une
classe qui possède des membres non statiques.
Par exemple notre classe Chien, qui possède un prénom et qui sait aboyer :
1 public class Chien
2 {
3 private string prenom ;
4
5 public Chien ( string prenomDuChien )
6 {
7 prenom = prenomDuChien ;
8 }
9
10 public void Aboyer ()
11 {
12 Console . WriteLine ( " Wouaf ! Je suis " + prenom ) ;
13 }
14 }
pourrait posséder une méthode permettant de calculer l’âge d’un chien dans le référen-
tiel des humains.
Comme beaucoup le savent, il suffit de multiplier l’âge du chien par 7 !
1 public class Chien
2 {
3 private string prenom ;
4
5 public Chien ( string prenomDuChien )
6 {
7 prenom = prenomDuChien ;
8 }
9
10 public void Aboyer ()
11 {
12 Console . WriteLine ( " Wouaf ! Je suis " + prenom ) ;
13 }
14
15 public static int CalculerAge ( int ageDuChien )
16 {
17 return ageDuChien * 7 ;
18 }
19 }
Ici, la méthode CalculerAge() est statique car elle ne travaille pas directement avec
une instance d’un chien.
241
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
Vous me direz qu’il est possible de faire en sorte que la méthode travaille sur une
instance d’un objet Chien, ce qui serait peut-être plus judicieux ici. Il suffirait de
rajouter une propriété Age au Chien et de transformer la méthode pour qu’elle ne soit
plus statique. Ce qui donnerait :
1 public class Chien
2 {
3 private string prenom ;
4
5 public int Age { get ; set ; }
6
7 public Chien ( string prenomDuChien )
8 {
9 prenom = prenomDuChien ;
10 }
11
12 public void Aboyer ()
13 {
14 Console . WriteLine ( " Wouaf ! Je suis " + prenom ) ;
15 }
16
17 public int CalculerAge ()
18 {
19 return Age * 7 ;
20 }
21 }
Ici, c’est plus une histoire de conception. C’est à vous de décider, mais sachez que
c’est possible. Il est également possible d’utiliser le mot-clé static avec des propriétés.
Imaginons que nous souhaitions avoir un compteur sur le nombre d’instances de la classe
Chien. Nous pourrions créer une propriété statique de type entier qui s’incrémente à
chaque fois que l’on crée un nouveau chien :
242
LES CLASSES INTERNES
Nous pourrons voir combien de fois nous sommes passés dans le constructeur :
À noter que pour une variable statique, cela se passe de la même façon qu’avec les
propriétés statiques.
243
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
4
5 public void Mourir ()
6 {
7 coeur . Stop () ;
8 }
9
10 private class Coeur
11 {
12 public void Stop ()
13 {
14 Console . WriteLine ( " The end " ) ;
15 }
16 }
17 }
Ici, la classe Coeur ne peut être utilisée que par la classe Chien car elle est privée.
Nous pourrions également mettre la classe en protected afin qu’une classe dérivée de
la classe Chien puisse également utiliser la classe Coeur :
1 public class ChienSamois : Chien
2 {
3 private Coeur autreCoeur = new Coeur () ;
4 }
Avec ces niveaux de visibilité, une autre classe comme la classe Chat ne pourra pas se
servir de ce cœur. Si nous mettons la classe Coeur en public ou internal, elle sera
utilisable par tout le monde, comme une classe normale. Dans ce cas, pour nos chats,
nous pourrions avoir :
1 public class Chat
2 {
3 private Chien . Coeur coeur = new Chien . Coeur () ;
4
5 public void Mourir ()
6 {
7 coeur . Stop () ;
8 }
9 }
Notez que nous préfixons la classe par le nom de la classe qui contient la classe Coeur.
Dans ce cas, l’intérêt d’utiliser une classe interne est moindre. Cela permet éventuelle-
ment de regrouper les classes de manière sémantique, et encore, c’est plutôt le but des
espaces de nom. Voilà pour les classes internes. C’est une fonctionnalité qui est peu
utilisée, mais vous la connaissez désormais !
244
LES TYPES ANONYMES ET LE MOT-CLÉ VAR
Le mot-clé var sert à indiquer que nous ne voulons pas nous préoccuper de ce qu’est
le type et que c’est au compilateur de le trouver.
Cela implique qu’il faut que la variable soit initialisée (et non nulle) au moment où elle
est déclarée afin que le compilateur puisse déduire le type de la variable, en l’occurrence,
il devine qu’en lui affectant « Nicolas », il s’agit d’une chaîne de caractères.
Je ne recommande pas l’utilisation de ce mot-clé car le fait de ne pas mettre le type de
la variable fait perdre de la clarté au code. Ici, c’est facile. On déduit facilement nous
aussi que le type de prenom est string et que le type de age est int.
Mais c’est aussi parce qu’on est trop fort !
Par contre, si jamais la variable est initialisée grâce à une méthode :
1 var calcul = GetCalcul () ;
Parce que ce mot-clé peut servir dans un cas précis, celui des types anonymes. Lorsqu’il
est conjointement utilisé avec l’opérateur new, il permet de créer des types anonymes,
c’est-à-dire des types dont on n’a pas défini la classe au préalable. Une classe sans nom.
Cette classe est déduite grâce à son initialisation :
1 var u ne Pe rso nn eA non ym e = new { Prenom = " Nico " , Age = 30 };
Ici nous créons une variable qui contient une propriété Prenom et une propriété Age.
Forcément, il est impossible de donner un type à cette variable vu qu’elle n’a pas de
définition. C’est pour cela qu’on utilise le mot-clé var.
Ici, on sait juste que la variable unePersonneAnonyme possède deux propriétés, un
prénom et un âge. En interne, le compilateur va générer un nom de classe pour ce type
anonyme, mais il n’a pas de sens pour nous. En l’occurrence, si nous écrivons le code
suivant où GetType() (hérité de la classe object) renvoie le nom du type :
1 var u ne Pe rso nn eA non ym e = new { Prenom = " Nico " , Age = 30 };
2 Console . WriteLine ( un ePe rs on neA no ny me . GetType () ) ;
245
CHAPITRE 23. NOTIONS AVANCÉES DE POO EN C#
c’est-à-dire sans préciser le type du tableau après le new. Le compilateur déduit le type
à partir de l’initialisation. Ce n’est pas un type anonyme en soi, mais le principe de
déduction est le même.
En résumé
– Une interface est un contrat que s’engage à respecter un objet.
– Il est possible d’implémenter plusieurs interfaces dans une classe.
– Une classe abstraite est une classe qui possède au moins une méthode ou propriété
abstraite. Elle ne peut pas être instanciée.
– Une classe concrète dérivant d’une classe abstraite est obligée de substituer les
membres abstraits.
– Il est possible de créer des types anonymes grâce à l’opérateur new suivi de la des-
cription des propriétés de ce type. Les instances sont manipulées grâce au mot-clé
var.
246
Chapitre 24
TP : programmation orientée objet
Difficulté :
D
ans ce TP, nous allons essayer de mettre en pratique ce que nous avons appris en
programmation orientée objet avec le C#. Il est difficile d’avoir un exercice faisant
appel à toutes les notions que nous avons apprises. Aussi, certaines sont laissées de
côté.
Le TP est intéressant pour s’entraîner à créer des classes, à manipuler l’héritage et à se
confronter à des situations un peu différentes de la théorie.
Le but de ce TP est de créer une mini application de gestion bancaire où nous pourrons
gérer des comptes pouvant effectuer des opérations bancaires entre eux. Ne vous attendez
pas non plus à refaire les applications de la Banque de France ; on est là pour s’entraîner !
247
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
248
INSTRUCTIONS POUR RÉALISER LE TP
Op é rations :
+100
-50
-20
+20
+200
*******************************************
Op é rations :
+20
+100
-20
#####################################
Bon, j’ai bien expliqué, ce n’est pas si compliqué, sauf si bien sûr vous n’avez pas lu
ou pas compris les chapitres précédents. Dans ce cas, n’hésitez pas à y rejeter un coup
d’œil.
Sinon, il suffit de bien décomposer tout en créant les classes les unes après les autres.
249
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
Bon courage. . . !
Correction
Ne regardez pas tout de suite la correction ! Prenez votre temps pour faire ce TP, car il
est important de savoir bien manipuler les classes. Vous allez faire ça très régulièrement ;
ça doit devenir un réflexe, il faut donc pratiquer !
Toujours est-il que voici ma correction. Le plus dur était certainement de modéliser
correctement l’application.
Il y a plusieurs solutions bien sûr pour créer ce petit programme, voici celle que je
propose.
Tout d’abord, nous devons manipuler des comptes courants et des comptes épargne
entreprise, qui sont des sortes de comptes.
On en déduit qu’un compte courant hérite d’un compte. De même, un compte épargne
entreprise hérite également d’un compte. D’ailleurs, un compte a-t-il vraiment une
raison d’être à part entière ? Nous ne créons jamais de compte « généraliste », seulement
des comptes spécialisés. Nous allons donc créer la classe Compte en tant que classe
abstraite.
Ce qui nous donnera les classes suivantes :
1 public abstract class Compte
2 {
3 }
4
5 public class CompteCourant : Compte
6 {
7 }
8
9 public class C o m p t e E p a r g n e E n t r e p r i s e : Compte
10 {
11 }
Nous avons également dit qu’un compte était composé d’une liste d’opérations qui
possèdent un montant et un type de mouvement. Le type de mouvement pouvant
prendre deux valeurs, il paraît logique d’utiliser une énumération :
1 public enum Mouvement
2 {
3 Credit ,
4 Debit
5 }
250
CORRECTION
La liste des opérations est une variable membre, déclarée en protected car nous allons
en avoir besoin dans la classe CompteCourant qui en hérite, afin d’afficher un résumé
des opérations.
La propriété Solde n’est pas très compliquée en soit, il suffit de parcourir la liste des
opérations et en fonction du type de mouvement, ajouter ou retrancher le montant.
Comme c’est une propriété en lecture seule, seul le get est défini.
N’oubliez pas que la liste doit être initialisée avant d’être utilisée, sinon nous aurons
une erreur. Le constructeur est un endroit approprié pour le faire :
1 public abstract class Compte
2 {
3 // [...]
4 // [ Code pr é c é dent enlev é pour plus de lisibilit é ]
5 // [...]
6
7 public Compte ()
8 {
251
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
Le principe est de créer une opération et de l’ajouter à la liste des opérations. L’astuce
ici consiste à réutiliser les méthodes de la classe pour écrire les autres formes des
méthodes, ce qui simplifie le travail et facilitera les éventuelles futures opérations de
maintenance.
Enfin, chaque classe dérivée de la classe Compte doit afficher un résumé du compte.
Nous pouvons donc forcer ces classes à devoir implémenter cette méthode en utilisant
une méthode abstraite :
1 public abstract class Compte
2 {
252
CORRECTION
3 // [...]
4 // [ Code pr é c é dent enlev é pour plus de lisibilit é ]
5 // [...]
6
7 public abstract void AfficherResume () ;
8 }
Voilà pour la classe Compte. En toute logique, c’est elle qui contient le plus de méthodes
afin de factoriser le maximum de code commun dans la classe mère.
Passons à la classe CompteEpargneEntreprise, qui hérite de la classe Compte. Elle
possède un taux d’abondement qui est défini à la création du compte. Il est donc
ici intéressant de forcer le positionnement de ce taux lors de la création de la classe,
c’est-à-dire en utilisant un constructeur avec un paramètre :
1 public class C o m p t e E p a r g n e E n t r e p r i s e : Compte
2 {
3 private double tauxAbondement ;
4
5 public C o m p t e E p a r g n e E n t r e p r i s e ( double taux )
6 {
7 tauxAbondement = taux ;
8 }
9 }
Ce taux est utilisé pour calculer le solde du compte en faisant la somme de toutes les
opérations et en appliquant le taux ; ce qui revient à appeler le calcul du solde de la
classe mère et à lui appliquer le taux. Nous substituons donc la propriété Solde et
utilisons le calcul fait dans la classe Compte :
1 public class C o m p t e E p a r g n e E n t r e p r i s e : Compte
2 {
3 // [...]
4 // [ Code pr é c é dent enlev é pour plus de lisibilit é ]
5 // [...]
6
7 public override decimal Solde
8 {
9 get
10 {
11 return base . Solde * ( decimal ) ( 1 + tauxAbondement ) ;
12 }
13 }
14 }
Rien de plus simple, en utilisant le mot-clé base pour appeler le solde de la classe mère.
Vous noterez également que nous avons eu besoin de caster le taux qui est un double
afin de pouvoir le multiplier à un décimal.
Enfin, cette classe se doit de fournir une implémentation de la méthode AfficherResume() :
1 public class C o m p t e E p a r g n e E n t r e p r i s e : Compte
253
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
2 {
3 // [...]
4 // [ Code pr é c é dent enlev é pour plus de lisibilit é ]
5 // [...]
6
7 public override void AfficherResume ()
8 {
9 Console . WriteLine ( "
##################################### ");
10 Console . WriteLine ( " Compte é pargne entreprise de " +
Proprietaire ) ;
11 Console . WriteLine ( " \ tSolde : " + Solde ) ;
12 Console . WriteLine ( " \ tTaux : " + tauxAbondement ) ;
13 Console . WriteLine ( " \ n \ nOp é rations : " ) ;
14 foreach ( Operation operation in listeOperations )
15 {
16 if ( operation . TypeDeMouvement == Mouvement . Credit )
17 Console . Write ( " \ t + " ) ;
18 else
19 Console . Write ( " \t - " ) ;
20 Console . WriteLine ( operation . Montant ) ;
21 }
22 Console . WriteLine ( "
##################################### ");
23 }
24 }
Il s’agit d’une banale méthode d’affichage où nous parcourons la liste contenant les
opérations et affichons le montant en fonction du type de mouvement.
Voilà pour la classe CompteEpargneEntreprise.
Il ne reste plus que la classe CompteCourant qui doit également dériver de Compte :
1 public class CompteCourant : Compte
2 {
3 }
254
CORRECTION
Voilà pour nos objets. Cela en fait un petit paquet. Mais ce n’est pas fini, il faut
maintenant créer des comptes et faire des opérations.
L’énoncé consiste à faire les instanciations suivantes, depuis notre méthode Main() :
1 CompteCourant compteNicolas = new CompteCourant ( 2000 ) {
Proprietaire = " Nicolas " };
2 C o m p t e E p a r g n e E n t r e p r i s e c o m p t e E p a r g n e Ni c o l a s = new
C o m p t e E p a r g n e E n t r e p r i s e ( 0 . 02 ) { Proprietaire = " Nicolas " };
3 CompteCourant compteJeremie = new CompteCourant ( 500 ) {
Proprietaire = " J é r é mie " };
4
5 compteNicolas . Crediter ( 100 ) ;
6 compteNicolas . Debiter ( 50 ) ;
7
8 c o m p t e E p a rg n e N i c o l a s . Crediter ( 20 , compteNicolas ) ;
9 c o m p t e E p a rg n e N i c o l a s . Crediter ( 100 ) ;
10
11 c o m p t e E p a rg n e N i c o l a s . Debiter ( 20 , compteNicolas ) ;
12
13 compteJeremie . Debiter ( 500 ) ;
14 compteJeremie . Debiter ( 200 , compteNicolas ) ;
15
255
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
Rien d’extraordinaire.
Il ne reste plus qu’à afficher le résumé des deux comptes demandés :
1 Console . WriteLine ( " R é sum é du compte de Nicolas " ) ;
2 compteNicolas . AfficherResume () ;
3 Console . WriteLine ( " \ n " ) ;
4
5 Console . WriteLine ( " R é sum é du compte é pargne de Nicolas " ) ;
6 c o m p t e E p a r g ne N i c o l a s . AfficherResume () ;
7 Console . WriteLine ( " \ n " ) ;
256
ALLER PLUS LOIN
Il ne reste plus qu’à utiliser cette méthode statique depuis nos méthodes AfficherResume(),
par exemple pour le compte épargne entreprise :
1 public override void AfficherResume ()
2 {
3 Console . WriteLine ( " # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # " ) ;
4 Console . WriteLine ( " Compte é pargne entreprise de " +
Proprietaire ) ;
5 Console . WriteLine ( " \ tSolde : " + Solde ) ;
6 Console . WriteLine ( " \ tTaux : " + tauxAbondement ) ;
7 Helper . Affich eOperatio ns ( listeOperations ) ;
8 Console . WriteLine ( " # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # " ) ;
9 }
La méthode a tout intérêt à être déclarée en protected, afin qu’elle puisse servir aux
classes filles mais pas à l’extérieur. Elle s’utilisera de cette façon, par exemple dans la
classe CompteEpargneEntreprise :
1 public override void AfficherResume ()
2 {
3 Console . WriteLine ( " # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # " ) ;
4 Console . WriteLine ( " Compte é pargne entreprise de " +
Proprietaire ) ;
5 Console . WriteLine ( " \ tSolde : " + Solde ) ;
6 Console . WriteLine ( " \ tTaux : " + tauxAbondement ) ;
7 Affic heOperat ions () ;
8 Console . WriteLine ( " # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # " ) ;
9 }
257
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
Dès que l’on peut factoriser du code, il ne faut pas hésiter. Si nous avons demain
besoin de créer un nouveau type de compte, nous serons ravis de pouvoir nous servir
de méthodes toutes prêtes nous simplifiant le travail.
Il pourrait être intéressant également d’encapsuler l’enregistrement d’une opération
dans une méthode. Ça permettrait de moins se répéter (même si ici, le code est vraiment
petit) mais surtout de séparer la logique d’enregistrement d’une opération afin que cela
soit plus facile ultérieurement à modifier, maintenir ou complexifier. Par exemple, via
une méthode :
1 private void E n r e g i s t r e r O p e r a t io n ( Mouvement typeDeMouvement ,
decimal montant )
2 {
3 Operation operation = new Operation { Montant = montant ,
TypeDeMouvement = typeDeMouvement };
4 listeOperations . Add ( operation ) ;
5 }
Cela permet également de faire en sorte que le type de mouvement soit un paramètre
de la méthode.
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 729727 ✠
Deuxième partie du TP
Nous voici maintenant dans la deuxième partie du TP. La banque souhaite proposer un
nouveau type de compte, le livret ToutBénéf. Dans cette banque, le livret ToutBénéf
fonctionne comme le compte épargne entreprise. C’est-à-dire qu’il accepte un taux en
paramètres et applique ce taux au moment de la restitution du solde.
La première idée qui vient à l’esprit est de créer une classe LivretToutBenef qui hérite
de CompteEpargneEntreprise. Mais ceci pose un problème si jamais le compte épargne
entreprise doit évoluer, et c’est justement ce que le directeur de la banque vient de me
confier. Donc, il vous interdit à juste titre d’hériter de ses fonctionnalités.
Ce que vous allez donc faire ici, c’est de considérer que le fait qu’un compte puisse faire
des bénéfices soit en fait un comportement qui est fourni au moment où on instancie un
compte. Il existe plusieurs comportements dont on doit fournir les implémentations :
– le comportement de bénéfice à taux fixe ;
– le comportement de bénéfice aléatoire ;
– le comportement où il n’y a aucun bénéfice.
Chaque comportement est une classe qui respecte le contrat suivant :
1 public interface I C a l c u l a t e u r D e B e n e f i c e
2 {
3 decimal CalculeBenefice ( decimal solde ) ;
258
CORRECTION
Écrivez donc ces trois classes de comportement ainsi que le livret ToutBénéf qui possède
un taux fixe de 2.75% et qui a été crédité une première fois de 800 € et une seconde
fois de 200 €. Affichez enfin son résumé qui devra tenir compte du taux du calculateur
de bénéfice. Réécrivez ensuite la classe CompteCourant de manière à ce qu’elle ait un
comportement où il n’y a pas de bénéfice. Enfin, la classe CompteEpargneEntreprise
subira une évolution pour fonctionner avec un comportement de bénéfice aléatoire (tiré
entre 0 et 1).
C’est parti.
Correction
Ici c’est un peu plus compliqué. Vous n’êtes sans doute pas complètement familiers
avec la notion d’interface, aussi avant de vous donner la correction, je vais vous donner
quelques pistes. Le fait d’avoir un comportement est finalement très simple. Il suffit
d’avoir un membre privé dans la classe LivretToutBenef du type de l’interface. Ce
membre privé sera affecté à la valeur passée en paramètre du constructeur. C’est-à-dire :
1 public class LivretToutBenef : Compte
2 {
3 private I C a l c u l a t e u r D e B e n e f i c e c a l c u l a t e u r D e B e n e f i c e ;
4
5 public LivretToutBenef ( I C a l c u l a t e u r D e B e n e f i c e calculateur )
6 {
7 c a l c u l a t e u r D e B e n e f i c e = calculateur ;
8 }
9 }
Nous avons besoin d’écrire plusieurs classes qui implémentent cette interface. La pre-
mière est la classe de bénéfice à taux fixe :
1 public class Be neficeAT auxFixe : I C a l c u l a t e u r D e B e n e f i c e
2 {
3 private double taux ;
259
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
4
5 public Benefic eATauxFix e ( double tauxFixe )
6 {
7 taux = tauxFixe ;
8 }
9
10 public decimal CalculeBenefice ( decimal solde )
11 {
12 return solde * ( decimal ) ( 1 + taux ) ;
13 }
14
15 public double Taux
16 {
17 get
18 {
19 return taux ;
20 }
21 }
22 }
Nous avons dit qu’elle devait prendre un taux en paramètre ; le constructeur est l’en-
droit indiqué pour cela. Ensuite, la méthode de calcul est très simple, il suffit d’appliquer
la formule au solde. Enfin, la propriété retourne le taux.
La classe suivante est la classe de bénéfice aléatoire. Là, pas besoin de paramètres, il suf-
fit de tirer le nombre aléatoire dans le constructeur grâce à la méthode NextDouble(),
ce qui donne :
1 public class Be neficeAle atoire : I C a l c u l a t e u r D e B e n e f i c e
2 {
3 private double taux ;
4 private Random random ;
5
6 public Benefic eAleatoir e ()
7 {
8 random = new Random () ;
9 taux = random . NextDouble () ;
10 }
11
12 public decimal CalculeBenefice ( decimal solde )
13 {
14 return solde * ( decimal ) ( 1 + taux ) ;
15 }
16
17 public double Taux
18 {
19 get
20 {
21 return taux ;
22 }
23 }
260
CORRECTION
24 }
Rien de sorcier.
Là où ça se complique un peu, c’est pour la classe LivretToutBenef. Elle doit bien sûr
dériver de la classe de base Compte et posséder un membre privé de type
ICalculateurDeBenefice :
1 public class LivretToutBenef : Compte
2 {
3 private I C a l c u l a t e u r D e B e n e f i c e c a l c u l a t e u r D e B e n e f i c e ;
4
5 public LivretToutBenef ( I C a l c u l a t e u r D e B e n e f i c e calculateur )
6 {
7 c a l c u l a t e u r D e B e n e f i c e = calculateur ;
8 }
9
10 public override decimal Solde
11 {
12 get
13 {
14 decimal solde = base . Solde ;
15 return solde + c a l c u l a t e u r D e B e n e f i c e .
CalculeBenefice ( solde ) ;
16 }
17 }
18
19 public override void AfficherResume ()
20 {
21 Console . WriteLine ( " ^^ ^^^^^^^^ ^^^^^^^ " ) ;
22 Console . WriteLine ( " Livret ToutB é n é f de " + Proprietaire
);
23 Console . WriteLine ( " \ tSolde : " + Solde ) ;
261
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
Op é rations :
+800
+200
^^^^^ ^^^^^^^^ ^^^^
262
CORRECTION
7 c a l c u l a t e u r D e B e n e f i c e = calculateur ;
8 }
9
10 public override decimal Solde
11 {
12 get
13 {
14 decimal solde = base . Solde ;
15 return solde + c a l c u l a t e u r D e B e n e f i c e .
CalculeBenefice ( solde ) ;
16 }
17 }
18
19 public override void AfficherResume ()
20 {
21 Console . WriteLine ( "
##################################### ");
22 Console . WriteLine ( " Compte é pargne entreprise de " +
Proprietaire ) ;
23 Console . WriteLine ( " \ tSolde : " + Solde ) ;
24 Console . WriteLine ( " \ tTaux : " + c a l c u l a t e u r D e B e n e f i c e .
Taux ) ;
25 Affic heOperat ions () ;
26 Console . WriteLine ( "
##################################### ");
27 }
28 }
263
CHAPITRE 24. TP : PROGRAMMATION ORIENTÉE OBJET
Et voilà ! L’avantage ici est d’avoir séparé les responsabilités dans différentes classes.
Si jamais nous créons un nouveau compte qui est rémunéré grâce à un bénéfice à taux
fixe, il suffira de réutiliser ce comportement et le tour est joué.
À noter que les trois calculs de la propriété Solde sont identiques, il pourrait être
judicieux de le factoriser dans la classe mère Compte. Ceci implique que la classe mère
possède elle-même le membre protégé du type de l’interface.
Voilà pour ce TP. J’espère que vous aurez réussi avec brio la création de toutes les
classes et que vous ne vous êtes pas perdus dans les mots-clés. Vous verrez que vous
aurez très souvent besoin d’écrire des classes dans ce genre afin de créer une application.
C’est un élément indispensable du C# qu’il est primordial de maîtriser.
N’hésitez pas à faire des variations sur ce TP ou à créer d’autres petits programmes
simples vous permettant de vous entraîner !
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 817413 ✠
264
Chapitre 25
Mode de passage des paramètres à une
méthode
Difficulté :
D
ans les chapitres précédents, nous avons décrit comment on pouvait passer des pa-
ramètres à une méthode. Nous avons également vu que les types du framework .NET
pouvaient être des types valeur ou des types référence. Ceci influence la façon dont
sont traités les paramètres d’une méthode. Nous allons ici préciser un peu ce fonctionne-
ment.
Dans ce chapitre, je vais illustrer mes propos en utilisant des méthodes statiques écrites dans
la classe Program, générée par Visual C# Express. Le but est de simplifier l’écriture et de
ne pas s’encombrer d’objets inutiles. Évidemment, tout ce que nous allons voir fonctionne
également de la même façon avec les méthodes non statiques présentes dans des objets.
265
CHAPITRE 25. MODE DE PASSAGE DES PARAMÈTRES À UNE MÉTHODE
Au tout début de ce livre, nous avons appelé des méthodes en passant des types simples
(int, string, etc.). Nous avons vu qu’il s’agissait de types valeur qui possèdent direc-
tement la valeur dans la variable.
Lorsque nous passons des types valeur en paramètres d’une méthode, nous utilisons un
passage de paramètre par valeur.
Décortiquons l’exemple suivant :
Nous déclarons dans la méthode Main() une variable age, de type entier, à laquelle
nous affectons la valeur 30. Nous appelons ensuite la méthode Doubler() en lui passant
cette variable en paramètre.
Ce qu’il se passe c’est que le compilateur fait une copie de la valeur de la variable age
pour la mettre dans la variable valeur de la méthode Doubler(). La variable valeur
a une portée égale au corps de la méthode Doubler().
Nous modifions ensuite la valeur de la variable valeur en la multipliant par deux.
Étant donné que la variable valeur a reçu une copie de la variable age, c’est-à-dire
que le compilateur a dupliqué la valeur 30, le fait de modifier la variable valeur ne
change en rien la valeur de la variable age :
60
30
266
PASSAGE DE PARAMÈTRES EN MISE À JOUR
Pour pouvoir modifier la valeur du paramètre passé, il faut indiquer que la variable
est en mode « mise à jour ». Cela se fait grâce au mot-clé « ref » que nous pourrons
utiliser ainsi :
1 static void Main ( string [] args )
2 {
3 int age = 30 ;
4 Doubler ( ref age ) ;
5 Console . WriteLine ( age ) ;
6 }
7
8 public static void Doubler ( ref int valeur )
9 {
10 valeur = valeur * 2 ;
11 }
267
CHAPITRE 25. MODE DE PASSAGE DES PARAMÈTRES À UNE MÉTHODE
C’est ce que nous avons toujours fait auparavant. Voici maintenant une autre façon de
faire qui peut être bien utile quand il y a plus d’une valeur à renvoyer.
Bleue
Il est à noter quand même que la variable voiture de la méthode Repeindre est une
copie de la variable voiture de la méthode Main(). Elles contiennent toutes les deux
une référence vers l’objet de type Voiture. Cela veut dire que l’on accède bien au
même objet, d’où le résultat, mais que les deux variables sont indépendantes. Si nous
modifions directement la variable, avec par exemple :
1 static void Main ( string [] args )
2 {
3 Voiture voiture = new Voiture { Couleur = " Grise " };
4 Repeindre ( voiture ) ;
5 Console . WriteLine ( voiture . Couleur ) ;
6 }
7
8 public static void Repeindre ( Voiture voiture )
9 {
10 voiture . Couleur = " Bleue " ;
268
PASSAGE DE PARAMÈTRES EN SORTIE
11 voiture = null ;
12 }
provoquera une erreur. En effet, cette fois-ci, c’est bien la référence qui est passée à
nulle et pas une copie de la variable contenant la référence. . .
Une différence subtile !
La méthode TryParse permet de tenter la conversion d’une chaîne. Elle renvoie vrai
ou faux en fonction du résultat de la conversion et met à jour l’entier qui est passé
en paramètre en utilisant le mot-clé out. Si la conversion réussit, alors l’entier nombre
est initialisé avec la valeur de la conversion, calculée dans la méthode TryParse. Nous
pouvons utiliser ensuite la variable nombre car le mot-clé out garantit que la variable
sera initialisée dans la méthode.
En effet, si nous prenons l’exemple suivant :
1 static void Main ( string [] args )
269
CHAPITRE 25. MODE DE PASSAGE DES PARAMÈTRES À UNE MÉTHODE
2 {
3 int age = 30 ;
4 int ageDouble ;
5 Doubler ( age , out ageDouble ) ;
6 }
7
8 public static void Doubler ( int age , out int resultat )
9 {
10 }
En effet, il faut absolument que la variable resultat qui est marquée en sortie ait une
valeur avant de pouvoir sortir de la méthode.
Nous pourrons corriger cet exemple avec :
1 static void Main ( string [] args )
2 {
3 int age = 30 ;
4 int ageDouble ;
5 Doubler ( age , out ageDouble ) ;
6 }
7
8 public static void Doubler ( int age , out int resultat )
9 {
10 resultat = age * 2 ;
11 }
Ici, aucun. C’est pertinent quand nous souhaitons renvoyer plusieurs valeurs, comme
c’est le cas pour la méthode TryParse qui renvoie le résultat de la conversion et si la
conversion s’est bien passée.
Bien sûr, si l’on n’aime pas trop le mot-clé out, il est toujours possible de créer un
objet contenant deux valeurs que l’on retournera à l’appelant, par exemple :
1 public class Program
2 {
3 static void Main ( string [] args )
4 {
5 string nombre = " 1234 " ;
6 Resultat resultat = TryParse ( nombre ) ;
270
PASSAGE DE PARAMÈTRES EN SORTIE
7 if ( resultat . ConversionOk )
8 Console . WriteLine ( resultat . Valeur ) ;
9 }
10
11 public static Resultat TryParse ( string chaine )
12 {
13 Resultat resultat = new Resultat () ;
14 int valeur ;
15 resultat . ConversionOk = int . TryParse ( chaine , out valeur
);
16 resultat . Valeur = valeur ;
17 return resultat ;
18 }
19
20 }
21
22 public class Resultat
23 {
24 public bool ConversionOk { get ; set ; }
25 public int Valeur { get ; set ; }
26 }
Ici, notre méthode TryParse renvoie un objet Resultat qui contient les deux valeurs
résultantes de la conversion.
En résumé
– Le type d’une variable passée en paramètres d’une méthode influence la façon dont
elle est traitée.
– Un passage par valeur effectue une copie de la valeur de la variable et la met dans
la variable de la méthode.
– Un passage par référence effectue une copie de la référence mais continue de pointer
sur le même objet.
– On utilise le mot-clé ref pour passer une variable de type valeur à une méthode afin
de la modifier.
271
CHAPITRE 25. MODE DE PASSAGE DES PARAMÈTRES À UNE MÉTHODE
272
Chapitre 26
Les structures
Difficulté :
N
ous allons aborder dans ce chapitre les structures, qui sont une nouvelle sorte d’ob-
jets que nous pouvons créer dans des applications C#. Les structures sont presque
comme des classes. Elles permettent également de créer des objets, possèdent des
variables ou propriétés, des méthodes et même un constructeur, mais avec quelques subtiles
différences. . .
Découvrons-les dès à présent !
273
CHAPITRE 26. LES STRUCTURES
Une des premières différences entre la classe et la structure est la façon dont .NET
gère ces deux objets. Nous avons vu que les classes étaient des types référence. Les
variables de type référence ne possèdent donc pas la valeur de l’objet mais une référence
vers cet objet en mémoire. La structure, quant à elle, est un type valeur et contient
donc directement la valeur de l’objet.
Une autre différence est que la structure, bien qu’étant un objet, ne peut pas utiliser
les principes d’héritage. On ne peut donc pas hériter d’une structure et une structure
ne peut pas hériter des comportements d’un objet.
Les structures vont être utiles pour stocker de petits objets amenés à être souvent
manipulés, comme les int ou les bool que nous avons déjà vus.
La raison tient en un seul mot : performance.
Étant gérées en mémoire différemment, les structures sont optimisées pour améliorer les
performances des petits objets. Comme il n’y a pas de référence, on utilisera directement
l’objet sans aller le chercher via sa référence. On gagne donc un peu de temps lorsqu’on
a besoin de manipuler ces données.
C’est tout à fait pertinent pour des programmes où la vitesse est déterminante, comme
les jeux vidéo.
Donc dans ce cas, autant utiliser tout le temps des structures, non ?
Eh bien non, déjà vous vous priveriez de tous les mécanismes d’héritage que nous avons
vus. Ensuite, si nous surchargeons trop la mémoire avec des structures, l’optimisation
prévue par .NET risque de se retourner contre nous et notre application pourrait être
plus lente que si nous avions utilisé des classes.
D’une manière générale, et à moins de savoir exactement ce que vous faites ou d’avoir
mesuré les performances, vous allez utiliser plus généralement les classes que les struc-
tures. Vous pouvez à ce sujet lire les recommandations de Microsoft, disponibles grâce
au code web suivant :
☛ ✟
Documentation Microsoft
⊲ .
✡Code web : 520338 ✠
274
CRÉER UNE STRUCTURE
Pour instancier cette structure, nous pourrons utiliser le mot-clé new, comme pour les
classes. La différence est que la variable sera un type valeur, avec les conséquences que
ce type impose en matière de gestion en mémoire ou de passages par paramètres :
1 Personne nicolas = new Personne () { Prenom = " Nicolas " , Age =
30 };
2 Console . WriteLine ( nicolas . Prenom + " a " + nicolas . Age + " ans "
);
Comme nous avons dit, il est impossible qu’une structure hérite d’une autre structure ou
d’un objet ; sauf bien sûr du fameux type de base object, pour qui c’est automatique.
Une structure hérite donc des quelques méthodes d’Object (comme ToString()) que
nous pouvons éventuellement spécialiser :
1 public struct Personne
2 {
3 public string Prenom { get ; set ; }
4 public int Age { get ; set ; }
5
6 public override string ToString ()
7 {
8 return Prenom + " a " + Age + " ans " ;
9 }
10 }
Qui renverra :
Nicolas a 30 ans
Comme pour les classes, il est possible d’avoir des constructeurs sur une structure, à
l’exception du constructeur par défaut qui est interdit.
Aussi le code suivant :
275
CHAPITRE 26. LES STRUCTURES
Par contre, les autres formes des constructeurs sont possibles, comme :
1 public struct Personne
2 {
3 private int age ;
4 public Personne ( int agePersonne )
5 {
6 age = agePersonne ;
7 }
8 }
L ’ objet ’ this ’ ne peut pas ê tre utilis é avant que tous ses
champs soient assign é s
276
CRÉER UNE STRUCTURE
Ce qui peut sembler tout à fait inutile dans ce cas-là. Mais comme le compilateur fait
certaines vérifications, il sera impossible de compiler un code de ce genre sans que
toutes les variables soient initialisées explicitement.
Par contre, vous aurez un souci si vous utilisez des propriétés automatiques. Si nous
tentons de faire :
1 public struct Personne
2 {
3 public int Age { get ; set ; }
4 public Personne ( int agePersonne )
5 {
6 Age = agePersonne ;
7 }
8 }
nous nous retrouverons avec la même erreur de compilation. Pour la corriger, il faudra
appeler le constructeur par défaut de la structure qui va permettre d’initialiser toutes
les variables de la classe :
1 public struct Personne
2 {
3 public int Age { get ; set ; }
4 public Personne ( int agePersonne ) : this ()
5 {
6 Age = agePersonne ;
277
CHAPITRE 26. LES STRUCTURES
7 }
8 }
Cela se fait comme pour les classes, en utilisant le mot-clé this suivi de parenthèses,
qui permettra d’appeler le constructeur par défaut.
Rappelez-vous que le constructeur par défaut s’occupe d’initialiser toutes les variables
d’une classe ou d’une structure.
affichera 30, bien que nous modifions l’âge de la personne dans la méthode.
Comme nous l’avons déjà vu, la méthode travaille sur une copie de la structure.
Cela veut bien sûr dire que si nous souhaitons modifier une structure à partir d’une
méthode, nous devrons utiliser le mot-clé ref :
1 static void Main ( string [] args )
2 {
3 Personne nicolas = new Personne () { Age = 30 };
4 FaitVieillir ( ref nicolas ) ;
5 Console . WriteLine ( nicolas . Age ) ;
6 }
7
8 private static void FaitVieillir ( ref Personne personne )
9 {
10 personne . Age ++;
11 }
Prenez quand même garde. Si la structure est très grosse, le fait d’en faire une
copie à chaque utilisation de méthode risque d’être particulièrement chrono-
phage et pourra perturber les performances de votre application.
278
D’AUTRES STRUCTURES ?
D’autres structures ?
Vous l’aurez peut-être deviné, mais les entiers que nous avons déjà vus et que nous
utilisions grâce au mot-clé int sont en fait des structures.
Étant très souvent utilisés et n’ayant pas non plus énormément de choses à stocker, ils
sont créés en tant que structures et sont optimisés par .NET pour que nos applications
s’exécutent de façon optimale ; ce qui est un choix tout à fait pertinent.
C’est le cas également pour les bool, les double, etc.
À noter en revanche que d’autres objets, comme la classe String, sont bel et bien des
classes.
D’une manière générale, vous allez créer peu de structures en tant que débutant. Il
sera plus judicieux de créer des classes dès que vous en avez besoin. En effet, plus vos
objets sont gros et plus ils auront intérêt à être des classes pour éviter d’être recopiés
à chaque utilisation.
L’utilisation de structures pourra se révéler pertinente dans des situations bien précises,
mais en général, il faut bien maîtriser les rouages du framework .NET pour que les
bénéfices de leur utilisation se fassent ressentir.
Dans tous les cas, il sera important de mesurer (avec par exemple des outils de profilage)
le gain de temps avant de mettre des structures partout.
En résumé
– Les structures sont des types valeur qui sont optimisés par le framework .NET.
– Une structure est un objet qui ressemble beaucoup à une classe, mais qui possède
des restrictions.
– Les structures possèdent des propriétés, des méthodes, des constructeurs, etc.
– Il n’est pas possible d’utiliser l’héritage avec les structures.
279
CHAPITRE 26. LES STRUCTURES
280
Chapitre 27
Les génériques
Difficulté :
L
es génériques sont une fonctionnalité du framework .NET apparus avec la version
2. Vous vous en souvenez peut-être, nous avons cité le mot dans le chapitre sur
les tableaux et les listes. Ils permettent de créer des méthodes ou des classes qui
sont indépendantes d’un type. Il est très important de connaître leur fonctionnement car
c’est un mécanisme clé qui permet de faire beaucoup de choses, notamment en termes de
réutilisabilité et d’amélioration des performances.
N’oubliez pas vos tubes d’aspirine et voyons à présent de quoi il retourne !
281
CHAPITRE 27. LES GÉNÉRIQUES
Nous indiquons entre les chevrons le type qui sera utilisé avec le type générique.
Oui mais, si nous voulons pouvoir mettre n’importe quel type d’objet dans une
liste, il suffirait de créer une ListeObject ? Puisque tous les objets dérivent
d’object. . .
En fait, c’est le choix qui avait été fait dans la toute première version de .NET. On
utilisait l’objet ArrayList qui possède une méthode Add prenant en paramètre un
object. Cela fonctionne. Sauf que nous nous trouvions face à des limitations.
Premièrement, nous pouvions mélanger n’importe quel type d’objet dans la liste, des
entiers, des voitures, des chiens, etc. Cela devenait une classe fourre-tout et nous ne
savions jamais ce qu’il y avait dans la liste.
Deuxièmement, même si nous savions qu’il n’y avait que des entiers dans la liste, nous
étions obligés de les traiter en tant qu’object et donc d’utiliser le boxing et l’unboxing
pour mettre les objets dans la liste ou pour les récupérer.
Cela engendrait donc confusion et perte de performance. Grâce aux génériques, il deve-
nait donc possible de créer des listes de n’importe quel type avec la garantie de savoir
exactement quel type nous allions récupérer dans la liste.
282
CRÉER UNE MÉTHODE GÉNÉRIQUE
1 Queue < int > file = new Queue < int >() ;
2 file . Enqueue ( 3 ) ;
3 file . Enqueue ( 1 ) ;
4 int valeur = file . Dequeue () ; // valeur contient 3
5 valeur = file . Dequeue () ; // valeur contient 1
Citons encore le dictionnaire d’éléments qui est une espèce d’annuaire où l’on accède
aux éléments grâce à une clé :
1 Dictionary < string , Personne > annuaire = new Dictionary < string ,
Personne >() ;
2 annuaire . Add ( " 06 01 02 03 04 " , new Personne { Prenom = " Nicolas
" }) ;
3 annuaire . Add ( " 06 06 06 06 06 " , new Personne { Prenom = " Jeremie
" }) ;
4
5 Personne p = annuaire [ " 06 06 06 06 06 " ]; // p contient la
propri é t é Prenom valant Jeremie
Loin de moi l’idée de vous énumérer toutes les collections génériques du framework
.NET ; le but est de vous montrer rapidement qu’il existe beaucoup de classes génériques
dans le framework .NET.
Nous avons ici utilisé une classe statique permettant d’afficher le type d’un objet et sa
représentation. Nous pouvons l’utiliser ainsi :
1 int i = 5 ;
2 double d = 9 . 5 ;
283
CHAPITRE 27. LES GÉNÉRIQUES
Rappelez-vous, chaque fois qu’on passe dans cette méthode, l’objet est boxé en type
object quand il s’agit d’un type valeur.
Nous pouvons améliorer cette méthode en créant une méthode générique. Regardons
ce code :
1 public static class Afficheur
2 {
3 public static void Affiche <T >( T a )
4 {
5 Console . WriteLine ( " Afficheur d ' objet : " ) ;
6 Console . WriteLine ( " \ tType : " + a . GetType () ) ;
7 Console . WriteLine ( " \ tRepr é sentation : " + a . ToString () )
;
8 }
9 }
Cette méthode fait exactement la même chose mais avec les génériques.
Dans un premier temps, la méthode annonce qu’elle va utiliser un type générique
représenté par la lettre « T » entre chevrons.
Cela signifie que tout type utilisé dans cette méthode, déclaré avec T, sera du type
passé à la méthode. Ainsi, la variable a est du type générique qui sera précisé lors
de l’appel à cette méthode. Comme a est un objet, nous pouvons appeler la méthode
GetType() et la méthode ToString() sur cet objet.
Pour afficher un objet, nous pourrons faire :
1 int i = 5 ;
2 double d = 9 . 5 ;
3 string s = " abcd " ;
4 Voiture v = new Voiture () ;
5
6 Afficheur . Affiche < int >( i ) ;
7 Afficheur . Affiche < double >( d ) ;
8 Afficheur . Affiche < string >( s ) ;
9 Afficheur . Affiche < Voiture >( v ) ;
284
CRÉER UNE MÉTHODE GÉNÉRIQUE
Dans le premier appel, nous indiquons que nous souhaitons afficher i dont le type
générique est int. Tout se passe comme si le CLR créait la surcharge de la méthode
Affiche, prenant un entier en paramètre :
1 public static void Affiche ( int a )
2 {
3 Console . WriteLine ( " Afficheur d ' objet : " ) ;
4 Console . WriteLine ( " \ tType : " + a . GetType () ) ;
5 Console . WriteLine ( " \ tRepr é sentation : " + a . ToString () ) ;
6 }
De même pour l’affichage suivant, où l’on indique le type double entre les chevrons.
C’est comme si le CLR créait la surcharge prenant un double en paramètre :
1 public static void Affiche ( double a )
2 {
3 Console . WriteLine ( " Afficheur d ' objet : " ) ;
4 Console . WriteLine ( " \ tType : " + a . GetType () ) ;
5 Console . WriteLine ( " \ tRepr é sentation : " + a . ToString () ) ;
6 }
Et ceci pour tous les types utilisés, à savoir ici int, double, string et Voiture.
À noter que dans cet exemple, nous pouvons remplacer les quatre lignes suivantes :
1 Afficheur . Affiche < int >( i ) ;
2 Afficheur . Affiche < double >( d ) ;
3 Afficheur . Affiche < string >( s ) ;
4 Afficheur . Affiche < Voiture >( v ) ;
par :
1 Afficheur . Affiche ( i ) ;
2 Afficheur . Affiche ( d ) ;
3 Afficheur . Affiche ( s ) ;
4 Afficheur . Affiche ( v ) ;
En effet, nul besoin de préciser quel type nous souhaitons traiter ici, le compilateur est
assez malin pour le déduire du type de la variable. La variable i étant un entier, il est
obligatoire que le type générique soit un entier. Il est donc facultatif ici de le préciser.
Une fois qu’il est précisé entre les chevrons, le type générique s’utilise dans la méthode
comme n’importe quel autre type. Nous pouvons avoir autant de paramètres génériques
que nous le voulons dans les paramètres et utiliser le type générique dans le corps de
la méthode. Par exemple, la méthode suivante :
1 public static void Echanger <T >( ref T t1 , ref T t2 )
2 {
3 T temp = t1 ;
4 t1 = t2 ;
5 t2 = temp ;
6 }
285
CHAPITRE 27. LES GÉNÉRIQUES
permet d’échanger le contenu de deux variables entre elles. C’est donc une méthode
générique puisqu’elle précise entre les chevrons que nous pourrons utiliser le type T.
En paramètres de la méthode, nous passons deux variables de types génériques.
Dans le corps de la méthode, nous créons une variable du type générique qui sert
de mémoire temporaire puis nous échangeons les références des deux variables. Nous
pourrons utiliser cette méthode ainsi :
1 int i = 5 ;
2 int j = 10 ;
3 Echanger ( ref i , ref j ) ;
4 Console . WriteLine ( i ) ;
5 Console . WriteLine ( j ) ;
6
7 Voiture v1 = new Voiture { Couleur = " Rouge " };
8 Voiture v2 = new Voiture { Couleur = " Verte " };
9 Echanger ( ref v1 , ref v2 ) ;
10 Console . WriteLine ( v1 . Couleur ) ;
11 Console . WriteLine ( v2 . Couleur ) ;
Qui donnera :
10
5
Verte
Rouge
Il est bien sûr possible de créer des méthodes prenant en paramètres plusieurs types gé-
nériques différents. Il suffit alors de préciser autant de types différents entre les chevrons
qu’il y a de types génériques différents :
286
CRÉER UNE CLASSE GÉNÉRIQUE
Nous allons réaliser une implémentation toute basique de cette classe histoire de voir un
peu à quoi ressemble une classe générique. Cette classe n’a d’intérêt que pour étudier
les génériques, vous lui préférerez évidemment la classe List<> du framework .NET.
Nous avons besoin de trois variables privées. La capacité de la liste, le nombre d’élé-
ments dans la liste et le tableau générique.
1 public class MaListeGenerique <T >
2 {
3 private int capacite ;
4 private int nbElements ;
5 private T [] tableau ;
6
7 public MaListeGenerique ()
8 {
9 capacite = 10 ;
10 nbElements = 0 ;
11 tableau = new T [ capacite ];
12 }
13 }
287
CHAPITRE 27. LES GÉNÉRIQUES
2 {
3 [ Code enlev é pour plus de clart é ]
4
5 public void Ajouter ( T element )
6 {
7 if ( nbElements >= capacite )
8 {
9 capacite *= 2 ;
10 T [] copieTableau = new T [ capacite ];
11 for ( int i = 0 ; i < tableau . Length ; i ++)
12 {
13 copieTableau [ i ] = tableau [ i ];
14 }
15 tableau = copieTableau ;
16 }
17 tableau [ nbElements ] = element ;
18 nbElements ++;
19 }
20 }
Il s’agit simplement de mettre la valeur que l’on souhaite ajouter à l’emplacement adé-
quat dans le tableau. Nous le mettons en dernière position, c’est-à-dire à l’emplacement
correspondant au nombre d’éléments.
Au début, nous avons commencé par vérifier si le nombre d’éléments était supérieur
à la capacité du tableau. Si c’est le cas, alors nous devons augmenter la capacité du
tableau. J’ai ici décidé encore complètement arbitrairement que je doublais la capacité.
Il ne reste plus qu’à créer un nouveau tableau de cette nouvelle capacité et à copier les
éléments du premier tableau dans celui-ci.
Vous aurez noté que le paramètre de la méthode Ajouter est bien du type générique.
Pour le plaisir, rajoutons enfin une méthode permettant de récupérer un élément d’in-
dice donné :
1 public class MaListeGenerique <T >
2 {
3 [ Code enlev é pour plus de clart é ]
4
5 public T ObtenirElement ( int indice )
6 {
7 return tableau [ indice ];
8 }
9 }
Il s’agit juste d’accéder au tableau pour renvoyer la valeur à l’indice concerné. L’élément
intéressant ici est de constater que le type de retour de la méthode est bien du type
générique.
Cette liste peut s’utiliser de la manière suivante :
1 MaListeGenerique < int > maListe = new MaListeGenerique < int >() ;
2 maListe . Ajouter ( 25 ) ;
288
LA VALEUR PAR DÉFAUT D’UN TYPE GÉNÉRIQUE
3 maListe . Ajouter ( 30 ) ;
4 maListe . Ajouter ( 5 ) ;
5
6 Console . WriteLine ( maListe . ObtenirElement ( 0 ) ) ;
7 Console . WriteLine ( maListe . ObtenirElement ( 1 ) ) ;
8 Console . WriteLine ( maListe . ObtenirElement ( 2 ) ) ;
9
10 for ( int i = 0 ; i < 30 ; i ++)
11 {
12 maListe . Ajouter ( i ) ;
13 }
Ici, nous utilisons la liste avec un entier, mais elle fonctionnerait tout aussi bien avec un
autre type. N’hésitez pas à passer en debug dans la méthode Ajouter() pour observer
ce qui se passe exactement lors de l’augmentation de capacité.
Voilà comment on crée une classe générique !
Une fois qu’on a compris que le type générique s’utilise comme n’importe quel autre
type, cela devient assez facile.
Rappelez-vous, toute classe qui manipule des object est susceptible d’être
améliorée en utilisant les génériques.
289
CHAPITRE 27. LES GÉNÉRIQUES
Je souhaite pouvoir comparer des voitures entre elles, mais le framework .NET me
fournit un object en paramètres de la méthode CompareTo(). Quelle idée ! Comme si
je voulais comparer des voitures avec des chats ou des chiens. Cela me force en plus à
faire un cast. Pourquoi il ne me passe pas directement une Voiture en paramètre ?
Vous en avez l’intuition ? Un object ! Oui, mais c’est un peu lourd à manier. . . et je
ne parle pas des performances !
C’est là où les génériques vont voler à notre secours. L’interface IComparable date
de la première version du framework .NET. Le C# ne possédait pas encore les types
génériques. Depuis leur apparition, il est possible d’implémenter la version générique
de cette interface.
Pour cela, nous faisons suivre l’interface du type que nous souhaitons utiliser entre les
chevrons. Cela donne :
1 public class Voiture : IComparable < Voiture >
2 {
3 public string Couleur { get ; set ; }
4 public string Marque { get ; set ; }
5 public int Vitesse { get ; set ; }
6
7 public int CompareTo ( Voiture obj )
8 {
9 return Vitesse . CompareTo ( obj . Vitesse ) ;
10 }
11 }
290
LES RESTRICTIONS SUR LES TYPES GÉNÉRIQUES
Nous devons toujours implémenter la méthode CompareTo() sauf que nous avons dé-
sormais un objet Voiture en paramètre, ce qui nous évite de le caster.
Contrainte Description
where T : struct Le type générique doit être un type valeur
where T : class Le type générique doit être un type référence
where T : new() Le type générique doit posséder un constructeur par
défaut
where T : IMonInterface Le type générique doit implémenter l’interface
IMonInterface
where T : MaClasse Le type générique doit dériver de la classe MaClasse
where T1 : T2 Le type générique doit dériver du type générique T2
Par exemple, nous pouvons définir une restriction sur une méthode générique afin
qu’elle n’accepte en type générique que des types qui implémentent une interface.
Soit l’interface suivante :
1 public interface IVolant
2 {
3 void DeplierLesAiles () ;
4 void Voler () ;
5 }
291
CHAPITRE 27. LES GÉNÉRIQUES
Nous pouvons créer une méthode générique qui s’occupe d’instancier ces objets et
d’appeler les méthodes de l’interface :
1 public static T Creer <T >() where T : IVolant , new ()
2 {
3 T t = new T () ;
4 t . DeplierLesAiles () ;
5 t . Voler () ;
6 return t ;
7 }
Ici, la restriction porte sur deux niveaux. Il faut dans un premier temps que le type gé-
nérique implémente l’interface IVolant. En outre, il faut qu’il possède un constructeur,
bref qu’il soit instanciable.
Nous pouvons donc utiliser cette méthode de cette façon :
1 Oiseau oiseau = Creer < Oiseau >() ;
2 Avion a380 = Creer < Avion >() ;
Nous appelons la méthode Créer() avec le type générique Oiseau, qui implémente bien
IVolant et qui est aussi instanciable. Grâce à cela, nous pouvons utiliser l’opérateur
new pour créer notre type générique, appeler les méthodes de l’interface et renvoyer
l’instance. Ce qui donne :
Si nous tentons d’utiliser la méthode avec un type qui n’implémente pas l’interface
IVolant, comme :
1 Voiture v = Creer < Voiture >() ;
292
LES RESTRICTIONS SUR LES TYPES GÉNÉRIQUES
Oui, mais dans ce cas, plutôt que d’utiliser une méthode générique, pourquoi
la méthode ne renvoie pas IVolant ?
C’est une judicieuse remarque, mais elle implique quelques modifications de code. En
effet, il faudrait indiquer quel type instancier.
Nous pourrions par exemple faire :
1 public enum TypeDeVolant
2 {
3 Oiseau ,
4 Avion
5 }
6
7 public static IVolant Creer ( TypeDeVolant type )
8 {
9 IVolant volant ;
10 switch ( type )
11 {
12 case TypeDeVolant . Oiseau :
13 volant = new Oiseau () ;
14 break ;
15 case TypeDeVolant . Avion :
16 volant = new Avion () ;
17 break ;
18 default :
19 return null ;
20 }
21 volant . DeplierLesAiles () ;
22 volant . Voler () ;
23 return volant ;
24 }
Ce qui complique un peu les choses et rajoute des casts dont on pourrait volontiers
se passer. De plus, si nous créons un nouvel objet qui implémente cette interface, il
faudrait tout modifier.
Avouez qu’avec les types génériques, c’est quand même plus propre ! Nous pouvons bien
sûr avoir des restrictions sur les types génériques d’une classe.
293
CHAPITRE 27. LES GÉNÉRIQUES
Pour le montrer, nous allons créer une classe dont l’objectif est de disposer de types
valeur qui pourraient ne pas avoir de valeur. Pour les types référence, il suffit d’utiliser
le mot-clé null. Mais pour les types valeur comme les entiers, nous n’avons rien pour
indiquer que ceux-ci n’ont pas de valeur.
Par exemple :
1 public class TypeValeurNull <T > where T : struct
2 {
3 private bool aUneValeur ;
4 public bool AUneValeur
5 {
6 get { return aUneValeur ; }
7 }
8
9 private T valeur ;
10 public T Valeur
11 {
12 get
13 {
14 if ( aUneValeur )
15 return valeur ;
16 throw new I n v a l i d O p e r a t i o n E x c e p t i o n () ;
17 }
18 set
19 {
20 aUneValeur = true ;
21 valeur = value ;
22 }
23 }
24 }
Ici, nous utilisons une classe possédant un type générique qui sera un type valeur,
grâce à la condition where T : struct. Cette classe encapsule le type générique pour
indiquer avec un booléen si le type a une valeur ou pas. Ne faites pas attention à la
ligne :
1 throw new I n v a l i d O p e r a t i o n E x c e p t i o n () ;
qui permet juste de renvoyer une erreur, nous étudierons les exceptions un peu plus
loin. Elle pourra s’utiliser ainsi :
1 TypeValeurNull < int > entier = new TypeValeurNull < int >() ;
2 if (! entier . AUneValeur )
3 {
4 Console . WriteLine ( "l ' entier n ' a pas de valeur " ) ;
5 }
6 entier . Valeur = 5 ;
7 if ( entier . AUneValeur )
8 {
9 Console . WriteLine ( " Valeur de l ' entier : " + entier . Valeur ) ;
10 }
294
LES TYPES NULLABLES
Et si nous souhaitons faire de même pour un autre type valeur, il n’y a rien à coder de
plus :
1 TypeValeurNull < double > valeur = new TypeValeurNull < double >() ;
C’est quand même super pratique comme classe ! ! Mais ne rêvons pas, cette idée ne
vient pas de moi. C’est en fait une fonctionnalité du framework .NET : les types
nullables.
Le principe est grosso modo le même sauf que nous pouvons utiliser le mot-clé null
ou affecter directement la valeur à l’entier en utilisant l’opérateur d’affectation, sans
passer par la propriété Valeur. Il peut aussi être comparé au mot-clé null ou être
utilisé avec l’opérateur +, etc. Ceci est possible grâce à certaines fonctionnalités du
C# que nous n’avons pas vues et qui sortent de l’étude de ce livre.
Cette classe est tellement pratique que le compilateur a été optimisé pour simplifier son
écriture. En effet, utiliser Nullable<> est un peu long pour nous autres informaticiens
qui sommes des paresseux !
Aussi, l’écriture :
1 Nullable < int > entier = null ;
peut se simplifier en :
1 int ? entier = null ;
295
CHAPITRE 27. LES GÉNÉRIQUES
En résumé
– Avec les génériques, vous pouvez créer des méthodes ou des classes qui sont indépen-
dantes d’un type. On les appellera des méthodes génériques et des types génériques.
– On utilise les chevrons <> pour indiquer le type d’une classe ou d’une méthode
générique.
– Les interfaces peuvent aussi être génériques, comme l’interface IEnumerable<>.
– Les types nullables constituent un exemple d’utilisation très pratique des classes
génériques.
296
Chapitre 28
TP : types génériques
Difficulté :
A
h , un peu de pratique histoire de vérifier que nous avons bien compris les génériques.
C’est un concept assez facile à appréhender mais relativement difficile à mettre en
œuvre. Quand en ai-je besoin ? Comment ?
Voici donc un petit exercice qui va vous permettre d’essayer de mettre en œuvre une classe
générique.
297
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
Voilà, il faut donc créer une telle liste chaînée d’éléments. Le but est bien sûr de faire
en sorte que l’élément soit générique.
N’hésitez pas à réfléchir un peu avant de vous lancer. Cela pourrait paraître un peu
simpliste, mais en fait cela occasionne quelques nœuds au cerveau.
Toujours est-il que je souhaiterais disposer d’une propriété en lecture seule permettant
d’accéder au premier élément ainsi qu’une autre propriété également en lecture seule
298
CORRECTION
permettant d’accéder au dernier élément. Bien sûr, il faut pouvoir naviguer d’élément
en élément avec des propriétés Precedent et Suivant.
Il faut évidemment une méthode permettant d’ajouter un élément à la fin de la liste.
Nous aurons également besoin d’une méthode permettant d’accéder à un élément à
partir de son indice et enfin d’une méthode permettant d’insérer un élément à un
indice, décalant tous les suivants.
Voilà pour la création de la classe.
Ensuite, notre programme instanciera notre liste chaînée pour lui ajouter les entiers 5,
10 et 4. Puis nous afficherons les valeurs de cette liste en nous basant sur la première
propriété et en naviguant d’élément en élément.
Nous afficherons ensuite les différents éléments en utilisant la méthode d’accès à un
élément par son indice.
Enfin, nous insérerons la valeur 99 à la première position (position 0), puis la valeur
33 à la deuxième position et enfin la valeur 30 à nouveau à la deuxième position.
Puis nous afficherons tout ce beau monde.
Fin de l’énoncé, ouf !
Pour ceux qui n’ont pas besoin d’aide, les explications sont terminées. Ouvrez vos
Visual C# Express (ou vos Visual Studio si vous êtes riches !) et à vos claviers.
Pour les autres, je vais essayer de vous guider un peu plus en essayant tout de même
de ne pas trop vous donner d’indications non plus.
En fait, votre liste chaînée n’est pas vraiment une liste, comme pourrait l’être la List<>
que nous connaissons. Cette liste chaînée possède un point d’entrée qui est le premier
élément. L’ajout du premier élément est très simple, il suffit de mettre à jour une
propriété. Pour ajouter l’élément suivant, il faut en fait brancher la propriété Suivant
du premier élément à l’élément que nous sommes en train d’ajouter. Et inversement,
la propriété Precedent de l’élément que nous souhaitons ajouter sera mise à jour avec
le premier élément.
On se rend compte que l’élément est un peu plus complexe qu’un simple type. Nous
allons donc avoir une classe générique possédant trois propriétés (Precedent, Suivant
et Valeur). Et nous aurons également une classe du même type générique possédant
la propriété Premier et la propriété Dernier et les méthodes d’ajout, d’obtention de
l’élément et d’insertion.
Allez, je vous en ai assez dit. À vous de jouer !
Correction
Pas si facile hein ? Mais bon, comme vous êtes super entraînés, cela n’a pas dû vous
poser trop de problèmes.
Voici la correction que je propose.
La première chose à faire est de créer la classe générique permettant de stocker un
299
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
élément :
1 public class Chainage <T >
2 {
3 public Chainage <T > Precedent { get ; set ; }
4 public Chainage <T > Suivant { get ; set ; }
5 public T Valeur { get ; set ; }
6 }
C’est une classe générique toute simple qui possède une valeur du type générique et
deux propriétés du même type que l’élément pour obtenir le précédent ou le suivant.
Peut-être que la plus grande difficulté réside, ici, dans le fait de bien modéliser la classe
qui permet d’encapsuler l’élément.
Il faudra ensuite créer la liste générique et ses méthodes :
1 public class ListeChainee <T >
2 {
3 }
La liste chaînée possède également un type générique. Nous créons sa propriété Premier :
1 public class ListeChainee <T >
2 {
3 public Chainage <T > Premier { get ; private set ; }
4 }
Là, c’est très simple, il s’agit juste d’une propriété en lecture seule stockant le premier
élément. C’est la méthode Ajouter() qui permettra de mettre à jour cette valeur.
Notez quand même que nous utilisons le type générique comme type générique de la
classe encapsulante.
Par contre, pour la propriété Dernier, c’est un peu plus compliqué. Pour la retrouver,
nous allons parcourir tous les éléments à partir de la propriété Premier. Ce qui donne :
1 public class ListeChainee <T >
2 {
3 [... Code supprim é pour plus de clart é ...]
4
5 public Chainage <T > Dernier
6 {
7 get
8 {
9 if ( Premier == null )
10 return null ;
11 Chainage <T > dernier = Premier ;
12 while ( dernier . Suivant != null )
13 {
14 dernier = dernier . Suivant ;
15 }
16 return dernier ;
17 }
300
CORRECTION
18 }
19 }
On parcourt les éléments en bouclant sur la propriété Suivant, tant que celle-ci n’est
pas nulle. Il s’agit là d’un parcours assez classique où on utilise une variable temporaire
qui passe au suivant à chaque itération.
Nous pouvons à présent créer la méthode Ajouter :
1 public class ListeChainee <T >
2 {
3 [... Code supprim é pour plus de clart é ...]
4
5 public void Ajouter ( T element )
6 {
7 if ( Premier == null )
8 {
9 Premier = new Chainage <T > { Valeur = element };
10 }
11 else
12 {
13 Chainage <T > dernier = Dernier ;
14 dernier . Suivant = new Chainage <T > { Valeur =
element , Precedent = dernier };
15 }
16 }
17 }
Cette méthode traite tout d’abord le cas du premier élément. Il s’agit simplement de
mettre à jour la propriété Premier. De même, grâce au calcul interne de la propriété
Dernier, il sera facile d’ajouter un nouvel élément en se branchant sur la propriété
Suivant du dernier élément.
Notez que vu que nous ne la renseignons pas, la propriété Suivant du nouvel élément
sera bien à null.
Pour obtenir un élément à un indice donné, il suffira de reprendre le même principe
que lors du parcours pour obtenir le dernier élément, sauf qu’il faudra s’arrêter au bon
moment :
1 public class ListeChainee <T >
2 {
3 [... Code supprim é pour plus de clart é ...]
4
5 public Chainage <T > ObtenirElement ( int indice )
6 {
7 Chainage <T > temp = Premier ;
8 for ( int i = 1 ; i <= indice ; i ++)
9 {
10 if ( temp == null )
11 return null ;
12 temp = temp . Suivant ;
301
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
13 }
14 return temp ;
15 }
16 }
Ici, plusieurs solutions. J’ai choisi d’utiliser une boucle for. Nous aurions très bien pu
garder la boucle while comme pour la propriété Dernier.
Enfin, il ne reste plus qu’à insérer un élément :
1 public class ListeChainee <T >
2 {
3 [... Code supprim é pour plus de clart é ...]
4
5 public void Inserer ( T element , int indice )
6 {
7 if ( indice == 0 )
8 {
9 Chainage <T > temp = Premier ;
10 Premier = new Chainage <T > { Suivant = temp , Valeur
= element };
11 temp . Precedent = Premier ;
12 }
13 else
14 {
15 Chainage <T > elementAIndice = ObtenirElement ( indice )
;
16 if ( elementAIndice == null )
17 Ajouter ( element ) ;
18 else
19 {
20 Chainage <T > precedent = elementAIndice .
Precedent ;
21 Chainage <T > temp = precedent . Suivant ;
22 precedent . Suivant = new Chainage <T > { Valeur =
element , Precedent = precedent , Suivant =
temp };
23 }
24 }
25 }
26 }
Nous traitons, dans un premier temps, le cas où l’on doit insérer en tête. Il suffit de
mettre à jour la valeur du premier en ayant au préalable décalé ce dernier d’un cran.
Attention, si Premier est null, nous allons avoir un problème. Dans ce cas, soit nous
laissons le problème ; en effet, peut-on vraiment insérer un élément avant les autres
s’il n’y en a pas ? Soit nous gérons le cas et décidons d’insérer l’élément en tant que
Premier :
1 public class ListeChainee <T >
2 {
302
CORRECTION
Pour les autres cas, si nous tentons d’insérer à un indice qui n’existe pas, nous insérons
à la fin en utilisant la méthode Ajouter() existante. Sinon, on intercale le nouvel
élément dans la liste en prenant soin de brancher le précédent sur notre nouvel élément
et de brancher le suivant sur notre nouvel élément.
Voilà pour notre classe. Il reste à l’utiliser :
1 static void Main ( string [] args )
2 {
3 ListeChainee < int > listeChainee = new ListeChainee < int >() ;
4 listeChainee . Ajouter ( 5 ) ;
5 listeChainee . Ajouter ( 10 ) ;
6 listeChainee . Ajouter ( 4 ) ;
7 Console . WriteLine ( listeChainee . Premier . Valeur ) ;
8 Console . WriteLine ( listeChainee . Premier . Suivant . Valeur ) ;
9 Console . WriteLine ( listeChainee . Premier . Suivant . Suivant .
303
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
Valeur ) ;
10 Console . WriteLine ( " ************* " ) ;
11 Console . WriteLine ( listeChainee . ObtenirElement ( 0 ) . Valeur ) ;
12 Console . WriteLine ( listeChainee . ObtenirElement ( 1 ) . Valeur ) ;
13 Console . WriteLine ( listeChainee . ObtenirElement ( 2 ) . Valeur ) ;
14 Console . WriteLine ( " ************* " ) ;
15 listeChainee . Inserer ( 99 , 0 ) ;
16 listeChainee . Inserer ( 33 , 2 ) ;
17 listeChainee . Inserer ( 30 , 2 ) ;
18 Console . WriteLine ( listeChainee . ObtenirElement ( 0 ) . Valeur ) ;
19 Console . WriteLine ( listeChainee . ObtenirElement ( 1 ) . Valeur ) ;
20 Console . WriteLine ( listeChainee . ObtenirElement ( 2 ) . Valeur ) ;
21 Console . WriteLine ( listeChainee . ObtenirElement ( 3 ) . Valeur ) ;
22 Console . WriteLine ( listeChainee . ObtenirElement ( 4 ) . Valeur ) ;
23 Console . WriteLine ( listeChainee . ObtenirElement ( 5 ) . Valeur ) ;
24 }
5
10
4
*************
5
10
4
*************
99
5
30
33
10
4
304
INSTRUCTIONS POUR RÉALISER LA DEUXIÈME PARTIE DU TP
Si vous vous le sentez, allez-y ! Je pense quand même que vous allez avoir besoin d’être
un peu guidés car c’est une opération assez particulière.
Vous l’aurez deviné, il faut que notre liste implémente l’interface IEnumerable<T>.
Le fait d’implémenter cette interface va vous forcer à implémenter deux méthodes
GetEnumerator(), la version normale et la version explicite. Sachez dès à présent que
les deux méthodes feront exactement la même chose.
C’est vrai ! Allez, je vous en parle après la correction. Pour l’instant, cela ne devrait
pas vous perturber, car les deux méthodes font exactement la même chose.
En l’occurrence, elles renverront un Enumerator personnalisé.
Il va donc falloir créer cet énumérateur qui va s’occuper de la mécanique permettant
de naviguer dans notre liste. Il s’agit d’une nouvelle classe qui va devoir implémenter
l’interface IEnumerator<T>, c’est-à-dire :
1 public class ListeChaineeEnumerator <T > : IEnumerator <T >
2 {
3 }
Cette interface permet d’indiquer que notre énumérateur va respecter le contrat lui
permettant de fonctionner avec un foreach. Avec cette interface, vous allez devoir
implémenter :
– la propriété Current ;
– la propriété explicite Current (qui sera la même chose que la précédente) ;
– la méthode MoveNext qui permet de passer à l’élément suivant ;
– la méthode Reset, qui permet de revenir au début de la liste ;
– la méthode Dispose.
La méthode Dispose est en fait héritée de l’interface IDisposable dont hérite l’inter-
face IEnumerator<T>. C’est une interface particulière qui offre l’opportunité de faire
tout ce qu’il faut pour nettoyer la classe, c’est-à-dire libérer les variables qui en auraient
besoin. En l’occurrence, ici nous n’aurons rien à faire mais il faut quand même que la
méthode soit présente. Elle sera donc vide.
Pour implémenter les autres méthodes, il faut que l’énumérateur connaisse la liste
qu’il doit énumérer. Il faudra donc que la classe ListeChaineeEnumerator prenne en
paramètre de son constructeur la liste à énumérer. Dans ce constructeur, on initialise
la variable membre indice qui contient l’indice courant. La propriété Current renverra
l’élément à l’indice courant. La méthode MoveNext passe à l’élément suivant et renvoie
faux s’il n’y a plus d’éléments et vrai sinon. Enfin la méthode Reset repasse l’indice à
sa valeur initiale.
À noter que la valeur initiale de l’indice est -1, car la boucle foreach commence par
appeler la méthode MoveNext qui commence par aller à l’élément suivant, c’est-à-dire
305
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
à l’élément 0.
Il ne reste plus qu’à vous dire exactement quoi mettre dans les méthodes GetEnumerator
de la liste chaînée, car vous ne trouverez peut-être pas du premier coup :
1 public IEnumerator <T > GetEnumerator ()
2 {
3 return new ListeChaineeEnumerator <T >( this ) ;
4 }
5
6 IEnumerator IEnumerable . GetEnumerator ()
7 {
8 return new ListeChaineeEnumerator <T >( this ) ;
9 }
Correction
Encore moins facile ! Tant qu’on ne l’a pas fait une première fois, implémenter l’interface
IEnumerable est un peu déroutant. Après, c’est toujours pareil.
Voici donc ma correction.
Tout d’abord, la liste chaînée doit implémenter IEnumerable<T>, ce qui donne :
1 public class ListeChainee <T > : IEnumerable <T >
2 {
3 [... Code identique au TP pr é c é dent ...]
4
5 public IEnumerator <T > GetEnumerator ()
6 {
7 return new ListeChaineeEnumerator <T >( this ) ;
8 }
9
10 IEnumerator IEnumerable . GetEnumerator ()
11 {
12 return new ListeChaineeEnumerator <T >( this ) ;
13 }
14 }
Là, c’est du tout cuit vu que je vous avais donné la solution un peu plus tôt . J’espère
que vous avez au moins réussi ça !
Maintenant, il faut donc créer un nouvel énumérateur personnalisé en lui passant notre
liste chaînée en paramètre. Cet énumérateur doit implémenter l’interface IEnumerator,
ce qui donne :
1 public class ListeChaineeEnumerator <T > : IEnumerator <T >
2 {
3 }
306
CORRECTION
Comme prévu, il faut donc un constructeur qui prend en paramètre la liste chaînée :
1 public class ListeChaineeEnumerator <T > : IEnumerator <T >
2 {
3 private int indice ;
4 private ListeChainee <T > listeChainee ;
5 public L i s t e C h a i n e e E n u m e r a t o r ( ListeChainee <T > liste )
6 {
7 indice = -1 ;
8 listeChainee = liste ;
9 }
10
11 public void Dispose ()
12 {
13 }
14 }
Cette liste sera enregistrée dans une variable membre de la classe. Tant que nous y
sommes, nous ajoutons un indice privé que nous initialisons à -1, comme déjà expliqué.
Notez également que la méthode Dispose() est vide. Il reste à implémenter les autres
méthodes :
1 public class ListeChaineeEnumerator <T > : IEnumerator <T >
2 {
3 private int indice ;
4 private ListeChainee <T > listeChainee ;
5 public L i s t e C h a i n e e E n u m e r a t o r ( ListeChainee <T > liste )
6 {
7 indice = -1 ;
8 listeChainee = liste ;
9 }
10
11 public void Dispose ()
12 {
13 }
14
15 public bool MoveNext ()
16 {
17 indice ++;
18 Chainage <T > element = listeChainee . ObtenirElement (
indice ) ;
19 return element != null ;
20 }
21
22 public T Current
23 {
24 get
25 {
26 Chainage <T > element = listeChainee . ObtenirElement (
indice ) ;
27 if ( element == null )
307
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
28 return default ( T ) ;
29 return element . Valeur ;
30 }
31 }
32
33 object IEnumerator . Current
34 {
35 get { return Current ; }
36 }
37
38 public void Reset ()
39 {
40 indice = -1 ;
41 }
42 }
Commençons par la méthode MoveNext(). Elle passe à l’indice suivant et renvoie faux
ou vrai, selon qu’on arrive au bout de la liste ou pas. N’oubliez pas que c’est la première
méthode qui sera appelée dans le foreach, donc pour passer à l’élément suivant, on
incrémente l’indice pour le positionner à l’élément 0. C’est pour cela que l’indice a été
initialisé à -1. On utilise ensuite la méthode existante de la liste pour obtenir l’élément
à un indice afin de savoir si notre liste peut continuer à s’énumérer.
La propriété Current renvoie l’élément à l’indice courant, pour cela on utilise l’indice
pour accéder à l’élément courant, en utilisant les méthodes de la liste. L’autre propriété
Current fait la même chose, il suffit de l’appeler.
Enfin, la méthode Reset permet de réinitialiser l’énumérateur en retournant à l’indice
initial.
Finalement, ce n’est pas si compliqué que ça. Mais il faut avouer que la première fois,
c’est un peu déroutant !
À mon sens, c’est un bon exercice pratique. Peut-être que mes explications ont suffi à
vous guider. Sans doute avez-vous dû regarder un peu la documentation de IEnumerable
sur internet. Peut-être avez-vous cherché des ressources traitant du même sujet. Dans
tous les cas, devoir implémenter une interface du framework .NET est une situation
que vous allez fréquemment devoir rencontrer. Il est bon de s’y entraîner.
308
ALLER PLUS LOIN
unique 1 . Plus une classe fait de choses, plus une modification impacte les autres choses.
Ici, il est judicieux de garder le découplage des deux classes.
Il y a quand même un élément que l’on peut améliorer dans le code de la correction.
En effet, cette liste n’est pas extrêmement optimisée car lorsque nous obtenons un
élément, nous reparcourons toute la liste depuis le début, notamment dans le cas de
la gestion de l’énumérateur. Il pourrait être judicieux qu’à chaque foreach, nous ne
parcourions pas tous les éléments et qu’on évite d’appeler continuellement la méthode
ObtenirElement(). Une idée ? Cela pourrait se faire en éliminant l’indice et en utilisant
une variable de type Chainage<T>, par exemple :
1 public class ListeChaineeEnumerator <T > : IEnumerator <T >
2 {
3 private Chainage <T > courant ;
4 private ListeChainee <T > listeChainee ;
5 public L i s t e C h a i n e e E n u m e r a t o r ( ListeChainee <T > liste )
6 {
7 courant = null ;
8 listeChainee = liste ;
9 }
10
11 public void Dispose ()
12 {
13 }
14
15 public bool MoveNext ()
16 {
17 if ( courant == null )
18 courant = listeChainee . Premier ;
19 else
20 courant = courant . Suivant ;
21
22 return courant != null ;
23 }
24
25 public T Current
26 {
27 get
28 {
29 if ( courant == null )
30 return default ( T ) ;
31 return courant . Valeur ;
32 }
33 }
34
35 object IEnumerator . Current
36 {
37 get { return Current ; }
38 }
309
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
39
40 public void Reset ()
41 {
42 courant = null ;
43 }
44 }
Ici, c’est la variable courant qui nous permet d’itérer au fur et à mesure de la liste
chaînée. C’est le même principe que dans la méthode ObtenirElement, sauf qu’on ne
reparcourt pas toute la liste à chaque fois.
Ici, cette optimisation est négligeable pour notre utilisation. Elle peut s’avérer intéres-
sante si la liste grossit énormément.
Dans tous les cas, ça ne fait pas de mal d’aller plus vite !
Remarquons avant de terminer qu’il est possible de simplifier encore la classe grâce à
un mot-clé que nous découvrirons dans la partie suivante : yield. Il permet de créer
facilement des énumérateurs. Ce qui fait que le code complet de la liste chaînée pourra
être :
1 public class ListeChainee <T > : IEnumerable <T >
2 {
3 public Chainage <T > Premier { get ; private set ; }
4
5 public Chainage <T > Dernier
6 {
7 get
8 {
9 if ( Premier == null )
10 return null ;
11 Chainage <T > dernier = Premier ;
12 while ( dernier . Suivant != null )
13 {
14 dernier = dernier . Suivant ;
15 }
16 return dernier ;
17 }
18 }
19
20 public void Ajouter ( T element )
21 {
22 if ( Premier == null )
23 {
24 Premier = new Chainage <T > { Valeur = element };
25 }
26 else
27 {
28 Chainage <T > dernier = Dernier ;
29 dernier . Suivant = new Chainage <T > { Valeur =
element , Precedent = dernier };
30 }
310
ALLER PLUS LOIN
31 }
32
33 public Chainage <T > ObtenirElement ( int indice )
34 {
35 Chainage <T > temp = Premier ;
36 for ( int i = 1 ; i <= indice ; i ++)
37 {
38 if ( temp == null )
39 return null ;
40 temp = temp . Suivant ;
41 }
42 return temp ;
43 }
44
45 public void Inserer ( T element , int indice )
46 {
47 if ( indice == 0 )
48 {
49 if ( Premier == null )
50 Premier = new Chainage <T > { Valeur = element };
51 else
52 {
53 Chainage <T > temp = Premier ;
54 Premier = new Chainage <T > { Suivant = temp ,
Valeur = element };
55 temp . Precedent = Premier ;
56 }
57 }
58 else
59 {
60 Chainage <T > elementAIndice = ObtenirElement ( indice )
;
61 if ( elementAIndice == null )
62 Ajouter ( element ) ;
63 else
64 {
65 Chainage <T > precedent = elementAIndice .
Precedent ;
66 Chainage <T > temp = precedent . Suivant ;
67 precedent . Suivant = new Chainage <T > { Valeur =
element , Precedent = precedent , Suivant =
temp };
68 }
69 }
70 }
71
72 public IEnumerator <T > GetEnumerator ()
73 {
74 Chainage <T > courant = Premier ;
75 while ( courant != null )
311
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
76 {
77 yield return courant . Valeur ;
78 courant = courant . Suivant ;
79 }
80 }
81
82 IEnumerator IEnumerable . GetEnumerator ()
83 {
84 return GetEnumerator () ;
85 }
86 }
312
IMPLÉMENTER UNE INTERFACE EXPLICITEMENT
17 }
18
19 class Program
20 {
21 static void Main ( string [] args )
22 {
23 Homme homme = new Homme () ;
24 homme . Manger () ;
25 (( ICarnivore ) homme ) . Manger () ;
26 (( IFrugivore ) homme ) . Manger () ;
27 }
28 }
Ici, ce code compile car la classe Homme implémente la méthode Manger qui est commune
aux deux interfaces. Par contre, il n’est pas possible de faire la distinction entre le fait
de manger en tant qu’homme, en tant que ICarnivore ou en tant que IFrugivore.
Ce code affichera :
Je mange
Je mange
Je mange
Si c’est le comportement attendu, tant mieux. Si ce n’est pas le cas, il va falloir implé-
menter au moins une des interfaces de manière explicite :
1 public class Homme : ICarnivore , IFrugivore
2 {
3 public void Manger ()
4 {
5 Console . WriteLine ( " Je mange " ) ;
6 }
7
8 void IFrugivore . Manger ()
9 {
10 Console . WriteLine ( " Je mange en tant que IFrugivore " ) ;
11 }
12
13 void ICarnivore . Manger ()
14 {
15 Console . WriteLine ( " Je mange en tant que ICarnivore " ) ;
16 }
17 }
Je mange
Je mange en tant que ICarnivore
Je mange en tant que IFrugivore
313
CHAPITRE 28. TP : TYPES GÉNÉRIQUES
Si vous vous rappelez, nous avions vu au moment du chapitre sur les interfaces que Vi-
sual C# Express nous proposait de nous aider dans l’implémentation de l’interface. Par
le bouton droit, vous aviez également accès au sous-menu implémenter l’interface
explicitement. Vous pouvez vous en servir dans ce cas précis.
Je m’arrête là sur l’implémentation d’une interface explicite, même s’il y aurait d’autres
points à voir. Globalement, en situation réelle, cela ne vous servira jamais.
Voilà pour ce TP. Nous avons créé une classe générique permettant de gérer les listes
chaînées. Ceci nous a permis de manipuler ces types ô combien indispensables et de nous
entraîner à la généricité. Nous en avons même profité pour voir comment faire en sorte
qu’une classe soit énumérable, en implémentant la version générique de IEnumerable.
Notez bien sûr que cette classe est fonctionnellement incomplète. Il aurait été judicieux
de rajouter une méthode permettant de supprimer un élément par exemple. À noter
qu’une classe qui fait à peu près le même travail existe dans le framework .NET, elle
s’appelle LinkedList. Vous trouverez sa documentation via le code web suivant :
☛ ✟
Documentation LinkedList
⊲
✡Code web : 607852 ✠
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 972348 ✠
314
Chapitre 29
Les méthodes d’extension
Difficulté :
E
n général, pour ajouter des fonctionnalités à une classe, nous pourrons soit modifier
le code source de la classe, soit créer une classe dérivée de notre classe et ajouter ces
fonctionnalités.
Dans ce chapitre nous allons voir qu’il existe un autre moyen pour étendre une classe : ceci
est possible grâce aux méthodes d’extension. Elles sont intéressantes si nous n’avons pas
la main sur le code source de la classe ou si la classe n’est pas dérivable.
Partons à la découverte de ces fameuses méthodes. . .
315
CHAPITRE 29. LES MÉTHODES D’EXTENSION
316
UTILISER UNE MÉTHODE D’EXTENSION
Bon, je vous l’accorde, notre encodage secret ne l’est pas tant que ça ! Il s’avère que j’uti-
lise ici un encodage en base 64, algorithme archiconnu. Mais bon, c’est pour l’exemple !
Nous pourrons désormais utiliser ces méthodes comme si elles faisaient partie de la
classe String :
1 string chaineNormale = " Bonjour à tous " ;
2 string chaineCryptee = chaineNormale . Crypte () ;
3 Console . WriteLine ( chaineCryptee ) ;
4 chaineNormale = chaineCryptee . Decrypte () ;
5 Console . WriteLine ( chaineNormale ) ;
Pas mal non ? De plus, si nous regardons dans la complétion automatique, nous pour-
rons voir apparaître nos fameuses méthodes (voir figure 29.1).
Plutôt pratique. Évidemment, en créant une méthode d’extension, nous n’avons pas
accès aux méthodes privées ou variables membres internes à la classe. La preuve, les
méthodes d’extension sont des méthodes statiques qui travaillent hors de toute instance
de classe.
Ces méthodes doivent donc être statiques et situées à l’intérieur d’une classe
statique.
317
CHAPITRE 29. LES MÉTHODES D’EXTENSION
Par contre, il faut faire attention à l’espace de nom où se situent nos méthodes d’ex-
tension. Si le using correspondant n’est pas inclus, nous ne verrons pas les méthodes
d’extension.
Remarquez que les méthodes d’extension fonctionnent aussi avec les interfaces. Plus
précisément, elles viennent étendre toutes les classes qui implémentent une interface.
Par exemple, avec deux classes implémentant l’interface IVolant :
1 public interface IVolant
2 {
3 void Voler () ;
4 }
5
6 public class Oiseau : IVolant
7 {
8 public void Voler ()
9 {
10 Console . WriteLine ( " Je vole " ) ;
11 }
12 }
13
14 public class Avion : IVolant
15 {
16 public void Voler ()
17 {
18 Console . WriteLine ( " Je vole " ) ;
19 }
20 }
318
UTILISER UNE MÉTHODE D’EXTENSION
6 }
7 }
Je peux ensuite accéder à cette méthode pour les objets Avion et Oiseau :
1 Avion a = new Avion () ;
2 Oiseau b = new Oiseau () ;
3 a . Atterrir () ;
4 b . Atterrir () ;
À noter que le framework .NET se sert de ceci pour proposer un grand nombre de
méthodes d’extension sur les objets implémentant IEnumerable ; nous les étudierons
un peu plus tard.
En résumé
– Une méthode d’extension permet d’étendre une classe en lui rajoutant des méthodes.
– Il n’est pas recommandé d’utiliser des méthodes d’extension lorsqu’on dispose déjà
du code source de la classe ou qu’on peut facilement en créer un type dérivé.
– On utilise le mot-clé this en premier paramètre d’une classe statique pour étendre
une classe.
319
CHAPITRE 29. LES MÉTHODES D’EXTENSION
320
Chapitre 30
Délégués, événements et expressions
lambdas
Difficulté :
D
ans ce chapitre, nous allons aborder les délégués, les événements et les expressions
lambdas. Les délégués et les événements sont des types du framework .NET que nous
n’avons pas encore vus. Ils permettent d’adresser des solutions notamment dans le
cadre d’une programmation par événements, comme c’est le cas lorsque nous réalisons des
applications nécessitant de réagir à une action faite par un utilisateur. Nous verrons dans
ce chapitre que les expressions lambdas vont de pair avec les délégués.
321
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
crée un délégué privé à la classe TrieurDeTableau qui permettra de pointer vers des
méthodes qui ne retournent rien (void) et qui acceptent un tableau d’entiers en para-
mètre.
C’est justement le cas des méthodes TriAscendant() et TriDescendant() que nous
allons ajouter à la classe (ça tombe bien !) :
1 public class TrieurDeTableau
2 {
3 private delegate void DelegateTri ( int [] tableau ) ;
4
5 private void TriAscendant ( int [] tableau )
6 {
7 Array . Sort ( tableau ) ;
8 }
9
10 private void TriDescendant ( int [] tableau )
11 {
12 Array . Sort ( tableau ) ;
13 Array . Reverse ( tableau ) ;
14 }
15 }
Vous aurez compris que la méthode TriAscendant utilise la méthode Array.Sort pour
trier un tableau par ordre croissant. Inversement, la méthode TriDescendant() trie le
tableau par ordre décroissant en triant par ordre croissant et en inversant ensuite le
tableau.
Il ne reste plus qu’à créer une méthode dans la classe permettant d’utiliser le tri as-
cendant et le tri descendant, grâce à notre délégué :
1 public class TrieurDeTableau
1. En anglais, delegate.
322
LES DÉLÉGUÉS (DELEGATE)
2 {
3 [... Code supprim é pour plus de clart é ...]
4
5 public void DemoTri ( int [] tableau )
6 {
7 DelegateTri tri = TriAscendant ;
8 tri ( tableau ) ;
9 foreach ( int i in tableau )
10 {
11 Console . WriteLine ( i ) ;
12 }
13
14 Console . WriteLine () ;
15 tri = TriDescendant ;
16 tri ( tableau ) ;
17 foreach ( int i in tableau )
18 {
19 Console . WriteLine ( i ) ;
20 }
21 }
22 }
Nous voyons ici que dans la méthode DemoTri nous commençons par déclarer une
variable du type du délégué DelegateTri, qui est le délégué que nous avons créé. Puis
nous faisons pointer cette variable vers la méthode TriAscendant().
Nul besoin ici d’utiliser les parenthèses, mais juste le nom de la méthode. Il
s’agit juste d’une affectation.
Notre code affichera au final les entiers triés par ordre croissant, puis les mêmes entiers
triés par ordre décroissant.
Ok, mais pourquoi utiliser ce délégué ? On pourrait très bien appeler d’abord
la méthode de tri ascendant et ensuite la méthode de tri descendant. Comme
on l’a toujours fait !
323
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
Eh bien, l’intérêt ici est que le délégué est très souple et va permettre de réorganiser le
code (on parle également de refactoriser du code). Ainsi, en rajoutant la méthode
suivante dans la classe :
1 private void TrierEtAfficher ( int [] tableau , DelegateTri
methodeDeTri )
2 {
3 methodeDeTri ( tableau ) ;
4 foreach ( int i in tableau )
5 {
6 Console . WriteLine ( i ) ;
7 }
8 }
324
DIFFUSION MULTIPLE, LE MULTICAST
13
14 public void DemoTri ( int [] tableau )
15 {
16 TrierEtAfficher ( tableau , delegate ( int [] leTableau )
17 {
18 Array . Sort ( leTableau ) ;
19 }) ;
20
21 Console . WriteLine () ;
22
23 TrierEtAfficher ( tableau , delegate ( int [] leTableau )
24 {
25 Array . Sort ( tableau ) ;
26 Array . Reverse ( tableau ) ;
27 }) ;
28
29 }
30 }
325
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
6 {
7 Array . Sort ( tableau ) ;
8 foreach ( int i in tableau )
9 {
10 Console . WriteLine ( i ) ;
11 }
12 Console . WriteLine () ;
13 }
14
15 private void TriDescendant ( int [] tableau )
16 {
17 Array . Sort ( tableau ) ;
18 Array . Reverse ( tableau ) ;
19 foreach ( int i in tableau )
20 {
21 Console . WriteLine ( i ) ;
22 }
23 }
24
25 public void DemoTri ( int [] tableau )
26 {
27 DelegateTri tri = TriAscendant ;
28 tri += TriDescendant ;
29 tri ( tableau ) ;
30 }
31 }
Ici, j’utilise Console.WriteLine directement dans chaque méthode de tri afin de bien
voir le résultat du tri du tableau. L’important est de voir que dans la méthode DemoTri,
je commence par créer un délégué que je fais pointer vers la méthode TriAscendant.
Puis j’ajoute à ce délégué, avec l’opérateur +=, une nouvelle méthode, à savoir
TriDescendant. Désormais, le fait d’invoquer le délégué va invoquer les deux mé-
thodes ; ce qui produira en sortie :
1
4
5
6
8
10
10
8
6
5
4
1
Ce détail prend toute son importance avec les événements que nous verrons plus loin.
326
LES DÉLÉGUÉS GÉNÉRIQUES ACTION ET FUNC
À noter que le résultat de ce code est évidemment identique en utilisant les méthodes
anonymes :
1 public void DemoTri ( int [] tableau )
2 {
3 DelegateTri tri = delegate ( int [] leTableau )
4 {
5 Array . Sort ( leTableau ) ;
6 foreach ( int i in tableau )
7 {
8 Console . WriteLine ( i ) ;
9 }
10 Console . WriteLine () ;
11 };
12 tri += delegate ( int [] leTableau )
13 {
14 Array . Sort ( tableau ) ;
15 Array . Reverse ( tableau ) ;
16 foreach ( int i in tableau )
17 {
18 Console . WriteLine ( i ) ;
19 }
20 };
21 tri ( tableau ) ;
22 }
Il faut quand même remarquer que l’ordre dans lequel sont appelées les mé-
thodes n’est pas garanti et ne dépend pas forcément de l’ordre dans lequel
nous les avons ajoutées au délégué.
C’est très bien tout ça, mais cela veut dire qu’à chaque fois que je vais avoir
besoin d’utiliser un délégué, je vais devoir créer un nouveau type en utilisant
le mot-clé delegate ?
Pas forcément, c’est là qu’interviennent les délégués génériques Action et Func. Action
est un délégué qui permet de pointer vers une méthode qui ne renvoie rien et qui peut
accepter jusqu’à 16 types différents. Cela veut dire que le code précédent peut être
remplacé par :
1 public class TrieurDeTableau
2 {
3 private void TrierEtAfficher ( int [] tableau , Action < int [] >
methodeDeTri )
4 {
327
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
5 methodeDeTri ( tableau ) ;
6 foreach ( int i in tableau )
7 {
8 Console . WriteLine ( i ) ;
9 }
10 }
11
12 public void DemoTri ( int [] tableau )
13 {
14 TrierEtAfficher ( tableau , delegate ( int [] leTableau )
15 {
16 Array . Sort ( leTableau ) ;
17 }) ;
18
19 Console . WriteLine () ;
20
21 TrierEtAfficher ( tableau , delegate ( int [] leTableau )
22 {
23 Array . Sort ( tableau ) ;
24 Array . Reverse ( tableau ) ;
25 }) ;
26 }
27 }
328
LES EXPRESSIONS LAMBDAS
17 }
18
19 private double Calcul ( Func < int , int , double >
methodeDeCalcul , int a , int b )
20 {
21 return methodeDeCalcul (a , b ) ;
22 }
23 }
Ici, dans la méthode Calcul, on utilise le délégué Func pour indiquer que la méthode
prend deux entiers en paramètres et renvoie un double. Si nous utilisons cette classe
avec le code suivant :
1 class Program
2 {
3 static void Main ( string [] args )
4 {
5 new Operations () . DemoOperations () ;
6 }
7 }
nous aurons :
Division : 0 ,8
Puissance : 1024
329
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
Quand il y a deux paramètres, on les sépare par une virgule. À noter qu’on n’indique
nulle part le type de retour, s’il y en a un. Notre expression lambda remplaçant le
calcul de la division peut donc s’écrire ainsi :
1 double division = Calcul (( a , b ) = >
2 {
3 return ( double ) a / ( double ) b ;
4 }, 4, 5);
Lorsque l’instruction possède une unique ligne, on peut encore en simplifier l’écriture ;
ce qui donne :
1 double division = Calcul (( a , b ) = > ( double ) a / ( double )b , 4 , 5 )
;
Les événements
Les événements sont un mécanisme du C# permettant à une classe d’être notifiée d’un
changement. Par exemple, on peut vouloir s’abonner à un changement de prix d’une
voiture. La base des événements est le délégué. On pourra stocker dans un événement
un ou plusieurs délégués qui pointent vers des méthodes respectant la signature de
l’événement.
Un événement est défini grâce au mot-clé event. Prenons cet exemple :
330
LES ÉVÉNEMENTS
Dans la classe Voiture, nous définissons un délégué qui ne retourne rien et qui prend
en paramètre un décimal. Nous définissons ensuite un événement basé sur ce délégué,
avec, comme nous l’avons vu, l’utilisation du mot-clé event. Enfin, dans la méthode de
promotion, après un changement de prix (division par 2), nous notifions les éventuels
objets qui se seraient abonnés à cet événement en invoquant l’événement et en lui
fournissant en paramètre le nouveau prix.
À noter que nous testons d’abord s’il y a un abonné à l’événement (en testant s’il est
différent de null) avant de le lever.
Pour s’abonner à cet événement, il suffit d’utiliser le code suivant :
1 class Program
2 {
3 static void Main ( string [] args )
4 {
5 new DemoEvenement () . Demo () ;
6 }
7 }
8
9 public class DemoEvenement
10 {
11 public void Demo ()
12 {
13 Voiture voiture = new Voiture { Prix = 10000 };
14
15 Voiture . D e l e g a t e D e C h a n g e m e n t D e P r i x
delegateChangementDePrix = voiture_ChangementDePrix ;
16 voiture . ChangementDePrix += d e l e g a t e C h a n g e m e n t D e P r i x ;
17
18 voiture . PromoSurLePrix () ;
19 }
20
21 private void v o i t u r e _ C h a n g e m e n t D e P r i x ( decimal nouveauPrix )
22 {
23 Console . WriteLine ( " Le nouveau prix est de : " +
331
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
nouveauPrix ) ;
24 }
25 }
Nous créons une voiture, et nous créons un délégué du même type que l’événement.
Nous le faisons pointer vers une méthode qui respecte la signature du délégué. Ainsi, à
chaque changement de prix, la méthode voiture_ChangementDePrix va être appelée
et le paramètre nouveauPrix possédera le nouveau prix qui vient d’être calculé.
Appelons la promotion en invoquant la méthode ChangementDePrix(). Nous pouvons
nous rendre compte que l’application nous affiche le nouveau prix qui est l’ancien divisé
par 2.
Lorsque nous commençons à écrire le code qui va permettre de nous abonner à l’évé-
nement, la complétion automatique nous propose facilement de créer une méthode qui
respecte la signature de l’événement.
✄ Il suffit de saisir l’événement, d’ajouter ensuite
un += et d’appuyer sur la touche ✂Tab ✁pour que Visual C# Express nous propose de
tout insérer automatiquement (voir la figure 30.1).
comme on l’a déjà vu. Notez que vous pouvez également rajouter la visibilité private
sur la méthode générée afin que cela soit plus explicite.
1 private void v o i t u r e _ C h a n g e m e n t D e P r i x ( decimal nouveauPrix )
332
LES ÉVÉNEMENTS
2 {
3 }
333
CHAPITRE 30. DÉLÉGUÉS, ÉVÉNEMENTS ET EXPRESSIONS LAMBDAS
6 }
7 }
8
9 public class DemoEvenement
10 {
11 public void Demo ()
12 {
13 Voiture voiture = new Voiture { Prix = 10000 };
14
15 voiture . ChangementDePrix += v o i t u r e _ C h a n g e m e n t D e P r i x ;
16
17 voiture . PromoSurLePrix () ;
18 }
19
20 private void v o i t u r e _ C h a n g e m e n t D e P r i x ( object sender ,
ChangementDePrixEventArgs e)
21 {
22 Console . WriteLine ( " Le nouveau prix est de : " + e . Prix )
;
23 }
24 }
En résumé
– Les délégués permettent de créer des variables pointant vers des méthodes.
– Les délégués sont à la base des événements.
– On utilise les expressions lambdas pour simplifier l’écriture des délégués.
– Les événements sont un mécanisme du C# permettant à une classe d’être notifiée
d’un changement.
334
Chapitre 31
Gérer les erreurs : les exceptions
Difficulté :
N
ous avons parlé rapidement des erreurs dans nos applications C# en disant qu’il
s’agissait d’exceptions. C’est le moment d’en savoir un peu plus et surtout d’apprendre
à les gérer ! Dans ce chapitre, nous allons apprendre comment créer et intercepter
une exception.
335
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
L’intérêt des exceptions est qu’elles sont typées. C’en est fini des codes d’erreurs incom-
préhensibles. C’est le type de l’exception, c’est-à-dire sa classe, qui va nous permettre
d’identifier le problème.
Pour éviter le plantage de l’application, nous devons gérer ces cas limites et intercepter
les exceptions. Pour ce faire, il faut encadrer les instructions pouvant atteindre des cas
limites avec le bloc d’instruction try...catch, par exemple :
1 try
2 {
3 string chaine = " dix " ;
4 int valeur = Convert . ToInt32 ( chaine ) ;
5 Console . WriteLine ( " Ce code ne sera jamais affich é " ) ;
6 }
7 catch ( Exception )
8 {
336
INTERCEPTER UNE EXCEPTION
Ainsi le code ci-dessus affichera uniquement que la conversion est bonne et, en toute
logique, il ne passera pas dans le bloc de traitement d’erreur, car il n’y en a pas eu. Il
est possible d’obtenir des informations sur l’exception en utilisant un paramètre dans
le bloc catch. Ce paramètre est une variable du type Exception, qui dans l’exemple
suivant s’appelle ex :
1 try
2 {
3 string chaine = " dix " ;
4 int valeur = Convert . ToInt32 ( chaine ) ;
5 }
6 catch ( Exception ex )
7 {
8 Console . WriteLine ( " Il y a un eu une erreur , plus d '
informations ci - dessous : " ) ;
9 Console . WriteLine () ;
10 Console . WriteLine ( " Message d ' erreur : " + ex . Message ) ;
11 Console . WriteLine () ;
12 Console . WriteLine ( " Pile d ' appel : " + ex . StackTrace ) ;
13 Console . WriteLine () ;
14 Console . WriteLine ( " Type de l ' exception : " + ex . GetType () ) ;
15 }
Ici, nous affichons le message d’erreur, la pile d’appel et le type de l’exception, ce qui
donne le message suivant :
337
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
Ce code affichera :
Les exceptions peuvent être de beaucoup de formes. Ici nous remarquons que l’exception
est de type System.FormatException. Cette exception est utilisée en général lorsque
338
INTERCEPTER UNE EXCEPTION
le format d’un paramètre ne correspond pas à ce qui est attendu. En l’occurrence ici,
nous attendons un paramètre de type chaîne de caractères qui représente un entier.
C’est une exception spécifique qui est dédiée à un type d’erreur précis.
Il faut savoir que comme beaucoup d’autres objets du framework .NET, les exceptions
spécifiques dérivent d’une classe de base, à savoir la classe Exception, dont vous pouvez
trouver la documentation via le code web suivant :
☛ ✟
Exceptions
⊲
Code web : 368326
✡ ✠
Il existe une hiérarchie entre les exceptions. Globalement, deux grandes familles d’ex-
ceptions existent : ApplicationException et SystemException. Elles dérivent toutes
les deux de la classe de base Exception. La première est utilisée lorsque des erreurs
récupérables sur des applications apparaissent ; la seconde est utilisée pour toutes les
exceptions générées par le framework .NET. Par exemple, l’exception que nous avons
vue, FormatException dérive directement de SystemException, qui dérive elle-même
de la classe Exception.
Le framework .NET dispose de beaucoup d’exceptions correspondant à beaucoup de
situations. Notons encore au passage une autre exception bien connue des développeurs
qui est la NullReferenceException. Elle se produit lorsqu’on essaie d’accéder à un
objet qui vaut null. Par exemple :
1 Voiture voiture = null ;
2 voiture . Vitesse = 10 ;
nous voyons que le bloc catch prend en paramètre la classe de base Exception.
Cela veut dire que nous souhaitons intercepter toutes les exceptions qui dérivent de
Exception ; c’est-à-dire en fait toutes les exceptions, car toute exception dérive forcé-
ment de la classe Exception. C’est utile lorsque nous voulons attraper toutes les ex-
ceptions. Mais savons-nous forcément quoi faire dans le cas de toutes les erreurs ? Il est
possible d’être plus précis afin de n’attraper qu’un seul type d’exception. Il suffit de pré-
ciser le type de l’exception attendue comme paramètre du catch. Par exemple le code
suivant nous permet d’intercepter toutes les exceptions du type FormatException :
1 try
2 {
3 string chaine = " dix " ;
339
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
Cela veut par contre dire que si nous avons une autre exception à ce moment-là, du style
NullReferenceException, elle ne sera pas attrapée ; ce qui fait que le code suivant va
planter :
1 try
2 {
3 Voiture v = null ;
4 v . Vitesse = 10 ;
5 }
6 catch ( FormatException ex )
7 {
8 Console . WriteLine ( " Erreur de format : " + ex ) ;
9 }
Dans ce code, cela veut dire que si le code surveillé provoque une FormatException,
alors nous afficherons le message « Erreur de format. . . » ; s’il provoque une
NullReferenceException, alors nous afficherons le message « Erreur de référence
nulle. . . ». Notez bien que nous ne pouvons rentrer à chaque fois que dans un seul bloc
catch. Une autre solution serait d’attraper une exception plus généraliste par exemple
SystemException dont dérive FormatException et NullReferenceException :
340
INTERCEPTER PLUSIEURS EXCEPTIONS
1 try
2 {
3 // code provoquant une exception
4 }
5 catch ( SystemException ex )
6 {
7 Console . WriteLine ( " Erreur syst è me : " + ex ) ;
8 }
Par contre, le code précédent attrape toutes les exceptions dérivant de SystemException.
C’est le cas de FormatException et NullReferenceException, mais c’est aussi le cas
pour beaucoup d’autres exceptions. Lorsqu’on surveille un bloc de code, on commence
en général par surveiller les exceptions les plus fines possible et on remonte en consi-
dérant les exceptions les plus générales, jusqu’à terminer par la classe Exception :
1 try
2 {
3 // code provoquant une exception
4 }
5 catch ( FormatException ex )
6 {
7 Console . WriteLine ( " Erreur de format : " + ex ) ;
8 }
9 catch ( N u l l R e f e r e n c e E x c e p t i o n ex )
10 {
11 Console . WriteLine ( " Erreur de r é f é rence nulle : " + ex ) ;
12 }
13 catch ( SystemException ex )
14 {
15 Console . WriteLine ( " Erreur syst è me autres que
FormatException et N u l l R e f e r e n c e E x c e p t i o n : " + ex ) ;
16 }
17 catch ( Exception ex )
18 {
19 Console . WriteLine ( " Toutes les autres exceptions : " + ex ) ;
20 }
À chaque exécution, c’est le bloc catch qui se rapproche le plus de l’exception levée
qui est utilisé. C’est un peu comme une opération conditionnelle. Le programme vérifie
dans un premier temps que l’exception n’est pas une FormatException. Si ce n’est
pas le cas, il vérifiera qu’il n’a pas à faire à une NullReferenceException. Ensuite,
il vérifiera qu’il ne s’agit pas d’une SystemException. Enfin, il interceptera toutes les
exceptions dans le dernier bloc de code, car Exception étant la classe mère, toutes les
exceptions sont interceptées avec ce type.
À noter qu’il est possible d’imbriquer les try...catch si cela s’avère pertinent. Par
exemple :
1 string saisie = Console . ReadLine () ;
2 try
3 {
341
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
Ce code nous permet de tester si la saisie est un entier. Si une exception se produit,
alors nous tentons de le convertir en double. S’il y a encore une exception, alors nous
affichons un message indiquant que les deux conversions ont échoué.
342
PROPAGATION DE L’EXCEPTION
4 {
5 double racine = RacineCarree ( - 5 ) ;
6 }
7 catch ( Exception ex )
8 {
9 Console . WriteLine ( " Impossible d ' effectuer le calcul : "
+ ex . Message ) ;
10 }
11 }
Ce qui affiche :
Il aurait été possible de lever une exception plus générique avec la classe de base
Exception :
1 throw new Exception ( " Le param è tre doit ê tre positif " ) ;
Mais n’oubliez pas que plus l’exception est finement typée, plus elle sera facile à traiter
précisément. Cela permet d’éviter d’attraper toutes les exceptions dans le même catch
avec la classe de base Exception. À noter que lorsque notre programme rencontre le
mot-clé throw, il s’arrête pour partir dans le bloc try...catch correspondant (s’il
existe). Cela signifie qu’une méthode qui doit renvoyer un paramètre pourra compiler
si son chemin se termine par une levée d’exception, comme c’est le cas pour le calcul
de la racine carrée.
Propagation de l’exception
Il est important de noter que lorsqu’un bout de code se situe dans un bloc try...catch,
tout le code qui est dessous est surveillé, même s’il y a plusieurs méthodes qui s’ap-
pellent les unes les autres. Par exemple, si nous appelons depuis la méthode Main()
une Methode1(), qui appelle une Methode2(), qui appelle une Methode3(), qui lève
une exception, alors nous serons capables de l’intercepter depuis la méthode Main()
avec un try...catch :
1 static void Main ( string [] args )
2 {
3 try
4 {
5 Methode1 () ;
6 }
7 catch ( N o t I m p l e m e n t e d E x c e p t i o n )
8 {
9 Console . WriteLine ( " On intercepte l ' exception de la m é
thode 3 " ) ;
343
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
10 }
11 }
12
13 public static void Methode1 ()
14 {
15 Methode2 () ;
16 }
17
18 public static void Methode2 ()
19 {
20 Methode3 () ;
21 }
22
23 public static void Methode3 ()
24 {
25 throw new N o t I m p l e m e n t e d E x c e p t i o n () ;
26 }
344
CRÉER UNE EXCEPTION PERSONNALISÉE
24 {
25 Console . WriteLine ( " La saisie n ' est pas un entier " ) ;
26 }
27 catch ( Exception ex )
28 {
29 E n r e g i s t r e r E r r e u r D a n s U n F i c h i e r D e L o g ( ex ) ;
30 throw ;
31 }
32 }
J’ai une saisie à faire et à convertir en entier. Si la conversion échoue, je suis capable
de l’indiquer à l’utilisateur en surveillant la FormatException. Par contre, si une ex-
ception inattendue se produit, je souhaite pouvoir faire quelque chose, en l’occurrence
enregistrer l’exception dans un fichier ; mais comme c’est un cas limite non prévu je
souhaite que l’exception continue à se propager afin qu’elle soit attrapée par le bloc
try...catch qui permettra d’envoyer un mail à l’administrateur. Dans ce cas, j’utilise
un catch généraliste pour traiter les exceptions inattendues afin de loguer l’exception,
puis j’utilise directement le mot-clé throw afin de permettre de relever l’exception.
345
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
8 {
9 Console . WriteLine ( " Erreur : " + ex . Message ) ;
10 }
11 }
12
13 public static Produit ChargerProduit ( string nomProduit )
14 {
15 Produit produit = new Produit () ; // à remplacer par le
chargement du produit
16 if ( produit . Stock <= 0 )
17 throw new P r o d u i t N o n E n S t o c k E x c e p t i o n () ;
18 return produit ;
19 }
Il serait intéressant de pouvoir rendre l’exception plus explicite en modifiant par exemple
la propriété message de l’exception. Pour ce faire, il suffit d’utiliser la surcharge du
constructeur prenant une chaîne de caractères en paramètre afin de pouvoir mettre à
jour la propriété Message (qui est en lecture seule) :
1 public class P r o d u i t N o n E n S t o c k E x c e p t i o n : Exception
2 {
3 public P r o d u i t N o n E n S t o c k E x c e p t i o n () : base ( " Le produit n '
est pas en stock " )
4 {
5 }
6 }
Nous pouvons également créer un constructeur qui prend le nom du produit en para-
mètre afin de rendre le message encore plus précis :
1 public class P r o d u i t N o n E n S t o c k E x c e p t i o n : Exception
2 {
3 public P r o d u i t N o n E n S t o c k E x c e p t i o n ( string nomProduit ) : base
( " Le produit " + nomProduit + " n ' est pas en stock " )
4 {
5 }
6 }
346
LE MOT-CLÉ FINALLY
À noter que pour construire cette exception personnalisée, nous avons dérivé de la
classe de base Exception. Il aurait été également possible de dériver de la classe
ApplicationException pour conserver une hiérarchie cohérente d’exceptions.
Le mot-clé finally
Nous avons vu que lorsqu’un code est surveillé dans un bloc try...catch, on sortait
du bloc try si jamais une exception était levée pour atterrir dans le bloc catch. Cela
veut dire qu’il est impossible de garantir qu’une instruction sera exécutée dans notre
code, si jamais une exception nous fait sortir du bloc. C’est là qu’intervient le mot-clé
finally. Il permet d’indiquer que dans tous les cas, un code doit être exécuté, qu’une
exception intervienne ou pas. Par exemple :
1 try
2 {
3 string saisie = Console . ReadLine () ;
4 int valeur = Convert . ToInt32 ( saisie ) ;
5 Console . WriteLine ( " Vous avez saisi un entier " ) ;
6 }
7 catch ( FormatException )
8 {
9 Console . WriteLine ( " Vous avez saisi autre chose qu ' un entier
");
10 }
11 finally
12 {
13 Console . WriteLine ( " Merci d ' avoir saisi quelque chose " ) ;
14 }
Nous demandons une saisie utilisateur. Nous tentons de convertir cette saisie en entier.
Si la conversion fonctionne, nous restons dans le bloc try :
111
Merci d ’ avoir saisi quelque chose
Si la conversion lève une FormatException, le texte affiché dans la console sera diffé-
rent :
abc
Vous avez saisi autre chose qu ’ un entier
Merci d ’ avoir saisi quelque chose
Dans les deux cas, nous passons obligatoirement dans le bloc finally pour afficher le
remerciement.
Le bloc finally est utile par exemple quand il s’agit de libérer la mémoire, d’enregistrer
des données, d’écrire dans un fichier de log, etc.
347
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
Il est important de remarquer que quelle que soit la construction, le bloc finally est
toujours exécuté. Ainsi, même si on relève une exception dans le bloc catch :
1 try
2 {
3 Convert . ToInt32 ( " ppp " ) ;
4 }
5 catch ( FormatException )
6 {
7 throw new N o t I m p l e m e n t e d E x c e p t i o n () ;
8 }
9 finally
10 {
11 Console . WriteLine ( " Je suis quand m ê me pass é ici " ) ;
12 }
348
LE MOT-CLÉ FINALLY
En résumé
– Les exceptions permettent de gérer les cas limites d’une méthode.
– On utilise le bloc try...catch pour encapsuler un bout de code à surveiller.
– Il est possible de créer des exceptions personnalisées en dérivant de la classe de base
Exception.
– On peut lever à tout moment une exception grâce au mot-clé throw.
– Les exceptions ne doivent pas servir à masquer les bugs.
349
CHAPITRE 31. GÉRER LES ERREURS : LES EXCEPTIONS
350
Chapitre 32
TP : événements et météo
Difficulté :
B
ienvenue dans le dernier TP de cette partie. Tenez bon, après on change de domaine
pour aborder d’autres notions. Dans ce TP, nous allons pratiquer les événements.
Le but est de savoir en créer un et de pouvoir s’y abonner pour être notifié d’une
information.
Vous êtes prêts ? Alors c’est parti !
351
CHAPITRE 32. TP : ÉVÉNEMENTS ET MÉTÉO
Correction
Allez, c’est parti pour la correction. Tout d’abord, nous devons créer notre simulateur
météo. Il sera représenté par une classe :
1 public class SimulateurMeteo
2 {
3 }
Nous aurons également besoin de quelque chose pour représenter le temps : soleil,
nuage, pluie et orage. Une énumération semble appropriée :
1 public enum Temps
2 {
3 Soleil ,
4 Nuage ,
5 Pluie ,
6 Orage
7 }
352
CORRECTION
Étant donné que nous allons déterminer plusieurs nombres aléatoires, il est pertinent de
ne pas réallouer à chaque fois le générateur de nombre aléatoire. C’est pour cela qu’on le
crée une unique fois dans le constructeur de la classe. Créons maintenant une méthode
permettant de démarrer le simulateur et codons les règles métier du simulateur :
1 public class SimulateurMeteo
2 {
3 // [...] Code supprim é pour plus de clart é [...]
4
5 public void Demarrer ()
6 {
7 for ( int i = 0 ; i < n o mb r e De R ep e ti t i on s ; i ++)
8 {
9 int valeur = random . Next (0 , 100 ) ;
10 if ( valeur < 5 )
11 GererTemps ( Temps . Soleil ) ;
12 else
13 {
14 if ( valeur < 50 )
15 GererTemps ( Temps . Nuage ) ;
16 else
17 {
18 if ( valeur < 90 )
19 GererTemps ( Temps . Pluie ) ;
20 else
21 GererTemps ( Temps . Orage ) ;
22 }
23 }
24 }
25 }
26 }
C’est très simple, on boucle sur le nombre de répétitions. Un nombre aléatoire est
déterminé à chaque itération. La méthode GererTemps prend en paramètre le temps
déterminé à partir du nombre aléatoire. C’est cette méthode GererTemps qui aura pour
but de lever un événement quand le temps change :
353
CHAPITRE 32. TP : ÉVÉNEMENTS ET MÉTÉO
Ici, j’ai choisi de créer un seul événement quand le temps change et de lui indiquer le
temps qu’il fait en paramètre. Nous avons donc besoin d’un délégué qui prend un Temps
en paramètre et qui ne renvoie rien - c’est d’ailleurs souvent le cas des événements. Puis
nous avons besoin d’un événement du type du délégué. Ensuite, si le temps a changé
et que quelqu’un s’est abonné à l’événement, alors nous levons l’événement.
Il ne reste plus qu’à remplir notre classe Statisticien. Cette classe a besoin de tra-
vailler sur une instance de la classe SimulateurMeteo ; nous pouvons donc lui en pas-
ser une dans les paramètres du constructeur. Nous utiliserons également des variables
membres privées permettant de stocker ses analyses :
1 public class Statisticien
2 {
3 private SimulateurMeteo simulateurMeteo ;
4 private int n o m b r e D e F o i s O u L e T e m p s A C h a n g e ;
5 private int n o m b r e D e F o i s O u I l A F a i t S o l e i l ;
6
7 public Statisticien ( SimulateurMeteo simulateur )
8 {
9 simulateurMeteo = simulateur ;
10 nombreDeFoisOuLeTempsAChange = 0;
11 nombreDeFoisOuIlAFaitSoleil = 0;
12 }
13
14 public void DemarrerAnalyse ()
15 {
16 simulateurMeteo . Q ua ndL eT em psC ha ng e +=
simulateurMeteo_QuandLeTempsChange ;
17 simulateurMeteo . Demarrer () ;
18 }
19
20 public void AfficherRapport ()
21 {
22 Console . WriteLine ( " Nombre de fois o ù le temps a chang é
: " + nombreDeFoisOuLeTempsAChange );
354
CORRECTION
1 class Program
2 {
3 static void Main ( string [] args )
4 {
5 SimulateurMeteo simulateurMeteo = new SimulateurMeteo (
1000 ) ;
6 Statisticien statisticien = new Statisticien (
simulateurMeteo ) ;
7 statisticien . DemarrerAnalyse () ;
8 statisticien . AfficherRapport () ;
9 }
10 }
Ici, je travaille sur 1000 répétitions. Voici le résultat que j’obtiens à l’exécution de
l’application :
Évidemment, vu que nous travaillons avec des nombres aléatoires, chacun aura un
résultat différent.
Et voilà, c’est terminé pour ce TP. Notre application est fonctionnelle !
Terminé ? hmm. . . pas tout à fait, allons un peu plus loin.
355
CHAPITRE 32. TP : ÉVÉNEMENTS ET MÉTÉO
Ce qui affichera :
Nombre de fois o ù le temps a chang é : 635
Nombre de fois o ù il a fait soleil : 47
Pourcentage de beau temps : 7 %
Nombre de fois o ù le temps a chang é : 1917
Nombre de fois o ù il a fait soleil : 163
Pourcentage de beau temps : 8 %
Nombre de fois o ù le temps a chang é : 3879
Nombre de fois o ù il a fait soleil : 322
Pourcentage de beau temps : 8 %
Les valeurs augmentent. . . alors qu’elles ne devraient pas. Eh oui, nous ne réinitialisons
pas les entiers permettant de stocker les statistiques. Ce n’est pas forcément un bug.
Ici, comme je n’avais rien dit dans l’énoncé, il peut paraître pertinent de continuer à
incrémenter ces valeurs, comme ça, je peux travailler sur une moyenne. Bon, disons
que nous souhaitons les réinitialiser à chaque fois ; il suffit alors de remettre à zéro les
compteurs dans la méthode :
1 public void DemarrerAnalyse ()
2 {
3 nombreDeFoisOuLeTempsAChange = 0;
4 nombreDeFoisOuIlAFaitSoleil = 0;
5 simulateurMeteo . Q ua ndL eT em psC ha ng e +=
simulateurMeteo_QuandLeTempsChange ;
6 simulateurMeteo . Demarrer () ;
7 }
Cependant, les compteurs augmentent toujours, moins vite, mais quand même ! Je suis
sûr que vous l’aviez deviné et que vous n’avez pas fait l’erreur. En fait, cela vient
356
ALLER PLUS LOIN
de l’abonnement à l’événement. Chaque fois que nous démarrons l’analyse, nous nous
réabonnons à l’événement. Comme l’événement est multidiffusion, nous rajoutons en
fait à chaque fois un appel à la méthode avec le +=. Ce qui veut dire qu’à la deuxième
fois, nous appellerons la méthode deux fois, ce qui fera doubler les résultats. À la
troisième fois, ils triplent. . . Nous avons donc une erreur. Soit il faut s’abonner une
seule fois à l’événement, soit il faut se désabonner à la fin de l’analyse, quand ce
n’est plus utile de recevoir l’événement. C’est cette dernière méthode que je vais vous
montrer. Il suffit de faire :
1 public void DemarrerAnalyse ()
2 {
3 nombreDeFoisOuLeTempsAChange = 0;
4 nombreDeFoisOuIlAFaitSoleil = 0;
5 simulateurMeteo . Q ua ndL eT emp sC ha nge +=
simulateurMeteo_QuandLeTempsChange ;
6 simulateurMeteo . Demarrer () ;
7 simulateurMeteo . Q ua ndL eT emp sC ha nge -=
simulateurMeteo_QuandLeTempsChange ;
8 }
Cela permet d’éviter d’encombrer la mémoire qui ne saurait pas forcément se libérer
toute seule. Je n’en dis pas plus car ceci est un concept avancé de gestion de mémoire.
Gardez seulement à l’esprit que si on a l’opportunité de se désabonner d’un événement,
il faut le faire.
Enfin, nous pouvons simplifier notre code en ne créant pas notre délégué. Effectivement,
dans la mesure où celui-ci possède un seul paramètre, il est possible de le remplacer par
le délégué Action<T>. Il faut juste supprimer la déclaration du délégué et remplacer la
déclaration de l’événement par :
1 public class SimulateurMeteo
2 {
3 public event Action < Temps > Qu an dLe Te mp sCh an ge ;
4 }
Vous pouvez télécharger tous les codes sources de cet exercice grâce au code web sui-
vant :
☛ ✟
Copier ce code
⊲
✡Code web : 179137 ✠
357
CHAPITRE 32. TP : ÉVÉNEMENTS ET MÉTÉO
358
Quatrième partie
C# avancé
359
Chapitre 33
Créer un projet bibliothèques de classes
Difficulté :
P
our l’instant, nous n’avons créé que des projets de type application console. Il existe
plein d’autres modèles de projet. Un des plus utiles est le projet permettant de créer des
bibliothèques de classes. Il s’agit d’un projet qui va permettre de contenir des classes
que nous pourrons utiliser dans des applications. Exactement comme la bibliothèque de
classes du framework .NET qui nous permet d’utiliser la classe Math ou les exceptions,
ou plein d’autres choses. . . Ce type de projet permet donc de créer une assembly sous la
forme d’un fichier avec l’extension .dll. Ces assemblys sont donc des bibliothèques qui
seront utilisables par n’importe quelle application utilisant un langage compatible avec le
framework .NET.
361
CHAPITRE 33. CRÉER UN PROJET BIBLIOTHÈQUES DE CLASSES
Réutilisabilité :
Architecture :
362
CRÉER UN PROJET DE BIBLIOTHÈQUE DE CLASSE
une autre qui gère les règles métier et une autre qui s’occupe d’afficher le tout. Je
m’arrête là sur l’architecture, vous aurez l’occasion de vous y confronter bien assez
tôt !
Si nous faisons cela, Visual C# Express va nous créer une nouvelle solution contenant
le projet de bibliothèques de classes. C’est possible sauf qu’en général, on ajoute une
bibliothèque de classes pour qu’elle serve dans le cadre d’une application. Cela veut dire
que nous pouvons ajouter ce nouveau projet à notre solution actuelle. Ainsi, celle-ci
contiendra deux projets :
– L’application console, qui sera exécutable par le système d’exploitation.
– La bibliothèque de classes, qui sera utilisée par l’application.
Pour ce faire, on peut faire un clic droit sur notre solution et faire Ajouter > Nouveau
Projet, comme indiqué à la figure 33.2.
Ici, on choisit le projet de type bibliothèque de classes, sans oublier de lui donner un
nom, par exemple MaBibliotheque (voir figure 33.3). Visual C# Express nous crée
notre projet et l’ajoute à la solution, comme on peut le voir dans l’explorateur de
solutions (voir figure 33.4).
363
CHAPITRE 33. CRÉER UN PROJET BIBLIOTHÈQUES DE CLASSES
Figure 33.4 – L’explorateur de solution affiche les deux projets contenus dans la
solution
364
PROPRIÉTÉS DE LA BIBLIOTHÈQUE DE CLASSE
Figure 33.5 – Le nom de l’assembly et l’espace de noms par défaut dans les propriétés
du projet
365
CHAPITRE 33. CRÉER UN PROJET BIBLIOTHÈQUES DE CLASSES
que le projet possède un nom d’assembly, qui correspond ici au nom du projet ainsi
qu’un espace de noms par défaut, qui correspond également au nom du projet. Le nom
de l’assembly servira à identifier l’assembly, alors que l’espace de noms par défaut sera
celui donné lorsque nous ajouterons une nouvelle classe (ou autre) à notre projet. Cet
espace de noms pourra être changé, mais en général on garde un nom cohérent avec les
arborescences de notre projet.
Notons au passage qu’il y a une option permettant d’indiquer la version du framework
.NET que nous souhaitons utiliser. Ici, nous gardons la version 4.
366
GÉNÉRER ET UTILISER UNE BIBLIOTHÈQUE DE CLASSE
367
CHAPITRE 33. CRÉER UN PROJET BIBLIOTHÈQUES DE CLASSES
Vous pouvez aussi, bien sûr, inclure l’espace de noms MaBibliotheque pour éviter
d’avoir à préfixer la classe. En général, l’espace de noms d’une bibliothèque est différent
de celui de l’application. Notez que pour qu’une classe, comme la classe Bonjour, puisse
être utilisée par une application référençant son assembly, elle doit être déclarée en
public afin qu’elle soit visible par tout le monde.
Le mot-clé internal
Nous avons déjà vu ce mot-clé lorsque nous avons parlé de visibilité avec notamment
les autres mots-clés public, private et protected.
C’est avec les assemblys que le mot-clé internal prend tout son sens. Il permet d’in-
diquer qu’une classe, méthode ou propriété ne sera accessible qu’à l’intérieur d’une
assembly. Cela permet par exemple qu’une classe soit utilisable par toutes les autres
classes de cette assembly mais qu’elle ne puisse pas être utilisée par une application
référençant l’assembly.
Créons par exemple dans notre bibliothèque de classes les trois classes suivantes :
1 public class Client
2 {
3 private string login ;
4 public string Login
5 {
6 get { return login ; }
7 }
8
9 private string motDePasse ;
10 public string MotDePasse
11 {
12 get { return motDePasse . Crypte () ; }
13 }
14
15 public Client ( string loginClient , string motDePasseClient )
16 {
17 login = loginClient ;
18 motDePasse = motDePasseClient ;
19 }
20 }
21
22 public static class Generateur
23 {
24 public static string O b t e n i r I d e n t i f i a n t U n i q u e ()
25 {
26 Random r = new Random () ;
27 string chaine = string . Empty ;
28 for ( int i = 0 ; i < 10 ; i ++)
29 {
30 chaine += r . Next (0 , 100 ) ;
368
LE MOT-CLÉ INTERNAL
31 }
32 return chaine . Crypte () ;
33 }
34 }
35
36 internal static class Encodage
37 {
38 internal static string Crypte ( this string chaine )
39 {
40 return Convert . ToBase64String ( Encoding . Default . GetBytes
( chaine ) ) ;
41 }
42
43 internal static string Decrypte ( this string chaine )
44 {
45 return Encoding . Default . GetString ( Convert .
FromBase64String ( chaine ) ) ;
46 }
47 }
Par contre, la classe Encodage n’est accessible que pour les deux autres classes, car
elles sont dans la même assembly. Cela veut dire que si nous tentons de l’utiliser depuis
notre méthode Main(), nous aurons des erreurs de compilation :
1 static void Main ( string [] args )
2 {
3 string chaine = " 12345 " . Crypte () ;
4
5 Encodage . Crypte ( " 12345 " ) ;
6 }
369
CHAPITRE 33. CRÉER UN PROJET BIBLIOTHÈQUES DE CLASSES
Cet exemple permet d’illustrer l’intérêt, qui n’est pas toujours évident, du mot-clé
internal.
À noter qu’il existe enfin le mot-clé protected internal qui permet d’indiquer que des
éléments sont accessibles à un niveau internal pour les classes d’une même assembly
mais protected pour les autres assemblys, ce qui permet d’appliquer les principes de
substitutions ou d’héritage.
Voilà, vous avez vu comment créer une bibliothèque de classes ! N’hésitez pas à créer ce
genre de projet afin d’y mettre toutes les classes qui peuvent être utilisées par plusieurs
applications. Comme ça, il suffit d’une simple référence pour accéder au code qui y est
contenu. Vous vous en servirez également pour mieux architecturer vos applications, le
code s’en trouvera plus clair et plus maintenable.
En résumé
– Un projet de bibliothèque de classes permet de regrouper des classes pouvant être
utilisées entre plusieurs applications.
– Les bibliothèques de classes génèrent des assemblys avec l’extension .dll.
– Elles permettent également de mieux architecturer un projet.
370
Chapitre 34
Plus loin avec le C# et .NET
Difficulté :
M
aintenant que nous en savons plus, nous allons pouvoir aborder quelques notions qui
me paraissent importantes et que nous n’avons pas encore traitées. Ce sera l’occasion
d’approfondir nos connaissances et de comprendre un peu mieux certains points qui
auraient pu vous paraître douteux.
Nous allons voir des instructions C# supplémentaires qui vont nous permettre de com-
pléter nos connaissances en POO. Nous en profiterons également pour détailler comment
le framework .NET gère les types en mémoire. Vous en saurez plus sur le formatage des
chaînes et aurez un aperçu du côté obscur du framework .NET, la réflexion !
Bref, tout plein de nouvelles cordes à nos arcs nous permettant d’être de plus en plus
efficace avec le C#. Ces nouveaux concepts vous serviront sans doute moins souvent, mais
sont importants à connaître.
371
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
C’est quoi ce mystère ? Nous, quand nous créons une classe, n’importe qui
peut en hériter. Pourquoi pas la classe String ?
C’est parce que je vous avais caché le mot-clé sealed. Il permet d’empêcher la classe
d’être héritée. Tout simplement !
Pourquoi vouloir empêcher d’étendre une classe en la dérivant ? Pour plusieurs raisons,
mais généralement nous allons recourir à ce mot-clé lorsqu’une classe est trop spécifique
à une méthode ou à une bibliothèque et que l’on souhaite empêcher quelqu’un de
pouvoir obtenir du code instable en étendant son fonctionnement. C’est typiquement
le cas pour la classe String. Il y a beaucoup de classes dans le framework .NET qui
sont marquées comme sealed afin d’éviter que l’on puisse faire n’importe quoi.
Il faut par contre faire attention car l’emploi de ce mot-clé restreint énormément la
façon dont on pourrait employer cette classe. Pour déclarer une classe sealed 1 il suffit
de faire précéder le mot-clé class du mot-clé sealed :
1 public sealed class C l a s s e I m p o s s i b l e A D e r i v e r
2 {
3 }
Le mot-clé sealed fonctionne également pour les méthodes, dans ce cas, cela permet
d’empêcher la substitution d’une méthode. Reprenons notre exemple du chien muet :
1 public class Chien
2 {
3 public virtual void Aboyer ()
4 {
5 Console . WriteLine ( " Wouf " ) ;
6 }
7 }
372
PRÉCISIONS SUR LES TYPES ET GESTION MÉMOIRE
8
9 public class ChienMuet : Chien
10 {
11 public sealed override void Aboyer ()
12 {
13 Console . WriteLine ( " ... " ) ;
14 }
15 }
Ici, nous marquons la méthode comme sealed, ce qui fait qu’une classe qui dérive de
ChienMuet ne sera pas capable de remplacer la méthode permettant d’aboyer. Le code
suivant :
1 public class C h i e n A v e c S y n t h e s e V o c a l e : ChienMuet
2 {
3 public override void Aboyer ()
4 {
5 Console . WriteLine ( " Bwarf " ) ;
6 }
7 }
’ M a P r e m i e r e A p p l i c a t i o n . C h i e n A v e c S y n t h e s e V o c a l e . Aboyer () ’ : ne
peut pas se substituer à un membre h é rit é ’
M a P r e m i e r e A p p l i c a t i o n . ChienMuet . Aboyer () ’, car il est sealed
373
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Voici ce qu’il se passe : la variable age est une adresse mémoire contenant la valeur 10.
C’est la même chose pour la variable pourcentage sauf que l’emplacement mémoire
est plus important car on peut stocker plus de choses dans un double que dans un int.
Je vous renvoie à la figure 34.2 pour une explication en images ! En ce qui concerne
les types référence, lorsque nous instancions par exemple une classe, le C# instancie
la variable maVoiture dans la pile et lui met dedans une référence vers le vrai objet
qui lui est alloué sur une zone mémoire de capacité plus importante, mais à accès plus
lent, que l’on appelle « le tas ». Comme il est géré par le framework .NET, on l’appelle
« le tas managé ».
374
PRÉCISIONS SUR LES TYPES ET GESTION MÉMOIRE
Peugeot " };
nous aurons en mémoire le processus que vous pouvez observer à la figure 34.3.
Figure 34.3 – Les types référence ont leur référence allouée sur la pile et leur objet
alloué sur le tas managé
À noter que lorsqu’une variable sort de sa portée et qu’elle n’est plus utilisable par
qui que ce soit, alors la case mémoire sur la pile est marquée comme de nouveau libre.
Voilà grosso modo comment est gérée la mémoire.
Il manque encore un élément fondamental du mécanisme de gestion mémoire : le
ramasse-miettes. Nous en avons déjà parlé brièvement, le ramasse-miettes sert à
libérer la mémoire qui n’est plus utilisée dans le tas managé. Il y aurait de quoi écrire
un gros tutoriel rien que sur son fonctionnement, aussi nous allons expliquer rapidement
comment il fonctionne sans trop rentrer dans les détails.
Pourquoi libérer la mémoire ? On a vu que quand une variable sort de portée, alors la
case mémoire sur la pile est marquée comme à nouveau libre. C’est très bien avec les
types valeur qui sont uniquement sur la pile. Mais avec les types référence ? Que donne
le code suivant lorsque nous quittons la méthode CreerVoiture ?
1 public class Voiture
2 {
3 public int Vitesse { get ; set ; }
4 public string Marque { get ; set ; }
5 }
6
7 static void Main ( string [] args )
8 {
9 CreerVoiture () ;
10 }
11
12 public static void CreerVoiture ()
13 {
375
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Nous avons dit que la mémoire sur la pile redevenait libre. La référence vers l’objet est
donc cassée. Cependant, la mémoire sur le tas est toujours occupée (voir figure 34.4).
Figure 34.4 – L’objet est toujours présent sur le tas, mais n’est plus référencé
Cela veut dire que notre objet est toujours en mémoire sauf que la variable maVoiture
n’existe plus et donc, ne référence plus cet objet. Dans un langage comme le C++,
nous aurions été obligés de vider explicitement la mémoire référencée par la variable.
En C#, pas besoin ! C’est le ramasse-miettes qui le fait pour nous. Encore un truc de
fainéant ça !
En fait, c’est plutôt une sécurité qu’un truc de fainéant. C’est la garantie que la mémoire
utilisée par les objets qui n’existent plus est vidée et, du coup, redevient disponible
pour de nouveaux objets. Grâce au ramasse-miettes, nous évitons ce que l’on appelle
les fuites mémoire.
La réponse est simple : on ne sait pas quand il passe. En fait, il passe quand il a besoin de
mémoire ou quand l’application semble ne rien faire. Évidemment, lorsque le ramasse-
miettes passe, les performances de l’application sont un petit peu pénalisées. C’est
pour cela que l’algorithme de passage du ramasse-miettes est optimisé pour essayer
de déranger le moins possible l’application, lors des moments d’inactivité par exemple.
Par contre, quand il y a besoin de mémoire, il ne se pose pas la question : il passe
quand même.
Il y aurait beaucoup de choses à dire encore sur ce mécanisme mais arrêtons-nous
là. Retenons que le ramasse-miettes est un superbe outil qui nous permet de garantir
l’intégrité de notre mémoire et qu’il s’efforce de nous embêter le moins possible. Il est
important de libérer les ressources dont on n’a plus besoin quand c’est possible, par
exemple en se désabonnant d’un événement ou lorsque nous avons utilisé des ressources
natives (ce que nous ne verrons pas dans ce livre) ou lors d’accès à des fichiers ou des
bases de données.
376
MASQUER UNE MÉTHODE
Nous aurons :
Wouaf !
Wiiff
Wiiff
En effet, le premier objet est un chien et les deux suivants sont des caniches. Grâce à
la substitution, nous faisons aboyer notre chien, notre caniche et notre caniche qui se
fait manipuler comme un chien.
Il est possible de masquer des méthodes qui ne sont pas forcément marquées comme
virtuelles grâce au mot-clé new. À ce moment-là, la méthode ne se substitue pas à la
méthode dérivée mais la masque pour les types dérivés. Voyons cet exemple :
1 public class Chien
377
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
2 {
3 public void Aboyer ()
4 {
5 Console . WriteLine ( " Wouaf ! " ) ;
6 }
7 }
8
9 public class Caniche : Chien
10 {
11 public new void Aboyer ()
12 {
13 Console . WriteLine ( " Wiiff " ) ;
14 }
15 }
Remarquons que la méthode Aboyer() de la classe Chien n’est pas marquée virtual
et que la méthode Aboyer de la classe Caniche est marquée avec le mot-clé new. Il
ne s’agit pas ici de substitution. Il s’agit juste de deux méthodes qui portent le même
nom, mais la méthode Aboyer() de la classe Caniche se charge de masquer la méthode
Aboyer() de la classe mère. Ce qui fait que les précédentes instanciations :
1 static void Main ( string [] args )
2 {
3 Chien chien = new Chien () ;
4 Caniche caniche = new Caniche () ;
5 Chien c a n i c h e T r a i t e C o m m e U n C h i e n = new Caniche () ;
6
7 chien . Aboyer () ;
8 caniche . Aboyer () ;
9 c a n i c h e T r a i t e C o m m e U n C h i e n . Aboyer () ;
10 }
Wouaf !
Wiiff
Wouaf !
C’est le dernier cas qui peut surprendre. Rappelez-vous, il ne s’agit pas d’un rempla-
cement de méthode cette fois-ci, et donc le fait de manipuler le caniche en tant que
Chien ne permet pas de remplacer le comportement de la méthode Aboyer(). Dans ce
cas, on appelle bien la méthode Aboyer correspondant au type Chien, ce qui a pour
effet d’afficher « Wouaf ! ». Si nous n’avions pas utilisé le mot-clé new, nous aurions
eu le même comportement mais le compilateur nous aurait avertis de ceci grâce à un
avertissement :
378
LE MOT-CLÉ YIELD
Il nous précise que nous masquons effectivement la méthode de la classe mère et que
si c’est fait exprès, alors il faut l’indiquer grâce au mot-clé new afin de lui montrer que
nous savons ce que nous faisons. Notez quand même que dans la majorité des cas, en
POO, ce que vous voudrez faire, c’est bien substituer la méthode et non la masquer.
C’est le même principe pour les variables, il est également possible de masquer une
variable, quoiqu’en général, cela reflète plutôt une erreur dans le code. Si nous faisons :
1 public class Chien
2 {
3 protected int age ;
4 }
5
6 public class Caniche : Chien
7 {
8 private int age ;
9
10 public Caniche ()
11 {
12 age = 0 ;
13 }
14
15 public override string ToString ()
16 {
17 return "J ' ai " + age + " ans " ;
18 }
19 }
Alors le compilateur nous indique que la variable age de la classe Caniche masque
la variable age initialement héritée de la classe Chien. Si c’est intentionnel, il nous
propose de la marquer avec le mot clé new. Sinon, cela nous permet de constater que
cette variable est peut-être inutile. . . (sûrement d’ailleurs !)
Le mot-clé yield
Nous avons vu dans le TP sur les génériques comment créer un énumérateur. C’est une
tâche un peu lourde qui nécessite pas mal de code. Oublions un instant que la classe
String est énumérable et créons pour l’exemple une classe qui permet d’énumérer une
chaîne de caractères. Nous aurions pu faire quelque chose comme ça :
1 public class ChaineEnumerable : IEnumerable < char >
2 {
3 private string chaine ;
4 public ChaineEnumerable ( string valeur )
5 {
6 chaine = valeur ;
7 }
8
9 public IEnumerator < char > GetEnumerator ()
379
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
10 {
11 return new C haineEnum erateur ( chaine ) ;
12 }
13
14 IEnumerator IEnumerable . GetEnumerator ()
15 {
16 return new C haineEnum erateur ( chaine ) ;
17 }
18 }
19
20 public class Ch aineEnume rateur : IEnumerator < char >
21 {
22 private string chaine ;
23 private int indice ;
24
25 public ChaineE numerateu r ( string valeur )
26 {
27 indice = -1 ;
28 chaine = valeur ;
29 }
30
31 public char Current
32 {
33 get { return chaine [ indice ]; }
34 }
35
36 public void Dispose ()
37 {
38 }
39
40 object IEnumerator . Current
41 {
42 get { return Current ; }
43 }
44
45 public bool MoveNext ()
46 {
47 indice ++;
48 return indice < chaine . Length ;
49 }
50
51 public void Reset ()
52 {
53 indice = -1 ;
54 }
55 }
380
LE MOT-CLÉ YIELD
Nous avons une classe ChaineEnumerable qui encapsule une chaîne de caractères de
type string et une classe ChaineEnumerateur qui permet de parcourir une chaîne et
de renvoyer à chaque itération un caractère, représenté par le type char 2 .
Rien de bien sorcier, mais un code plutôt lourd.
Regardons à présent le code suivant :
1 public class ChaineEnumerable : IEnumerable < char >
2 {
3 private string chaine ;
4 public ChaineEnumerable ( string valeur )
5 {
6 chaine = valeur ;
7 }
8
9 public IEnumerator < char > GetEnumerator ()
10 {
11 for ( int i = 0 ; i < chaine . Length ; i ++)
12 {
13 yield return chaine [ i ];
14 }
15 }
16
17 IEnumerator IEnumerable . GetEnumerator ()
18 {
19 return GetEnumerator () ;
20 }
21 }
Il fait la même chose, mais d’une manière bien simplifiée. Nous remarquons l’apparition
du mot-clé yield. Il permet de créer facilement des énumérateurs. Combiné au mot-clé
return, il renvoie un élément d’une collection et passe à l’élément suivant. Il peut être
combiné au mot-clé break également pour permettre d’arrêter l’énumération. Ce qui
veut dire que nous aurions pu écrire la méthode GetEnumerator() de cette façon :
1 public IEnumerator < char > GetEnumerator ()
2 {
3 int i = 0 ;
4 while ( true )
5 {
6 if ( i == chaine . Length )
7 yield break ;
8 yield return chaine [ i ];
9 i ++;
10 }
11 }
Ce qui est plus laid, je le conçois ! mais qui permet d’illustrer le mot-clé yield break.
381
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Figure 34.5 – Le programme est en pause sur le point d’arrêt avant l’énumération
✄
Appuyez sur ✂F11 ✁plusieurs fois pour rentrer dans la méthode ObtenirListeDePrenoms,
nous voyons l’indicateur se déplacer sur les différentes parties de l’instruction foreach.
Puis nous arrivons sur la première instruction yield return (voir la figure 34.6).
✄
Appuyons à nouveau sur ✂F11 ✁et nous pouvons constater que nous sortons de la mé-
thode et que nous rentrons à nouveau dans le✄ corps de la boucle foreach qui va nous
afficher le premier prénom. Continuons les ✂F11 ✁ jusqu’à repasser sur le mot-clé in
et rentrer à nouveau dans la méthode ObtenirListeDePrenoms() et nous pouvons
constater que nous retournons directement au deuxième mot-clé yield, ainsi que vous
pouvez le voir à la figure 34.7.
Et ainsi jusqu’à ce qu’il n’y ait plus rien dans la méthode ObtenirListeDePrenoms()
et que, du coup, le foreach se termine. À la première lecture, ce n’est pas vraiment ce
comportement que nous aurions attendu. . .
382
LE MOT-CLÉ YIELD
383
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Notez qu’un yield break nous aurait fait sortir directement de la boucle
foreach.
Il est important de savoir qu’il est possible de formater la chaîne un peu plus sim-
plement et surtout beaucoup plus efficacement. Ceci est possible grâce à la méthode
string.Format. Le code précédent peut par exemple être remplacé par :
1 int age = 30 ;
384
LE FORMATAGE DE CHAÎNES, DE DATES ET LA CULTURE
Ce qui donne :
Vous avez pu remarquer qu’il est possible d’utiliser le même paramètre à plusieurs
endroits. La méthode Console.WriteLine dispose aussi de la capacité de formater des
chaînes de caractères. Ce qui veut dire que le code précédent peut s’écrire :
1 double valeur1 = 10 . 5 ;
2 double valeur2 = 4 . 2 ;
3 Console . WriteLine ( " { 0 } multipli é par { 1 } s ' é crit \"{ 0 } * { 1 }\"
et donne comme r é sultat : { 2 } " , valeur1 , valeur2 , valeur1 *
valeur2 ) ;
385
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
« régions » représentées par un couple de lettres. Par exemple, le français est représenté
par les lettres « fr ». Et même plus précisément, le français de France est représenté
par « fr-FR » alors que celui du Canada est représenté par « fr-CA ». C’est ce que
l’on retrouve dans les paramètres de notre système d’exploitation lorsqu’on va (sous
Windows 7) dans le panneau de configuration, Horloge langue et région, comme
vous pouvez le voir à la figure 34.8.
fr - FR
Ce qui est intéressant, c’est que le formatage des chiffres ou des dates change en fonction
de la culture. Ainsi, si nous utilisons le français de France, pour le code suivant :
1 public static void Main ( string [] args )
2 {
3 Affiche () ;
386
LE FORMATAGE DE CHAÎNES, DE DATES ET LA CULTURE
387
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
4 }
5
6 public static void Affiche ()
7 {
8 Console . WriteLine ( System . Threading . Thread . CurrentThread .
CurrentCulture ) ;
9 decimal dec = 5 . 5M ;
10 double dou = 4 . 8 ;
11 DateTime date = new DateTime ( 2011 , 12 , 25 ) ;
12 Console . WriteLine ( " D é cimal : { 0 } " , dec ) ;
13 Console . WriteLine ( " Double : { 0 } " , dou ) ;
14 Console . WriteLine ( " Date : { 0 } " , date ) ;
15 }
nous aurons :
fr - FR
D é cimal : 5 ,5
Double : 4 ,8
Date : 25/12/2011 00:00:00
Il est possible de changer la culture courante de notre application, si par exemple nous
souhaitons qu’elle soit multiculture, en utilisant le code suivant :
1 System . Threading . Thread . CurrentThread . CurrentCulture = new
CultureInfo ( " en - US " ) ;
Ici par exemple, j’indique à mon application que je souhaite travailler avec la culture
des États-Unis. Ainsi, le code précédent, en changeant juste la culture :
1 public static void Main ( string [] args )
2 {
3 System . Threading . Thread . CurrentThread . CurrentCulture = new
CultureInfo ( " en - US " ) ;
4 Affiche () ;
5 }
6
7 public static void Affiche ()
8 {
9 Console . WriteLine ( System . Threading . Thread . CurrentThread .
CurrentCulture ) ;
10 decimal dec = 5 . 5M ;
11 double dou = 4 . 8 ;
12 DateTime date = new DateTime ( 2011 , 12 , 25 ) ;
13 Console . WriteLine ( " D é cimal : { 0 } " , dec ) ;
14 Console . WriteLine ( " Double : { 0 } " , dou ) ;
15 Console . WriteLine ( " Date : { 0 } " , date ) ;
16 }
388
LE FORMATAGE DE CHAÎNES, DE DATES ET LA CULTURE
en - US
D é cimal : 5.5
Double : 4.8
Date : 12/25/2011 12:00:00 AM
Ici, « dd » sert à afficher le jour, « MM » le mois et « yyyy » l’année sur quatre chiffres,
ce qui donne :
Ce qui donne :
Pour les nombres, il existe différents formatages que l’on peut retrouver dans le tableau
ci-après.
389
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Qui donne :
d é cembre
Voilà pour le formatage des chaînes de caractères. Lister tous les formatages possibles
serait une tâche bien ennuyeuse. Vous aurez l’occasion de rencontrer d’autres forma-
tages qui permettent de faire un peu tout et n’importe quoi. Vous pourrez retrouver la
plupart des formatages à partir de ce code web :
☛ ✟
Formatages
⊲
Code web : 500571
✡ ✠
390
LES ATTRIBUTS
Les attributs
Dans la liste des choses qui vont vous servir, on peut ajouter les attributs. Ils sont
utilisés pour fournir des informations complémentaires sur une méthode, une classe, des
variables, etc. Ils vont nous servir régulièrement dans le framework .NET et nous aurons
même la possibilité de créer nos propres attributs. Un attribut est déclaré entre crochets
avec le nom de sa classe. Il peut se mettre au-dessus d’une classe, d’une méthode, d’une
propriété, etc. Par exemple, dans le code ci-dessous, je positionne l’attribut Obsolete
au-dessus de la déclaration d’une méthode :
1 [ Obsolete ( " Utilisez plut ô t la m é thode ToString () pour avoir une
repr é sentation de l ' objet " ) ]
2 public void Affiche ()
3 {
4 // ...
5 }
Ici, j’utilise un attribut existant du framework .NET, l’attribut Obsolete qui permet
d’indiquer qu’une méthode est obsolète et qu’il vaudrait mieux ne pas l’utiliser. En
général, il est possible de fournir une information permettant de dire pourquoi la mé-
thode est obsolète et par quoi il faut la remplacer. Ainsi, si nous tentons d’utiliser cette
méthode :
1 public class Program
2 {
3 static void Main ( string [] args )
4 {
5 new Program () . Affiche () ;
6 }
7
8 [ Obsolete ( " Utilisez plut ô t la m é thode ToString () pour avoir
une repr é sentation de l ' objet " ) ]
9 public void Affiche ()
10 {
11 // ...
12 }
13 }
Cet attribut est un exemple, il en existe beaucoup dans le framework .NET et vous
aurez l’occasion d’en voir d’autres dans les chapitres suivants. Remarquons que la classe
s’appelle ObsoleteAttribute et peut s’utiliser soit suffixée par Attribute, soit sans,
et dans ce cas, ce suffixe est ajouté automatiquement.
Grâce à l’héritage, nous allons nous aussi pouvoir définir nos propres attributs. Il suffit
pour cela de dériver de la classe de base Attribute. Par exemple, dans sa forme la plus
391
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
simple :
1 public class D e s c r i p t i o n C l a s s e A t t r i b u t e : Attribute
2 {
3 }
Heureusement, il est possible d’indiquer des restrictions sur les attributs en indiquant
par exemple qu’ils ne sont valables que pour les classes. Cela se fait avec. . . un attribut !
1 [ AttributeUsage ( AttributeTargets . Class , AllowMultiple = true ) ]
2 public class D e s c r i p t i o n C l a s s e A t t r i b u t e : Attribute
3 {
4 }
Ici par exemple, j’indique que mon attribut n’est valable que sur les classes et qu’il
est possible de le mettre plusieurs fois sur la classe. Le code précédent où l’attribut
est positionné sur une méthode ne compilera donc plus, avec l’erreur de compilation
suivante :
L ’ attribut ’ DescriptionClasse ’ n ’ est pas valide dans ce type de
d é claration . Il n ’ est valide que dans les d é clarations ’ class
’.
392
LA RÉFLEXION
C’est très bien ces attributs, mais nous ne sommes pas encore capables de les exploiter.
Voyons maintenant comment le faire.
La réflexion
La réflexion en C# ne donne pas mal à la tête, quoique. . . ! C’est une façon de faire
de l’introspection sur nos types de manière à obtenir des informations sur eux, c’est-
à-dire sur les méthodes, les propriétés, etc. La réflexion permet également de charger
dynamiquement des types et de faire de l’introspection sur les assemblys. C’est un
mécanisme assez complexe et plutôt coûteux en terme de temps. Le point de départ
est la classe Type, qui permet de décrire n’importe quel type. Le type d’une classe peut
être obtenu grâce au mot-clé typeof :
1 Type type = typeof ( string ) ;
Cet objet Type nous permet d’obtenir plein d’informations sur nos classes au moment
de l’exécution. Par exemple si le type est une classe, c’est-à-dire ni un type valeur, ni
une interface, alors le code suivant :
1 Type type = typeof ( string ) ;
2 Console . WriteLine ( type . IsClass ) ;
nous renverra true. En effet, la propriété IsClass permet d’indiquer si le type est une
classe. Nous pouvons aussi obtenir le nom de méthodes grâce à GetMethods() :
393
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Pas très exploitable comme résultat, mais bon, c’est pour l’exemple ! Il y a encore
pleins d’infos dans cet objet Type, qui permettent d’obtenir ce que nous voulons. Cela
est possible grâce aux métadonnées qui sont des informations de description des types.
Vous me voyez venir. . . eh oui, nous allons pouvoir obtenir nos attributs. Pour connaître
la liste des attributs d’un type, on peut utiliser la méthode statique
Attribute.GetCustomAttributes(), en lui passant en paramètre le System.Type qui
est hérité de la classe abstraite MemberInfo. Si l’on souhaite récupérer un attribut
particulier, on peut le passer en paramètre de la méthode :
1 Attribute [] lesAttributs = Attribute . G e tC u st o mA t t ri b ute s ( type ,
typeof ( D e s c r i p t i o n C l a s s e A t t r i b u t e ) ) ;
394
LA RÉFLEXION
Nous commençons par instancier un objet animal et un objet chien. Puis nous de-
mandons leurs descriptions. La méthode générique VoirDescription() s’occupe dans
un premier temps d’obtenir le type de la classe grâce à typeof. On s’octroie une vérifi-
cation permettant de nous assurer que le type est bien une classe. Ceci permet d’éviter
d’aller plus loin car de toute façon, notre attribut ne s’applique qu’aux classes. Puis
nous utilisons la méthode GetCustomAttributes pour obtenir les attributs de type
DescriptionClasseAttribute. S’il y en a, alors on les affiche.
Ce qui donne :
395
CHAPITRE 34. PLUS LOIN AVEC LE C# ET .NET
Voilà pour les attributs, vous aurez l’occasion de rencontrer des attributs du framework
.NET un peu plus loin dans ce tutoriel, mais globalement, il est assez rare d’avoir à
créer soi-même ses attributs.
En ce qui concerne la réflexion, il faut bien comprendre que le sujet est beaucoup plus
vaste que ça. Nous avons cependant vu ici un aperçu de ce que permet de faire la
réflexion, en illustrant le mécanisme d’introspection sur les attributs.
En résumé
– Il est possible d’empêcher une classe d’être dérivée grâce au mot-clé sealed.
– Le garbage collector est le mécanisme permettant de libérer la mémoire allouée sur
le tas managé qui n’est plus référencée dans une application.
– Le mot-clé yield permet de créer des énumérateurs facilement et participe au mé-
canisme d’exécution différée.
– Le framework .NET gère les différents formats des chaînes grâce à la culture de
l’application.
– La réflexion est un mécanisme d’introspection sur les types permettant d’obtenir des
informations sur ces derniers lors de l’exécution.
396
Chapitre 35
La configuration d’une application
Difficulté :
D
ans le cycle de vie d’une application, il est fréquent que de petites choses changent.
Imaginons que mon application doive se connecter à un serveur FTP pour sauvegarder
les données sur lesquelles j’ai travaillé. Pendant mes tests, effecutés en local, mon
serveur FTP pourrait avoir l’adresse ftp://localhost avec un login et un mot de passe
« test ». Évidemment, lors de l’utilisation réelle de mon application, l’adresse changera,
et le login et le mot de passe varieront en fonction de l’utilisateur. Il faut être capable de
changer facilement ces paramètres sans avoir à modifier le code ni à recompiler l’application.
C’est là qu’interviennent les fichiers de configuration : ils permettent de stocker toutes ces
petites choses qui servent à faire varier notre application. Pour les clients lourds, comme
nos applications console, il s’agit du fichier app.config : un fichier XML qui contient
la configuration de notre application. Il stocke des chaînes de connexion à une base de
données, une url de service web, des préférences utilisateurs, etc. Bref, tout ce qui va
permettre de configurer notre application !
397
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
La balise <prenom> est ce qu’on appelle une balise ouvrante, cela signifie que ce qui se
trouve après (en l’occurrence la chaîne « Nicolas ») fait partie de cette balise jusqu’à
ce que l’on rencontre la balise fermante </prenom> qui est comme la balise ouvrante à
l’exception du / précédant le nom de la balise. Le XML est un fichier facile à lire par
nous autres humains. On en déduit assez facilement que le fichier contient la chaîne
« Nicolas » et qu’il s’agit sémantiquement d’un prénom. Une balise peut contenir des
attributs permettant de donner des informations sur la donnée. Les attributs sont
entourés de guillemets : "" et font partie de la balise. Par exemple :
1 < client nom = " Nicolas " age = " 30 " > </ client >
Ici, la balise client possède un attribut nom ayant la valeur « Nicolas » et un attribut
age ayant la valeur « 30 ». Encore une fois, c’est très facile à lire pour un humain.
Il est possible que la balise n’ait pas de valeur, comme c’est le cas dans l’exemple ci-
dessus. On peut dans ce cas-là remplacer la balise ouvrante et la balise fermante par
cet équivalent :
1 < client nom = " Nicolas " age = " 30 " / >
Enfin, et nous allons terminer notre aperçu rapide du XML avec ce dernier point, il
est important de noter que le XML peut imbriquer ses balises et qu’il peut ne posséder
qu’un seul élément racine, ce qui nous permet d’avoir une hiérarchie de données. Par
exemple nous pourrons avoir :
1 < listesDesClient >
2 < client type = " Particulier " >
3 < nom > Nicolas </ nom >
4 < age > 30 </ age >
5 </ client >
6 < client type = " Professionel " >
7 < nom >J é r é mie </ nom >
8 < age > 40 </ age >
9 </ client >
10 </ listesDesClient >
On voit tout de suite que le fichier décrit une liste de deux clients. Nous en avons un
qui est un particulier, qui s’appelle Nicolas et qui a 30 ans alors que l’autre est un
professionnel, prénommé Jérémie et qui a 40 ans.
398
CRÉER LE FICHIER DE CONFIGURATION
399
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
400
LECTURE SIMPLE DANS LA SECTION DE CONFIGURATION PRÉDÉFINIE :
APPSETTINGS
Compilons notre application et rendons-nous dans le répertoire où le fichier exécutable
est généré. Nous y trouvons également un fichier qui porte le même nom que notre exé-
cutable et dont l’extension est .exe.config. C’est bien notre fichier de configuration.
Visual C# Express le déploie au même endroit que notre application et lui donne un
nom qui va lui permettre d’être exploité par notre application. Pour être utilisables,
ces fichiers doivent se situer dans le même répertoire que l’exécutable.
On peut également utiliser des index numériques pour y accéder, mais je trouve que ça
manque vraiment de clarté :
1 string prenom = C o n f i g u r a ti o n M a n a g e r . AppSettings [ 0 ];
2 string age = C o n f i g u r a t i o nM a n a g e r . AppSettings [ 1 ];
401
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
3
4 Console . WriteLine ( " Pr é nom : " + prenom + " / Age : " + age ) ;
De plus, cela oblige à connaître l’ordre dans lequel les clés ont été mises dans le fichier.
Il est possible aussi de boucler sur toutes les valeurs contenues dans la section :
1 foreach ( string cle in C o n fi g u r a t i o n M a n a g e r . AppSettings )
2 {
3 Console . WriteLine ( " Cl é : " + cle + " / Valeur : " +
C o n f i g u r a ti o n M a n a g e r . AppSettings [ cle ]) ;
4 }
renverra bien notre valeur de configuration. Cependant, si la valeur n’existe pas, nous
obtiendrons la valeur null dans la chaîne de caractères. Il est important de remarquer
que nous récupérons des chaînes et que nous aurons besoin potentiellement de faire une
402
LECTURE DES CHAÎNES DE CONNEXION À LA BASE DE DONNÉES
conversion pour manipuler l’âge en tant qu’entier. Rien ne vous empêche d’écrire une
petite méthode d’extension maintenant que vous savez faire :
1 public static class C o n f i g u r a t i o n M a n a g e r E x t e n s i o n s
2 {
3 public static int O b t e n i rV a l e u r E n t i e r e ( this
N am e Va l u eC o ll e c ti o n appSettings , string cle )
4 {
5 string valeur = appSettings [ cle ];
6 return Convert . ToInt32 ( valeur ) ;
7 }
8 }
403
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
1 C o n n e c t i o n S t r i n g S e t t i n g s chaineConnexion = C o n f i g u ra t io n M a n a g e r
. Connecti onStrings [ " MaConnection " ];
2 Console . WriteLine ( chaineConnexion . Name ) ;
3 Console . WriteLine ( chaineConnexion . ConnectionString );
4 Console . WriteLine ( chaineConnexion . ProviderName ) ;
Nous utiliserons les chaînes de connexion dans le chapitre sur l’accès aux données.
Le type DictionarySectionhandler
Oui mais si tu nous dis que c’est un système de clé / valeur, c’est comme
pour les AppSettings que nous avons vus. Pourquoi utiliser une section
particulière DictionarySectionhandler alors ?
L’intérêt de pouvoir faire des sections particulières est d’organiser sémantiquement son
fichier de configuration, pour découper logiquement son fichier au lieu d’avoir tout dans
la même section. Regardons cette configuration :
1 < configuration >
2 < configSections >
3 < section name = " I n f o r m a t i o n s U t i l i s a t e u r " type = " System .
Configuration . D i c t i o n a r y S e c t i o n H a n d l e r " / >
4 </ configSections >
5 < InformationsUtilisateur >
6 < add key = " login " value = " nico " / >
7 < add key = " motdepasse " value = " 12345 " / >
8 < add key = " age " value = " 30 " / >
9 </ I n f o r m a t i o n s U t i l i s a t e u r >
404
CRÉER SA SECTION DE CONFIGURATION DEPUIS UN TYPE PRÉDÉFINI
La première chose à voir est que nous indiquons à notre application que nous défi-
nissons une section de configuration du type DictionarySectionHandler et qui va
s’appeler InformationsUtilisateur. Cela permet ensuite de définir notre propre sec-
tion InformationsUtilisateur, qui ressemble beaucoup à la section AppSettings
sauf qu’ici, on se rend tout de suite compte qu’il s’agit d’informations utilisateurs.
Pour accéder à notre section depuis notre programme, nous devrons utiliser le code
suivant :
1 Hashtable section = ( Hashtable ) C o n f i gu r a t i o n M a n a g e r . GetSection (
" InformationsUtilisateur ");
Nous pouvons boucler sur les valeurs de la section en utilisant le code suivant :
1 foreach ( DictionaryEntry d in section )
2 {
3 Console . WriteLine ( " Cl é : " + d . Key + " / Valeur : " + d .
Value ) ;
4 }
Le type NameValueSectionHandler
C’est la même que précédemment, à l’exception du type de la section, qui cette fois-ci
est NameValueSectionHandler. Ce qui implique que nous obtenons un type différent
en retour de l’appel à la méthode GetSection, à savoir un NameValueCollection :
405
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
1 N am e V al u eC o ll e c ti o n section = ( N a me V al u e Co l le c ti o n )
C o n f i g u r a ti o n M a n a g e r . GetSection ( " I n f o r m a t i o n s U t i l i s a t e u r " ) ;
À vous de voir laquelle des deux méthodes vous préférez, mais dans tous les cas, il
faudra fonctionner avec un système de clé / valeur.
Le type SingleTagSectionHandler
Ce troisième type permet de gérer une section différente des deux précédentes. Il sera
possible d’avoir autant d’attributs que l’on souhaite dans la section. Prenez par exemple
cette configuration :
1 < configuration >
2 < configSections >
3 < section name = " MonUtilisateur " type = " System . Configuration .
SingleTagSectionHandler " />
4 </ configSections >
5 < MonUtilisateur prenom = " Nico " age = " 30 " adresse = " 9 rue des
bois " / >
6 </ configuration >
Nous voyons que je peux mettre autant d’attributs que je le souhaite. Par contre, il
ne sera possible de faire apparaître la section MonUtilisateur qu’une seule fois, alors
que dans les sections précédentes, nous avions une liste de clé / valeur. Nous pourrons
récupérer notre configuration avec le code suivant :
1 Hashtable section = ( Hashtable ) C o n f ig u r a t i o n M a n a g e r . GetSection (
" MonUtilisateur " ) ;
2 Console . WriteLine ( section [ " prenom " ]) ;
3 Console . WriteLine ( section [ " age " ]) ;
4 Console . WriteLine ( section [ " adresse " ]) ;
Attention par contre, cette fois-ci la casse est importante pour obtenir la valeur de notre
attribut. Notons notre boucle habituelle permettant de retrouver tous les attributs de
notre section :
406
LES GROUPES DE SECTIONS
Nous voyons ici que j’ai défini un groupe qui s’appelle Utilisateur, en utilisant la
balise sectionGroup, contenant deux sections de configuration. Remarquons plus bas
le contenu des sections et nous remarquons que la balise <Utilisateur> contient nos
sections de configuration comme précédemment. Pour obtenir nos valeurs de configu-
ration, la seule chose qui change est la façon de charger la section. Ici, nous mettons le
nom de la section précédée du nom du groupe. Ce qui donne :
1 Hashtable section1 = ( Hashtable ) C o n fi g u r a t i o n M a n a g e r . GetSection
( " Utilisateur / P ar ame tr eC onn ex io n " ) ;
2 Hashtable section2 = ( Hashtable ) C o n fi g u r a t i o n M a n a g e r . GetSection
( " Utilisateur / InfoPersos " ) ;
407
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
Le grand intérêt ici est de pouvoir typer les propriétés. Ainsi, nous pouvons avoir une
section de configuration qui travaille avec un entier par exemple. Tout est fait par
la classe mère ici et il suffit d’utiliser ses propriétés indexées en y accédant par son
nom. Pour que notre section personnalisée soit reconnue, il faut la déclarer avec notre
nouveau type :
1 < configSections >
2 < section name = " PersonneSection " type = " M a P r e m i e r e A p p l i c a t i o n .
PersonneSection , M a P r e m i e r e A p p l i c a t i o n " / >
3 </ configSections >
Le nom du type est constitué du nom complet du type (espace de noms + nom de la
classe) suivi d’une virgule et du nom de l’assembly. Ici, l’espace de noms est le même
que l’assembly, car j’ai créé mes classes à la racine du projet. Si vous avez un doute,
vous devez vérifier l’espace de noms dans lequel est déclarée la classe. Ensuite, nous
pourrons définir notre section, ce qui donne au final :
1 < configuration >
2 < configSections >
3 < section name = " PersonneSection " type = "
M a P r e m i e r e A p p l i c a t i o n . PersonneSection ,
408
CRÉER UNE SECTION PERSONNALISÉE AVEC UNE COLLECTION
Pour accéder aux informations contenues dans la section, il faudra charger la section
comme d’habitude :
1 PersonneSection section = ( PersonneSection ) C o n f i g u r a t io n Ma n a g e r
. GetSection ( " PersonneSection " ) ;
2 Console . WriteLine ( section . Prenom + " a " + section . Age + " ans "
);
Ce qui est intéressant de remarquer ici, c’est qu’on accède directement à nos propriétés
via notre section personnalisée. Ce qui est une grande force et permet de travailler avec
un entier et une chaîne de caractères. Il faudra faire attention à deux choses ici. La
première est la casse. Comme on l’a vu dans le code, le nom est écrit en minuscule.
Il faudra être cohérent entre le nom indiqué dans l’attribut ConfigurationProperty,
celui indiqué en paramètre de l’opérateur d’indexation et celui écrit dans le fichier de
configuration. Tout doit être orthographié de la même façon, sinon nous aurons une
exception :
De même, si nous ne saisissons pas une valeur entière dans l’attribut age, il va y avoir
un problème de conversion :
409
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
Ici, je définis deux propriétés, Prenom et Age, qui me permettent bien sûr d’y stocker
un prénom et un âge qui sont respectivement une chaîne de caractères et un entier.
À noter que nous avons besoin de décrire ces propriétés dans le constructeur statique
de la classe. Pour cela, il faut lui indiquer son nom, c’est-à-dire la chaîne qui sera
utilisée comme attribut dans l’élément de la section de configuration. Ensuite, nous
lui indiquons son type ; pour cela on utilise le mot-clé typeof qui permet justement
de renvoyer le type (dans le sens objet Type) d’un type. Enfin nous lui indiquons une
option, par exemple le prénom sera la clé de mon élément (qui est une valeur unique et
obligatoire à saisir) et l’âge, qui sera un élément obligatoire à saisir également. Ensuite,
nous avons besoin d’utiliser cette classe à travers une collection d’éléments. Pour ce
faire, il faut créer une classe qui dérive de ConfigurationElementCollection :
1 public class C l i e n t E l e m e n t C o l l e c t i o n :
ConfigurationElementCollection
410
CRÉER UNE SECTION PERSONNALISÉE AVEC UNE COLLECTION
2 {
3 public override C o n f i g u r a t i o n E l e m e n t C o l l e c t i o n T y p e
CollectionType
4 {
5 get { return C o n f i g u r a t i o n E l e m e n t C o l l e c t i o n T y p e .
BasicMap ; }
6 }
7 protected override string ElementName
8 {
9 get { return " Client " ; }
10 }
11
12 protected override C o n f i g u r a t i o n P r o p e r t y C o l l e c t i o n
Properties
13 {
14 get { return new C o n f i g u r a t i o n P r o p e r t y C o l l e c t i o n () ; }
15 }
16
17 public ClientElement this [ int index ]
18 {
19 get { return ( ClientElement ) BaseGet ( index ) ; }
20 set
21 {
22 if ( BaseGet ( index ) != null )
23 {
24 BaseRemoveAt ( index ) ;
25 }
26 BaseAdd ( index , value ) ;
27 }
28 }
29
30 public new ClientElement this [ string nom ]
31 {
32 get { return ( ClientElement ) BaseGet ( nom ) ; }
33 }
34
35 public void Add ( ClientElement item )
36 {
37 BaseAdd ( item ) ;
38 }
39
40 public void Remove ( ClientElement item )
41 {
42 BaseRemove ( item ) ;
43 }
44
45 public void RemoveAt ( int index )
46 {
47 BaseRemoveAt ( index ) ;
48 }
411
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
49
50 public void Clear ()
51 {
52 BaseClear () ;
53 }
54
55 protected override C o n f i g u r a t i o n E l e m e nt CreateNewElement ()
56 {
57 return new ClientElement () ;
58 }
59
60 protected override object GetElementKey (
C o n f i g u r a ti o n E l e m e n t element )
61 {
62 return (( ClientElement ) element ) . Prenom ;
63 }
64 }
412
CRÉER UNE SECTION PERSONNALISÉE AVEC UNE COLLECTION
15 }
16
17 public new ClientElement this [ string nom ]
18 {
19 get { return Listes [ nom ]; }
20 }
21
22 protected override C o n f i g u r a t i o n P r o p e r t y C o l l e c t i o n
Properties
23 {
24 get { return proprietes ; }
25 }
26 }
Notons dans cette classe comment nous utilisons l’opérateur d’indexation pour renvoyer
un élément à partir de sa clé et renvoyer la liste des éléments. Maintenant, nous pouvons
écrire notre configuration :
1 < configuration >
2 < configSections >
3 < section name = " Lis te Cl ien tS ec tio n " type = "
M a P r e m i e r e A p p l i c a t i o n . ListeClientSection ,
MaPremiereApplication " />
4 </ configSections >
5 < L is te Cli en tS ect io n >
6 < Client prenom = " Nicolas " age = " 30 " / >
7 < Client prenom = " J é r é mie " age = " 20 " / >
8 </ L is teC li en tSe ct io n >
9 </ configuration >
Nous avons besoin à nouveau de définir la section en indiquant son type. Puis nous
pouvons créer notre section et positionner notre liste de clients. Pour accéder à cette
section, nous pouvons charger notre section comme avant avec la méthode GetSection :
1 Li st eC lie nt Se cti on section = ( L is teC li en tSe ct ion )
C o n f i g u r a t io n M a n a g e r . GetSection ( " Li st eCl ie nt Sec ti on " ) ;
413
CHAPITRE 35. LA CONFIGURATION D’UNE APPLICATION
Ce qui donnera :
Nicolas a 30 ans
J é r é mie a 20 ans
Voilà pour la section personnalisée avec une liste d’éléments. Cette partie était peut-
être un peu plus compliquée, mais retenons que la structure est souvent la même, il
sera facile d’adapter vos sections à partir de ce code.
Nous avons découvert plusieurs façons de stocker des paramètres utilisables par notre
application. Il faut savoir que beaucoup de composants du framework .NET sont in-
timement liés à ce fichier de configuration, comme une application web créée avec
ASP.NET ou lorsqu’on utilise des services web.
Il est important de remarquer que ce fichier est un fichier de configuration d’appli-
cation. Il y a d’autres endroits du même genre pour stocker de la configuration pour
les applications .NET, comme le machine.config qui est un fichier de configuration
partagé par toutes les applications de la machine. Il y a un héritage entre les différents
fichiers de configuration. Si l’on définit une configuration au niveau machine (dans le
machine.config), il est possible de la redéfinir pour notre application (app.config).
En général, le fichier machine.config se trouve dans le répertoire d’installation du
framework .NET, c’est-à-dire dans un sous répertoire du système d’exploitation, dé-
pendant de la version du framework .NET installée. Chez moi par exemple, il se trouve
dans le répertoire : C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config.
Enfin, remarquons qu’il est possible de faire des modifications du fichier de configuration
directement depuis le code de notre application. C’est un point qui est rarement utilisé
et que j’ai choisi de ne pas présenter pour que le chapitre ne soit pas trop long !
En résumé
– Les fichiers de configuration sont des fichiers XML qui possèdent les paramètres de
configuration de notre application.
– Pour les applications console, ils s’appellent app.config.
– On peut définir toutes sortes de valeurs de configuration grâce aux sections prédéfi-
nies ou en ajoutant son propre type de section personnalisée.
414
Chapitre 36
Introduction à LINQ
Difficulté :
L
INQ signifie Language INtegrated Query. C’est un ensemble d’extensions du langage
permettant de faire des requêtes sur des données en faisant abstraction de leur type.
Il permet d’utiliser facilement un jeu d’instructions supplémentaires afin de filtrer des
données, faire des sélections, etc. Il existe plusieurs domaines d’applications pour LINQ :
– Linq To Entities ou Linq To SQL qui utilisent ces extensions de langage sur les bases de
données.
– Linq To XML qui utilise ces extensions de langage pour travailler avec les fichiers XML.
– Linq To Object qui permet de travailler avec des collections d’objets en mémoire.
L’étude de LINQ nécessiterait un livre en entier, aussi nous allons nous concentrer sur
la partie qui va le plus nous servir en tant que débutant et qui va nous permettre de
commencer à travailler simplement avec cette nouvelle syntaxe, à savoir Linq To Object. Il
s’agit d’extensions permettant de faire des requêtes sur les objets en mémoire et notamment
sur toutes les listes ou collections. En fait, sur tout ce qui implémente IEnumerable<>.
415
CHAPITRE 36. INTRODUCTION À LINQ
Ce using est en général inclus par défaut lorsqu’on crée un nouveau fichier.
Jusqu’à maintenant, si nous voulions afficher les entiers d’une liste d’entiers qui sont
strictement supérieurs à 5, nous aurions fait :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2 foreach ( int i in liste )
3 {
4 if ( i > 5 )
5 {
6 Console . WriteLine ( i ) ;
7 }
8 }
Grâce à Linq To Objet, nous allons pouvoir filtrer en amont la liste afin de ne parcourir
que les entiers qui nous intéressent, en faisant :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2 IEnumerable < int > requeteFiltree = from i in liste
3 where i > 5
4 select i ;
5 foreach ( int i in requeteFiltree )
6 {
7 Console . WriteLine ( i ) ;
8 }
ce qui donnera :
6
9
15
8
Nous avons ici créé une requête Linq qui contient des mots-clés, comme from, in, where
et select. Ici, cette requête veut dire que nous partons (from) de la liste « liste » en
analysant chaque entier de la liste dans (in) la variable i où (where) i est supérieur à
5. Dans ce cas, l’entier est sélectionné comme faisant partie du résultat de la requête
grâce au mot-clé select, qui fait un peu office de return. Cette requête renvoie un
IEnumerable<int>. Le type générique est ici le type int car c’est le type de la variable
i qui est retourné par le select. Le fait de renvoyer un IEnumerable<> va permettre
potentiellement de réutiliser le résultat de cette requête pour un filtre successif ou une
expression différente. En effet, Linq travaille sur des IEnumerable<>. Nous pourrions
416
LES REQUÊTES LINQ
par exemple ordonner cette liste par ordre croissant grâce au mot-clé orderby. Cela
donnerait :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2 IEnumerable < int > requeteFiltree = from i in liste
3 where i > 5
4 select i ;
5 IEnumerable < int > requeteOrdonnee = from i in requeteFiltree
6 orderby i
7 select i ;
8 foreach ( int i in requeteOrdonnee )
9 {
10 Console . WriteLine ( i ) ;
11 }
Et nous aurons :
6
8
9
15
L’intérêt est que grâce à ces syntaxes, nous pouvons combiner facilement plusieurs
filtres et construire des requêtes plus ou moins complexes. Par exemple, imaginons des
clients :
1 public class Client
2 {
3 public int Identifiant { get ; set ; }
4 public string Nom { get ; set ; }
5 public int Age { get ; set ; }
6 }
dont ou voudrait savoir s’ils sont majeurs, puis qu’on voudrait trier par Age puis par
Nom, nous pourrions faire :
1 List < Client > listeClients = new List < Client >
2 {
3 new Client { Identifiant = 1 , Nom = " Nicolas " , Age = 30 } ,
4 new Client { Identifiant = 2 , Nom = " J é r é mie " , Age = 20 } ,
417
CHAPITRE 36. INTRODUCTION À LINQ
Ce qui donnera :
J é r é mie
Delphine
Nicolas
Notez ici que mon select renvoie le nom du client, qui est un string. Il est donc normal
que ma requête renvoie un IEnumerable<string> car j’ai choisi qu’elle ne sélectionne
que les noms qui sont de type string. Il est assez fréquent de construire des objets
anonymes à l’intérieur d’une requête. Par exemple, plutôt que de renvoyer uniquement
le nom du client, je pourrais également renvoyer l’âge, mais comme je n’ai pas besoin
de l’identifiant, je peux créer un objet anonyme juste avec ces deux propriétés :
1 List < Client > listeClients = new List < Client >
2 {
3 new Client { Identifiant = 1 , Nom = " Nicolas " , Age = 30 } ,
4 new Client { Identifiant = 2 , Nom = " J é r é mie " , Age = 20 } ,
5 new Client { Identifiant = 3 , Nom = " Delphine " , Age = 30 } ,
6 new Client { Identifiant = 4 , Nom = " Bob " , Age = 10 } ,
7 };
8
9 var requete = from client in listeClients
10 where client . Age > 18
11 orderby client . Age , client . Nom
12 select new { client . Nom , client
. Age };
13 foreach ( var obj in requete )
14 {
15 Console . WriteLine ( " { 0 } a { 1 } ans " , obj . Nom , obj . Age ) ;
16 }
Et nous aurons :
J é r é mie a 20 ans
Delphine a 30 ans
Nicolas a 30 ans
418
LES REQUÊTES LINQ
Mon objet anonyme contient ici juste une propriété Nom et une propriété Age. À noter
que je suis obligé à ce moment-là d’utiliser le mot-clé var pour définir la requête, car
je n’ai pas de type à donner à cette requête. De même, dans le foreach je dois utiliser
le mot-clé var pour définir le type anonyme. Les requêtes peuvent être de plus en plus
compliquées, comme faisant des jointures. Par exemple, ajoutons une classe Commande :
1 public class Commande
2 {
3 public int Identifiant { get ; set ; }
4 public int I dentifia ntClient { get ; set ; }
5 public decimal Prix { get ; set ; }
6 }
419
CHAPITRE 36. INTRODUCTION À LINQ
Ce qui donne :
On utilise le mot-clé join pour faire la jointure entre les deux listes puis on utilise le
mot-clé on et le mot-clé equals pour indiquer sur quoi on fait la jointure. À noter que
ceci peut se réaliser en imbriquant également les from et en filtrant sur l’égalité des
identifiants clients :
1 var liste = from commande in listeCommandes
2 from client in listeClients
3 where client . Identifiant == commande .
Ident ifiantCl ient
4 select new { client . Nom , commande . Prix };
5
6 foreach ( var element in liste )
7 {
8 Console . WriteLine ( " Le client { 0 } a pay é { 1 } " , element . Nom ,
element . Prix ) ;
9 }
Il est intéressant de pouvoir regrouper les objets qui ont la même valeur. Par exemple
pour obtenir toutes les commandes, groupées par client, on fera :
1 var liste = from commande in listeCommandes
2 group commande by commande . Iden tifiantC lient ;
3
4 foreach ( var element in liste )
5 {
6 Console . WriteLine ( " Le client : { 0 } a r é alis é { 1 } commande ( s
) " , element . Key , element . Count () ) ;
7 foreach ( Commande commande in element )
8 {
9 Console . WriteLine ( " \ tPrix : { 0 } " , commande . Prix ) ;
10 }
11 }
420
LES REQUÊTES LINQ
Prix : 80
Prix : 10
Il est possible de cumuler le group by avec notre jointure précédente histoire d’avoir
également le nom du client :
1 var liste = from commande in listeCommandes
2 join client in listeClients on commande .
Iden tifiantCl ient equals client . Identifiant
3 group commande by new { commande . IdentifiantClient ,
client . Nom };
4
5 foreach ( var element in liste )
6 {
7 Console . WriteLine ( " Le client { 0 } ({ 1 }) a r é alis é { 2 }
commande ( s ) " , element . Key . Nom , element . Key .
IdentifiantClient , element . Count () ) ;
8 foreach ( Commande commande in element )
9 {
10 Console . WriteLine ( " \ tPrix : { 0 } " , commande . Prix ) ;
11 }
12 }
Et nous obtenons :
À noter que le group by termine la requête, un peu comme le select. Ainsi, si l’on
veut sélectionner quelque chose après le group by, il faudra utiliser le mot-clé into et
la syntaxe suivante :
1 var liste = from commande in listeCommandes
2 join client in listeClients on commande .
Iden tifiantCl ient equals client . Identifiant
3 group commande by new { commande . IdentifiantClient ,
client . Nom } into com mandesGro upees
4 select
5 new
6 {
7 comma ndesGrou pees . Key . IdentifiantClient ,
8 comma ndesGrou pees . Key . Nom ,
9 Nombr eDeComma ndes = comman desGroup ees . Count
()
421
CHAPITRE 36. INTRODUCTION À LINQ
10 };
11
12 foreach ( var element in liste )
13 {
14 Console . WriteLine ( " Le client { 0 } ({ 1 }) a r é alis é { 2 }
commande ( s ) " , element . Nom , element . IdentifiantClient ,
element . Nombre DeComman des ) ;
15 }
L’intérêt d’utiliser le mot-clé into est également de pouvoir enchaîner avec une autre
jointure ou un autre filtre permettant de continuer la requête. Il est également possible
d’utiliser des variables à l’intérieur des requêtes grâce au mot-clé let. Cela va nous
permettre de stocker des résultats temporaires pour les réutiliser ensuite :
1 var liste = from commande in listeCommandes
2 join client in listeClients on commande .
Ident ifiantCl ient equals client . Identifiant
3 group commande by new { commande . IdentifiantClient ,
client . Nom } into comm andesGro upees
4 let total = c ommandes Groupees . Sum ( c = > c . Prix )
5 where total > 50
6 orderby total
7 select new
8 {
9 comma ndesGrou pees . Key . IdentifiantClient ,
10 comma ndesGrou pees . Key . Nom ,
11 Nombr eDeComma ndes = comman desGroupe es . Count
() ,
12 PrixTotal = total
13 };
14
15 foreach ( var element in liste )
16 {
17 Console . WriteLine ( " Le client { 0 } ({ 1 }) a r é alis é { 2 }
commande ( s ) pour un total de { 3 } " , element . Nom , element .
IdentifiantClient , element . NombreDeCommandes , element .
PrixTotal ) ;
18 }
Par exemple, ici j’utilise le mot-clé let pour stocker le total d’une commande groupée
dans la variable total (nous verrons la méthode Sum() un tout petit peu plus bas),
ce qui me permet ensuite de filtrer avec un where pour obtenir les commandes dont le
total est supérieur à 50 et de les trier par ordre de prix croissant.
Ce qui donne :
422
LES MÉTHODES D’EXTENSION LINQ
Nous allons nous arrêter là pour cet aperçu des requêtes LINQ. Nous avons pu voir
que le C# dispose d’un certain nombre de mots-clés qui permettent de manipuler nos
données de manière très puissante mais d’une façon un peu inhabituelle.
Cette façon d’écrire des requêtes LINQ s’appelle en anglais la sugar syntax,
que l’on peut traduire par « sucre syntaxique ». Il désigne de manière générale
les constructions d’un langage qui facilitent la rédaction du code sans modifier
l’expressivité du langage.
s’écrit véritablement :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2 IEnumerable < int > requeteFiltree = liste . Where ( i = > i > 5 ) ;
3 foreach ( int i in requeteFiltree )
4 {
5 Console . WriteLine ( i ) ;
6 }
Nous utilisons la méthode d’extension Where() en lui fournissant une expression lambda
servant de prédicat pour filtrer la liste. C’est de cette façon que le compilateur traduit
la sugar syntax. Ce n’est donc qu’une façon plus élégante d’utiliser ces méthodes d’ex-
tension. Chaque méthode d’extension renvoie un IEnumerable<T> ce qui permet d’en-
chaîner facilement les filtres successifs. Par exemple, rajoutons une date et un nombre
d’articles à notre classe Commande :
423
CHAPITRE 36. INTRODUCTION À LINQ
nous pouvons obtenir les commandes dont le prix est supérieur à 100, où le nombre
d’articles est supérieur à 10, triées par prix puis par date d’achat.
De plus, ces méthodes d’extension font beaucoup plus de choses que ce que l’on peut
faire avec la sugar syntax. Il existe pas mal de méthodes intéressantes, que nous ne
pourrons pas toutes étudier. Regardons par exemple la méthode Sum() (qui a été utilisée
dans le paragraphe précédent) qui permet de faire la somme des éléments d’une liste
ou la méthode Average() qui permet d’en faire la moyenne :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2 Console . WriteLine ( " Somme : { 0 } " , liste . Sum () ) ;
3 Console . WriteLine ( " Moyenne : { 0 } " , liste . Average () ) ;
Somme : 51
Moyenne : 6 ,375
Tout est déjà fait, c’est pratique ! Évidemment, les surcharges de ces deux méthodes
d’extension ne fonctionnent qu’avec des types int ou double ou decimal. . . Qui envi-
sagerait de faire une moyenne sur une chaîne ? Par contre, il est possible de définir une
expression lambda dans la méthode Sum() afin de faire la somme sur un élément d’un
objet, comme le prix de notre commande :
1 decimal prixTotal = listeCommandes . Sum ( commande = > commande .
Prix ) ;
D’autres méthodes sont bien utiles. Par exemple la méthode d’extension Take() nous
permet de récupérer les N premiers éléments d’une liste :
1 IEnumerable < Client > extrait = listeClients . Order ByDescend ing (
client = > client . Age ) . Take ( 5 ) ;
Ici, je trie dans un premier temps ma liste par âge décroissant, et je prends les 5
premiers. Ce qui signifie que je prends les 5 plus vieux clients de ma liste. Et s’il n’y
424
LES MÉTHODES D’EXTENSION LINQ
J’obtiens alors le premier élément de la liste qui est strictement supérieur à 5. À noter
que le filtre peut également se faire dans l’expression lambda de la méthode First() :
1 int premier = liste . First ( i = > i > 5 ) ;
Ce qui revient exactement au même. Attention, s’il n’y a aucun élément dans la liste,
alors la méthode First() lève l’exception :
Il est possible dans ce cas-là d’éviter une exception avec la méthode FirstOrDefault()
qui renvoie la valeur par défaut du type de la liste (0 si c’est un type valeur, null si
c’est un type référence) :
1 Client nicolas = listeClients . FirstOrDefault ( client = > client .
Nom == " Nicolas " ) ;
2 if ( nicolas == null )
3 Console . WriteLine ( " Client non trouv é " ) ;
Ici, je cherche le premier des clients dont le nom est Nicolas. S’il n’est pas trouvé, alors
FirstOrDefault() me renvoie null, sinon, il me renvoie bien sûr le bon objet Client.
Dans le même genre, nous pouvons compter grâce à la méthode Count() le nombre
d’éléments d’une source de données suivant un critère :
1 int n o m b r e C l i e nt s M a j e u r s = listeClients . Count ( client = > client .
Age >= 18 ) ;
Ici, j’obtiendrai le nombre de clients majeurs dans ma liste. De la même façon qu’avec la
sugar syntax, il est possible de faire une sélection précise des données que l’on souhaite
extraire, grâce à la méthode Select() :
1 var requete = listeClients . Where ( client = > client . Age >= 18 ) .
Select ( client = > new { client . Age , client . Nom }) ;
Cela me permettra d’obtenir une requête contenant les clients majeurs. À noter que
seront retournés des objets anonymes possédant une propriété Age et une propriété
Nom. Bien sûr, nous retrouverons nos jointures avec la méthode d’extension Join() ou
les groupes avec la méthode GroupBy(). Il existe beaucoup de méthodes d’extension et
il n’est pas envisageable dans ce livre de toutes les décrire. Je vais finir en vous parlant
des méthodes ToList() et ToArray() qui, comme leurs noms le suggèrent, permettent
de forcer la requête à être mise dans une liste ou dans un tableau :
1 List < Client > l es P lu s V ie u xC l i en t s = listeClients .
Order ByDescend ing ( client = > client . Age ) . Take ( 5 ) . ToList () ;
425
CHAPITRE 36. INTRODUCTION À LINQ
ou
1 Client [] l e sP l us V ie u x Cl i en t s = listeClients . OrderBy Descendin g (
client = > client . Age ) . Take ( 5 ) . ToArray () ;
Plutôt que d’avoir un IEnumerable<>, nous obtiendrons cette fois-ci une List<> ou un
tableau. Le fait d’utiliser ces méthodes d’extension a des conséquences que nous allons
décrire.
Exécution différée
Les méthodes d’extension LINQ ou sa syntaxe sucrée c’est bien joli, mais quel est
l’intérêt de s’en servir plutôt que d’utiliser des boucles foreach, des if ou d’autres
choses ? Déjà, parce qu’il y a plein de choses déjà toutes faites : la somme, la moyenne,
la récupération de N éléments, etc. Mais aussi pour une autre raison plus importante :
l’exécution différée.
Nous en avons déjà parlé, l’exécution différée est possible grâce au mot-clé yield. Les
méthodes d’extensions Linq utilisent fortement ce principe. Cela veut dire que lorsque
nous construisons une requête, elle n’est pas exécutée tant que l’on n’itère pas sur le
contenu de la requête. Ceci permet de stocker la requête, d’empiler éventuellement
des filtres ou des jointures et de ne pas calculer le résultat tant qu’on n’en a pas
explicitement besoin.
Ainsi, imaginons que nous souhaitions trier une liste d’entiers. Avant cela, nous aurions
fait :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2
3 liste . Sort () ;
4 liste . Add ( 7 ) ;
5
6 foreach ( int i in liste )
7 {
8 Console . WriteLine ( i ) ;
9 }
Ce qui aurait affiché en toute logique la liste triée puis, à la fin, l’entier 7 rajouté,
c’est-à-dire :
1
3
4
5
6
8
9
15
7
426
EXÉCUTION DIFFÉRÉE
1
3
4
5
6
7
8
9
15
Bien que nous ayons ajouté la valeur 7 après avoir trié la liste avec OrderBy, on se rend
compte que tous les entiers sont quand même triés lorsque nous les affichons.
En effet, la requête n’a été exécutée qu’au moment du foreach. Ceci implique donc que
le tri va tenir compte de l’ajout du 7 à la liste. La requête est construite en mémorisant
les conditions comme notre OrderBy, mais cela fonctionne également avec un where,
et tout ceci n’est exécuté que lorsqu’on le demande explicitement ; c’est-à-dire avec
un foreach dans ce cas-là. En fait, tant que le C# n’est pas obligé de parcourir les
éléments énumérables alors il ne le fait pas. Ce qui permet d’enchaîner les éventuelles
conditions et d’éviter les parcours inutiles. Par exemple, dans le cas ci-dessous, il est
inutile d’exécuter le premier filtre :
1 List < int > liste = new List < int > { 4 , 6 , 1 , 9 , 5 , 15 , 8 , 3 };
2 IEnumerable < int > requete = liste . Where ( i = > i > 5 ) ;
3 // plein de choses qui n ' ont rien à voir avec la requete
4 requete = requete . Where ( i = > i > 10 ) ;
En effet, le deuxième filtre a tout intérêt à être combiné au premier afin d’être simplifié.
Et encore, ici, on n’utilise même pas la requête, il y a encore moins d’intérêt à effectuer
nos filtres si nous ne nous servons pas du résultat. Ceci peut paraître inattendu, mais
c’est très important dans la façon dont Linq s’en sert afin d’optimiser ses requêtes. Ici,
le parcours en mémoire pourrait paraître peu coûteux, mais dans la mesure où Linq
doit fonctionner aussi bien avec des objets, qu’avec des bases de données ou du XML
(ou autres. . .), cette optimisation prend tout son sens.
Le maître mot est la performance, primordial quand on accède aux bases de données.
Cette exécution différée est gardée pour le plus tard possible. C’est-à-dire que le fait
427
CHAPITRE 36. INTRODUCTION À LINQ
428
RÉCAPITULATIF DES OPÉRATEURS DE REQUÊTES
En résumé
– Linq consiste en un ensemble d’extensions du langage permettant de faire des re-
quêtes sur des données en faisant abstraction de leur type.
– Il existe plusieurs domaines d’applications de Linq, comme Linq to Object, Linq to
Sql, etc.
– La sugar syntax ajoute des mots-clés qui permettent de faire des requêtes qui res-
semblent aux requêtes faites avec le langage SQL.
– Derrière cette syntaxe se cache un bon nombre de méthodes d’extension qui tirent
parti des mécanismes d’exécution différée.
429
CHAPITRE 36. INTRODUCTION À LINQ
430
Chapitre 37
Accéder aux données avec Entity
Framework
Difficulté :
N
ous allons voir dans ce chapitre comment connecter nos applications à une base
de données. Bien qu’il soit possible de travailler avec quasiment n’importe quelle
base de données (oracle, mysql,. . .), le framework .NET offre toute sa puissance en
fonctionnant avec SQL Server.
Si vous vous souvenez, lors de l’installation de Visual C# Express, nous avons également
installé Microsoft SQL Server 2008 express Service Pack 1. SQL Server 2008 Express est
un moteur de base de données light, idéal pour travailler en local sur son PC. Dans un
environnement de production, nous aurons tout intérêt à travailler avec la version complète
de SQL Server, mais pour ce livre, la version express est amplement suffisante. En plus. . .
elle est gratuite !
431
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
432
ENTITY FRAMEWORK ET LE MAPPING OBJET RELATIONNEL
objets Produit, des objets Rayon. Un rayon pourrait contenir des produits, etc. Ne
pourrions-nous pas essayer de représenter la base de données sous la forme d’un modèle
orienté objet plutôt qu’un modèle relationnel ? C’est le principe de ce que l’on appelle
un ORM 1 que l’on traduit en français par « mapping objet-relationnel ». L’ORM est
un outil qui permet de générer une couche d’accès aux données orientée objet à partir
d’une base de données relationnelle en définissant des correspondances entre cette base
de données et des objets. Il existe plusieurs outils d’ORM pour .NET permettant de
générer des objets C#, comme nhibernate, Entity Framework, etc.
Nous allons utiliser ici Entity Framework. C’est l’ORM de Microsoft. Il est totalement
intégré à Visual C# Express et aux autres versions. Son travail consiste, entre autres,
à:
– modéliser ses données et générer la base correspondante ;
– générer un modèle à partir d’une base de données existante ;
– gérer tous les accès à la base de données (lecture, écriture, suppression,. . .).
Le grand intérêt est que nous allons travailler directement avec des objets et c’est lui
qui s’occupera de tout ce qui est persistance dans la base de données.
Allez, fini le blabla, passons à son utilisation ! Commençons par créer une application
console MonApplicationBaseDeDonnées, ainsi qu’indiqué à la figure 37.2.
À présent, ajoutons un nouvel élément de type ADO.NET Entity Data Model à notre
projet, que l’on va appeler ModelCommerce.edmx (voir figure 37.3).
C’est ce type de fichier qui va nous permettre de modéliser nos données. L’assistant
s’ouvre et nous choisissons Modèle vide, comme à la figure 37.4.
On voit apparaître une nouvelle fenêtre intitulée Entity Data Model Designer. Cette
fenêtre « designer » va nous permettre de modéliser nos données. Elle nous donne
également accès à la boîte à outils, ainsi que vous pouvez le voir à la figure 37.5.
Lorsque nous accédons à la boîte à outils, nous pouvons voir plusieurs éléments. Celui
qui nous intéresse est l’entité (voir figure 37.6).
1. C’est l’acronyme de l’expression anglaise : object relationnal mapping.
433
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
434
ENTITY FRAMEWORK ET LE MAPPING OBJET RELATIONNEL
Nous allons pouvoir modéliser nos entités en les faisant glisser sur le designer. Comme
l’illustre la figure 37.7, nous la voyons apparaître sur le designer.
Nous pouvons renommer cette entité, soit en allant modifier la propriété Nom dans
la fenêtre de propriétés, soit en cliquant directement sur le nom dans le designer.
Appelons cette entité Rayon. Nous pouvons constater que cette entité est générée avec
une propriété par défaut : ID. C’est l’identifiant de notre rayon. Cet identifiant possède
des propriétés que nous pouvons voir dans la fenêtre de propriétés (voir figure 37.8).
Nous pouvons par exemple voir (ou modifier) son nom (ID) et son type (Int32, qui
est l’équivalent de int). Nous voyons également que cette propriété est la clé d’entité.
Ce qui veut dire que c’est ce qui va nous permettre d’identifier notre rayon de manière
unique (c’est un peu plus complexe que ça, mais retenons ce point). Nous pouvons
ajouter une nouvelle propriété à l’entité ; pour cela il suffit de faire un clic droit sur
l’entité et d’ajouter une propriété scalaire (voir figure 37.9).
Une nouvelle propriété apparaît dans notre entité Rayon (voir figure 37.10).
Nous pouvons la renommer en Nom. Nous pouvons voir sur la figure 37.11 qu’elle est
par défaut du type String, ce qui nous va très bien.
435
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
436
ENTITY FRAMEWORK ET LE MAPPING OBJET RELATIONNEL
Rajoutons une troisième propriété Description, toujours de type String mais qui
pourra être nulle, il suffit de déclarer la propriété Nullable à True (voir la figure 37.12).
Ça y est, notre entité Rayon est modélisée ! Rajoutons maintenant une nouvelle entité,
Produit, qui possède également un identifiant, un nom de type String et un prix de
type Decimal. Pour mettre le type à Decimal, il suffit de changer le type dans la fenêtre
des propriétés (voir la figure 37.13).
Ensuite, rajoutons une propriété Stock de type Int32 et une propriété UrlImage de
type String. Nous obtenons deux superbes entités (voir la figure 37.14).
Il est temps de relier les entités entre elles grâce à une association : faites un clic droit
sur l’entité Rayon et ajoutez une association, comme indiqué à la figure 37.15.
437
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
438
ENTITY FRAMEWORK ET LE MAPPING OBJET RELATIONNEL
Un nouvel écran s’ouvre qui permet de définir l’association. Indiquons que la multipli-
cité est à « plusieurs » sur les deux entités, ce qui permet de dire qu’un rayon peut
contenir de 0 à N produits et inversement, un produit peut être contenu dans 0 à N
rayons. Notons au passage que les choix possibles sont 1, 0 ou 1 et plusieurs. C’est ce
qui nous permet d’indiquer la cardinalité de nos relations. Changez ensuite le nom des
propriétés de navigation en rajoutant un « s » à Produit et à Rayon, comme indiqué à
la figure 37.16.
Le designer est mis à jour avec la relation et on peut voir apparaître des propriétés
de navigation dans les entités. L’entité Rayon a une propriété Produits, ce qui va
permettre d’obtenir la liste des produits d’un Rayon. De même, l’entité Produit possède
une propriété de navigation Rayons qui va permettre d’obtenir la liste des rayons qui
contiennent le produit (voir la figure 37.17).
Nous allons encore faire une petite modification à ce modèle. Sélectionnez l’entité
Rayon. Nous pouvons voir dans ses propriétés que le nom du jeu d’entité vaut RayonJeu,
modifiez-le en Rayons, comme indiqué à la figure 37.18.
Faites pareil pour l’entité Produit : changez ProduitJeu en Produits.
Voilà, tout ça, c’est notre modèle ! Il faut maintenant faire en sorte que notre base de
données soit cohérente avec le modèle. Il suffit de faire un clic droit sur le designer et
de choisir de générer la base de données à partir du modèle (voir la figure 37.19).
439
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
440
ENTITY FRAMEWORK ET LE MAPPING OBJET RELATIONNEL
Une nouvelle fenêtre s’ouvre nous permettant de choisir notre source de données. Celle-
ci étant vide, cliquez sur Nouvelle connexion (voir la figure 37.20).
Une nouvelle fenêtre apparaît nous permettant de choisir notre source de données (voir
la figure 37.21).
441
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
Attention, ici dans la version express de Visual C#, il n’est possible de choisir
que parmi deux options : un fichier de base de données ou Microsoft SQL
Server compact. Dans d’autres versions express (notamment la version per-
mettant de faire du développement web) et dans les versions payantes de
Visual Studio, il est possible de choisir directement un serveur de base de
données.
Cela aurait été plus pratique. Tant pis, nous allons faire avec ; choisissons le fichier
de base de données. Il faut lui donner un emplacement et un nom, par exemple dans
le répertoire des projets, je l’appelle basecommerce.mdf. Ensuite, pour pouvoir s’y
connecter, nous utiliserons l’authentification Windows (voir la figure 37.22).
Au moment de la validation, il nous est demandé si l’on souhaite créer le fichier de base
de données. Répondez « oui » !
Puis nous arrivons sur un récapitulatif et nous voyons en bas la chaîne de connexion à
la base de données, comme l’illustre la figure 37.23.
Nous pouvons choisir d’enregistrer les paramètres de connexion, ou bien de ne pas le
faire, en cochant ou décochant cette case. Dans tous les cas, cette chaîne de connexion
ne nous servira pas en l’état. Cliquez sur suivant. Le designer d’Entity Framework
nous a finalement créé un fichier contenant des instructions SQL qu’il nous propose
d’enregistrer. Ces instructions SQL vont permettre de générer les tables de la base
de données (voir la figure 37.24). Nous allons revenir sur ces instructions. Ce fichier
s’ouvre également dans Visual C# Express (voir la figure 37.25).
442
ENTITY FRAMEWORK ET LE MAPPING OBJET RELATIONNEL
443
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
444
INSTALLER ET UTILISER L’OUTIL DE GESTION DE BDD
445
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
il nous servira un peu plus loin. Puis nous arrivons sur l’écran suivant qui nous indique
que nous allons ajouter l’outil de gestion de base (voir la figure 37.28).
Et voilà, nous avons terminé cette installation ! Maintenant, nous pouvons enfin dé-
marrer SQL Server Management Studio !
Au démarrage, il nous demande de nous connecter à notre instance de base de données.
Par défaut, l’instance s’appelle SQLEXPRESS, comme nous l’avons vu, et nous pouvons
nous y connecter en la préfixant par le nom de notre machine ou bien simplement
en utilisant le point « . », ce qui donne .\SQLEXPRESS. Conservez l’authentification
Windows (voir la figure 37.29).
Vous arrivez dans l’outil et vous pouvez voir dans l’explorateur d’objets à gauche qu’il
n’y a pas (encore) de base de données (voir la figure 37.30).
Nous allons devoir créer notre base de données. Faites un clic droit sur le dossier Base
de données et choisissez Nouvelle base de données. Donnez-lui le même nom que
le fichier de base de données que nous avions précédemment créé : basecommerce.
Validez : la base de données est créée ! Nous la voyons apparaître dans l’explorateur
d’objets (voir la figure 37.31) et nous voyons également qu’il n’y a pas de tables dedans.
Cliquons maintenant sur Nouvelle requête, une fenêtre vide s’ouvre où nous allons
coller le contenu du fichier SQL qui a été généré par le designer de Visual C# Express.
Parlons un peu du contenu de ce script. Remarquons déjà que les commentaires sont
préfixés par --, mais, malins comme nous sommes, nous les aurions reconnus, en plus
ils sont en vert ! Ensuite, la ligne suivante permet d’indiquer que nous allons nous
positionner sur la base basecommerce :
1 USE [ basecommerce ];
446
INSTALLER ET UTILISER L’OUTIL DE GESTION DE BDD
447
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
Figure 37.31 – Il n’y a pas de tables dans la base de données que nous avons créée
Si jamais vous n’avez pas donné le même nom à la base de données, c’est ici qu’il faut
le changer. Allons un peu plus bas, nous voyons l’instruction :
1 CREATE TABLE [ dbo ].[ Rayons ] (
2 [ ID ] int IDENTITY (1 , 1 ) NOT NULL ,
3 [ Nom ] nvarchar ( max ) NOT NULL ,
4 [ Description ] nvarchar ( max ) NULL
5 );
qui permet de créer la table contenant les rayons, suivi du même genre d’instruction
qui permet de créer la table contenant les produits. Sans trop nous attarder dessus,
nous pouvons voir la syntaxe permettant de créer la table (avec CREATE TABLE) et la
syntaxe permettant de créer les champs de la table, ainsi que leurs types.
Après la création des tables, et comme l’indiquent les commentaires pour les anglo-
phones, la suite est une histoire de clé primaire et de clé étrangère.
Ce sont des notions de base de données. Sans trop entrer dans les détails, je vais vous
expliquer rapidement de quoi il s’agit. Une clé primaire est une contrainte d’unicité
qui permet d’identifier de manière unique un enregistrement dans une table. La clé
primaire correspond dans notre cas à l’identifiant d’un rayon ou à l’identifiant d’un
produit dans leurs tables respectives. À noter qu’elles ont une propriété complémen-
taire, à savoir un auto-incrément. C’est-à-dire que c’est SQL Server qui va s’occuper
de numéroter automatiquement ces identifiants, en les incrémentant à chaque inser-
tion. Une clé étrangère est une contrainte qui garantit l’intégrité référentielle entre
448
INSTALLER ET UTILISER L’OUTIL DE GESTION DE BDD
deux tables. Elle identifie une colonne d’une autre table. Cela permet de faire des liens
sémantiques entre les tables.
Vous n’avez pas besoin de savoir exactement ce qu’il se passe dans ce script
SQL. Nous le regardons vite fait pour la culture, mais il faut juste savoir
l’exécuter afin qu’il nous crée les tables.
Ce petit aparté terminé, retournons dans SQL Server Management Studio et collons-y
notre requête. Il ne reste plus qu’à exécuter le script en cliquant sur le bouton Exécuter,
comme indiqué à la figure 37.32.
Comme tout s’est bien passé, nous pouvons rafraîchir l’explorateur d’objets et constater
que les nouvelles tables sont créées (voir la figure 37.33).
Maintenant, nous avons besoin de données dans ces tables. Il y a plusieurs façons
de faire. La première est d’utiliser le designer de SQL Server Management Studio, la
seconde serait d’utiliser un script SQL, la troisième serait d’utiliser du code C#. Regar-
dons la première solution et faisons un clic droit sur la table produit pour « modifier
les 200 lignes du haut ». Nous pouvons ensuite insérer des valeurs, comme l’illustre la
figure 37.34.
Ne le faites pas, car pour vous éviter du travail, je l’ai fait pour vous grâce à la deuxième
méthode, le script SQL :
1 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
2 VALUES ( 'T é l é HD ' , 299 , 50 , ' tele . jpg ')
3 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
4 VALUES ( ' Console de jeux ' , 150 , 25 , ' console . jpg ')
5 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
6 VALUES ( ' Canap é ' , 400 , 10 , ' canape . jpg ')
7 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
8 VALUES ( ' Cuisini è re ' , 280 , 20 , ' cuisiniere . jpg ')
9 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
10 VALUES ( ' Bouilloire ' , 19 , 100 , ' bouilloire . jpg ')
11 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
12 VALUES ( ' Lit 2 places ' , 149 , 15 , ' lit . jpg ')
13 INSERT INTO [ Produits ] ([ Nom ] ,[ Prix ] ,[ Stock ] ,[ UrlImage ])
449
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
Figure 37.34 – Ajout de données par l’interface de SQL Server Management Studio
450
INSTALLER ET UTILISER L’OUTIL DE GESTION DE BDD
Il vous suffit d’exécuter ce script pour insérer les données. Nous n’allons pas détailler
la syntaxe de ce script, mais il est quand même assez facile à lire comme ça. À noter
que nous n’avons pas besoin d’indiquer d’identifiant car il est auto-incrémenté par SQL
Server. Créons maintenant des rayons, avec la même technique :
1 INSERT INTO [ Rayons ] ([ Nom ] ,[ Description ])
2 VALUES ( ' Salon ' , ' Tout ce qu ' ' on trouve dans un salon ')
3 INSERT INTO [ Rayons ] ([ Nom ] ,[ Description ])
4 VALUES ( ' Cuisine ' , ' Venez d é couvrir l ' ' univers de la
cuisine ')
5 INSERT INTO [ Rayons ] ([ Nom ] ,[ Description ])
6 VALUES ( ' Dormir ' , null )
7 INSERT INTO [ Rayons ] ([ Nom ] ,[ Description ])
8 VALUES ( 'Hi - Tech ' , ' Les produits hi - tech ... ')
9 INSERT INTO [ Rayons ] ([ Nom ] ,[ Description ])
10 VALUES ( 'V ê tements ' , null )
451
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
Il n’y a plus qu’à relier les produits et les rayons. Pour cela, il faut relier les identifiants
entre eux. Par exemple, avec le script suivant j’indique que le rayon Salon (identifiant
1) contient la télé HD (identifiant 1), la console de jeux (identifiant 2), le canapé
(identifiant 3), la tablette PC (identifiant 10). J’indique également que le rayon Cuisine
(identifiant 2) contient la cuisinière (identifiant 4), ainsi que la bouilloire (identifiant
5). Et ainsi de suite. . . :
1 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
2 VALUES (1 , 1 )
3 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
4 VALUES (1 , 2 )
5 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
6 VALUES (1 , 3 )
7 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
8 VALUES (1 , 10 )
9 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
10 VALUES (2 , 4 )
11 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
12 VALUES (2 , 5 )
13 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
14 VALUES (3 , 3 )
15 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
16 VALUES (3 , 6 )
17 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
18 VALUES (3 , 9 )
19 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
20 VALUES (4 , 1 )
21 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
22 VALUES (4 , 2 )
23 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
24 VALUES (4 , 10 )
25 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
26 VALUES (4 , 11 )
27 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
452
SE CONNECTER À LA BASE DE DONNÉES, LIRE ET ÉCRIRE
28 VALUES (5 , 7 )
29 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
30 VALUES (5 , 8 )
31 INSERT INTO [ RayonProduit ] ([ Rayons_ID ] ,[ Produits_ID ])
32 VALUES (5 , 9 )
Nous indiquons ici que notre chaîne de connexion va être accessible par le nom
NotreBaseDeDonnees. Il y a plein d’informations dans l’attribut connectionString,
mais ce qui nous intéresse surtout, c’est d’indiquer la source de données (à savoir :
data source=.\SQLEXPRESS) ce qui va nous permettre d’indiquer que notre serveur
est accessible à cette adresse, puis le nom de la base que nous avons créée (Initial
Catalog=basecommerce) et enfin d’indiquer que nous utilisons l’authentification Win-
dows (integrated security=True). Le reste permet de donner des informations de
453
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
description du modèle. Enfin, nous indiquons que nous utilisons les méthodes d’En-
tity Framework pour l’accès aux données, à travers System.Data.EntityClient. Bref,
beaucoup de ces informations sont issues de la chaîne de connexion générée par Visual
C# Express ; nous avons simplement changé le mode de connexion pour qu’il corres-
ponde à nos besoins. Maintenant, nous allons pouvoir accéder à la chaîne de connexion
avec le ConfigurationManager que nous connaissons bien désormais :
1 string chaineConnexion = C o n f i g u r a t i o n M a n a g e r . Connecti onString s
[ " No tr eBa se De Don ne es " ]. ConnectionString ;
Vous n’avez bien sûr pas oublié de référencer l’assembly System.Configuration ! Re-
tournons dans notre designer et cliquons dessus pour observer les propriétés du modèle.
Nous allons modifier le nom du conteneur d’entités pour y mettre un nom un peu
plus parlant, à savoir BaseDeDonnees (voir la figure 37.37).
C’est le point d’entrée de notre accès aux données. Il s’agit en fait d’une classe qui a
été générée par le designer d’Entity Framework.
454
SE CONNECTER À LA BASE DE DONNÉES, LIRE ET ÉCRIRE
car non seulement il génère toutes les classes représentant les données en base (comme
la classe Produit ou la classe Rayon) mais il s’occupe également de nous simplifier
la création, la lecture ou la modification des données en base. Si nous avions dû le
faire à la main comme c’était le cas avant l’utilisation d’ORMs, cela aurait mérité une
centaine de pages supplémentaires d’explications et de code à comprendre. Là, nous
nous positionnons en tant qu’utilisateur de ces classes générées et vous allez voir que
c’est facile à utiliser ; vous n’imaginez pas le plaisir que c’est de constater que l’ORM
a travaillé pour nous !
Nous pouvons désormais instancier la classe BaseDeDonnees générée en lui passant en
paramètre la chaîne de connexion :
1 string chaineConnexion = C o n f i g u r a t i o n M a n a g e r . Connecti onString s
[ " No tr eBa se De Don ne es " ]. ConnectionString ;
2 BaseDeDonnees baseDeDonnees = new BaseDeDonnees ( chaineConnexion
);
Vous voilà connectés à la base de données. Nous allons pouvoir utiliser les objets que
Visual C# Express a générés à travers cette variable de type BaseDeDonnees, comme
par exemple la propriété Rayons qui nous permet d’accéder aux rayons de notre base :
1 foreach ( Rayon rayon in baseDeDonnees . Rayons )
2 {
3 Console . WriteLine ( " { 0 } ({ 1 }) " , rayon . Nom , rayon . Description
);
4 }
Ici, nous pouvons parcourir la liste des rayons avec un foreach car la propriété Rayons
est du type ObjectSet<> qui implémente IEnumerable<>.
Ce qui donne :
De même, nous pouvons parcourir tous les produits grâce à la propriété Produits :
1 foreach ( Produit produit in baseDeDonnees . Produits )
2 {
3 Console . WriteLine ( " { 0 } : { 1 } " , produit . Nom , produit . Prix ) ;
4 }
Ce qui donne :
T é l é HD : 299
Console de jeux : 150
Canap é : 400
Cuisini è re : 280
Bouilloire : 19
455
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
Et ce qui est formidable, c’est qu’étant donné que la propriété Produits est énumé-
rable, nous allons pouvoir y faire toutes les requêtes LINQ que nous le souhaitons, par
exemple :
1 IEnumerable < Produit > produits = from produit in baseDeDonnees .
Produits
2 where produit . Prix > 150
3 orderby produit . Prix descending
4 select produit ;
5 foreach ( Produit produit in produits )
6 {
7 Console . WriteLine ( " { 0 } : { 1 } " , produit . Nom , produit . Prix ) ;
8 }
Nous obtenons tous les produits dont le prix est supérieur à 150, triés par prix décrois-
sant :
Canap é : 400
Tablette PC : 350
Smartphone : 320
T é l é HD : 299
Cuisini è re : 280
Pratique ! Tout le SQL nécessaire pour renvoyer cette liste de produits filtrée a été
généré par Entity Framework. Nous n’avons rien à faire d’autre que d’utiliser le C#.
Et voilà. Avouez que c’est quand même super simple, non ? Avouez également que, si
vous avez l’habitude de tout faire à la main dans un autre langage de programmation,
vous êtes émerveillés ! J’exagère peut-être un peu, mais Entity Framework nous fait
gagner un temps considérable au développement ainsi que tout au long de la vie de
l’application.
Merci à lui de nous avoir généré tout le code adéquat. À propos de génération de code,
souvenez-vous que les classes générées sont partielles. Nous en avons déjà parlé dans le
chapitre dédié, mais je vous rappelle le but ici. Il s’agit de permettre d’ajouter des fonc-
tionnalités à la classe sans avoir à modifier le fichier ModelCommerce.Designer.cs. En
effet, à chaque fois que nous faisons une modification sur notre modèle (ajout d’entité,
changement de nom, etc.), il régénère toutes les classes de ce fichier. Si nous avions
modifié des choses à la main dedans, elles vont disparaître. . . Le mot-clé partial nous
offre l’opportunité d’ajouter des fonctionnalités à la classe depuis un autre fichier. Nous
pouvons en profiter pour rajouter nos propres méthodes, par exemple une méthode qui
renvoie les produits dont le prix est supérieur à un prix passé en paramètre. Il suffit de
456
SE CONNECTER À LA BASE DE DONNÉES, LIRE ET ÉCRIRE
déclarer une classe partielle du même nom que la classe BaseDeDonnees, située dans le
même espace de noms et de rajouter des méthodes. Par exemple :
1 public partial class BaseDeDonnees
2 {
3 public IEnumerable < Produit > P r od u it s P lu s Ch e rQ u e ( decimal
prix )
4 {
5 return from produit in Produits
6 where produit . Prix > prix
7 select produit ;
8 }
9 }
Ce qui donnera :
T é l é HD : 299
Canap é : 400
Cuisini è re : 280
Tablette PC : 350
Smartphone : 320
Remarquons que chaque Rayon possède également une propriété Produits ; c’est la
propriété de navigation que nous avons renommée précédemment. Entity Framework
a donc compris qu’il y avait une relation entre les rayons et les produits et il permet
d’accéder aux produits qui font partie du rayon, grâce à cette propriété. Ainsi, nous
pouvons écrire un code, comme le suivant, qui accède à la propriété Produits d’un
rayon et permet d’afficher la liste de tous les produits de chaque rayon :
1 foreach ( Rayon rayon in baseDeDonnees . Rayons )
2 {
3 Console . WriteLine ( " { 0 } ({ 1 }) " , rayon . Nom , rayon . Description
);
4 foreach ( Produit produit in rayon . Produits )
5 {
6 Console . WriteLine ( " \ t { 0 } : { 1 } " , produit . Nom , produit .
Prix ) ;
7 }
8 }
Sauf qu’ici nous rencontrons un problème. Si nous exécutons ce bout de code, nous
aurons l’exception suivante :
457
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
En fait, cela vient de la façon dont sont récupérées les données. Lorsque nous accédons
à la propriété Rayons, Entity Framework génère une requête en base de données pour
récupérer la liste des rayons. Puis à l’intérieur de la boucle, lorsque nous accédons à
la propriété Produits d’un rayon, il génère à nouveau une requête pour récupérer les
produits de ce rayon. Il y a donc deux connexions à la base de données en même temps,
et ça, il ne sait pas le faire par défaut.
Il y a plusieurs façons de corriger le problème. La première est de changer la chaîne
de connexion en rajoutant une directive permettant de préciser qu’on autorise l’accès
multiple, à savoir :
1 m u l t i p l e a c t i v e r e s u l t s e t s = True ;
458
SE CONNECTER À LA BASE DE DONNÉES, LIRE ET ÉCRIRE
La deuxième solution est de faire en sorte que la première requête soit terminée avant
l’exécution des suivantes. Pour cela, il suffit de forcer l’évaluation de la requête en
utilisant par exemple un ToList() :
1 foreach ( Rayon rayon in baseDeDonnees . Rayons . ToList () )
2 {
3 Console . WriteLine ( " { 0 } ({ 1 }) " , rayon . Nom , rayon . Description
);
4 foreach ( Produit produit in rayon . Produits )
5 {
6 Console . WriteLine ( " \ t { 0 } : { 1 } " , produit . Nom , produit .
Prix ) ;
7 }
8 }
Ceci est possible, car Entity Framework bénéficie de l’exécution différée ; le ToList()
résout le problème en forçant l’exécution de la requête.
Enfin, la dernière solution est de faire en sorte que la première requête qui charge les
Rayons inclue également le chargement des produits. Ainsi, il n’y a qu’une seule et
unique requête qui charge tout. Cela se passe avec la méthode Include, en précisant
le nom de la propriété de navigation à charger :
1 foreach ( Rayon rayon in baseDeDonnees . Rayons . Include ( " Produits "
))
2 {
3 Console . WriteLine ( " { 0 } ({ 1 }) " , rayon . Nom , rayon . Description
);
4 foreach ( Produit produit in rayon . Produits )
5 {
6 Console . WriteLine ( " \ t { 0 } : { 1 } " , produit . Nom , produit .
Prix ) ;
7 }
8 }
459
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
qu’Entity Framework va aller lire le contenu associé en base de données. Ceci implique
qu’à chaque tentative d’accès à la propriété Produits d’un rayon, Entity Framework
va effectuer une requête en base de données pour ramener les produits concernés. C’est
très bien si on fait ça une ou deux fois, mais dans notre cas au final on fait autant de
requête qu’il y a de rayons. Ce qui n’est pas très performant. . . Le troisième scénario
montre l’utilisation de la méthode Include qui permet de tout rapatrier en une seule
requête, ce qui est évidemment plus performant.
Alors, vous ne trouvez pas que la lecture en base de données est particulièrement aisée ?
Merci Entity Framework ! Notons que nous pouvons également accéder aux rayons
dans lesquels sont positionnés les produits grâce à la propriété Rayons. Nous pourrions
éventuellement nous en servir pour afficher le nombre de rayons dans lesquels le produit
est présent.
L’écriture en base de données est tout aussi aisée. Le principe est d’ajouter des valeurs
à notre objet de base de données et de sauvegarder les modifications.
Pour ajouter un nouveau rayon, il suffit d’appeler la méthode AddObject disponible
sur la propriété Rayons. Il ne faudra pas oublier d’appeler la méthode SaveChanges qui
s’occupe d’insérer physiquement les valeurs en base de données. Pour créer un nouveau
rayon, il suffira d’instancier un objet Rayon, de renseigner des propriétés, de créer des
produits et de les ajouter au rayon, par exemple :
1 Rayon rayon = new Rayon () ;
2 rayon . Nom = " Vins " ;
3 rayon . Description = " Venez d é couvrir notre s é lection des plus
grands ch â teaux " ;
4
5 Produit produit1 = new Produit () ;
6 produit1 . Nom = " Ch â teau ronto " ;
7 produit1 . Prix = 9 . 99M ;
8 produit1 . Stock = 60 ;
9 produit1 . UrlImage = " à compl é ter ... " ;
10
11 Produit produit2 = new Produit () ;
12 produit2 . Nom = " Ch â teau toro " ;
13 produit2 . Prix = 15 ;
14 produit2 . Stock = 6 ;
15 produit2 . UrlImage = " à compl é ter ... " ;
16
17 rayon . Produits . Add ( produit1 ) ;
18 rayon . Produits . Add ( produit2 ) ;
19
20 baseDeDonnees . Rayons . AddObject ( rayon ) ;
21 baseDeDonnees . SaveChanges () ;
460
SE CONNECTER À LA BASE DE DONNÉES, LIRE ET ÉCRIRE
Ainsi, si nous réaffichons la liste des rayons, nous pourrons voir un rayon de plus
contenant des produits en plus. . .
De même, si vous allez voir en base de données, vous aurez bien les nouveaux éléments,
ainsi que l’illustre la figure 37.38.
On observe la création d’un nouveau rayon, de deux nouveaux produits et nous avons
bien dans la table de relation les nouveaux produits reliés au nouveau rayon. Il est
également possible d’ajouter un produit à un rayon. Nous pouvons le faire de deux
manières différentes. La première est d’ajouter un Produit directement dans la collec-
tion Produits d’un rayon, Il sera directement ajouté dans le rayon de notre choix. La
deuxième est d’ajouter un produit dans la collection Produits et si nous voulons qu’il
soit présent dans un rayon, il faudra que sa propriété Rayons contienne les rayons dans
lesquels nous souhaitons ajouter le produit. Voyons la première méthode :
1 Rayon rayon = baseDeDonnees . Rayons . First ( r = > r . Nom == " Vins " ) ;
2
3 Produit produit = new Produit () ;
4 produit . Nom = " Chateau pini è re " ;
5 produit . Prix = 12 . 50M ;
6 produit . Stock = 40 ;
7 produit . UrlImage = " vin . jpg " ;
8 produit . Rayons . Add ( rayon ) ;
9
10 baseDeDonnees . SaveChanges () ;
461
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
Figure 37.38 – Le nouveau rayon est les nouveaux produits sont visibles dans les tables
Nous commençons par récupérer un rayon, puis nous instancions un objet de type
Produit. Enfin, nous faisons le lien entre le produit et le rayon en ajoutant le rayon à la
collection Rayons de notre objet produit. Comme d’habitude, la méthode SaveChanges()
permet de faire persister les informations.
La seconde méthode est un peu plus simple à appréhender ; il suffit d’instancier un
objet Produit et de l’ajouter à la collection Produits d’un rayon :
1 Rayon rayon = baseDeDonnees . Rayons . First ( r = > r . Nom == " Vins " ) ;
2
3 Produit produit = new Produit () ;
4 produit . Nom = " Chateau r é ro " ;
5 produit . Prix = 3 . 20M ;
6 produit . Stock = 10 ;
7 produit . UrlImage = " vin1 . jpg " ;
8
9 rayon . Produits . Add ( produit ) ;
10
11 baseDeDonnees . SaveChanges () ;
Dans les deux cas, Entity Framework arrive à faire le lien entre un rayon et un produit.
Le château pinière et le château réro ont bien été ajoutés. . . ! Vous pouvez également
vérifier en base de données que la relation entre le rayon et le produit a bien été faite.
Il est également possible de modifier des enregistrements en base de données. Le prin-
cipe est de modifier l’élément concerné dans l’objet de base de données et d’appeler la
462
SE CONNECTER À LA BASE DE DONNÉES, LIRE ET ÉCRIRE
méthode SaveChanges() :
1 Rayon rayon = baseDeDonnees . Rayons . First ( r = > r . Nom == " Vins " ) ;
2 rayon . Description = " Les meilleurs vins " ;
3 baseDeDonnees . SaveChanges () ;
Enfin, nous pouvons aussi supprimer des données en base. Le principe est le même
que pour l’ajout. Nous appelons une méthode qui s’occupe de la suppression et nous
appelons la méthode SaveChanges. Par contre, il faut faire attention à l’intégrité des
données. On ne peut pas supprimer un rayon qui a des produits dedans. Il faut com-
mencer par retirer la relation entre les produits et le rayon :
1 Rayon rayon = baseDeDonnees . Rayons . First ( r = > r . Nom == " Vins " ) ;
2 rayon . Produits . Clear () ;
3 baseDeDonnees . DeleteObject ( rayon ) ;
4 baseDeDonnees . SaveChanges () ;
Le fait d’appeler la méthode Clear() sur les produits du rayon vide le rayon de ses
produits. Il n’y a donc plus de produits dans ce rayon, mais ils existent toujours en
base de données car ils n’ont pas été supprimés physiquement. C’est important car
ces produits peuvent également être présents dans d’autres rayons, nous ne pouvons
donc pas les supprimer. Ensuite, on utilise la méthode DeleteObject pour supprimer
un élément de la base de données. Ici, nous supprimons le rayon et nous validons les
modifications avec SaveChanges().
Notez que si nous n’avions pas vidé le rayon de ses produits, la suppression du rayon
aurait été impossible car comme il existe une relation entre les produits et les rayons
et que notre base de données possède une contrainte d’intégrité (la clé étrangère) entre
les produits et le rayon, la suppression aurait provoqué une erreur. En effet, la table
RayonProduit contiendrait un identifiant de rayon qui n’existe plus. C’est impossible !
La contrainte de la clé étrangère est là pour nous assurer que nous ne faisons pas
n’importe quoi dans la base de données et qu’elle est toujours cohérente. Si nous l’avions
fait, nous aurions eu l’exception suivante :
Exception non g é r é e : System . Data . UpdateException : Une erreur s ’
est produite lors de la mise à jour des entr é es . Pour plus d ’
informations , consultez l ’ exception interne . ---> System . Data
. SqlClient . SqlException : L ’ instruction DELETE est en conflit
avec la contrainte REFERENCE " FK \ _RayonProduit \ _Rayon ". Le
conflit s ’ est produit dans la base de donn é es " basecommerce " ,
table " dbo . RayonProduit " , column ’ Rayons \ _ID ’.
L ’ instruction a é t é arr ê t é e . [...]
Remarquez que nous aurons désormais des produits orphelins. Tout dépend de ce que
l’on veut faire maintenant. Souhaitons-nous qu’ils soient supprimés également vu qu’ils
ne sont plus dans aucun rayon ? Souhaitons-nous qu’ils restent présents pour pouvoir
les ajouter ultérieurement à un autre rayon ? Ça, c’est vous qui décidez ! Maintenant
que vous savez supprimer des objets, vous pouvez faire comme bon vous semble.
Notons avant de terminer qu’il est tout à fait possible de faire plusieurs ajouts, modi-
fications ou suppressions en même temps. Il suffira de terminer toutes les instructions
463
CHAPITRE 37. ACCÉDER AUX DONNÉES AVEC ENTITY FRAMEWORK
En résumé
– Entity Framework est l’outil de mapping objet relationnel de Microsoft permettant
de travailler sur une base de données relationnelle avec une approche orientée objet.
– Entity Framework est capable de modéliser des données et de générer les tables
correspondantes en base de données sans qu’il soit nécessaire de maîtriser le SQL.
– Il simplifie grandement la lecture et l’écriture des données en base et tire parti, si
besoin, des mécanismes de chargement paresseux.
464
Chapitre 38
Les tests unitaires
Difficulté :
U
ne des grandes préoccupations des créateurs de logiciels est d’être certains que
leur application informatique fonctionne et surtout qu’elle fonctionne dans toutes les
situations possibles. Nous avons tous déjà vu notre système d’exploitation planter,
ou bien notre logiciel de traitement de texte nous faire perdre les 50 pages de rapport que
nous étions en train de taper. Ou encore, un élément inattendu dans un jeu où l’on arrive
à passer à travers un mur alors qu’on ne devrait pas. . .
Bref, pour être sûr que son application fonctionne, il faut faire des tests.
465
CHAPITRE 38. LES TESTS UNITAIRES
Un test est donc un bout de code qui permet de tester un autre code.
Faire un test consiste à écrire des bouts de code permettant de s’assurer que le code
fonctionne. Cela peut-être par exemple :
1. En français, le TDD se dit « développement piloté par les tests ».
466
NOTRE PREMIER TEST
Ici, le test passe bien, ouf ! Pour être complet, le test doit couvrir un maximum de
situations ; il faut donc tester notre code avec d’autres valeurs, et ne pas oublier les
valeurs limites :
1 static void Main ( string [] args )
2 {
3 int a = 1 ;
4 int b = 2 ;
5 int resultat = Addition (a , b ) ;
6 if ( resultat != 3 )
7 Console . WriteLine ( " Le test a rat é " ) ;
8 a = 0;
9 b = 0;
10 resultat = Addition (a , b ) ;
11 if ( resultat != 0 )
12 Console . WriteLine ( " Le test a rat é " ) ;
13 a = -5 ;
14 b = 5;
15 resultat = Addition (a , b ) ;
16 if ( resultat != 0 )
17 Console . WriteLine ( " Le test a rat é " ) ;
18 }
Voilà pour le principe. Ici, nous considérons avoir écrit suffisamment de tests pour
nous assurer que cette méthode est bien fonctionnelle. Bien sûr, cette méthode était
par définition fonctionnelle, mais il est important de prendre le réflexe de tester des
fonctionnalités qui sont déterminantes pour notre application.
Voyons maintenant comment nous pourrions tester une méthode avec l’approche TDD.
Pour rappel, lors d’une approche TDD, le but est de pouvoir faire un développement à
partir des cas de tests préalablement établis par la personne qui exprime le besoin ou
suivant les spécifications fonctionnelles.
Imaginons que nous voulions tester une méthode qui calcule la factorielle d’un nombre.
Nous savons que la factorielle de 0 vaut 1, la factorielle de 1 vaut 1. Commençons par
écrire les tests :
1 static void Main ( string [] args )
2 {
3 int valeur = 0 ;
467
CHAPITRE 38. LES TESTS UNITAIRES
Le code ne compile pas ! Forcément, nous n’avons pas encore créé la méthode Factorielle.
C’est la première étape. La suite de la méthode est de faire en sorte que le test compile,
mais il échouera puisque la méthode n’est pas encore implémentée :
1 public static int Factorielle ( int a )
2 {
3 throw new N o t I m p l e m e n t e d E x c e p t i o n () ;
4 }
Il faudra ensuite écrire le code minimal qui servira à faire passer nos deux tests. Cela
peut-être :
1 public static int Factorielle ( int a )
2 {
3 return 1 ;
4 }
Si nous exécutons nos tests, nous voyons que cette méthode est fonctionnelle car ils
passent tous. La suite de la méthode consiste à refactoriser le code, à l’optimiser. Ici,
il n’y a rien à faire, car c’est vraiment simple. On se rend compte par contre qu’on
n’a pas couvert énormément de cas de tests ; faire juste des tests avec 0 et 1 c’est un
peu léger. . . Nous savons que la factorielle de 2 vaut 2, la factorielle de 3 vaut 6, la
factorielle de 4 vaut 24, etc. Continuons à écrire des tests (il faut bien sûr garder les
anciens tests afin d’être sûrs qu’on couvre un maximum de cas) :
1 static void Main ( string [] args )
2 {
3 int valeur = 0 ;
4 int resultat = Factorielle ( valeur ) ;
5 if ( resultat != 1 )
6 Console . WriteLine ( " Le test a rat é " ) ;
7
8 valeur = 1 ;
9 resultat = Factorielle ( valeur ) ;
10 if ( resultat != 1 )
11 Console . WriteLine ( " Le test a rat é " ) ;
12
13 valeur = 2 ;
14 resultat = Factorielle ( valeur ) ;
15 if ( resultat != 2 )
16 Console . WriteLine ( " Le test a rat é " ) ;
468
NOTRE PREMIER TEST
17
18 valeur = 3 ;
19 resultat = Factorielle ( valeur ) ;
20 if ( resultat != 6 )
21 Console . WriteLine ( " Le test a rat é " ) ;
22
23 valeur = 4 ;
24 resultat = Factorielle ( valeur ) ;
25 if ( resultat != 24 )
26 Console . WriteLine ( " Le test a rat é " ) ;
27 }
Et nous pouvons écrire une méthode Factorielle qui fait passer ces tests :
1 public static int Factorielle ( int a )
2 {
3 if ( a == 2 )
4 return 2 ;
5 if ( a == 3 )
6 return 6 ;
7 if ( a == 4 )
8 return 24 ;
9 return 1 ;
10 }
Lançons les tests : nous voyons que tout est Ok. Cependant, nous n’allons pas faire des
if en déclinant tous les cas possibles, il faut donc repasser par l’étape d’amélioration
et de refactorisation du code, afin d’éviter les redondances de code, d’améliorer les
algorithmes, etc. Cette opération devient sans risque puisque le test est là pour nous
assurer que la modification que l’on vient de faire est sans régression, si le test passe
toujours bien sûr. . . Nous voyons que nous pouvons améliorer le code en utilisant la
vraie formule de la factorielle :
1 public static int Factorielle ( int a )
2 {
3 int total = 1 ;
4 for ( int i = 1 ; i <= a ; i ++)
5 {
6 total *= i ;
7 }
8 return total ;
9 }
Ce qui permet d’illustrer que, par exemple, la factorielle de 5 est égale à 1×2×3×4×5.
Relançons nos tests, ils passent tous. Parfait. Nous sommes donc certains que notre
changement de code n’a pas altéré la fonctionnalité, car les tests continuent de passer.
On peut même rajouter des tests pour le plaisir, comme la factorielle de 10, histoire
d’avoir quelque chose d’un peu plus grand :
1 valeur = 10 ;
2 resultat = Factorielle ( valeur ) ;
469
CHAPITRE 38. LES TESTS UNITAIRES
3 if ( resultat != 3628800 )
4 Console . WriteLine ( " Le test a rat é " ) ;
Est-ce que cette méthode est optimisable ? Sûrement. Est-ce qu’il y a un risque à opti-
miser cette méthode ? Aucun ! En effet, nos tests nous garantissent que s’ils continuent
à passer, alors une optimisation n’entraîne pas de régression dans la fonctionnalité. On
sait par exemple qu’il y a un autre moyen pour calculer une factorielle. Par exemple,
pour calculer la factorielle de 5, il suffit de multiplier 5 par la factorielle de 4. Pour
calculer la factorielle de 4, il faut multiplier 4 par la factorielle de 3, et ainsi de suite
jusqu’à arriver à 1. . . Bref, pour obtenir une factorielle on peut se servir du résultat de
la factorielle du nombre précédent. Ce qui peut s’écrire :
1 public static int Factorielle ( int a )
2 {
3 if ( a <= 1 )
4 return 1 ;
5 return a * Factorielle ( a - 1 ) ;
6 }
Ici la méthode Factorielle est une méthode récursive, c’est-à-dire qu’elle s’appelle
elle-même. Cela nous permet d’indiquer que la factorielle d’un nombre correspond à ce
nombre multiplié par la factorielle du nombre précédent. Bien sûr, il faut s’arrêter à un
moment dans la récursion. On s’arrête ici quand on atteint le chiffre 1. Pour s’assurer
que cette factorielle fonctionne bien, il suffit de relancer les tests. Tout est Ok, c’est
parfait !
Voilà donc un exemple de TDD. Bien sûr, la méthode est ici poussée au maximum
pour que vous compreniez l’intérêt de cette pratique. On peut gagner du temps en
partant directement sur la bonne implémentation. Vous verrez qu’il y a toujours des
premiers essais qui satisfont les tests mais qu’il sera possible d’améliorer régulièrement
le code. Ceci devient possible grâce aux tests qui nous assurent que tout continue à bien
fonctionner. La pratique du TDD dépend de la façon dont le développeur appréhende
sa philosophie de développement. Elle est présentée ici pour sensibiliser ce dernier à
cette pratique mais son utilisation n’est pas du tout obligatoire. Voilà pour les tests
basiques. Cependant, utiliser une application console pour faire ses tests, ce n’est pas
très pratique, vous en conviendrez. Nous avons besoin d’outils !
Le framework de test
Un framework de test est aux tests ce qu’un IDE est au développement. Il fournit un
environnement structuré permettant l’exécution de tests, ainsi que des méthodes pour
aider au développement de ceux-ci. Il existe plusieurs frameworks de test. Microsoft
dispose de son framework, mstest, qui est disponible dans les versions payantes de
Visual Studio. Son intérêt est qu’il est fortement intégré à l’IDE. Son défaut est qu’il
ne fonctionne pas avec les versions gratuites de l’environnement de développement.
Comme nous sommes partis dans ce livre avec la version gratuite, Visual C# Express,
nous n’allons pas pouvoir utiliser mstest.
470
LE FRAMEWORK DE TEST
Par contre, il existe d’autres framework de test, gratuits, comme le très connu NUnit.
NUnit est la version .NET du framework XUnit, qui se décline pour plusieurs envi-
ronnements, avec par exemple PHPUnit pour le langage PHP, JUnit, pour java, etc.
Première chose à faire : installer NUnit. Pour cela, utilisez le code web suivant :
☛ ✟
Télécharger NUnit
⊲
Code web : 481629
✡ ✠
La version que j’utilise dans ce livre est la version 2.5.10. Démarrez l’installation, comme
indiqué à la figure 38.1.
L’installation est en anglais, mais assez facile à suivre. Cliquez sur Next puis, après
avoir accepté la licence, vous pouvez choisir l’installation classique (voir la figure 38.2).
Une fois le framework de test installé, nous pouvons créer un nouveau projet qui
contiendra une fonctionnalité à tester. Je l’appelle MaBibliothequeATester. En gé-
néral, nous allons surtout tester des assemblys avec NUnit. Je crée donc un projet de
type bibliothèque de classes. Ce projet ne sera pas exécutable, car il ne s’agit pas d’une
application console. À l’intérieur, je vais pouvoir créer une classe utilitaire, disons Math,
qui contiendra notre méthode de calcul de factorielle :
1 public static class Math
2 {
3 public static int Factorielle ( int a )
4 {
5 if ( a <= 1 )
6 return 1 ;
7 return a * Factorielle ( a - 1 ) ;
8 }
9 }
471
CHAPITRE 38. LES TESTS UNITAIRES
Nous allons pouvoir créer des méthodes à l’intérieur de cette classe. De la même façon,
une méthode pourra être reconnue comme une méthode de test si elle est décorée de
l’attribut [Test]. Ici aussi, il est intéressant de suivre une règle de nommage afin de
pouvoir identifier rapidement l’intention de la méthode de test. Je vous propose le
nommage suivant : MethodeTestee_EtatInitial_EtatAttendu(). Par exemple, une
méthode de test permettant de tester la factorielle pourrait s’appeler :
1 [ TestFixture ]
2 public class MathTests
3 {
4 [ Test ]
472
LE FRAMEWORK DE TEST
5 public void F a c t o r i e l l e _ A v e c V a l e u r 3 _ R e t o u r n e 6 ()
6 {
7 // test à faire
8 }
9 }
Il existe plein d’autres attributs que vous découvrirez ultérieurement. Il est temps de
passer à l’écriture du test et surtout à la vérification du résultat. Pour cela, on utilise
des méthodes de NUnit qui nous permettent de vérifier par exemple qu’une valeur est
égale à une autre attendue. Cela se fait grâce à la méthode Assert.AreEqual() :
1 [ Test ]
2 public void F a c t o r i e l l e _ A v e c V a l e u r 3 _ R e t o u r n e 6 ()
3 {
4 int valeur = 3 ;
5 int resultat = M a B i b l i o t h e q u e A T e s t e r . Math . Factorielle (
valeur ) ;
6 Assert . AreEqual (6 , resultat ) ;
7 }
Elle permet de vérifier que la variable valeur vaut bien 6. Rajoutons tant qu’on y est
une méthode de test qui échoue :
1 [ Test ]
2 public void F a c t o r i e l l e _ A v e c V a l e u r 1 0 _ R e t o u r n e 1 ()
3 {
4 int valeur = 10 ;
5 int resultat = M a B i b l i o t h e q u e A T e s t e r . Math . Factorielle (
valeur ) ;
473
CHAPITRE 38. LES TESTS UNITAIRES
Il est important que chaque méthode qui s’occupe de tester une fonctionnalité,
le fasse à l’aide d’un cas unique comme illustré juste au-dessus. La première
méthode teste la fonctionnalité Factorielle pour le cas où la valeur vaut
3 et la seconde s’occupe du cas où la valeur vaut 10. Vous pouvez rajouter
autant de méthodes de tests que vous le souhaitez tant qu’elles sont décorées
de l’attribut [Test].
J’en ai profité pour ajouter un message qui permettra d’indiquer des informations com-
plémentaires si le test échoue. Compilez maintenant le projet, allez dans le répertoire
d’installation de NUnit (C:\Program Files\NUnit 2.5.10\bin\net-2.0) et lancez
l’application nunit.exe (voir figure 38.4).
La première chose à faire est de créer un nouveau projet (voir la figure 38.5).
Appelez-le ProjetTest par exemple. Il faut ensuite ajouter une assembly de test ; allez
dans Project > Add Assembly comme indiqué à la figure 38.6.
Pour finir, allez pointer l’assembly de tests, à savoir MathTests.Unit.dll. NUnit ana-
lyse l’assembly et fait apparaître la liste des tests qui composent notre assembly, en se
basant sur les attributs TestFixture et Test (voir la figure 38.7).
Nous pouvons à présent lancer les tests en cliquant sur Run ! On s’aperçoit rapidement,
en observant la figure 38.8, qu’il y a un test qui passe (icône verte) et un test qui échoue
(icône rouge).
Forcément, notre test n’était pas bon, il faut le réécrire. Nous voyons également qu’il
474
LE FRAMEWORK DE TEST
475
CHAPITRE 38. LES TESTS UNITAIRES
476
LE FRAMEWORK DE TEST
nous indique que le résultat attendu était 1 alors que le résultat obtenu est de 3628800.
Nous pouvons également voir le message que nous avons demandé d’afficher en cas
d’erreur. Le souci avec NUnit, c’est qu’à partir du moment où il a chargé la dll pour
lancer les tests, il n’est plus possible de faire de modifications, car toute tentative de
compilation provoquera une erreur où il sera mentionné qu’il ne peut pas faire de modi-
fications car le fichier est déjà utilisé ailleurs. Ce qui est vrai. Nous serons donc obligés
de fermer puis de rouvrir NUnit. À noter que dans les versions payantes de Visual
Studio, nous avons la possibilité de configurer NUnit en tant qu’outil externe, ce que
nous ne pouvons pas faire avec la version gratuite. Il va falloir faire avec. . . C’est un
des inconvénients de la gratuité. . . ! Nous pouvons cependant un peu tricher en définis-
sant un événement de post-compilation, qui consiste à lancer NUnit automatiquement.
Pour cela, allez dans les propriétés du projet, onglet Événements de build et tapez la
commande suivante : "C:\Program Files\NUnit 2.5.10\bin\net-2.0\nunit.exe"
$(TargetFileName). Ici, nous indiquons qu’après la compilation, il va lancer le pro-
gramme nunit.exe en prenant en paramètre le résultat de la compilation, représenté
par la variable interne de Visual C# Express : « $(TargetFileName) » (voir la figure
38.9).
Par contre, cela veut dire que NUnit va se lancer à chaque compilation, ce qui n’est peut-
être pas le but recherché. . . Il faudra également fermer NUnit avant de pouvoir faire
quoi que ce soit d’autre. À noter que maintenant que nous savons faire de l’introspection
sur les méthodes et les attributs d’une classe, nous devrions être capables de créer une
petite application qui exécute les tests automatiquement ! Pour en finir avec NUnit,
notons qu’il y a beaucoup de méthodes permettant de vérifier si un résultat est correct.
Regardons les assertions suivantes :
1 bool b = true ;
477
CHAPITRE 38. LES TESTS UNITAIRES
2 Assert . IsTrue ( b ) ;
3 string s = null ;
4 Assert . IsNull ( s ) ;
5 int i = 10 ;
6 Assert . Greater (i , 6 ) ;
Elles parlent d’elles-mêmes. La première permet de vérifier qu’une condition est vraie.
La deuxième permet de vérifier la nullité d’une variable. La dernière permet de vé-
rifier qu’une variable est bien supérieure à une autre. À noter qu’elles ont chacune
leur pendant (IsFalse, IsNotNull, Less). En regardant la complétion automatique,
vous découvrirez d’autres méthodes de vérification, mais celles-ci sont globalement suf-
fisantes. Nous pouvons également utiliser une syntaxe un peu plus parlante, surtout
pour les anglophones :
1 Assert . That (i , Is . EqualTo ( 10 ) ) ;
Il est également possible d’utiliser un attribut pour vérifier qu’une méthode lève bien
une exception, par exemple :
1 [ Test ]
2 [ Expected Exceptio n ( typeof ( FormatException ) ) ]
3 public void T o I n t 3 2 _ A v e c C h a i n e N o n N u m e r i q u e _ L e v e U n e E x c e p t i o n ()
4 {
5 Convert . ToInt32 ( " abc " ) ;
6 }
478
LE FRAMEWORK DE SIMULACRE
Il existe plein d’autres choses utiles à dire sur NUnit, comme la description des autres
attributs, mais il n’est pas utile de les voir toutes à notre niveau. N’hésitez pas à aller
voir sur internet des informations plus poussées si vous ressentez le besoin d’approfondir
votre maîtrise des tests.
Le framework de simulacre
Un framework de simulacre fournit un moyen de tester une méthode en l’isolant du
reste du système. Imaginons par exemple une méthode qui permette de récupérer la
météo du jour, en allant la lire dans une base de données. Nous avons ici un problème,
car lorsque nous exécutons le test le lundi, il pleut. Quand nous exécutons le test le
mardi, il fait beau, etc. Nous avons ici une information qui varie au cours du temps. Il
est donc difficile de tester automatiquement que la méthode arrive bien à construire la
météo du jour à partir de ces informations, vu qu’elles varient. Le but de ces frameworks
est de pouvoir bouchonner le code dont notre développement dépend, afin de pouvoir
le tester unitairement, sans dépendance et isolé du reste du système. Cela veut dire
que dans notre test, nous allons remplacer la lecture en base de données par une fausse
méthode qui renvoie toujours qu’il fait beau. Cependant, ceci doit se faire sans modifier
notre application, sinon cela n’a pas d’intérêt. Voilà à quoi servent ces framework de
simulacres. Il en existe plusieurs, plus ou moins complexes. Citons par exemple Moq
(prononcez « moque-you ») ou encore Moles (il y en a plein d’autres). L’intérêt de
Moq est qu’il est simple d’accès, nous allons le présenter rapidement. Il permet de faire
des choses simples et facilement. Tandis que Moles est un peu plus évolué mais plus
complexe à prendre en main. Vous y reviendrez sans doute ultérieurement. Pour le
télécharger, utilisez le code web suivant :
☛ ✟
Télécharger Moq
⊲
✡Code web : 225970 ✠
Pas de système d’installation évolué, il y aura juste une dll à référencer. Ajoutez donc
la référence à la dll moq.dll qui se trouve dans le sous-répertoire NET40. Ensuite,
pour pouvoir bouchonner facilement une classe, elle doit implémenter une interface.
Imaginons la classe d’accès aux données suivante :
1 public class Dal : IDal
2 {
3 public Meteo O b t e n i r L a M e t e o Du J o u r ()
4 {
5 // ici , c ' est le code pour lire en base de donn é es
6 // mais finalement , peu importe ce qu ' on y met vu qu ' on
va bouchonner la m é thode
7 throw new N o t I m p l e m e n t e d E x c e p t i o n () ;
8 }
9 }
479
CHAPITRE 38. LES TESTS UNITAIRES
Comme nous allons le voir, nous pouvons également écrire un test qui bouchonne
l’appel à la méthode ObtenirLaMeteoDuJour, qui doit normalement aller lire en base
de données, pour renvoyer un objet à la place. Pour bien montrer ce fonctionnement,
j’ai fait en sorte que la méthode lève une exception, comme ça, si on passe dedans ça
sera tout de suite visible. La méthode de test classique devrait être :
1 [ Test ]
2 public void O b t e n i r L a M e t e o D u J o u r _ A v e c U n B o u c h o n _ R e t o u r n e S o l e i l ()
3 {
4 IDal dal = new Dal () ;
5 Meteo meteoDuJour = dal . O b t e n i r L a M e te o D u J o u r () ;
6 Assert . AreEqual ( 25 , meteoDuJour . Temperature ) ;
7 Assert . AreEqual ( Temps . Soleil , meteoDuJour . Temps ) ;
8 }
Si nous exécutons le test, nous aurons une exception. Utilisons maintenant Moq pour
bouchonner cet appel et le remplacer par ce que l’on veut :
1 [ Test ]
2 public void O b t e n i r L a M e t e o D u J o u r _ A v e c U n B o u c h o n _ R e t o u r n e S o l e i l ()
3 {
4 Meteo fausseMeteo = new Meteo { Temperature = 25 , Temps =
Temps . Soleil };
5 Mock < IDal > mock = new Mock < IDal >() ;
6 mock . Setup ( dal = > dal . O b t e ni r L a M e t e o D u J o u r () ) . Returns (
fausseMeteo ) ;
7
8 IDal fausseDal = mock . Object ;
9 Meteo meteoDuJour = fausseDal . O b t e n ir L a M e t e o D u J o u r () ;
480
LE FRAMEWORK DE SIMULACRE
On utilise l’objet générique Mock pour créer un faux objet du type de notre interface.
On utilise la méthode Setup à travers une expression lambda pour indiquer que la
méthode ObtenirLaMeteoDuJour retournera en fait un faux objet météo. Cela se fait
tout naturellement en utilisant la méthode Returns(). L’avantage de ces constructions
est que la syntaxe parle d’elle-même à partir du moment où l’on connaît les expressions
lambdas. On obtient ensuite une instance de notre objet grâce à la propriété Object
et c’est ce faux objet que nous pourrons comparer à nos valeurs. Bien sûr, ici, ce test
n’a pas grand intérêt. Mais il faut le voir à un niveau plus général. Imaginons que nous
ayons besoin de tester la fonctionnalité qui met en forme cet objet météo récupéré de la
base de données ou bien l’algorithme qui nous permet de faire des statistiques sur ces
données météos. . . Là, nous sommes sûrs de pouvoir nous baser sur une valeur connue
de la dépendance à la base de données. Cela permettra également de décliner tous les
cas possibles en changeant la valeur du bouchon et de faire les tests les plus exhaustifs
possibles. Nous pouvons faire la même chose avec les propriétés. Imaginons la classe
suivante dont la propriété valeur retourne un nombre aléatoire :
1 public interface IGenerateur
2 {
3 int Valeur { get ; }
4 }
5
6 public class Generateur : IGenerateur
7 {
8 private Random r ;
9 public Generateur ()
10 {
11 r = new Random () ;
12 }
13
14 public int Valeur
15 {
16 get
17 {
18 return r . Next (0 , 100 ) ;
19 }
20 }
21 }
Nous pourrions avoir besoin de bouchonner cette propriété pour qu’elle renvoie un
nombre connu à l’avance. Cela se fera de la même façon :
1 Mock < IGenerateur > mock = new Mock < IGenerateur >() ;
2 mock . Setup ( generateur = > generateur . Valeur ) . Returns ( 5) ;
3
4 Assert . AreEqual (5 , mock . Object . Valeur ) ;
481
CHAPITRE 38. LES TESTS UNITAIRES
En résumé
– Les tests unitaires sont un moyen efficace de tester des bouts de code dans une
application afin de garantir son bon fonctionnement.
– Ils sont un filet de sécurité permettant de faire des opérations de maintenance, de
refactoring ou d’optimisation sur le code.
– Les frameworks de tests unitaires sont en général accompagnés d’outils permettant
de superviser le bon déroulement des tests et la couverture de tests.
482
Chapitre 39
Les types d’applications pouvant être
développées en C#
Difficulté :
V
ous savez quoi ? Avec le C# on peut créer autre chose que des applications console !
On peut faire des applications avec des boutons et des menus, ou des sites web et
même des jeux ! Dans ce chapitre, je vais vous indiquer rapidement les différents
types d’applications qu’on peut développer avec le C#. Chaque paragraphe de ce chapitre
nécessiterait un livre entier pour être correctement traité, aussi, ce ne sera qu’un très
bref aperçu. J’espère que vous me pardonnerez d’aller si vite mais je souhaite néanmoins
piquer votre curiosité pour vous donner envie d’aller explorer tous ces nouveaux mondes
qui s’ouvrent à vous ! Il y aura certainement des notions que vous ne comprendrez pas
complètement en fonction des thèmes abordés. Il faudra aller jeter un œil complémentaire
sur internet ou attendre un prochain livre !
483
CHAPITRE 39. LES TYPES D’APPLICATIONS POUVANT ÊTRE
DÉVELOPPÉES EN C#
Créer une application Windows avec WPF
Les applications Windows sont ce qu’on appelle des applications clients lourds. Elles
s’exécutent directement depuis notre système d’exploitation. Nous pouvons créer toutes
sortes d’applications, comme un traitement de texte, une calculatrice, etc.
Nous avons déjà créé une application Windows à travers notre projet console, sauf que
nous étions rapidement limités. Avec WPF, nous allons pouvoir créer des applications
graphiques avec des boutons, des menus, etc. Bref, tout ce qui compose une application
habituelle. WPF signifie Windows Presentation Foundation. Il s’agit d’une bibliothèque
permettant de réaliser des applications graphiques. Ces applications sont dites événe-
mentielles car elles réagissent à des événements (clic sur un bouton, redimensionnement
de la fenêtre, saisie de texte, etc.)
Pour construire des applications WPF, nous aurons besoin de deux langages. Un lan-
gage de présentation qui va permettre de décrire le contenu de notre fenêtre : le
XAML(prononcez « xamelle ») et du C# qui va permettre de faire tout le code métier.
ASP.NET c’est la plateforme de Microsoft pour réaliser des applications web. C’est un
peu comme PHP, sauf que, vous vous en doutez, ASP.NET s’appuie massivement sur
le framework .NET. Et tout comme WPF, il s’agit de bibliothèques qui vont permettre
de réaliser facilement son site web.
Il existe deux ASP.NET : l’ASP.NET WebForms et l’ASP.NET MVC. ASP.NET
WebForms c’est tout un mécanisme qui permet de faciliter la création d’une application
web en faisant comme si c’était une application Windows. C’est-à-dire que le frame-
work s’occupe de gérer toute la persistance d’informations entre les différents états
des pages. Il permet aussi de travailler avec une approche événementielle, comme une
application Windows. Le premier but d’ASP.NET WebForms était de faire en sorte
que les personnes qui avaient déjà fait du développement Windows (avec des langages
comme le Visual Basic ou autre) puissent facilement faire du développement web, dans
un contexte qui leur serait familier.
ASP.NET MVC est plus récent et offre une approche où le développeur doit bien
connaître tous les mécanismes du web. Il offre également une plus grande maîtrise sur
le rendu du site web. Enfin, il intègre par défaut tous les mécanismes éprouvés du fa-
meux patron de conception (design pattern) MVC. On ne peut pas dire qu’ASP.NET
WebForms soit mieux ou moins bien qu’ASP.NET MVC. Il s’agit de deux façons diffé-
rentes de créer des sites web. Chacune a ses avantages et ses inconvénients. Par contre,
les deux se basent sur un socle commun qui est le cœur d’ASP.NET.
484
CRÉER UNE APPLICATION CLIENT RICHE AVEC SILVERLIGHT
485
CHAPITRE 39. LES TYPES D’APPLICATIONS POUVANT ÊTRE
DÉVELOPPÉES EN C#
le savoir que nous avons pu acquérir dans les autres types d’applications C#. En effet,
pour réaliser des applications de gestion, nous allons utiliser Silverlight et pour réaliser
des jeux, nous utiliserons XNA.
486
Pour conclure
Ça y est, ce livre est terminé.
Le C# n’a (presque) plus de secret pour nous. Tout au long de ce livre nous avons
découvert comment développer des applications avec le C#. Il a fallu dans un premier
temps apprendre les bases du C#, ce qui n’est pas sans douleurs quand on n’a jamais
fait de programmation ! Pouvoir appréhender correctement ce qu’est une variable ou
comment dérouler son premier programme n’est pas une mince affaire. Mais petit à
petit, nous avons relevé le défi en présentant des fonctionnalités qui nous serviront tout
le temps, pour la moindre petite application.
Forts de ces nouveaux apprentissages, nous avons ensuite découvert le monde de la
programmation orientée objet et comment le C# était un vrai langage orienté objet. À
travers des notions comme les classes et autres propriétés, nous avons pu tirer parti de
cette façon de modéliser des applications pour les adapter à nos besoins. Je le reconnais,
ce n’est pas une partie facile. Les différents concepts peuvent donner rapidement mal
à la tête ! Ce qui est certain c’est que petit à petit, ils vont devenir de plus en plus
clairs. On peut très bien commencer à développer en orienté objet sans vraiment en
maîtriser toutes les subtilités. C’est en pratiquant et en restant curieux sur le sujet que
vous progresserez.
Enfin, nous avons poussé un peu plus loin dans les arcanes du C#, nous permettant
de réaliser des applications de plus en plus compliquées. Nous avons également montré
comment utiliser les bases de données avec Entity Framework. Savoir lire et écrire dans
une base de données est un élément fondamental pour toute application de gestion qui
se respecte. Nous avons également aperçu ce que l’on pouvait faire avec le C#. De
l’application Windows, en passant par les jeux ou les applications web, le C# combiné
au framework .NET permet vraiment de faire beaucoup de choses !
Vous avez désormais les clés pour démarrer. Il ne reste plus qu’à vous plonger dans
les domaines qui vous intéressent afin de réaliser les applications dont vous avez envie.
Pourquoi pas une application pour les smartphones ? C’est très à la mode.
N’oubliez pas que c’est à force de pratiquer, d’essayer de créer des petites applications
de rien du tout, que vous finirez par être un développeur confirmé, capable à son tour
d’aider les autres. N’hésitez pas à faire et à refaire les TP. Après avoir fini le livre,
vous pourrez sûrement améliorer vos premières versions des TP. Créez-en, fixez vous
des petits défis et venez les échanger avec nous sur le Site du Zéro ! ;-)
Index
488
INDEX
489
Dépôt légal : mars 2012
ISBN : 978-2-9535278-6-5
Code éditeur : 978-2-9535278
Imprimé en France
Achevé d’imprimer le 20 mars 2012
sur les presses de Corlet Imprimeur (Condé-sur-Noireau)
Numéro imprimeur : 144485
Mentions légales :
Conception couverture : Fan Jiyong
Illustrations chapitres : Fan Jiyong