Initiation À La Programmation en C#
Initiation À La Programmation en C#
426 pages
SOMMAIRE
Types, opérateurs, instructions
Introduction …………………………………..…. ………. P.3
Les outils élémentaires …………………………………..……… P.4
Les éléments de base .…………………………………...……. P.12
Les opérateurs + exemples .……………………………………. P.22
Les instructions .…………………………………….………. P.36
A mon épouse Dominique pour son soutien et sa patience qui me permettent de consacrer de nombreuses heures à la
construction du package et des cours inclus et surtout comme la seule personne en dehors de moi qui a eu la constance
de relire entièrement toutes les pages de l'ouvrage, alors que l'informatique n'est pas sa tasse de thé.
Au club des développeurs francophones qui héberge un site miroir du précédent et qui recommande le package
pédagogique à ses visiteurs débutants.
Cette édition a été corrigée durant 2 mois de l’été 2004, elle a été optimisée en nombre de pages papier imprimables.
Remerciements : (anticipés)
Aux lecteurs qui trouveront nécessairement encore des erreurs, des oublis, et autres imperfections et qui voudront bien
les signaler à l’auteur afin d’améliorer le cours.
Une stratégie différente de répartition de l'information et de son traitement est proposée depuis
2001 par Microsoft, elle porte le nom de .NET (ou en anglais dot net). La conception de cette
nouvelle architecture s'appuie sur quelques idées fondatrices que nous énonçons ci-dessous :
Une disparition progressive des différences entre les applications et l'Internet, les serveurs ne
fourniront plus seulement des pages HTML, mais des services à des applications distantes.
Les informations au lieu de rester concentrées sur un seul serveur pourront être réparties sur
plusieurs machines qui proposeront chacune un service adapté aux informations qu'elles
détiennent.
A la place d'une seule appplication, l'utilisateur aura accès à une fédération d'applications
distantes ou locales capables de coopérer entre elles pour divers usages de traitement.
L'utilisateur n'aurait plus la nécessité d'acheter un logiciel, il louerait plutôt les services d'une
action spécifique.
Le micro-ordinateur reste l'intermédiaire incontournable de cette stratégie, il dispose en plus
de la capacité de terminal intelligent pour consulter et traiter les informations de l'utilisateur à
travers Internet où qu'elles se trouvent.
Offrir aux développeurs d'applications .NET un vaste ensemble de composants afin de faire de
la programmation par composant unifiée au sens des protocoles (comme l’utilisation du
protocole SOAP) et diversifiée quant aux lieux où se trouvent les composants.
Afin de mettre en place cette nouvelle stratégie, microsoft procède par étapes. Les fondations de
l'architecture .NET sont posées par l'introduction d'un environnement de développement et
d'exécution des applications .NET. Cet environnement en version stabilisée depuis 2002 porte la
dénomination de .NETFramework, il est distribué gratuitement par microsoft sur toutes les
versions de Windows (98, Me,..., Xp,...).
L'outil Visual Studio .NET contient l'environnement RAD de développement pour l'architecture
.NET. Visual Studio .NET permet le développement d'applications classiques Windows ou
Internet.
Dans ce document nous comparons souvent C# à ses deux parents Java et Delphi afin d'en
signaler les apports et surtout les différences.
Elle comporte plusieurs couches les unes abstraites, les autres en code exécutable :
Les langages de ..NET doivent savoir utiliser tous les composants du CLS
Le C# est le langage de base de .NET, il correspond à une synthèse entre Delphi et Java (le
concepteur principal de .NET. et de C# est l'ancien chef de projet Turbo pascal puis Delphi de
Borland).
Afin de rendre Visual Basic interopérable sur .NET, il a été entièrement reconstruit par microsoft
et devient un langage orienté objet dénommé VB.NET.
La seconde couche est un ensemble de composants graphiques disponibles dans Visual Studio
.NET qui permettent de construire des interfaces homme-machine orientées Web (services Web)
ou bien orientées applications classiques avec IHM.
Les données sont accédées dans le cas des services Web à travers les protocoles qui sont des
standards de l'industrie : HTTP, XML et SOAP.
La troisième couche est constituée d'une vaste librairie de plusieurs centaines de classes :
Toutes ces classes sont accessibles telles quelles à tous les langages de .NET et cette librairie peut
être étendue par adjonction de nouvelles classes. Cette librairie a la même fonction que la
bibliothèque des classes de Java.
Un nom complet de classe comporte le "chemin" hiérarchique de son espace de nom et se termine
par le nom de la classe exemples :
La classe Console qui se trouve dans l'espace de noms "System" se déclare comme
"System.Console".
Rappelons qu'un ordinateur ne sait exécuter que des programmes écrits en instructions machines
compréhensibles par son processeur central. C# comme pascal, C etc... fait partie de la famille des
langages évolués (ou langages de haut niveau) qui ne sont pas compréhensibles immédiatement
par le processeur de l'ordinateur. Il est donc nécesaire d'effectuer une "traduction" d'un
programme écrit en langage évolué afin que le processeur puisse l'exécuter.
Les deux voies utilisées pour exécuter un programme évolué sont la compilation ou
l'interprétation :
Lorsque le processeur P n'est pas une machine qui existe physiquement mais un
logiciel simulant (ou interprétant) une machine on appelle cette machine pseudo-
machine ou p-machine. Le programme source est alors traduit par le compilateur
en instructions de la pseudo-machine et se dénomme pseudo-code. La p-machine
standard peut ainsi être implantée dans n'importe quel ordinateur physique à
travers un logiciel qui simule son comportement; un tel logiciel est appelé
interpréteur de la p-machine.
La première p-machine d'un langage évolué a été construite pour le langage pascal assurant ainsi
une large diffusion de ce langage et de sa version UCSD dans la mesure où le seul effort
d'implementation pour un ordinateur donné était d'écrire l'interpréteur de p-machine pascal, le
reste de l'environnement de développement (éditeurs, compilateurs,...) étant écrit en pascal était
fourni et fonctionnait dès que la p-machine était opérationnelle sur la plate-forme cible.
Donc dans le cas d'une p-machine le programme source est compilé, mais le
programme cible est exécuté par l'interpréteur de la p-machine.
Beaucoup de langages possèdent pour une plate-forme fixée des interpréteurs ou des compilateurs,
moins possèdent une p-machine, Java de Sun est l'un de ces langages. Tous les langages de la
plateforme .NET fonctionnent selon ce principe, C# conçu par microsoft en est le dernier, un
programme C# compilé en p-code, s'exécute sur la p-machine virtuelle incluse dans le CLR.
Compilation native
La compilation native consiste en la traduction du source C# (éventuellement préalablement
traduit instantanément en code intermédiare) en langage binaire exécutable sur la plate-forme
concernée. Ce genre de compilation est équivalent à n'importe quelle compilation d'un langage
dépendant de la plate-forme, l'avantage est la rapidité d'exécution des instructions machines
par le processeur central. La stratégie de développement multi-plateforme de .Net, fait que
Microsoft ne fournit pas pour l’instant, de compilateur C# natif, il faut aller voir sur le net les
entreprises vendant ce type de produit.
Seule une p-machine (dénommée machine virtuelle .NET) est capable d'exécuter ce bytecode. Le
bytecode est aussi dénommé MSIL. En fait le bytecode MSIL est pris en charge par le CLR et
n'est pas interprété par celui-ci mais traduit en code natif du processeur et exécuté par le
processeur sous contrôle du CLR..
ATTENTION
Bien que se terminant par le suffixe exe, un programme issu d'une compilation sous .NET
n'est pas un exécutable en code natif, mais un bytecode en MSIL; ce qui veut dire que vous ne
pourrez pas faire exécuter directement sur un ordinateur qui n'aurait pas la machine virtuelle
.NET, un programme PE "xxx.exe" ainsi construit .
Ci-dessous le schéma d'un programme source Exemple.cs traduit par le compilateur C# sous
.NET en un programme cible écrit en bytecode nommé Exemple.exe
à travers le CTS (Common Type System) qui implémente le CLS (Common Language
Specification), le CLR assure la sécutrité de compatibilité des types connus mais
syntaxiquement différents selon les langages utilisés.
Une fois le programme source C# traduit en bytecode MSIL, la machine virtuelle du CLR se
charge de l'exécuter sur la machine physique à travers son système d'exploitation (Windows,
Unix,...)
On peut mentalement considérer qu'avec cette technique vous obtenez un programme C# cible
compilé en deux passages :
le second passage étant le compilateur JIT lui-même qui optimise et traduit localement à
la volée et à chaque appel de méthode, le bytecode MSIL en instructions du processeur de
la plate-forme. Ce qui donne au bout d'un temps très bref, un code totalement traduit en
instruction du processeur de la plateforme, selon le schéma ci-après :
Tout est objet dans C#, en outre C# est un langage fortement typé. Comme en Delphi et en Java
vous devez déclarer un objet C# ou une variable C# avec son type avant de l'utiliser. C# dispose
de types valeurs intrinsèques qui sont définis à partir des types de base du CLS (Common
Language Specification).
Struct
Les classes encapsulant les types élémentaires dans .NET Framework sont des classes de type
valeur du genre structures. Dans le CLS une classe de type valeur est telle que les allocations
d'objets de cette classe se font directement dans la pile et non dans le tas, il n'y a donc pas de
référence pour un objet de type valeur et lorsqu'un objet de type valeur est passé comme paramètre
il est passé par valeur.
Dans .NET Framework les classes-structures de type valeur sont déclarées comme structures et
ne sont pas dérivables, les classes de type référence sont déclarées comme des classes classiques et
sont dérivables.
Afin d'éclairer le lecteur prenons par exemple un objet x instancié à partir d'une classe de type
référence et un objet y instancié à partir d'un classe de type valeur contenant les mêmes membres
que la classe par référence. Ci-dessous le schéma d'allocation de chacun des deux objets :
Déclaration de classe-structure :
instanciation :
struct StructAmoi {
int b;
void meth(int a){ StructAmoi y = new StructAmoi ( ) ;
b = 1000+a;
}
}
Déclaration de classe :
instanciation :
class ClassAmoi {
int b;
void meth(int a) { ClassAmoi x = new ClassAmoi ( ) ;
b = 1000+a;
}
}
Les classes-structures de type valeur peuvent comme les autres classes posséder un constructeur
explicite, qui comme pour tout classe C# doit porter le même nom que celui de la classe-structure.
Exemple ci-desssous d'une classe-structure dénommée Menulang:
Decimal réeel = entier* 10n (au maximum 28 décimales exactes) 128 bits
Le type System.Int32 qui le type valeur entier signé sur 32 bits dans le CLS.
Voici selon 4 langages de .NET Framework ( VB, C#, C++, J# ) la déclaration syntaxique du
type Int32 :
[Visual Basic]
Public Structure Int32
Implements IComparable, IFormattable, IConvertible
[C#]
public struct Int32 : IComparable, IFormattable, IConvertible
[C++]
public __value struct Int32 : public IComparable, IFormattable,
IConvertible
[J#]
public class Int32 extends System.ValueType implements System.IComparable,
System.IFormattable, System.IConvertible
Les trois premières déclarations comportent syntaxiquement le mot clef struct ou Structure
indiquant le mode de gestion par valeur donc sur la pile des objets de ce type. La dernière
déclaration en J# compatible syntaxiquement avec Java, utilise une classe qui par contre gère ses
Premier pas dans .Net avec C# - ( rév. 05.09.2004) page 14
objets par référence dans le tas. C'est le CLR qui va se charger de maintenir une cohérence
interne entre ces différentes variantes; ici on peut raisonnablement supposer que grâce au
mécanisme d'emboîtage (Boxing) le CLR allouera un objet par référence encapsulant l'objet par
valeur, mais cet objet encapsulé sera marqué comme objet-valeur.
enum
Un type enum est un type valeur qui permet de déclarer un ensemble de constantes de base
comme en pascal. En C#, chaque énumération de type enum, possède un type sous-jacent, qui
peut être de n'importe quel type entier : byte, sbyte, short, ushort, int, uint, long ou ulong.
Le type int est le type sous-jacent par défaut des éléments de l'énumération. Par défaut, le premier
énumérateur a la valeur 0, et l'énumérateur de rang n a la valeur n-1.
Soit par exemple un type énuméré jour :
1°) Il est possible de déclarer classiquement une variable du type jour comme un objet de type
jour, de l'instancier et de l'affecter :
2°) Il est possible de déclarer d'une manière plus courte la même variable du type jour et de
l'affecter :
jour unJour ;
unJour = jour.lundi ;
int rang = (int)unJour;
System.Console.WriteLine("unJour = "+unJour.ToString()+" , place = '+rang);
Résultat de ces 3 lignes de code affiché sur la console :
unJour = lundi , place = 0
Dans cette éventualité faire attention, la comparaison de deux variables de deux types
différents, affectées chacune à une valeur de constante identique dans les deux types, ne
conduit pas à l'égalité de ces variables (c'est en fait le rang dans le type énuméré qui est testé).
L'exemple ci-dessous illustre cette remarque :
Rappelons qu'en C# toute variable qui sert de conteneur à une valeur d'un type élémentaire précis
doit préalablement avoir été déclarée sous ce type.
Remarque importante
Exemples :
Transtypage implicite en C# :
int n = 1234;
float x1 = n ;
double x2 = n ;
double x3 = x1 ;
long p = n ;
.....
Transtypage explicite en C# :
int x;
x = (int) y ; signifie que vous demandez de transtyper la valeur contenue dans la variable
y en un entier signé 32 bits avant de la mettre dans la variable x.
Tous les types élémentaires peuvent être transtypés à l'exception du type boolean qui ne peut
pas être converti en un autre type (différence avec le C).
Les conversions peuvent être restrictives quant au résultat; par exemple le transtypage du
réel 5.27e-2 en entier ( x = (int)5.27e-2) mettra l'entier zéro dans x.
Comme en Java, une variable C# peut contenir soit une valeur d'un type élémentaire, soit une
référence à un objet. Les variables jouent le même rôle que dans les langages de programmation
classiques impératifs, leur visibilité est étudié dans le prochain chapitre.
Identificateur C# :
fonctionnement de l'exemple :
Lorsque la variable car est l'un des caractères '0', '1', ... ,'9', la variable Valeur est égale à la valeur
numérique associée (il s'agit d'une conversion car = '0' ---> Valeur = 0, car = '1' ---> Valeur = 1, ...
, car = '9' ---> Valeur = 9).
const int x ; // erreur , le compilateur n'accepte pas une constante non initialisée.
const int x = 1000 ; // x est déclarée comme constante entière initialisée à 1000.
- Les constantes qualifiées par readonly sont uniquement des variables membre de classes, elles
peuvent être initialisées dans le constructeur de la classe (et uniquement dans le constructeur) :
-Rappelons enfin pour mémoire les constantes de base d'un type énuméré ( cf. enum )
1. Priorité d'opérateurs en C#
Les opérateurs du C# sont très semblables à ceux de Java et donc de C++, ils sont détaillés par
famille, plus loin . Ils sont utilisés comme dans tous les langages impératifs pour manipuler,
séparer, comparer ou stocker des valeurs. Les opérateurs ont soit un seul opérande, soit deux
opérandes, il n'existe en C# qu'un seul opérateur à trois opérandes (comme en Java) l'opérateur
conditionnel " ? : ".
Dans le tableau ci-dessous les opérateurs de C# sont classés par ordre de priorité croissante (0 est
le plus haut niveau, 13 le plus bas niveau). Ceci sert lorsqu'une expression contient plusieurs
opérateurs à indiquer l'ordre dans lequel s'effectueront les opérations.
Par exemple sur les entiers l'expression 2+3*4 vaut 14 car l'opérateur * est plus prioritaire que
l'opérateur +, donc l'opérateur * est effectué en premier.
Lorsqu'une expression contient des opérateurs de même priorité alors C# effectue les
évaluations de gauche à droite. Par exemple l'expression 12/3*2 vaut 8 car C# effectue le
parenthésage automatique de gauche à droite ((12/3)*2).
0 () [ ] . new
1 ! ~ ++ --
2 * / %
3 + -
4 << >>
6 == !=
8 ^
9 |
10 &&
11 ||
12 ?:
Les opérateurs d'affectation seront mentionnés plus loin comme cas particulier de l'instruction
d'affectation.
Ces opérateurs sont binaires (à deux opérandes) exceptés les opérateurs de signe positif ou négatif.
Ils travaillent tous avec des opérandes de types entiers ou réels. Le résultat de l'opération est
converti automatiquement en valeur du type des opérandes.
L'opérateur " % " de reste n'est intéressant que pour des calculs sur les entiers longs, courts,
signés ou non signés : il renvoie le reste de la division euclidienne de 2 entiers.
int x et int 2
y=x/2; y = 2 // type int
résultat : int
conversion implicite
y=b/2; erreur de conversion : interdit
impossible (float b --> int y)
conversion implicite
y = b / 2.0 ; erreur de conversion: interdit
impossible (float b --> int y)
float b et int 2
a=b/2; a = 2.5 // type float
résultat : float
int x et int 2
résultat : int
a=x/2; a = 2.0 // type float
conversion automatique
int 2 --> float 2.0
int x et float 2f
a = x / 2f ; a = 2.5 // type float
résultat : float
Pour l'instruction précédente " y = b / 2 " engendrant une erreur de conversion voici deux
corrections possibles utilisant le transtypage explicite :
y = (int)b / 2 ; // b est converti en int avant la division qui s'effectue sur deux int.
y = (int)(b / 2) ; // c'est le résultat de la division qui est converti en int.
L'objectif de ces opérateurs est l'optimisation de la vitesse d'exécution du bytecode MSIL dans le
CLR (cette optimisation n'est pas effective dans le version actuelle du MSIL) mais surtout la
reprise syntaxique aisée de code source Java et C++.
Exemple 1 :
int k = 5 , n ;
n=5 k=6
n = k++ ;
Exemple 2 :
int k = 5 , n ;
n = -1 k=6
n = k++ - k ;
Dans l'instruction k++ - k nous avons le calcul suivant : la valeur de k (k=5) est utilisée comme
premier opérande de la soustraction, puis elle est incrémentée (k=6), la nouvelle valeur de k est
maintenant utilisée comme second opérande de la soustraction ce qui revient à calculer n = 5-6 et
donne n = -1 et k = 6.
Exemple 3 :
int k = 5 , n ;
n=0 k=6
n = k - k++ ;
Dans l'instruction k - k++ nous avons le calcul suivant : la valeur de k (k=5) est utilisée comme
premier opérande de la soustraction, le second opérande de la soustraction est k++ c'est la valeur
actuelle de k qui est utilisée (k=5) avant incrémentation de k, ce qui revient à calculer n = 5-5 et
donne n = 0 et k = 6.
Exemple 4 :Utilisation de l'opérateur de post-incrémentation en combinaison avec un autre
opérateur unaire.
int nbr1, z , t , u , v ;
nbr1 = 10 ;
v = 10 nbr1 = 11
v = nbr1++
nbr1 = 10 ;
z = -11 nbr1 = 10
z = ~ nbr1 ;
nbr1 = 10 ;
t = -11 nbr1 = 11
t = ~ nbr1 ++ ;
nbr1 = 10 ;
u = -11 nbr1 = 11
u = ~ (nbr1 ++) ;
remarquons que les expressions "~nbr1 ++ " et "~ (nbr1 ++)" produisent les mêmes effets, ce
qui est logique puisque lorsque deux opérateurs (ici ~ et ++ )ont la même priorité, l'évaluation a
lieu de gauche à droite.
pré-incrémentation : ++k
la valeur de k est d'abord augmentée de un ensuite utilisée dans l'instruction.
Exemple1 :
int k = 5 , n ;
Exemple 2 :
int k = 5 , n ;
n=0 k=6
n = ++k - k ;
Dans l'instruction ++k - k nous avons le calcul suivant : le premier opérande de la soustraction
étant ++k c'est donc la valeur incrémentée de k (k=6) qui est utilisée, cette même valeur sert de
second opérande à la soustraction ce qui revient à calculer n = 6-6 et donne n = 0 et k = 6.
Exemple 3 :
int k = 5 , n ;
n = -1 k=6
n = k - ++k ;
Dans l'instruction k - ++k nous avons le calcul suivant : le premier opérande de la soustraction est
k (k=5), le second opérande de la soustraction est ++k, k est immédiatement incrémenté (k=6) et
c'est sa nouvelle valeur incrémentée qui est utilisée, ce qui revient à calculer n = 5-6 et donne n = -
1 et k = 6.
post-décrémentation : k--
la valeur de k est d'abord utilisée telle quelle dans l'instruction, puis elle est diminuée de un à la
fin.
Exemple1 :
int k = 5 , n ;
Exemple1 :
int k = 5 , n ;
Reprenez avec l'opérateur - - des exemples semblables à ceux fournis pour l'opérateur ++ afin
d'étudier le fonctionnement de cet opérateur (étudiez (- -k - k) et (k - - -k)).
3. Opérateurs de comparaison
Ces opérateurs employés dans une expression renvoient un résultat de type booléen (false ou
true). Nous en donnons la liste sans autre commentaire car ils sont strictement identiques à tous
les opérateurs classiques de comparaison de n'importe quel langage algorithmique (C, pascal,
etc...). Ce sont des opérateurs à deux opérandes.
4. Opérateurs booléens
Ce sont les opérateurs classiques de l'algèbre de boole { { V, F }, ! , & , | } où { V, F } représente
l'ensemble {Vrai,Faux}. Les connecteurs logiques ont pour syntaxe en C# : ! , & , |, ^:
V V F V F V
V F F F V V
F V V F V V
F F V F F F
Remarque :
C# dispose de 2 clones des opérateurs binaires & et | . Ce sont les opérateurs && et || qui se
différentient de leurs originaux & et | par leur mode d'exécution optimisé (application de
théorèmes de l'algèbre de boole) :
Théorème
q { V, F } , F &q = F
Donc si p est faux (p = F) , il est inutile d'évaluer q car l'expression p &q est fausse (p &q = F),
comme l'opérateur & évalue toujours l'expression q, C# à des fins d'optimisation de la vitesse
d'exécution du bytecode MSIL dans le CLR , propose un opérateur ou noté && qui a la même
table de vérité que l'opérateur & mais qui applique ce théorème.
L'opérateur ou optimisé : | |
Théorème
q { V, F } , V |q = V
Donc si p est vrai (p = V) , il est inutile d'évaluer q car l'expression p |q est vraie (p |q = V),
comme l'opérateur | évalue toujours l'expression q, C# à des fins d'optimisation de la vitesse
d'exécution du bytecode dans la machine virtuelle C#, propose un opérateur ou noté || qui
p { V, F } , q { V, F } , p ||q = p |q
Mais dans p||q , q n'est évalué que si p = F.
En résumé:
Nous allons voir ci-après une autre utilisation des opérateurs &et | sur des variables ou des
valeurs immédiates en tant qu'opérateur bit-level.
Les tables de vérités des opérateurs "&", " | " et celle du ou exclusif " ^ " au niveau du bit sont
identiques aux tables de verité booléennes ( seule la valeur des constantes V et F change, V est
remplacé par le bit 1 et F par le bit 0) .
p q ~p p&q p|q p ^ q
1 1 0 1 1 0
1 0 0 0 1 1
0 1 1 0 1 1
0 0 1 0 0 0
Afin de bien comprendre ces opérateurs, le lecteur doit bien connaître les différents codages des
entiers en machine (binaire pur, binaire signé, complément à deux) car les entiers C# sont codés
en complément à deux et la manipulation bit à bit nécessite une bonne compréhension de ce
codage.
Afin que le lecteur se familiarise bien avec ces opérateurs de bas niveau nous détaillons un
exemple pour chacun d'entre eux.
soit à représenter le nombre -14 dans la variable i de type int (entier signé sur 32 bits)
codage de |-14|= 14
complément à 1
addition de 1
Etude de l'instruction : j = ~ i
~i
Tous les bits 1 sont transformés en 0 et les bits 0 en 1, puis le résultat est stocké dans j qui
contient la valeur 13 (car 000...01101 représente +13 en complément à deux).
~ i >> 2
Tous les bits sont décalés de 2 positions vers la droite (vers le bit de poids faible), le bit de
signe (ici 1) est recopié à partir de la gauche (à partir du bit de poids fort) dans les
emplacements libérés (ici le bit 31 et le bit 30), puis le résultat est stocké dans j qui contient la
valeur -4 (car 1111...11100 représente -4 en complément à deux).
~ i << 2
Tous les bits sont décalés de 2 positions vers la gauche (vers le bit de poids fort), des 0 sont
introduits à partir de la droite (à partir du bit de poids faible) dans les emplacements libérés (ici
le bit 0 et le bit 1), puis le résultat est stocké dans j contient la valeur -56 (car 11...1001000
représente -56 en complément à deux).
using System;
namespace CsAlgorithmique
{
class AppliOperat_Arithme
{
static void Main(string[ ] args)
{
int x = 4, y = 8, z = 3, t = 7, calcul ;
calcul = x * y - z + t ;
System.Console.WriteLine(" x * y - z + t = "+calcul);
calcul = x * y - (z + t) ;
System.Console.WriteLine(" x * y - (z + t) = "+calcul);
calcul = x * y % z + t ;
System.Console.WriteLine(" x * y % z + t = "+calcul);
calcul = (( x * y) % z ) + t ;
System.Console.WriteLine("(( x * y) % z ) + t = "+calcul);
calcul = x * y % ( z + t ) ;
System.Console.WriteLine(" x * y % ( z + t ) = "+calcul);
calcul = x *(y % ( z + t ));
System.Console.WriteLine(" x *( y % ( z + t)) = "+calcul);
}
}
}
using System;
namespace CsAlgorithmique
{
class AppliOperat_BitBoole
{
static void Main(String[ ] args)
{
int x, y, z ,t, calcul=0 ;
x = 4; // 00000100
y = -5; // 11111011
z = 3; // 00000011
t = 7; // 00000111
calcul = x & y ;
System.Console.WriteLine(" x & y = "+calcul);
calcul = x & z ;
System.Console.WriteLine(" x & z = "+calcul);
calcul = x & t ;
System.Console.WriteLine(" x & t = "+calcul);
calcul = y & z ;
System.Console.WriteLine(" y & z = "+calcul);
calcul = x | y ;
System.Console.WriteLine(" x | y = "+calcul);
calcul = x | z ;
System.Console.WriteLine(" x | z = "+calcul);
calcul = x | t ;
System.Console.WriteLine(" x | t = "+calcul);
calcul = y | z ;
System.Console.WriteLine(" y | z = "+calcul);
calcul = z ^ t ;
System.Console.WriteLine(" z ^ t = "+calcul);
System.Console.WriteLine(" ~x = "+~x+", ~y = "+~y+", ~z = "+~z+", ~t = "+~t);
}
}
}
x&y =0 x|z =7
x&z =0 x|t =7
x&t =4 y | z = -5
y&z =3 z^t =4
x | y = -1 ~x = -5, ~y = 4, ~z = -4, ~t = -8
using System;
namespace CsAlgorithmique
{
class AppliOperat_BitDecalage
{
static void Main(String[ ] args)
{
int x,y, calcul = 0 ;
x = -14; // 1...11110010
y = x;
calcul = x 2; // 1...11111100
System.Console.WriteLine(" x 2 = "+calcul);
calcul = y <<2 ; // 1...11001000
System.Console.WriteLine(" y <<2 = "+calcul);
uint x1,y1, calcul1 = 0 ;
x1 = 14; // 0...001110
y1 = x1;
calcul1 = x1 2; // 0...000011
System.Console.WriteLine(" x1 2 = "+calcul1);
calcul1 = y1 <<2 ; // 0...00111000
System.Console.WriteLine(" y1 <<2 = "+calcul1);
}
}
}
using System;
namespace CsAlgorithmique
{
class AppliOperat_Boole
{
static void Main(String[ ] args)
{
int x = 4, y = 8, z = 3, t = 7, calcul=0 ;
bool bool1 ;
bool1 = x < y;
System.Console.WriteLine(" x < y = "+bool1);
bool1 = (x < y) & (z == t) ;
System.Console.WriteLine(" (x < y) & (z = = t) = "+bool1);
bool1 = (x < y) | (z == t) ;
System.Console.WriteLine(" (x < y) | (z = = t) = "+bool1);
bool1 = (x < y) && (z == t) ;
System.Console.WriteLine(" (x < y) && (z = = t) = "+bool1);
bool1 = (x < y) || (z == t) ;
System.Console.WriteLine(" (x < y) || (z = = t) = "+bool1);
bool1 = (x < y) || ((calcul=z) == t) ;
System.Console.WriteLine(" (x < y) || ((calcul=z) == t) = "+bool1+" ** calcul = "+calcul);
bool1 = (x < y) | ((calcul=z) == t) ;
System.Console.WriteLine(" (x < y) | ((calcul=z) == t) = "+bool1+" ** calcul = "+calcul);
System.Console.Read();
}
}
}
Les instructions de base de C# sont identiques syntaxiquement et sémantiquement à celles de Java, le lecteur qui
connaît déjà le fonctionnement des instructions en Java peut ignorer ces chapitres.
Une large partie de la norme ANSI du langage C est reprise dans C# , ainsi que la norme
Delphi.
Les commentaires sur une ligne débutent par //.... comme en Delphi
Ici, nous expliquons les instructions C# en les comparant à pascal-delphi. Voici la syntaxe
d'une instruction en C#:
instruction :
instruction complète :
int a, b = 12;
{ int x , y = 8 ;
{ int z =12;
x=z;
a=x+1;
{ int u = 1 ;
y=u-b;
}
} schéma d'imbrication des 3 blocs
}
2 - l'affectation
C# est un langage de la famille des langages hybrides, il possède la notion d'instruction
d'affectation.
x=y;
// x doit obligatoirement être un identificateur de variable.
Affectation simple
L'affectation peut être utilisée dans une expression :
int a , b = 56 ; a = ??? b = 56
a = (b = 12)+8 ; a = 20 b = 12
Il s'agit plus d'un raccourci syntaxique que d'un opérateur nouveau (sa traduction en MSIL
est exactement la même : la traduction de a op= b devrait être plus courte en instructions p-
code que a = a op b).
Ci-dessous le code MSIL engendré par i = i+5; et i +=5; est effectivement identique :
IL_0077: ldloc.1
IL_0078: ldc.i4.5 i=i+5;
IL_0079: add
IL_007a: stloc.1
IL_007b: ldloc.1
IL_007c: ldc.i4.5
IL_007d: add i += 5 ;
IL_007e: stloc.1
int a , b = 56 ; a = ??? b = 56
a = -8 ; a = -8 b = 56
a += b ; a = 48 b = 56
b *= 3 ; a = 48 b = 168
Remarques :
Ci-dessous le code MSIL engendré par "table[ f(i) ] = table[ f(i) ] +9 ;" et "table[ f(i) ] += 9 ;"
n'est pas le même :
IL_008e: ldloc.3
IL_008f: ldarg.0
table[ f(i) ]
IL_0090: ldloc.1
IL_0091: call instance int32 exemple.WinForm::f(int32)
IL_0096: ldelem.i4
IL_0097: ldc.i4.s 9
table[ f(i) ] = table[ f(i) ] + 9 ; (suite)
IL_0099: add
IL_009a: stelem.i4
table[ f(i) ] += 9 ;
IL_009b: ldloc.3
IL_009c: dup
IL_009d: stloc.s CS$00000002$00000000
IL_009f: ldarg.0 table[ f(i) ]
IL_00a0: ldloc.1
IL_00a1: call instance int32 exemple.WinForm::f(int32)
IL_00a6: dup
IL_00a7: stloc.s CS$00000002$00000001
IL_00a9: ldloc.s CS$00000002$00000000 +=
IL_00ab: ldloc.s CS$00000002$00000001
IL_00ad: ldelem.i4
IL_00ae: ldc.i4.s 9
table[ f(i) ] += 9 ;
IL_00b0: add
IL_00b1: stelem.i4
Au total, 14 instructions MSIL dont un seul appel :
Dans l'exemple qui précède, il y a réellement gain sur le temps d'exécution de l'instruction table[
f(i) ] += 9, si le temps d'exécution de l'appel à f(i) à travers l'instruction MSIL < call instance int32
exemple.WinForm::f(int32) > , est significativement long devant les temps d'exécution des opérations
ldloc et stloc.
En fait d'une manière générale en C# comme dans les autres langages, il est préférable d'adopter
l'attitude prise en Delphi qui consiste à encourager la lisibilité du code en ne cherchant pas à écrire
du code le plus court possible. Dans notre exemple précédent, la simplicité consisterait à utiliser
une variable locale x et à stocker la valeur de f(i) dans cette variable :
stloc : Dépile la pile d'évaluation et la stocke dans la liste de variables locales à un index
spécifié.
1 - l'instruction conditionnelle
Syntaxe :
if ( Expr ) Instr ;
Pascal-Delphi C#
var a , b , c : integer ;
int a , b , c ;
....
....
if b=0 then c := 1
if ( b = = 0 ) c =1 ;
else begin
else {
c := a / b;
c = a / b;
writeln("c = ",c);
System.Console.WriteLine ("c = " + c);
end;
}
c := a*b ;
if ((c = a*b) != 0) c += b;
if c <>0 then c:= c+b
else c = a;
else c := a
L'instruction " if ((c = a*b) != 0) c +=b; else c = a; " contient une affectation intégrée
dans le test afin de vous montrer les possibilités de C# : la valeur de a*b est rangée
dans c avant d'effectuer le test sur c.
Pascal-Delphi C#
if Expr1 then
if ( Expr1 ) {
begin
if ( Expr2 ) InstrA ;
if Expr2 then InstrA
}
end
else InstrB
else InstrB
2 - l'opérateur conditionnel
Il s'agit ici comme dans le cas des opérateurs d'affectation d'une sorte de raccourci entre
l'opérateur conditionnel if...else et l'affectation. Le but étant encore d'optimiser le MSIL
engendré.
Syntaxe :
Sémantique :
Exemple :
int a,b,c ;
c = a = = 0 ? b : a+1 ;
Si l'expression est true l'opérateur renvoie la première valeur, (dans l'exemple c vaut la valeur
de b)
Si l'expression est false l'opérateur renvoie la seconde valeur (dans l'exemple c vaut la valeur
de a+1).
Sémantique de l'exemple avec un if..else :
if (a = = 0) c = b; else c = a+1;
IL_0007: ldloc.0
IL_0008: brfalse.s IL_000f
IL_000a: ldloc.0 Opérateur conditionnel :
IL_000b: ldc.i4.1
IL_000c: add
IL_000d: br.s IL_0010
c = a == 0 ? b : a+1 ;
IL_000f: ldloc.1
IL_0010: stloc.2 une seule opération de stockage pour c :
IL_0010: stloc.2
IL_0011: ldloc.0
IL_0012: brtrue.s IL_0018
IL_0014: ldloc.1
IL_0015: stloc.2 Instruction conditionnelle :
IL_0016: br.s IL_001c
IL_0018: ldloc.0 if (a = = 0) c = b; else c = a+1;
IL_0019: ldc.i4.1
IL_001a: add
IL_001b: stloc.2 deux opérations de stockage pour c :
IL_0015: stloc.2
IL_001b: stloc.2
Le code MSIL engendré a la même structure classique de code de test pour les deux
instructions, la traduction de l'opérateur sera légèrement plus rapide que celle de l'instructions
car, il n'y a pas besoin de stocker deux fois le résultat du test dans la variable c (qui ici, est
représentée par l'instruction MSIL stloc.2)
3 - l'opérateur switch...case
Syntaxe :
switch :
bloc switch :
Sémantique :
La partie expression d'une instruction switch doit être une expression ou une
variable du type byte, char, int ou bien short.
La partie expression d'un bloc switch doit être une constante ou une valeur
immédiate du type byte, char, int ou bien short.
Exemple de switch..case..break
Pascal-Delphi C#
char x ;
var x : char ;
....
....
switch (x)
case x of
{
'a' : InstrA;
case 'a' : InstrA ; break;
'b' : InstrB;
case 'b' : InstrB ; break;
else InstrElse
default : InstrElse; break;
end;
}
Dans ce cas le déroulement de l'instruction switch après déroutement vers le bon case, est
interrompu par le break qui renvoie la suite de l'exécution après la fin du bloc switch. Une
telle utilisation correspond à une utilisation de if...else imbriqués (donc une utilisation
structurée) mais devient plus lisible que les if ..else imbriqués, elle est donc fortement
conseillée dans ce cas.
Exemples :
switch (x+1)
{ case 11 : System.Console.WriteLine (">> case 11");
break;
case 12 : System.Console.WriteLine (">> case 12"); >> case 11
break;
default : System.Console.WriteLine (">> default")");
break;
int x = 11;
switch (x+1)
{ case 11 : System.Console.WriteLine (">> case 11");
break;
case 12 : System.Console.WriteLine (">> case 12"); >> case 12
break;
default : System.Console.WriteLine (">> default")");
break;
}
C# - switch C# - if...else
int x = 10;
case 12 : else
if (x+1= = 12)
System.Console.WriteLine (">> case 12");
break; System.Console.WriteLine (">> case 12");
default : else
System.Console.WriteLine (">> default");
System.Console.WriteLine (">> default");
break;
Bien que la syntaxe du switch …break soit plus contraignante que celle du case…of de Delphi, le
fait que cette instruction apporte commme le case…of une structuration du code, conduit à une
amélioration du code et augmente sa lisibilité. Lorsque cela est possible, il est donc conseillé de
l'utiliser d'une manière générale comme alternative à des if...then…else imbriqués.
1 - l'instruction while
Syntaxe :
Où expression est une expression renvoyant une valeur booléenne (le test de l'itération).
Sémantique :
Identique à celle du pascal (instruction algorithmique tantque .. faire .. ftant) avec le même
défaut de fermeture de la boucle.
Pascal-Delphi C#
Où expression est une expression renvoyant une valeur booléenne (le test de l'itération).
Sémantique :
L'instruction "do Instr while ( Expr )" fonctionne comme l'instruction algorithmique répéter
Instr jusquà non Expr.
Pascal-Delphi C#
do
repeat
{
InstrA ;
InstrA ;
InstrB ; ...
InstrB ; ...
until not Expr
} while ( Expr )
3 - l'instruction for(...)
Syntaxe :
Sémantique :
Une boucle for contient 3 expressions for (Expr1 ; Expr2 ; Expr3 ) Instr, d'une manière
générale chacune de ces expressions joue un rôle différent dans l'instruction for. Une
instruction for en C# (comme en C) est plus puissante et plus riche qu'une boucle for dans
d'autres langages algorithmiques. Nous donnons ci-après une sémantique minimale :
Expr1 ;
while ( Expr2 )
for (Expr1 ; Expr2 ; Expr3 ) Instr { Instr ;
Expr3
}
Pascal-Delphi C#
i := 10; k := i;
while (i>-450) do
for ( i = 10, k = i ;i>-450 ; k += i , i -= 15)
begin
{
InstrA ;
InstrA ;
InstrB ; ...
InstrB ; ...
k := k+i;
}
i := i-15;
end
i := n;
while i<>1 do for ( i = n ;i !=1 ; i % 2 == 0 ? i /=2 : i++);
if i mod 2 = 0 then i := i div 2 // pas de corps de boucle !
else i := i+1
Le premier exemple montre une boucle for classique avec la variable de contrôle "i"
(indice de boucle), sa borne initiale "i=1" et sa borne finale "10", le pas
d'incrémentation séquentiel étant de 1.
Le second exemple montre une boucle toujours contrôlée par une variable "i", mais
dont le pas de décrémentation séquentiel est de -15.
Le troisème exemple montre une boucle aussi contrôlée par une variable "i", mais dont
la variation n'est pas séquentielle puisque la valeur de i est modifiée selon sa parité ( i
% 2 == 0 ? i /=2 : i++).
Voici une boucle ne possédant pas de variable de contrôle(f(x) est une fonction déjà déclarée) :
//inverse d'une suite de caractère dans un tableau par permutation des deux
extrêmes
char [ ] Tablecar ={'a','b','c','d','e','f'} ;
for ( i = 0 , j = 5 ; i<j ; i++ , j-- )
{ char car ;
car = Tablecar[i];
Tablecar[i ]= Tablecar[j];
Tablecar[j] = car;
}
dans cette dernière boucle ce sont les variations de i et de j qui contrôlent la boucle.
Syntaxe :
Sémantique :
Une instruction break ne peut se situer qu'à l'intérieur du corps d'instruction d'un bloc switch
ou de l'une des trois itérations while, do..while, for.
Lorsque break est présente dans l'une des trois itérations while, do..while, for :
Si break n'est pas suivi d'une étiquette, elle interrompt l'exécution de la boucle dans
laquelle elle se trouve, l'exécution se poursuit après le corps d'instruction.
Si break est suivi d'une étiquette, elle fonctionne comme un goto (utilisation
déconseillée en programmation moderne, c'est pourquoi nous n'en dirons pas plus pour
les boucles !)
Explications
Sémantique :
Une instruction continue ne peut se situer qu'à l'intérieur du corps d'instruction de l'une des
trois itérations while, do..while, for.
Lorsque continue est présente dans l'une des trois itérations while, do..while, for :
Si continue n'est pas suivi d'une étiquette elle interrompt l'exécution de la séquence des
instructions situées après elle, l'exécution se poursuit par rebouclage de la boucle. Elle
agit comme si l'on venait d'exécuter la dernière instruction du corps de la boucle.
Si continue est suivi d'une étiquette elle fonctionne comme un goto (utilisation
déconseillée en programmation moderne, c'est pourquoi nous n'en dirons pas plus !)
Explications
Si l'expression ( ta[i] = = 0 ) est true, la suite du corps des instructions de la boucle (tb[n] =
ta[i]; n++;) n'est pas exécutée et il y a rebouclage du for .
Attention
Nous avons déjà signalé plus haut que l'équivalence suivante entre un for et un while
Expr1 ;
while ( Expr2 )
for (Expr1 ; Expr2 ; Expr3 ) Instr { Instr ;
Expr3
}
valide dans le cas général, était mise en défaut si le corps d'instruction contenait un continue.
Voyons ce qu'il en est en reprenant l'exemple précédent. Essayons d'écrire la boucle while qui
lui serait équivalente selon la définition générale. Voici ce que l'on obtiendrait :
i = 0; n = 0 ;
for ( i = 0, n = 0 ; i<8 ; i++ , k = 2*n )
while ( i<8 )
{ if ( ta[i] = = 0 ) continue ;
{ if ( ta[i] = = 0 ) continue ;
tb[n] = ta[i];
tb[n] = ta[i];
n++;
n++;
}
i++ ; k = 2*n;
}
Une boucle while strictement équivalente au for précédent pourrait être la suivante :
i = 0; n = 0 ;
for ( i = 0, n = 0 ; i<8 ; i++ , k = 2*n ) while ( i<8 )
{ if ( ta[i] = = 0 ) continue ; { if ( ta[i] = = 0 )
tb[n] = ta[i]; { i++ ; k = 2*n;
n++; continue ;
} }
tb[n] = ta[i];
n++;
i++ ; k = 2*n;
}
Avant d'utiliser les possibilités offertes par les classes et les objets en C#, apprenons à utiliser
et exécuter des applications simples C# ne nécessitant pas la construction de nouveaux objets,
ni de navigateur pour s'exécuter. Lorsqu'il existe des différences avec le langage Java nous
les mentionnerons explicitement.
Comme C# est un langage entièrement orienté objet, un programme C# est composé de
plusieurs classes, nous nous limiterons à une seule classe.
Exemple1
class Application1
{
static void Main(string[ ] args)
{ /* inverse d'une suite de caractère dans un tableau par
permutation des deux extrêmes */
char [ ] Tablecar ={'a','b','c','d','e','f'} ;
int i, j ;
System.Console.WriteLine("tableau avant : " + new string(Tablecar));
for ( i = 0 , j = 5 ; i<j ; i++ , j-- )
{ char car ;
car = Tablecar[i];
Tablecar[i ]= Tablecar[j];
Tablecar[j] = car;
}
System.Console.WriteLine("tableau après : " + new string(Tablecar));
}
}
L'instruction "new string(Tablecar)" sert uniquement pour l'affichage, elle crée une string à
partir du tableau de char.
Exemple2
class Application2
{
static void main(String[ ] args)
{ // recherche séquentielle dans un tableau
int [ ] table= {12,-5,7,8,-6,6,4,78,2};
int elt = 4, i ;
for ( i = 0 ; i<8 ; i++ )
if (elt= =table[i]) break ;
if (i = = 8) System.out.println("valeur : "+elt+" pas trouvée.");
else System.out.println("valeur : "+elt+" trouvée au rang :"+i);
}
}
Après avoir sauvegardé la classe dans un fichier xxx.cs, ici dans notre exemple "AppliExo2.cs", la
compilation de ce fichier "AppliExo2.cs" produit le fichier "AppliExo2.exe" prêt à être exécuté
par la machine virtuelle du CLR.
Conseil de travail :
Reprenez tous les exemples simples du chapitre sur les instructions de boucle et le switch en
les intégrant dans une seule classe (comme nous venons de le faire avec les deux exemples
précédents) et exécutez votre programme.
Donc par la suite dans ce document lorsque nous emploierons le mot méthode sans autre adjectif,
il s'agira d'une méthode de classe, comme nos applications ne possèdent qu'une seule classe, nous
pouvons assimiler ces méthodes aux fonctions de l'application et ainsi retrouver une utilisation
classique de C# en mode appplication.
Attention, il est impossible en C# de déclarer une méthode à l'intérieur d'une autre
méthode comme en pascal; toutes les méthodes sont au même niveau de déclaration :
ce sont les méthodes de la classe !
Syntaxe :
corps de fonction :
Sémantique :
Une méthode peut renvoyer un résultat d'un type C# quelconque en particulier d'un des
types élémentaires (int, byte, short, long, bool, double, float, char...) et nous verrons
plus loin qu'elle peut renvoyer un résultat de type objet comme en Delphi. Ce mot clef
ne doit pas être omis.
En C#, ces trois modes de transmission (ou de passage) des paramètres (très semblables à Delphi)
sont implantés.
En C# tous les paramètres sont passés par défaut par valeur (lorsque le paramètre est un objet,
c'est en fait la référence de l'objet qui est passée par valeur). Pour ce qui est de la vision
algorithmique de C#, le passage par valeur permet à une variable d'être passée comme paramètre
d'entrée.
Lors de l'appel d'un paramètre passé par référence, le mot clef ref doit obligatoirement précéder
le paramètre effectif qui doit obligatoirement avoir été initialisé auparavant :
En C# pour indiquer un passage par résultat on précède la déclaration du paramètre formel du mot
Premier pas dans .Net avec C# - ( rév. 05.09.2004) page 63
out :
static int methode1(int a , out char b) {
//.......
return a+b;
}
Lors de l'appel d'un paramètre passé par résultat, le mot clef out doit obligatoirement précéder le
paramètre effectif qui n'a pas besoin d'avoir été initialisé :
Remarque :
Le choix de passage selon les types élimine les inconvénients dûs à l'encombrement
mémoire et à la lenteur de recopie de la valeur du paramètre par exemple dans un passage
par valeur, car nous verrons plus loin que les tableaux en C# sont des objets et que leur
structure est passée par référence.
Les méthodes de type fonction en C#, peuvent renvoyer un résultat de n'importe quel type et
acceptent des paramètres de tout type.
Une méthode- fonction ne peut renvoyer qu'un seul résultat comme en Java, mais l'utilisation
des passages de paramètres par référence ou par résultat, permet aussi d'utiliser les
paramètres de la fonction comme des variables de résultats comme en Delphi.
En C# comme en Java le retour de résultat est passé grâce au mot clef return placé n'importe
où dans le corps de la méthode.
Visibilité de bloc
C# est un langage à structure de blocs ( comme pascal et C ) dont le principe général de
visibilité est :
Le masquage des variables n'existe que pour les variables déclarées dans des méthodes :
Il est interdit de redéfinir une variable déjà déclarée dans une méthode
soit :
int f (int x, int a ) - Dans la méthode f, elle est masquée par le paramètre
{ return 3*x-a; du même nom qui est utilisé pour évaluer l'expression
} 3*x-a.
Contrairement à ce que nous avions signalé plus haut nous n'avons pas présenté un exemple
fonctionnant sur des méthodes de classes (qui doivent obligatoirement être précédées du mot
clef static), mais sur des méthodes d'instances dont nous verrons le sens plus loin en POO.
Remarquons avant de présenter le même exemple cette fois-ci sur des méthodes de classes,
que quelque soit le genre de méthode la visibilité des variables est identique.
static int g (int x ) - Elle est visible dans la méthode g et dans la méthode f.
{ return 3*x-a; C'est elle qui est utilisée dans la méthode g pour évaluer
} l'expression 3*x-a.
static int f (int x, int a ) - Dans la méthode f, elle est masquée par le paramètre
{ return 3*x-a; du même nom qui est utilisé pour évaluer l'expression
} 3*x-a.
Les variables définies dans une méthode (de classe ou d'instance) suivent les règles classiques
de la visibilité du bloc dans lequel elles sont définies :
Elles sont visibles dans toute la méthode et dans tous les blocs imbriqués dans cette méthode et
seulement à ce niveau (les autres méthodes de la classe ne les voient pas), c'est pourquoi on
emploie aussi le terme de variables locales à la méthode.
class ExempleVisible5 {
Comparaison C# , java : la même classe à gauche passe en Java mais fournit 6 conflits en C#
La classe string
Le type de données String (chaîne de caractère) est une classe de type référence dans l'espace de
noms System de .NET Framework. Donc une chaîne de type string est un objet qui n'est utilisable
qu'à travers les méthodes de la classe string.
Un littéral de chaîne est une suite de caractères entre guillemets : " abcdef " est un exemple
de littéral de String.
Etant donné que cette classe est très utilisée les variables de type string bénéficient d'un statut
d'utilisation aussi souple que celui des autres types élémentaires par valeurs. On peut les
considérer comme des listes de caractères Unicode numérotés de 0 à n-1 (si n figure le nombre
de caractères de la chaîne).
Déclaration d'une variable String avec String str1 = " abcdef ";
initialisation
Ou
String str1 = new String("abcdef ");
Remarque
En fait l'opérateur [ ] est un indexeur de la classe string (cf. chapitre indexeurs en C#), et
il est en lecture seule :
public char this [ int index ] { get ; }
Ce qui signifie au stade actuel de compréhension de C#, qu'il est possible d'accéder en
lecture seulement à chaque caractères d'une chaîne, mais qu'il est impossible de modifier
un caractère grâce à l'indexeur.
char car = ch1[7] ; // l'indexeur renvoie le caractère 'h' dans la variable car.
ch1[5] = car ; // Erreur de compilation : l'écriture dans l'indexeur est interdite !!
ch1[5] = 'x' ; // Erreur de compilation : l'écriture dans l'indexeur est interdite !!
String Concat(…)
Les deux écritures ci-dessous sont donc
équivalentes en C# :
str3 = str1+str2 str3 = str.Concat(str2)
dans
Attention :
les méthodes d'insertion, suppression, etc…ne modifient pas la chaîne objet qui invoque la
méthode mais renvoient un autre objet de chaîne différent, obtenu après action de la
méthode sur l'objet initial.
char [ ] tCarac ;
tCarac = str1.ToCharArray( ) ;
tCarac = "abcdefghijk".ToCharArray( );
System.Console.WriteLine("s1="+s1);
System.Console.WriteLine("s2="+s2);
System.Console.WriteLine("s3="+s3);
System.Console.WriteLine("ch="+ch);
if( s2 == ch )System.Console.WriteLine("s2=ch");
else System.Console.WriteLine("s2<>ch");
if( s2 == s3 )System.Console.WriteLine("s2=s3");
else System.Console.WriteLine("s2<>s3");
if( s3 == ch )System.Console.WriteLine("s3=ch");
else System.Console.WriteLine("s3<>ch");
if( s3.Equals(ch) )System.Console.WriteLine("s3 égal ch");
else System.Console.WriteLine("s3 différent de ch");
s2<>abcdef s2==abcdef
s2<>ch s2==ch
Il faut utiliser l'une des surcharges de la méthode de conversion ToString de la classe Convert :
System.Object
|__System.Convert
Le code suivant est correct, il permet de stocker un caractère char dans une string :
Remarque :
La classe Convert contient un grand nombre de méthodes de conversion de types.
Microsoft indique que cette classe : "constitue une façon, indépendante du langage,
d'effectuer les conversions et est disponible pour tous les langages qui ciblent le Common
Language Runtime. Alors que divers langages peuvent recourir à différentes techniques
pour la conversion des types de données, la classe Convert assure que toutes les
conversions communes sont disponibles dans un format générique."
2°) On peut concaténer avec l'opérateur +, des char à une chaîne string déjà existante et affecter
le résultat à une String :
string s1 , s2 ="abc" ;
char c = 'e' ;
s1 = s2 + 'd' ;
s1 = s2 + c ;
s1 = 'd' + "e";
Car il faut qu'au moins un des deux opérandes de l'opérateur + soit du type string :
Pour plus d'information sur toutes les méthodes de la classe string consulter la documentation
de .Net framework.
Chaque dimension d'un tableau en C# est définie par une valeur ou longueur, qui est un nombre ou
une variable N entier (char, int, long,…) dont la valeur est supérieur ou égal à zéro. Lorsqu'une
dimension a une longueur N, l'indice associé varie dans l'intervalle [ 0 , N – 1 ].
Tableau uni-dimensionnel
Ci-dessous un tableau 'tab' à une dimension, de n+1 cellules numérotées de 0 à n :
Il n'y a pas de mot clef spécifique pour la classe tableaux, mais l'opérateur symbolique
[ ] indique qu'une variable de type fixé est un tableau.
La taille d'un tableau doit obligatoirement avoir été définie avant que C# accepte que
vous l'utilisiez !
Les tableaux de C# sont des objets d'une classe dénommée Array qui est la classe de base
d'implémentation des tableaux dans le CLR de .NET framework (localisation :
System.Array) Cette classe n'est pas dérivable pour l'utilisateur : "public abstract class
Array : ICloneable, IList, ICollection, IEnumerable".
C'est en fait le compilateur qui est autorisé à implémenter une classe physique de tableau. Il
faut utiliser les tableaux selon la démarche ci-dessous en sachant que l'on dispose en plus des
propriétés et des méthodes de la classe Array si nécessaire (longueur, tri, etc...)
Le mot clef new correspond à la création d'un nouvel objet (un nouveau tableau) dont la taille
est fixée par la valeur indiquée entre les crochets. Ici 4 tableaux sont crées et prêts à être utilisés :
table1 contiendra 5 entiers 32 bits, table2 contiendra 12 caractères, table3 contiendra 8 réels en
simple précision et tableStr contiendra 9 chaînes de type string.
On peut aussi déclarer un tableau sous la forme de deux instructions : une instruction de
déclaration et une instruction de définition de taille avec le mot clef new, la seconde pouvant être
mise n'importe où dans le corps d'instruction, mais elle doit être utilisée avant toute manipulation
du tableau. Cette dernière instruction de définition peut être répétée plusieurs fois dans le
programme, il s'agira alors à chaque fois de la création d'un nouvel objet (donc un nouveau
tableau), l'ancien étant détruit et désalloué automatiquement par le ramasse-miettes (garbage
collector) de C#.
Exemple :
int [ ] table1 = {17,-9,4,3,57};
int taille;
taille = table1.Length; // taille = 5
Attention
Il est possible de déclarer une référence de tableau, puis de l'initialiser après uniquement ainsi :
int [ ] table1 ; // crée une référence table1 de type tableau de type int
table1 = new int {17,-9,4,3,57}; // instancie un tableau de taille 5 éléments reférencé par table1
…
table1 = new int {14,-7,9}; // instancie un autre tableau de taille 3 éléments reférencé par table1
table2[0] = '?' ;
table2[4] = 'a' ;
table2[14] = '#' ; <--- est une erreur de dépassement de la taille
for (int i = 0 ; i<= table2.Length-1; i++)
table2[i] =(char)('a'+i);
// après la boucle: table2 = {'a', 'b', 'c' ,'d', 'e', 'f'}
Remarque :
Dans une classe exécutable la méthode Main reçoit en paramètre un tableau
de string nommé args qui correspond en fait aux éventuels paramètres de
l'application elle-même:
static void Main(string [ ] args)
Ce schéma montre bien qu'un tel tableau T est constitué de tableaux unidimensionnels, les
tableaux composés de cases blanches contiennent des pointeurs (références). Chaque case blanche
est une référence vers un autre tableau unidimensionnel, seules les cases grisées contiennent les
informations utiles de la structure de données : les éléments de même type du tableau T.
int n = 10;
int [ ][ ] myArray = new int [n][ ];
myArray[0] = new int[7];
myArray[1] = new int[9];
myArray[2] = new int[3];
myArray[3] = new int[4];
…
myArray[n-2] = new int[2];
myArray[n-1] = new int[8];
Etc….
Attention
Dans le cas d'un tableau déchiqueté, le champ Length de la classe Array, contient la taille du sous-tableau uni-
dimensionnel associé à la référence.
Soit la déclaration :
Soit la déclaration :
C# initialise les tableaux par défaut à 0 pour les int, byte, ... et à null pour les objets.
On peut simuler une matrice avec un tableau déchiqueté dont tous les sous-tableaux ont
exactement la même dimension. Voici une figuration d'une matrice à n+1 lignes et à p+1 colonnes
avec un tableau en escalier :
- Contrairement à Java qui l'accepte, le code ci-
dessous ne sera pas compilé par C# :
Conseil
L'exemple précédent montre à l'évidence que si l'on souhaite réellement utiliser des
matrices en C#, il est plus simple d'utiliser la notion de tableau multi-dimensionnel
[ , ] que celle de tableau en escalier [ ] [ ].
char[ ] t1="abcdef".ToCharArray();
char[ ] t2="abcdef".ToCharArray();
if(t1==t2)System.Console.WriteLine("t1=t2");
else System.Console.WriteLine("t1<>t2");
if(t1.Equals(t2))System.Console.WriteLine("t1 égal t2");
else System.Console.WriteLine("t1 différent de t2");
Comme les tableaux sont des objets, l'affectation de références de deux tableaux distincts
donne les mêmes résultats que pour d'autres objets : les deux références de tableaux pointent
vers le même objet. Donc une affectation d'un tableau dans un autre t1 = t2 ne provoque pas la
recopie des éléments du tableau t2 dans celui de t1.
public static void Copy ( Array t1 , Array t2, int long) : méthode de classe qui copie dans un
tableau t2 déjà existant et déjà instancié, long éléments du tableau t1 depuis son premier élément
(si l'on veut une copie complète du tableau t1 dans t2, il suffit que long représente le nombre total
d'éléments soit long = t1.Length).
Attention
Dans le cas où le tableau t1 contient des références qui pointent vers des objets :
la recopie dans un autre tableau à travers les méthode Clone ou Copy ne recopie que les
références, mais pas les objets pointés, voici un "clone" du tableau t1 de la figure précédente dans
le tableau t2 :
Code source d'utilisation de ces deux méthodes sur un tableau unidimensionnel et sur une
matrice :
//-- tableau à une dimension :
Les instructions itératives for( …), while, do…while précédemment vues permettent le parcours
d'un tableau élément par élément à travers l'indice de tableau. Il existe une instruction d'itération
spécifique foreach…in qui énumère les éléments d'une collection, en exécutant un ensemble
d'actions pour chaque élément de la collection.
Syntaxe
Donc tout objet de cette classe (un tableau) est susceptible d'être parcouru par un instruction
foreach…in. Mais les éléments ainsi parcourus ne peuvent être utilisés qu'en lecture, ils ne
peuvent pas être modifiés, ce qui limite d'une façon importante la portée de l'utilisation d'un
foreach…in.
Lorsque T est un tableau multi-dimensionnel microsoft indique : … les éléments sont parcourus
de manière que les indices de la dimension la plus à droite soient augmentés en premier, suivis de
ceux de la dimension immédiatement à gauche, et ainsi de suite en continuant vers la gauche.
Dans l'exemple ci-après où une matrice table est instanciée et remplie il y a équivalence de
parcours de la matrice table, entre l'instruction for de gauche et l'instruction foreach de droite
(fonctionnement identique pour les autres types de tableaux multi-dimensionnels et en escalier) :
int [ , ] table = new int [ 3 , 2 ]; …. Remplissage de la matrice
Avantage : la simplicité d'écriture, toujours la même quelle que soit le type du tableau.
Inconvénient : on ne peut qu'énumérer en lecture les éléments d'un tableau.
IEnumerable :
contient une seule méthode qui renvoie un énumérateur (objet de type IEnumerator) qui
peut itérer sur les élément d'une collection (c'est une sorte de pointeur qui avance dans la
collection, comme un pointeur de fichier se déplace sur les enregistrements du fichier) :
IEnumerator :
Propriétés
public object Current {get;} Obtient l'élément en cours pointé actuellement par l'énumérateur dans la
collection.
Méthodes
public bool MoveNext( ); Déplace l'énumérateur d'un élément il pointe maintenant vers l'élément
suivant dans la collection (renvoie false si l'énumérateur est après le dernier élément de la collection sinon
renvoie true).
public void Reset( ); Déplace l'énumérateur au début de la collection, avant le premier élément (donc si
l'on effectue un Current on obtiendra la valeur null, car après un Reset( ), l'énumérateur ne pointe pas devant
le premier élément de la collection mais avant ce premier élément !).
public object SyncRoot {get;} Fournit un objet qui peut être utilisé pour synchroniser
(verrouiller ou déverrouiller) l'accès à ICollection.
Méthode
public void CopyTo ( Array table, Copie les éléments de ICollection dans un objet de type
int index) Array (table), commençant à un index fixé.
IList :
Propriétés
public bool IsFixedSize {get;} : indique si IList est de taille fixe.
public bool IsReadOnly {get;} : indique si IList est en lecture seule.
Les classes implémentant l'interface IList sont indexables par l'indexeur [ ].
public bool Contains( object elt ); Indique si IList contient l'élément elt en son sein.
public int IndexOf( object elt ); Indique le rang de l'élément elt dans IList.
Ce qui nous donne après exécution de la liste des instructions ci-dessous, un tableau TabCar ne
contenant plus rien :
char [ ] TableCar ;
TableCar = new char[8];
TableCar[0] = 'a';
TableCar[1] = '#';
...
TableCar[7] = '?';
TableCar = new char[10];
Un objet de classe ArrayList peut "grandir" automatiquement d'un certain nombre de cellules
pendant l'exécution, c'est le programmeur qui peut fixer la valeur d'augmentation du nombre de
cellules supplémentaires dès que la capacité maximale en cours est dépassée. Dans le cas où la
valeur d'augmentation n'est pas fixée, c'est la machine virtuelle du CLR qui procède à une
augmentation par défaut.
Vous pouvez utiliser le type ArrayList avec n'importe quel type d'objet puisqu'un ArrayList
contient des éléments de type dérivés d'object (ils peuvent être tous de types différents et le
vecteur est de type hétérogène).
Les principales méthodes permettant de manipuler les éléments d'un ArrayList sont :
public virtual int Add( object value ); Ajoute un l'objet value à la fin de ArrayList.
public virtual void Insert(int index, object value); Insère un élément dans ArrayList à l'index spécifié.
PROPRIETE
Voici un exemple simple de vecteur de chaînes utilisant quelques unes des méthodes précédentes :
static void afficheVector (ArrayList vect) //affiche un vecteur de string
{
System.Console.WriteLine( "Vecteur taille = " + vect.Count );
for ( int i = 0; i<= vect.Count-1; i++ )
System.Console.WriteLine( "Vecteur[" + i + "]=" + (string)vect[ i ] );
}
Rappelons qu'une liste linéaire (ou liste chaînée) est un ensemble ordonné d'éléments de même
type (structure de donnée homogène) auxquels on accède séquentiellement. Les opérations
minimales effectuées sur une liste chaînée sont l'insertion, la modification et la suppression d'un
élément quelconque de la liste.
Les listes peuvent être uni-directionnelles, elles sont alors parcourues séquentiellement dans un
seul sens :
ou bien bi-directionnelles dans lesquelles chaque élément possède deux liens de chaînage, l'un sur
l'élément qui le suit, l'autre sur l'élément qui le précède, le parcours s'effectuant en suivant l'un ou
l'autre sens de chaînage :
La classe ArrayList peut servir à une implémentation de la liste chaînée uni ou bi-directionnelle;
un ArrayList contient des éléments de type dérivés d'Object, la liste peut donc être hétérogène, cf
exercice sur les listes chaînées.
En revanche, si l'on stocke comme clef la valeur de Hashcode de l'élément, la recherche est
améliorée.
Les principales méthodes permettant de manipuler les éléments d'un SortedList sont :
public virtual object GetByIndex( int index ); Obtient la valeur à l'index spécifié de la liste SortedList.
public virtual object GetKey( int index ); Obtient la clé à l'index spécifié de SortedList.
public virtual void Remove( object key ); Supprime de SortedList l'élément ayant la clé key spécifiée.
PROPRIETE
La classe "public class Stack : ICollection, IEnumerable, ICloneable" représente une pile Lifo :
public virtual object Peek ( ); Renvoie la référence de l'objet situé au sommet de la pile.
public virtual object Pop( ); Dépile la pile (l'objet au sommet est enlevé et renvoyé)
public virtual void Push( object elt ); Empile un objet au sommet de la pile.
public virtual object [ ] ToArray( ); Recopie toute la pile dans un tableau d'objet depuis le sommet jusqu'au
fond de la pile (dans l'ordre du dépilement).
public virtual object Dequeue( ); L'objet au début de la file est enlevé et renvoyé.
public virtual void Enqueue ( object elt ); Ajoute un objet à la fin de la file.
public virtual object [ ] ToArray( ); Recopie toute la file dans un tableau d'objet depuis le début de la fifo
jusqu'à la fin de la file.
Le programme ci-dessous rempli avec les chaînes du tableau t1 grâce à la méthode getArray , la pile
Lifo construite. On tente ensuite de récupérer le contenu de la pile sous forme d'un tableau de
chaîne t2 (opération inverse) en utilisant la méthode ToArray. Le compilateur signale une erreur :
En effet la méthode ToArray renvoie un tableau d'object et non un tableau de string. On pourrait
penser à transtyper explicitement :
t2 = ( string [ ] ) piLifo.ToArray( ) ;
en ce cas C# réagit comme Java, en acceptant la compilation, mais en générant une exception de
cast invalide, car il est en effet dangereux d'accepter le transtypage d'un tableau d'object en un
tableau de quoique ce soit, car chaque object du tableau peut être d'un type quelconque et tous les
types peuvent être différents !
Nous avons mis le qualificateur new car cette méthode masque la méthode mère de la classe
Stack, nous avons maintenant une pile Lifo de string, construisons de la même manière la classe
Fifo de file de string dérivant de la classe Queue avec une méthode getArray et la méthode
ToArray redéfinie :
Plan général:
3. Variables et méthodes
3.1 Variables dans une classe en général
3.2 Variables et méthodes d'instance
3.3 Variables et méthodes de classe - static
3.4 Bilan et exemple d'utilisation
Modification de visibilité
Rappelons les classiques modificateurs de visibilité des variables et des méthodes dans les
langages orientés objets, dont C# dispose :
Les attributs d'accessibilité public, private, protected sont identiques à ceux de Delphi et
Java, pour les classes nous donnons ci-dessous des informations sur leur utilisation.
L'attribut internal joue à peu près le rôle (au niveau de l'assembly) des classes Java
déclarées sans mot clef dans le même package , ou des classes Delphi déclarées dans la même
unit (classes amies). Toutefois pour des raisons de sécurité C# ne possède pas la notion de
classe amie.
Rappelons un point fondamental déjà indiqué : tout programme C# contient une ou plusieurs
classes précédées ou non d'une déclaration d'utilisation d’autres classes contenues dans des
bibliothèques (clause using) ou dans un package complet composé de nombreuses classes. La
notion de module en C# est représentée par l'espace de noms (clause namespace) semblable au
package Java, en C# vous pouvez omettre de spécifier un namespace, par défaut les classes
déclarées le sont automatiquement dans un espace 'sans nom' (généralement qualifié de global) et
tout identificateur de classe déclaré dans cet espace global sans nom est disponible pour être utilisé
dans un espace de noms nommé. Contrairement à Java, en C# les classes non qualifiées par un
modificateur de visibilité (déclarées sans rien devant) sont public.
Delphi Java C#
Delphi Java C#
Delphi Java C#
interface class Appli3Classes
type { Un x; class Appli3Classes
Un = class Deux y; { Un x;
... public static void main(String [ ] Deux y;
end; arg) static void Main(String [ ] arg)
Deux = class {
{
... Un x; Un x;
end; Deux y; Deux y;
Appli3Classes = class ... ...
x : Un; }
}
y : Deux; } }
public class Un class Un
procedure main; { ... { ...
end; }
} class Deux
{ ...
implementation class Deux
}
procedure Appli3Classes.main; { ...
var }
x : Un;
y : Deux;
begin
...
end;
end.
Delphi Java C#
class Exemple extends Object
type class Exemple : Object
{
Exemple = class ( TObject ) {
.......
...... .......
}
end; }
class Exemple
type class Exemple
{
Exemple = class {
.......
...... .......
}
end; }
L'héritage en C# est tout fait classiquement de l'héritage simple comme en Delphi et en Java. Une
classe fille qui dérive d'une seule classe mère, hérite de sa classe mère toutes ses méthodes et tous
ses champs. En C# la syntaxe de l'héritage fait intervenir le symbole clef ':', comme dans "class
Exemple : Object".
Une déclaration du type :
class ClasseFille : ClasseMere {
}
signifie que la classe ClasseFille dispose de tous les attributs et de toutes les méthodes de la classe
ClasseMere.
Comparaison héritage :
Delphi Java C#
class ClasseMere
{ class ClasseMere
type
ClasseMere = class // champs de ClasseMere {
// champs de ClasseMere // méthodes de ClasseMere // champs de ClasseMere
// méthodes de ClasseMere } // méthodes de ClasseMere
}
end;
class ClasseFille extends class ClasseFille : ClasseMere
ClasseFille = class ( ClasseMere ) ClasseMere
{
// hérite des champs de ClasseMere { // hérite des champs de ClasseMere
// hérite des méthodes de ClasseMere // hérite des champs de ClasseMere // hérite des méthodes de ClasseMere
// hérite des méthodes de ClasseMere
end; }
}
Bien entendu une classe fille peut définir de nouveaux champs et de nouvelles méthodees qui lui
sont propres.
Remarque
La notion de classe interne de C# (qui n'existe pas en Delphi) est moins riche à ce jour qu'en
Java (pas de classe membre statique, pas de classe locale et pas de classe anonyme), elle
corespond à la notion de classe membre de Java.
Une classe C# peut se voir attribuer un modificateur de comportement sous la forme d'un mot
clef devant la déclaration de classe. Par défaut si aucun mot clef n'est indiqué la classe est
visible dans tout le namespace dans lequel elle est définie. Il y a 4 qualificateurs possibles pour
modifier le comportement de visibilité d'une classe selon sa position (imbriquée ou non) :
public, private, protected, internal (dénommés modificateurs d'accès) et abstract
(qualificateur d'abstraction pouvant être associé à l'un des 3 autres modificateurs d'accès). On
rappelle que sans qualificateur public, private, internal ou protected, une classe C# est
automatiquement public.
Le nom du fichier source dans lequel plusieurs classes C# sont stockées n'a aucun rapport avec
le nom d'une des classes déclarées dans le texte source, il est laissé au libre choix du
développeur et peut éventuellement être celui d'une classe du namespace etc...
Attention
Par défaut dans une classe tous les membres sans qualificateur de visibilité (classes internes
inclues) sont private.
mot clef abstract : classe abstraite non instanciable. Aucun objet ne peut être
abstract class ApplicationClasse1 { ... } créé.
mot clef public : classe visible par n'importe quel programme d'un autre
public class ApplicationClasse2 { ... } namespace
mot clef protected : classe visible seulement par toutes les autres classes
protected class ApplicationClasse3 { ... héritant de la classe conteneur de cette classe.
}
mot clef internal : classe visible seulement par toutes les autres classes du
internal class ApplicationClasse4 { ... } même assembly.
mot clef private : classe visible seulement par toutes les autres classes du
private class ApplicationClasse5 { ... } même namespace où elle est définie.
Nous remarquons donc qu'une classe dès qu'elle est déclarée dans l’espace de noms est toujours
visible et par défaut public, que le mot clef public soit présent ou non. Les mots clefs abstract
et protected n'ont de l'influence que pour l'héritage.
Remarque
La notion de classe sealed en C# correspond strictement à la notion de classe final de Java : ce
sont des classes non héritables.
Nous étudions ci-après la visibilité des classes précédentes dans deux contextes différents.
C# Explication
C# Explication
class AppliTestClasses
{
ApplicationClasses.ApplicationClasse2 a2;
}
}
1.6 Même exemple de classes non imbriquées situées dans le même espace de noms
Dans ce second exemple, ces mêmes 6 classes sont utilisées en étant incluses dans le même
namespace.
C#
dans deux namespace différents Explication
namespace Exemple Ici les toutes les 4 classes sont visibles pour la
{ classe AppliTestClasses.
class AppliTestClasses{
ApplicationClasse1 a1;
ApplicationClasse2 a2;
ApplicationClasse4 a4;
ApplicationClasse6 a6;
}
}
Remarque pratique :
Selon sa situation imbriquée ou non imbriquée, une classe peut ou ne peut pas être
qualifiée par les divers modificateurs de visibilité. En cas de doute le compilateur
fournit un diagnostique clair, comme ci-dessous :
[C# Erreur] Class.cs(nn): Les éléments namespace ne peuvent pas être déclarés
explicitement comme private, protected ou protected internal.
class Etre_Vivant { }
La classe Etre_Vivant est une classe mère générale pour les êtres vivants sur la planète, chaque
catégorie d'être vivant peut être représentée par une classe dérivée (classe fille de cette classe) :
Tous ces êtres se déplacent d'une manière générale, donc une méthode SeDeplacer est commune à
toutes les classes dérivées, toutefois chaque espèce exécute cette action d'une manière différente et
donc on ne peut pas dire que se déplacer est une notion concrète mais une notion abstraite que
chaque sous-classe précisera concrètement.
En C#, les méthodes abstraites sont automatiquement virtuelles, elles ne peuvent être déclarées
que public ou protected, enfin elles doivent être redéfinies avec le qualificateur override. Ci-
dessous deux déclarations possibles pour le déplacement des êtres vivants :
Delphi C#
type abstract class Etre_Vivant {
Etre_Vivant = class public abstract void SeDeplacer( );
En C# une méthode abstraite est une méthode virtuelle n’ayant pas d’implémentation dans la
classe où elle est déclarée. Son implémentation est déléguée à une classe dérivée. Les méthodes
abstraites doivent être déclarées en spécifiant la directive abstract .
Classe abstraite
Comme nous venons de le voir dans l'exemple précédent, une classe C# peut être précédée du mot
clef abstract, ce qui signifie alors que cette classe est abstraite, nous avons les contraintes de
définition suivantes pour une classe abstraite en C# :
Si une classe contient au moins une méthode abstract, elle doit impérativement être déclarée en
classe abstract elle-même. C'est ce que nous avons écrit au paragraphe précédent pour la classe
Etre_Vivant que nous avons déclarée abstract parce qu'elle contenait la méthode abstraite
SeDeplacer.
Une classe abstract ne peut pas être instanciée directement, seule une classe dérivée (sous-classe)
qui redéfinit obligatoirement toutes les méthodes abstract de la classe mère peut être instanciée.
Conséquence du paragraphe précédent, une classe dérivée qui redéfinit toutes les méthodes
abstract de la classe mère sauf une (ou plus d'une) ne peut pas être instanciée et subit la même
règle que la classe mère : elle contient au moins une méthode abstraite donc elle est aussi une
classe abstraite et doit donc être déclarée en abstract.
Une classe abstract peut contenir des méthodes non abstraites et donc implantées dans la classe.
Une classe abstract peut même ne pas contenir du tout de méthodes abstraites, dans ce cas une
classe fille n'a pas la nécessité de redéfinir les méthodes de la classe mère pour être instanciée.
Delphi contrairement à C# et Java, ne possède pas à ce jour le modèle de la classe abstraite,
seule la version Delphi8.Net pour le Net Framework possède les mêmes caractéristiques que C#.
Interface
Lorsqu'une classe est déclarée en abstract et que toutes ses méthodes sont déclarées en abstract,
on appelle en C# une telle classe une Interface.
Les interfaces ressemblent aux classes abstraites sur un seul point : elles contiennent des membres
expliquant certains comportements sans les implémenter.
Les classes abstraites et les interfaces se différencient principalement par le fait qu'une classe peut
implémenter un nombre quelconque d'interfaces, alors qu'une classe abstraite ne peut hériter que
d'une seule classe abstraite ou non.
Vocabulaire et concepts :
Une interface est un contrat, elle peut contenir des propriétés, des méthodes et des événements mais
ne doit contenir aucun champ ou attribut.
Pour pouvoir construire un objet à partir d'une interface, il faut définir une classe non abstraite
implémentant toutes les méthodes de l'interface.
Une classe peut implémenter plusieurs interfaces. Dans ce cas nous avons une excellente alternative à l'héritage
multiple.
Lorsque l'on crée une interface, on fournit un ensemble de définitions et de comportements qui ne devraient plus
être modifiés. Cette attitude de constance dans les définitions, protège les applications écrites pour utiliser cette
interface.
Les variables de types interface respectent les mêmes règles de transtypage que les variables de types classe.
Les objets de type classe clA peuvent être transtypés et reférencés par des variables d'interface IntfA dans la
mesure où la classe clA implémente l’interface IntfA. (cf. polymorphisme d'objet)
Si vous voulez utiliser la notion d'interface pour fournir un polymorphisme à une famille de
classes, elles doivent toutes implémenter cette interface, comme dans l'exemple ci-dessous.
Exemple :
l'interface Véhicule définissant 3 méthodes (abstraites) Démarrer, RépartirPassagers de
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 114
répartition des passagers à bord du véhicule (fonction de la forme, du nombre de places, du
personnel chargé de s'occuper de faire fonctionner le véhicule...), et PériodicitéMaintenance
renvoyant la périodicité de la maintenance obligatoire du véhicule (fonction du nombre de km
ou miles parcourus, du nombre d'heures d'activités,...)
Soit l'interface Véhicule définissant ces 3 méthodes :
interface IVehicule{
void Demarrer( );
void RépartirPassager( );
void PériodicitéMaintenance( );
}
Soient les deux classes Véhicule terrestre et Véhicule marin, qui implémentent partiellemnt
chacune l'interface Véhicule , ainsi que trois classes voiture, voilier et croiseur héritant de ces
deux classes :
Les trois méthodes de l'interface Véhicule sont abstraites et publiques par définition.
Les classes Véhicule terrestre et Véhicule marin sont abstraites, car la méthode
abstraite Démarrer de l'interface Véhicule n'est pas implémentée elles reste comme
"modèle" aux futurs classes. C'est dans les classes voiture, voilier et croiseur que l'on
implémente le comportement précis du genre de démarrage.
Dans cette vision de la hiérarchie on a supposé que les classes abstraites Véhicule terrestre et Véhicule marin
savent comment répartir leur éventuels passagers et quand effectuer une maintenance du véhicule.
Les classes voiture, voilier et croiseur , n'ont plus qu'à implémenter chacune son propre comportement de
démarrage.
Une interface C# peut être qualifiée par un des 4 modificateur public, protected, internal,
C# C#
méthode abstraite sans corps méthode virtuelle à corps vide
Pour les types valeurs, la gestion mémoire des objets est classiquement celle de la pile
dynamique, un tel objet se comporte comme une variable locale de la méthode dans laquelle il est
instancié et ne nécessite pas de gestion supplémentaire. Seuls les objets type référence instanciés
sur le tas, nécessitent une gestion mémoire spéciale que nous détaillons ci-après (dans un
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 118
programme C# les types références du développeur représentent près de 99% des objets du
programme).
Delphi C#
type class Un
Un = class { ...
...... }
end;
// la déclaration :
// la déclaration : Un x , y ;
var
x , y : Un; ....
....
// la création :
// la création : x = new Un( );
x := Un.create ; y = new Un( );
y := Un.create ;
Un programme C# est fait pour être exécuté par l'environnement CLR de .NetFramework. Deux
objets C# seront instanciés dans le CLR de la manière suivante :
En C#, la désallocation étant automatique, le bloc de données objet qui était référencé par y avant
Si vous ne déclarez pas de constructeur spécifique pour une classe, par défaut C# attribue
automatiquement un constructeur sans paramètres formels, portant le même nom que la
classe. A la différence de Delphi où le nom du constructeur est quelconque, en C# le( ou
les) constructeur doit obligatoirement porter le même nom que la classe (majuscules et
minuscules comprises).
Un constructeur d'objet d'une classe n'a d'intérêt que s'il est visible par tous les
programmes qui veulent instancier des objets de cette classe, c'est pourquoi l'on mettra
toujours le mot clef public devant la déclaration du constructeur.
Un constructeur est une méthode spéciale dont la fonction est de créer des objets, dans son
en-tête il n'a pas de type de retour et le mot clef void n'est pas non plus utilisé !
Soit une classe dénommée Un dans laquelle, comme nous l'avons fait jusqu'à présent nous
n'indiquons aucun constructeur spécifique :
class Un {
int a;
}
Automatiquement C# attribue un constructeur public à cette classe public Un ( ). C'est comme
si C# avait introduit dans votre classe à votre insu , une nouvelle méthode dénommée Un. Cette
méthode "cachée" n'a aucun paramètre et aucune instruction dans son corps. Ci-dessous un
exemple de programme C# correct illustrant ce qui se passe :
class Un {
public Un ( ) { }
int a;
}
C# Explication
class Un
{ public Un ( )
Le constructeur public Un sert ici à
{ a = 100
initialiser à 100 la valeur de l'attribut "int
}
a" de chaque objet qui sera instancié.
int a;
}
C# Explication
class Un
{ public Un (int b ) Le constructeur public Un sert ici à
{ a = b; initialiser la valeur de l'attribut "int a" de
} chaque objet qui sera instancié. Le
int a; paramètre int b contient cette valeur.
}
C# Explication
class Un
{ public Un (int b ) La classe Un possède 3 constructeurs
{ a = b; servant à initialiser chacun d'une manière
} différente le seul attribut int a.
public Un ( )
{ a = 100;
}
public Un (float b )
{ a = (int)b;
}
int a;
}
Il est possible de rappeller un constructeur de la classe dans un autre constructeur, pour cela C#
utilise comme Java le mot clef this, avec une syntaxe différente :
C# Explication
Delphi C#
class Un
Un = class {
a : integer; int a;
public
constructor creer; overload; public Un ( )
constructor creer (b:integer); overload; { a = 100;
constructor creer (b:real); overload; }
end;
public Un (int b )
implementation { a = b;
}
constructor Un.creer; begin
a := 100 public Un (float b )
end; { a = (int)b;
constructor Un.creer(b:integer); begin }
a := b
public Un (int x , float y ) :
end;
this(y)
constructor Un.creer(b:real); begin
{ a += 100;
a := trunc(b)
}
end;
constructor Un.creer(x:integer; y:real); }
begin
self.creer(y);
a := a+100;
end;
En Delphi un constructeur a un nom quelconque, tous les constructeurs peuvent avoir des noms
différents ou le même nom comme en C#.
Syntaxe
Pour un constructeur sans paramètres formels, l'instruction d'instanciation d'un nouvel objet à
partir d'un identificateur de variable déclarée selon un type de classe, s'écrit syntaxiquement ainsi :
Un x ;
Un x = new Un( )
x = new Un( );
Soit Un une classe de type référence et Deux une autre classe de type
valeur, ci-dessous une image des résulats de l'instanciation d'un objet de
chacune de ces deux classes :
Un x = new Un( ) ;
Deux y = new Deux ( ) ;
Dans l'exemple ci-dessous, nous utilisons le constructeur par défaut de la classe Un , pour créer
deux objets dans une autre classe :
class Un
{ ...
}
C# Explication
class AppliClassesReferences Ce programme C# contient deux classes :
{
public static void Main(String [ ] arg) { class AppliClassesReferences
Un x,y ; et
x = new Un( ); class Un
y = new Un( );
System.Console.WriteLine("x.a="+x.a); La classe AppliClassesReferences est une classe
System.Console.WriteLine"y.a="+y.a); exécutable car elle contient la méthode main. C'est
y = x; donc cette méthode qui agira dès l'exécution du
x.a =12; programme.
System.Console.WriteLine("x.a="+x.a);
System.Console.WriteLine("y.a="+y.a);
}
}
class Un
{ int a=10;
}
Affichage de :
System.Console.WriteLine("x.a="+x.a);
x.a = 10
System.Console.WriteLine("y.a="+y.a);
y.a = 10
Syntaxe
class Un
{ Un obj1 = newUn( );
int a ;
public Un (int b ) { Un obj2 = new Un( 15 );
a=b; } int k = 14;
public Un ( ) { Un obj3 = new Un( k );
a = 100 ; } Un obj4 = new Un( 3.25f );
public Un (float b ) { float r = -5.6;
a = (int)b ; } Un obj5 = new Un( r );
public Un (int x , float y ) : this(y) int x = 20;
{ a += 100; float y = -0.02;
} Un obj6 = new Un( x , y );
}
C# C# équivalent
class Un class Un
{ public Un ( ) { public Un ( )
{ a = 100; { this.a = 100;
} }
int a; int a;
} }
Dans le programme de droite le mot clef this fait référence à l'objet lui-même, ce qui dans ce cas
est superflu puisque la variable int a est un champ de l'objet.
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 126
Montrons deux exemples d'utilisation pratique de this.
Cas où l'objet est passé comme un paramètre dans une de ses méthodes :
C# Explications
class Un
{ public Un ( ) La methode1(Un x) reçoit un objet de type Exemple en
{ a = 100; paramètre et imprime son champ int a.
}
public void methode1(Un x) La methode2( int b ) reçoit un entier int b qu'elle
{ additionne au champ int a de l'objet, puis elle appelle la
System.Console.WriteLine("champ a = " méthode1 avec comme paramètre l'objet lui-même.
+ x.a);
}
public void methode2( int b )
{ a += b;
methode1(this);
}
int a;
}
Comparaison Delphi - C# sur cet exemple (similitude complète)
Delphi C#
Un = class class Un
{ public Un ( )
a : integer;
{ a = 100;
public
constructor creer; }
public void methode1(Un x)
procedure methode1( x:Un );
procedure methode2 ( b:integer ); {
end; System.Console.WriteLine("champ a ="+x.a);
}
implementation public void methode2( int b )
{ a += b;
constructor Un.creer; methode1(this);
begin }
a := 100 int a;
end; }
procedure Un.methode1( x:Un );
begin
showmessage( 'champ a ='+inttostr(x.a) )
end;
procedure Un.methode2 ( b:integer );
begin
a := a+b;
methode1(self)
end;
C# Explications
class Un { La methode1(float a) possède un paramètre float a
int a; dont le nom masque le nom du champ int a.
public void methode1(float a)
{ a = this.a + 7 ; Si nous voulons malgré tout accéder au champ de
} l'objet, l'objet étant référencé par this, "this.a" est
} donc le champ int a de l'objet lui-même.
Delphi C#
Un = class class Un
a : integer; {
public int a;
procedure methode( a:real ); public void methode(float a)
end; { a = this.a + 7 ;
}
implementation
}
procedure Un.methode( a:real );begin
a = self.a + 7 ;
end;
3. Variables et méthodes
Nous examinons dans ce paragraphe comment C# utilise les variables et les méthodes à l'intérieur
d'une classe. Il est possible de modifier des variables et des méthodes d'une classe ceci sera
examinée plus loin.
En C#, les champs et les méthodes sont classés en deux catégories :
class Exemple {
void calcul ( int x, int y )
La définition int a = 100; est locale à la méthode
{int a = 100; en général
for ( int i = 1; i<10; i++ )
{char carlu; La définition int i = 1; est locale à la boucle for.
System.Console.Write("Entrez un caractère : ");
carlu = (char)System.Console.Read( );
int b =15; Les définitions char carlu et int b sont locales au
a =.... corps de la boucle for.
.....
C# ne connaît pas la notion de variable globale au sens habituel donné à cette dénomination, dans
la mesure où toute variable ne peut être définie qu'à l'intérieur d'une classe, ou d'une méthode
inclue dans une classe. Donc à part les variables locales de méthode définies dans une
méthode, C# reconnaît une autre catégorie de variables, les variables définies dans une classe
mais pas à l'intérieur d'une méthode spécifique. Nous les dénommerons : attributs de classes
parce que ces variables peuvent être de deux catégories.
Les attributs de classe peuvent être soit de la catégorie des variables de classe, soit de la catégorie
des variables d'instance.
class AppliInstance
AppliInstance obj1 = new AppliInstance( );
{ int x ;
AppliInstance obj2 = new AppliInstance( );
int y ;
AppliInstance obj3 = new AppliInstance( );
}
Segment de mémoire associé à ces 3 objets si la classe AppliInstance était de type valeur (pour
mémoire):
struct AppliIn
stance
{
int x ;
int y ;
}
Programme C# exécutable
class AppliInstance
{ public int x = -58 ;
public int y = 20 ;
}
class Utilise
static int x ;
static int a = 5;
Une variable de classe est accessible comme une variable d'instance(selon sa visibilité), mais aussi
sans avoir à instancier un objet de la classe, uniquement en référençant la variable par le nom
de la classe dans la notation de chemin uniforme d'objet.
class AppliInstance
AppliInstance obj1 = new AppliInstance( );
{ static int x ;
AppliInstance obj2 = newAppliInstance( );
int y ;
AppliInstance obj3 = newAppliInstance( );
}
Nous pouvons utiliser la classe Math ( public sealed class Math ) qui contient des constantes et
des fonctions mathématiques courantes :
public static const double E; // la constante e représente la base du logarithme népérien.
public static const double PI; // la constante pi représente le rapport de la circonférence d'un
cercle à son diamètre.
Méthode de classe
Une méthode de classe est une méthode dont l'implémentation est la même pour tous les objets de
la classe, en fait la différence avec une méthode d'instance a lieu sur la catégorie des variables sur
lesquelles ces méthodes agissent.
De par leur définition les méthodes de classe ne peuvent travailler qu'avec des variables de
classe, alors que les méthodes d'instances peuvent utiliser les deux catégories de variables.
Un programme correct illustrant le discours :
C# Explications
C# Explications
2) - Pour utiliser une variable x1 ou une méthode meth1 de la classe Classe1, il suffit de
d'écrire Classe1.x1 ou bien Classe1.meth1.
C# Explications
class Exemple2
{ Dans la classe Exemple2, b est une
public static int b = 19; variable de classe, m2 une méthode de
public static void m2( ) {...} classe.
} La classe UtiliseExemple fait appel à la
class UtiliseExemple méthode m2 directement avec le nom de
{ Exemple2.b = 53; la classe, il en est de même avec le
Exemple2.m2( ); champ b de la classe Exemple2
...
}
3) - Une variable de classe (précédée du mot clef static) est partagée par tous les objets de la même
classe.
C# Explications
class AppliStatic
{ public static int x = -58 ; Dans la classe AppliStatic x est une variable de
public int y = 20 ; classe, et y une variable d'instance.
...
La classe Utilise crée 3 objets (obj1,obj2,obj3) de
}
classe AppliStatic.
class Utilise
{ L'instruction obj1.y = 100; est un accès au champ
public static void main(String [ ] arg) { y de l'instance obj1. Ce n'est que le champ x de
AppliStatic obj1 = new AppliStatic( ); cet objet qui est modifié,les champs x des objets
AppliStatic obj2 = new AppliStatic( ); obj2 et obj3 restent inchangés
AppliStatic obj3 = new AppliStatic( );
Au début lors de la création des 3 objets, chacun des champs x vaut -58 et chacun des champs y
vaut 20, l'affichage par System.out.println(...) donne les résultats suivants qui démontrent le
partage de la variable x par tous les objets.
Après exécution :
obj1.x = 101
obj1.y = 100
obj2.x = 101
obj2.y = 20
obj3.x = 101
obj3.y = 20
<AppliStatic>obj1.x = 99
4) - Une méthode de classe (précédée du mot clef static) ne peut utiliser que des variables de
classe (précédées du mot clef static) et jamais des variables d'instance.Une méthode d'instance peut
accéder aux deux catégories de variables
5) - Une méthode de classe (précédée du mot clef static) ne peut appeler (invoquer) que des
méthodes de classe (précédées du mot clef static).
C# Explications
class AppliStatic Nous reprenons l'exemple précédent en ajoutant à
{ la classe AppliStatic une méthode interne f1 :
static int x = -58 ; void f1(int a) {
int y = 20 ; AppliStatic.x = a;
y=6;
void f1(int a) }
{ AppliStatic.x = a; Cette méthode accède à la variable de classe
y=6; comme un champ d'objet.
}
} Nous rajoutons à la classe Utilise, un méthode
static (méthode de classe) notée f2:
class Utilise static void f2(int a)
{ { AppliStatic.x = a;
static void f2(int a) }
{ AppliStatic.x = a; Cette méthode accède elle aussi à la variable de
} classe parce qu c'est une méthode static.
Comme la méthode Main est static, elle peut invoquer la méthode f2 qui est aussi static.
Au paragraphe précédent, nous avons indiqué que C# ne connaissait pas la notion de variable
globale stricto sensu, mais en fait une variable static peut jouer le rôle d'un variable globale
pour un ensemble d'objets instanciés à partir de la même classe.
Plan général:
Polymorphisme d'objet
C'est une interchangeabilité entre variables d'objets de classes de la même hiérarchie sous certaines conditions,
que dénommons le polymorphisme d'objet.
Lorsqu'une classe enfant hérite d'une classe mère, des méthodes supplémentaires nouvelles peuvent être
implémentées dans la classe enfant mais aussi des méthodes des parents peuvent être substituées pour obtenir
des implémentations différentes.
Une classe abstraite est une classe qui ne peut pas s'instancier elle-même ; elle doit être héritée. Certains
membres de la classe peuvent ne pas être implémentés, et c'est à la classe qui hérite de fournir cette
implémentation.
Une interface décrit la signature complète des membres qu'une classe doit implémenter, mais elle laisse
l'implémentation de tous ces membres à la charge de la classe d'implémentation de l'interface.
Polymorphisme d'objet en C#
Soit une classe Mere et une classe Fille héritant de la classe Mere :
En C# :
public class Mere { L'héritage permet une variabilité entre variables
..... d'objets de classes de la même hiérarchie, c'est
}
public class Fille : Mere { cette variabilité que dénommons le
..... polymorphisme d'objet.
}
Nous envisageons toutes les situations possibles et les évaluons, les exemples explicatifs sont
écrits en C# (lorsqu'il y a discordance avec java ou Delphi autres langages, celle-ci est
mentionnée explicitement), il existe 3 possibilités différentes illustrées par le schéma ci-
dessous.
Il s'agit ici d'une utilisation la plus classique qui soit, dans laquelle une variable de référence
d'objet est utilisée dans son type de définition initial (valable dans tous les LOO)
En C# :
Mere x , u ;
Fille y , w ;
.....
x = new Mere( ) ; // instanciation dans le type initial
u = x ; // affectation de références du même type
Mere x ;
Fille ObjF = new Fille( ) ;
x = ObjF; // affectation de références du type descendant implicite
Nous pouvons en effet dire que x peut se référer implicitement à tout objet de classe Mere ou
de toute classe héritant de la classe Mere.
fig - 1 fig - 2
Dans la figure fig-1 ci-dessus, une hiérarchie de classes decendant toutes de la classe Mere,
dans fig-2 ci-contre le schéma montre une référence de type Mere qui peut 'pointer' vers
n'importe quel objet de classe descendante (polymorphisme d'objet).
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 140
D'une façon générale vous pourrez toujours écrire des affectations entre deux références
d'objets :
En C# :
Classe1 x ;
Classe2 y ;
..........
x=y;
si et seulement si Classe2 est une classe descendante de Classe1.
Une hiérarchie de classe de véhicules descendant toutes de la classe mère Vehicule, on peut
énoncer le fait suivant :
Un véhicule peut être de plusieurs sortes : soit un croiseur, soit une voiture, soit un
véhicule terrestre etc...
Traduit en termes informatiques, si l'on déclare une référence de type véhicule (vehicule x)
elle pourra pointer vers n'importe quel objet d'une des classe filles de la classe vehicule.
Comme il est possible de créer directement un objet de classe descendante à partir d'une
référence de classe mère, nous proposons les instanciations suivantes :
on crée une voiture référencée par la variable de classe vehicule,
on crée une berline référencée par la variable de classe voiture,
enfin on crée un break référencé par la variable de classe break.
En C# :
Mere x ;
Fille y ;
Attention
En C# :
Mere x ;
Fille y ;
x = new Mere( ) ; // instanciation dans le type initial
{ affectation de références du type ascendant explicite mais dangereuse si x est uniquement Mere : }
Puisque x pointe vers un objet de type voiture toute variable de référence acceptera de pointer
vers cet objet, en particulier la variable voiture après transtypage de la référence de x.
En C# l'affectation s'écrirait par application de l'opérateur de transtypage :
y = ( voiture ) x;
Pour pallier à cet inconvénient de programmation pouvant lever des exceptions lors de
l'exécution, C# offre au programmeur la possibilité de tester l'appartenance d'un objet
référencé par une variable quelconque à une classe ou plutôt une hiérarchie de classe ; en C#
cet opérateur se dénote is :
L'opérateur is, qui effectue une vérification de type dynamique, est utilisé pour vérifier quelle
est effectivement la classe d'un objet à l'exécution.
L'expression : objet is classeT
renvoie True si objet est une instance de la classe désignée par classeT ou de l'un de ses
descendants, et False sinon. Si objet a la valeur nil, le résultat est False.
En C# :
Mere x ;
Fille y ;
x = new Mere( ) ; // instanciation dans le type initial
if ( x is Fille) // test d'appartenance de l'objet référencé par x à la bonne classe
y = (Fille)x ;
Le polymorphisme d'objet associé au transtypage est très utile dans les paramètres des méthodes.
Lorsque vous déclarez une méthode meth avec un paramètre formel x de type ClasseT :
void meth ( ClasseT x );
{
........
}
Vous pouvez utiliser lors de l'appel de la méthode meth n'importe quel paramètre effectif de
ClasseT ou bien d'une quelconque classe descendant de ClasseT et ensuite à l'intérieur de la
procédure vous transtypez le paramètre. Cet aspect est utilisé en particulier en C# lors de la
création de gestionnaires d'événements communs à plusieurs composants :
// ou encore :
if (sender is System.Windows.Forms.TextBox)
( (TextBox)sender ).Text="Fin";
Plan général:
1. Le polymophisme de méthodes en C#
Rappel des notions de base
Résumé pratique
Rappel de base
L'objectif visé en terme de qualitié du logiciel est la réutilisabilité en particulier lorsque l'on réalise une même
opération sur des éléments différents :
opération = ouvrir ( )
ouvrir une fenêtre de texte, ouvrir un fichier, ouvrir une image etc ...
1.1 Surcharge
On rappelle que la signature d'une méthode est formée par l'en-tête de la méthode avec ses
paramètres formels et leur type.
Nous avons déjà utilisé cette fonctionnalité précédement dans le paragraphe sur les
constructeurs, où la classe Un disposait de quatre constructeurs surchargés (quatre signatures
différentes du constructeur) :
class Un
{
int a;
public Un ( )
{ a = 100; }
public Un (int b )
public Un (float b )
{ a = (int)b; }
Mais cette surcharge est possible aussi pour n'importe quelle méthode de la classe autre
que le constructeur.
Le compilateur n'éprouve aucune difficulté lorsqu'il rencontre un appel à l'une des versions
surchargée d'une méthode, il cherche dans la déclaration de toutes les surcharges celle dont la
signature (la déclaration des paramètres formels) coïncide avec les paramètres effectifs de
l'appel.
Remarque :
Redéfinition
La redéfinition de méthode (ou polymorphisme dynamique) est spécifique aux langages
orientés objet. Elle est mise en oeuvre lors de l'héritage d'une classe mère vers une classe fille
dans le cas d'une méthode ayant la même signature dans les deux classes. Dans ce cas les
actions dûes à l'appel de la méthode, dépendent du code inhérent à chaque version de la
méthode (celle de la classe mère, ou bien celle de la classe fille).
Dans l'exemple ci-dessous, nous supposons que dans la classe PortesEtFenetres la méthode
ouvrir(fenetre) explique le mode opératoire général d'ouverture d'une fenêtre, il est clair que
dans les deux classes descendantes l'on doit "redéfinir" le mode opératoire selon que l'on est en
présence d'une fenêtre à la française, ou une fenêtre à l'anglaise :
Ces deux actions sont différentes selon que le compilateur du langage met en place la laison du
code de la méthode immédiatement lors de la compilation (liaison statique ou précoce) ou
bien lorsque le code est lié lors de l'exécution (laison dynamique ou tardive). Ce phénomène
se dénomme la répartition des méthodes.
Le terme de répartition fait référence à la façon dont un programme détermine où il doit
rechercher une méthode lorsqu'il rencontre un appel à cette méthode.
Le code qui appelle une méthode ressemble à un appel classique de méthode. Mais les classes
ont des façons différentes de répartir les méthodes.
Le langage C# supporte d'une manière identique à Delphi, ces deux modes de laison du code,
la liaison statique étant comme en Delphi le mode par défaut.
Le développeur Java sera plus décontenancé sur ce sujet, car la laison statique en Java n'existe
que pour les methodes de classe static, de plus la laison du code par défaut est dynamique
en Java.
Donc en C# comme en Delphi , des mots clefs comme virtual et override sont nécessaires
pour la redéfinition de méthode, ils sont utilisés strictement de la même manière qu'en Delphi.
Toute méthode C# qui n'est précédée d'aucun des deux qualificateurs virtual ou override est
à liaison statique.
L'avantage principal des méthodes statiques est que leur répartition est très rapide. Comme le
compilateur peut déterminer l'adresse exacte de la méthode, il la lie directement (les méthodes
virtuelles, au contraire, utilisent un moyen indirect pour récupérer l'adresse des méthodes à
l'exécution, moyen qui nécessite plus de temps).
Une méthode statique ne change pas lorsqu'elle est transmise en héritage à une autre classe. Si
vous déclarez une classe qui inclut une méthode statique, puis en dérivez une nouvelle classe,
la classe dérivée partage exactement la même méthode située à la même adresse. Cela signifie
qu'il est impossible de redéfinir les méthodes statiques; une méthode statique fait toujours
exactement la même chose, quelque soit la classe dans laquelle elle est appelée.
Si vous déclarez dans une classe dérivée une méthode ayant le même nom qu'une méthode
statique de la classe ancêtre, la nouvelle méthode remplace simplement (on dit aussi masque)
la méthode héritée dans la classe dérivée.
Delphi C#
type public class ClasseMere
ClasseMere = class {
x : integer; int x = 10;
procedure f (a:integer);
end; public void f ( int a)
{ x +=a; }
ClasseFille = class ( ClasseMere )
y : integer; }
procedure f (a:integer);//masquage
end; public class ClasseFille : ClasseMere
{
implementation int y = 20;
public void f ( int a) //masquage
procedure ClasseMere.f (a:integer); begin... { x +=a*10+y; }
end;
}
procedure ClasseFille.f (a:integer); begin...
end;
Remarque importante :
L'expérience montre que les étudiants comprennent immédiatement le masquage lorsque le
polymorphisme d'objet n'est pas présent. Ci-dessous un exemple de classe UtiliseMereFille qui
instancie et utilise dans le même type un objet de classe ClasseMere et un objet de classe
ClasseFille :
Pour bien comprendre toute la portée du masquage statique et les risques de mauvaises
interprétations, il faut étudier le même exemple légèrement modifié en incluant le cas du
polymorphisme d'objet, plus précisément le polymorphisme d'objet implicite.
Delphi C#
type class ClasseMere
ClasseMere = class
{
x : integer; int x = 10;
procedure f (a:integer);virtual;//autorisation
procedure g(a,b:integer); public virtual void f ( int a)
end; { x +=a; }
void g ( int a, int b)
ClasseFille = class ( ClasseMere ) { x +=a*b; }
y : integer; }
procedure f (a:integer);override;//redéfinition
procedure g1(a,b:integer); class ClasseFille extends ClasseMere
end; {
int y = 20;
implementation public override void f ( int a) //redéfinition
procedure ClasseMere.f (a:integer); begin... { x +=a; }
end; void g1 (int a, int b) //nouvelle méthode
procedure ClasseMere.g(a,b:integer); begin... { ...... }
end; }
Comme delphi, C# peut combiner la surcharge et la redéfinition sur une même méthode, c'est
pourquoi nous pouvons parler de surcharge héritée :
C#
class ClasseMere
{
public int x = 10;
C'est le compilateur C# qui fait tout le travail de recherche de la bonne méthode. Prenons un
objet obj de classe Classe1, lorsque le compilateur C# trouve une instruction du genre
"obj.method1(paramètres effectifs);", sa démarche d'analyse est semblable à celle du
compilateur Delphi, il cherche dans l'ordre suivant :
Y-a-t-il dans Classe1, une méthode qui se nomme method1 ayant une signature
identique aux paramètres effectifs ?
Si aucune méthode ayant cette signature n'est trouvée il signale une erreur.
La redéfinition (polymorphisme dynamique) ne se produit que dans l'héritage d'une classe, par redéfinition (liaison
dynamique) de la méthode mère avec une méthode fille (ayant ou n'ayant pas la même signature).
Toute méthode est considérée à liaison statique sauf si vous la déclarez autrement.
Ce mot clef base est très semblable au mot clef inherited de Delphi qui joue le même rôle sur
les méthodes et les propriétés (il est en fait plus proche du mot clef super de Java car il ne
remonte qu'à la classe mère), il permet l'appel d'une méthode de la classe de base qui a été
substituée (masquée ou redéfinie) par une autre méthode dans la classe fille.
Exemple :
Remarques :
Le fait d'utiliser le mot clef base à partir d'une méthode statique constitue une erreur.
base est utile pour spécifier un constructeur de classe mère lors de la création d'instances
de la classe fille.
Nous développons ci-dessous l'utilisation du mot clef base afin d'initialiser un constructeur.
vous écrivez votre code comme ceci : il est complété implicitement ainsi :
class ClasseA { class ClasseA {
Remarque :
Lors de l'héritage d'une classe fille, différemment à Delphi et à Java, si un constructeur
d'instance C# de la classe fille ne fait pas figurer explicitement d'initialiseur de constructeur,
c'est qu'en fait un initialiseur de constructeur ayant la forme base( ) lui a été fourni
implicitement.
Soit par suite une classe fille ClasseB dérivant de ClasseA possédant elle aussi 2 constructeurs,
les deux déclarations ci-dessous sont équivalentes :
Dans les deux cas le corps du constructeur de la classe fille est initialisé par un premier appel
au constructeur de la classe mère ( ), en l'occurrence << public ClasseA ( ) ... /* premier
constructeur */ >>
Exemple :
vous écrivez votre code comme ceci : il est complété implicitement ainsi :
vous écrivez votre code comme ceci : il est complété implicitement ainsi :
class ClasseA { class ClasseA {
public int attrA ; public int attrA ;
public string attrStrA ; public string attrStrA ;
public ClasseA ( int a ) { public ClasseA ( int a ) {
} }
class ClasseB : ClasseA { }
public ClasseB ( ) { class ClasseB : ClasseA {
//..... public ClasseB ( ): base( ) {
} // ....
} }
}
La classe de base ClasseA ne comporte qu'un seul L'initialiseur implicite base( ) renvoie le compilateur
constructeur explicite à un paramètre. Le constructeur chercher dans la classe de base un constructeur sans
sans paramètres n'existe que si vous le déclarez paramètres.
explicitement, ou bien si la classe ne possède pas de Or il n'existe pas dans la classe de base (ClasseA) de
constructeur explicitement déclaré. constructeur par défaut sans paramètres. Donc la
tentative échoue !
Le message d'erreur sur la ligne " public ClasseB ( ) { ", est le suivant :
[C# Erreur] Class.cs(54): Aucune surcharge pour la méthode 'ClasseA' ne prend d'arguments
'0'
Lorsque l'on veut invoquer dans un constructeur d'une classe donnée un autre constructeur
de cette même classe étant donné que tous les constructeurs ont le même nom, il faut utiliser le
mot clef this comme nom d'appel.
Exemple :
Reprenons la même classe ClasseA possédant 2 constructeurs et la classe ClasseB dérivant de
ClasseA, nous marquons les actions des constructeurs par une chaîne indiquant le numéro du
constructeur invoqué ainsi que sa classe :
class ClasseA {
public int attrA ;
public string attrStrA ;
Créons quatre objets de ClasseB, chacun avec l'un des 4 constructeurs de la ClasseB :
class MaClass {
static void Main(string[] args) {
int x=68;
ClasseB ObjetB= new ClasseB( );
System.Console.WriteLine(ObjetB.attrA);
ObjetB= new ClasseB(x,"aaa");
System.Console.WriteLine(ObjetB.attrStrA);
ObjetB= new ClasseB((char)x,"bbb");
System.Console.WriteLine(ObjetB.attrStrA);
ObjetB= new ClasseB("ccc");
System.Console.WriteLine(ObjetB.attrStrA);
System.Console.ReadLine();
}
}
Voici le résultat console de l'exécution de ce programme :
Explications :
public ClasseB ( ) { C# sélectionne la signature du premier constructeur de la
attrA = 100+attrA ; ClasseB (le constructeur sans paramètres).
}
C# appelle d'abord implicitement le constructeur sans
ClasseB ObjetB= new ClasseB( ); paramètre de la classe mère ( : base( ) )
System.Console.WriteLine(ObjetB.attrA);
public ClasseA ( ) {
attrA = 57 ;
}
Le champ attrA vaut 57,
s = "bbb" ;
public ClasseA ( string s ) {
attrStrA = s +"...classeA1..." ;
}
Le champ attrStrA vaut "bbb...classeA1..."
C# Java
class ClasseA { class ClasseA {
public int attrA ; public int attrA ;
public string attrStrA ; public String attrStrA = "" ;
C# Delphi
C# Java
C# Delphi
class ClasseB : ClasseA ClasseB = class( ClasseA )
syntaxe de base :
class Voiture : Terrestre {
class Vehicule { }
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 167
} class Voilier : Marin {
class Terrestre :Vehicule { }
} class Croiseur : Marin {
class Marin :Vehicule { }
}
Supposons que la classe Véhicule contienne 3 méthodes, qu'elle n'implémente pas la méthode
Démarrer qui est alors abstraite, qu'elle fournit et implante à vide la méthode
"RépartirPassagers" de répartition des passagers à bord du véhicule, qu'elle fournit aussi et
implante à vide une méthode "PériodicitéMaintenance" renvoyant la périodicité de la
maintenance obligatoire du véhicule.
La classe Véhicule est abstraite : car la méthode Démarrer est abstraite et sert de "modèle"
aux futures classes dérivant de Véhicule. Supposons que l'on implémente le comportement
précis du genre de démarrage dans les classes Voiture , Voilier et Croiseur .
Dans cette hiérarchie, les classes Terrestre et Marin héritent de la classe Vehicule, mais
n'implémentent pas la méthode abstraite Démarrer, ce sont donc par construction des classes
abstraites elles aussi. Elles implantent chacune la méthode "RépartirPassagers" (fonction de
la forme, du nombre de places, du personnel chargé de s'occuper de faire fonctionner le
véhicule...) et la méthode "PériodicitéMaintenance" (fonction du nombre de km ou miles
parcourus, du nombre d'heures d'activités,...)
Les classes Voiture , Voilier et Croiseur savent par héritage direct comment répartir leur
éventuels passagers et quand effectuer une maintenance, chacune d'elle implémente son propre
comportement de démarrage.
Quelques implantations en C#
abstract class Terrestre : Vehicule { La classe Terrestre est abstraite car elle n'implémente pas
public new void RépartirPassagers( ){ la méthode abstraite Démarrer.
//...}
public new void PériodicitéMaintenance( ){ Les deux méthodes déclarées dans la classe Terrestre
//...} masquent chacune la méthode du même nom de la classe
} Vehicule (d'où l'utilisation du mot clef new)
class Voiture : Terrestre { La classe Voiture est la seule à être instanciable car toutes
public override void Demarrer( ){ ses méthodes sont concrètes :
//...}
} Elle hérite des 2 méthodes implémentées de la classe
Terrestre et elle implante (redéfinition avec override) la
méthode abstraite de l'ancêtre.
abstract class Terrestre : Vehicule { La classe Terrestre est abstraite car elle n'implémente pas
public override void RépartirPassagers( ){ la méthode abstraite Démarrer.
//...}
public override void PériodicitéMaintenance( ){ Les deux méthodes déclarées dans la classe Terrestre
//...} redéfinissent chacune la méthode du même nom de la
} classe Vehicule (d'où l'utilisation du mot clef override)
class Voiture : Terrestre { La classe Voiture est la seule à être instanciable car toutes
public override void Demarrer( ){ ses méthodes sont concrètes :
//...}
} Elle hérite des 2 méthodes implémentées de la classe
Terrestre et elle implante (redéfinition avec override) la
méthode abstraite de l'ancêtre.
Réponse
La méthode RépartirPassagers est non virtuelle, elle masque la méthode mère du même
nom, si nous voulons accèder au comportement de base d'un véhicule, il nous faut
utiliser le mot clef base permettant d'accèder aux membres de la classe mère :
abstract class Terrestre : Vehicule {
public new void RépartirPassagers( ){
base.RépartirPassagers( ); //... 1°; comportement du parent
//... 2° comportement propre
}
public new void PériodicitéMaintenance( ){
//...}
}
Il est conseillé au lecteur de reprendre le même schéma et d'implanter à l'identique les autres
classe de la hiérarchie pour la branche des véhicules Marin.
Plan général:
Les classes abstraites et les interfaces se différencient principalement par le fait qu'une
classe peut implémenter un nombre quelconque d'interfaces, alors qu'une classe
abstraite ne peut hériter que d'une seule classe abstraite ou non.
1. Vocabulaire et concepts en C#
Une interface C# est un contrat, elle peut contenir des propriétés, des méthodes , des événements ou
des indexeurs, mais ne doit contenir aucun champ ou attribut.
Une interface ne peut pas contenir des méthodes déjà implémentées.
Une interface ne contient que des signatures (propriétés, méthodes ).
Tous les membres d'une interface sont automatiquement public.
Une interface est héritable.
On peut construire une hiérarchie d'interfaces.
Pour pouvoir construire un objet à partir d'une interface, il faut définir une classe non abstraite
implémentant tous les membres de l'interface.
Dans tous les cas il faut une classe pour implémenter ces contrats :
La classe ClasseY doit implémenter tous les 8 membres provenant de l'héritage des interfaces
: les 2 événements OnTruc et Onchose, les 2 propriétés Prop1 et Prop2, et enfin les 4
méthodes meth1, ... , meth4 . La classe ClasseY est une classe concrète (instanciable), un
objet de cette classe possède en particulier tous les membres de l'interface IinterfB (et donc
IinterfA car IinterfB hérite de IinterfA)
Si, ClasseY n'implémente par exemple que 7 membres sur les 8 alors C# considère que c'est
une classe abstraite et vous devez la déclarer abstract :
abstract class ClasseY : ClasseX , IinterfB {
// ...n' implémente que certains membres de InterfB
}
class ClasseZ : ClasseY {
// ... implémente le reste des membres de InterfB
}
// ... construction d'un objet :
ClasseZ Obj = new ClasseZ( ) ;
Par défaut sans déclaration explicite, les membres (indexeurs, propriétés, événements,
méthodes) d'une interface ne nécessitent pas de qualificateurs de visibilité car ils sont
automatiquement déclarés par C# comme étant de visibilité public, contrairement à une classe
ou par défaut les membres sont du niveau assembly.
Ce qui signifie que toute classe qui implémente un membre de l'interface doit obligatoirement
le qualifier de public sous peine d'avoir un message d'erreur du compilateur, dans cette
éventualité le membre devient un membre d'instance. Comme la signature de la méthode n'est
qu'un contrat, le mode de liaison du membre n'est pas fixé; la classe qui implémente le
membre peut alors choisir de l'implémenter soit en liaison statique, soit en liaison
dynamique.
Soient une interface IinterfA et une classe ClasseX héritant directement de la classe Object
et implémentant cette interface, ci-dessous les deux seules implémentations possibles d'une
méthode avec un rappel sur les redéfinitions possibles dans des classes descendantes :
interface IinterfA {
void meth1 ( ); // méthode de l'interface
}
Une classe qui implémente une interface peut aussi implémenter de façon explicite un membre
de cette interface. Lorsqu'un membre est implémenté de façon explicite (le nom du membre
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 174
est préfixé par le nom de l'interface : InterfaceXxx.NomDuMembre ), il n'est pas accessible
via une référence de classe, il est alors invisible à tout objet instancié à partir de la classe où il
est défini. Un membre implémenté de façon explicite n'est donc pas un membre d'instance.
Pour utiliser un membre d'interface implémenté de manière explicite, il faut utiliser une
référence sur cette interface et non une référence de classe; il devient visible uniquement à
travers une référence sur l'interface.
interface IinterfA {
void meth2 ( int x ); // méthode de l'interface
void meth1 ( ); // méthode de l'interface
}
interface IinterfA {
void meth2 ( int x ); // méthode de l'interface
void meth1 ( ); // méthode de l'interface
}
class ClasseX : IinterfA {
void IinterfA.meth2 ( int x ){ ... }
public virtual void meth1 ( ){ ... }
}
Comprenons bien que la classe ClasseX ne possède pas à cet instant une méthode d'instance
qui se nommerait meth2, par exemple si dans la méthode virtuelle meth1 nous utilisons le
paramètre implicite this qui est une référence à la future instance, l'audit de code de C#Builder
nous renvoie 7 méthodes comme visibles (6 provenant de la classe mère Object et une seule
provenant de ClasseX), la méthode IinterfA.meth2 n'est pas visible :
Nous voyons bien que la méthode est qualifiée avec sa signature dans IinterfA, voyons dans l'exemple ci-dessous
que nous pouvons déclarer une méthode d'instance ayant la même signature que la méthode explicite, voir même
de surcharger cette méthode d'instance sans que le compilateur C# n'y voit de conflit car la méthode explicite n'est
pas rangé dans la table des méthodes d'instances de la classe :
La référence Obj2 sur IinterfA fonctionne comme nous l'avons montré plus haut, elle ne peut
voir de la méthode meth2 que son implémentation explicite :
Lorsque vous voulez qu'un membre (une méthode par exemple) implémenté d'une
interface soit privé dans une classe pour toutes les instances de classes qui en
dériveront, l'implémentation explicite vous permet de rendre ce membre (cette
méthode) inaccessible à tout objet.
Lors d'un conflit de noms si deux interfaces possèdent un membre ayant la même
signature et que votre classe implémente les deux interfaces.
Afin d'utiliser les possibilités de C#, l'interface IVehicule propose un contrat d'implémentation
pour un événement, un indexeur, une propriété et une méthode :
Nous souhaitons construire une classe abstraite UnVehicule qui "hérite" à la fois des
fonctionnalités de la classe Vehicule et de celles de l'interface IVehicule. Il nous suffit en C#
de faire hériter la classe UnVehicule de la classe Vehicule , puis que la classe UnVehicule
implémente les propositions de contrat de l'interface IVehicule :
Nous voulons maintenant proposer une spécialisation du véhicule en créant une classe
abstraite Terrestre , base des futurs véhicules terrestres. Cette calsse implantera de façon
explicite la méthode RépartirPassagers de répartition des passagers et la méthode
PériodicitéMaintenance renvoyant la périodicité de la maintenance. Cette classe Terrestre
reste abstraite car elle ne fournit pas l'implémentation de la méthode Demarrer :
Nous finissons notre hiérarchie par une classe Voiture qui descend de la classe Terrestre , qui
implante la méthode Demarrer( ) et qui redéfinie la méthode Stopper( ) :
Rappelons au lecteur que la liaison statique indique que le compilateur lie le code lors de la
compilation, alors que dans le cas d'une liaison dynamique le code n'est choisi et lié que lors
de l'exécution.
Analyse :
Sans qualification particulière une méthode est à liaison statique :
La méthode public void RépartirPassagers ( ) est donc à laison statique.
La méthode public void PériodicitéMaintenance ( ) est donc à laison statique.
interface IVehicule
{
event Starting OnStart ; // déclaration d'événement du type délégué : Starting
string this [ int index] // déclaration d'indexeur
{ get ; set ; }
string TypeEngin // déclaration de propriété
{ get ; set ; }
void Stopper ( ); // déclaration de méthode
}
Analyse :
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 185
Une interface n'est qu'un contrat, les membres déclarés comme signatures dans l'interface
n'étant pas implémentées, la question de leur liaison ne se pose pas au niveau de
l'interface, mais lors de l'implémentation dans une classe ultérieure :
La méthode void Stopper ( ); pourra donc être plus tard soit statique, soit
dynamique.
L'événement event Starting OnStart ; pourra donc être plus tard soit statique,
soit dynamique.
La propriété string TypeEngin , pourra donc être plus tard soit statique, soit
dynamique.
L'indexeur string this [ int index] , pourra donc être plus tard soit statique, soit
dynamique.
Analyse :
Le qualificateur virtual indique que l'élément qualifié est virtuel, donc à liaison
dynamique; sans autre qualification un élément est par défaut à liaison statique :
Analyse :
Analyse :
Une classe peut implémenter plusieurs interfaces. Dans ce cas nous avons une excellente
alternative à l'héritage multiple.
Lorsque l'on crée une interface, on fournit un ensemble de définitions et de comportements
qui ne devraient plus être modifiés. Cette attitude de constance dans les définitions, protège
les applications écrites pour utiliser cette interface.
Les variables de types interface respectent les mêmes règles de transtypage que les variables
de types classe.
Les objets de type classe clA peuvent être transtypés et reférencés par des variables
d'interface IntfA dans la mesure où la classe clA implémente l’interface IntfA. (cf.
polymorphisme d'objet)
interface IMoteur {
Energie carburant // déclaration de propriété
{ get ; }
int consommation ( ); // déclaration de méthode
}
Il est tout à fait possible d'utiliser des variables de référence sur des interfaces et de les
transtyper d'une manière identique à des variables de référence de classe. En particulier le
polymorphisme de référence s'applique aux références d'interfaces.
IVehicule Donc chacun de ces trois genres de paramètre peut être passé
|__UnVehiculeMoteur par polymorphisme de référence d'objet à la méthode public
|__Voiture string use ( IVehicule x )
Les opérateurs is et as sont utilisables avec des références d'interfaces en C#. Reprenons
l'exemple précédent :
class UseVoiture1 {
public string use ( IVehicule x ){
if (x is UnVehiculeMoteur) {
int consom = (x as UnVehiculeMoteur).consommation( );
return " consommation="+consom.ToString( ) ;
}
else
return x.TypeEngin ;
}
}
Il est possible que deux interfaces différentes possèdent des membres ayant la même signature.
Une classe qui implémente ces deux interfaces se trouvera confrontée à un conflit de nom
(ambiguïté). Le compilateur C# exige dès lors que l'ambiguïté soit levée avec le préfixage du
nom du membre par celui de l'interface correspondante (implémentation explicite).
L'exemple ci-dessous est figuré avec deux interfaces IntA et IntB contenant chacune deux
méthodes portant les mêmes noms et plus particulièrement la méthode meth1 possède la même
signature dans chaque interface. Soit ClasseUn une classe implémentant ces deux interfaces.
Voici comment fait C# pour choisir les appels de méthodes implantées.
Il est aussi possible de transtyper une référence d'objet de classe ClasseUn en une référence
d'interface dont elle hérite (les appels sont identiques à ceux du schéma précédent) :
class Tests {
void methTest( ) {
ClasseUn Obj1 = new ClasseUn( );
IntA Obj2 = ( IntA )Obj1;
IntB Obj3 = ( IntB )Obj1;
Obj1.meth2( );
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 193
Obj1.meth2(74);
Obj2.meth2( );
Obj3.meth2(100);
Obj1.meth1(50);
Obj2.meth1(40);
Obj3.meth1(40);
}
}
Nous remarquons qu'aucun conflit et aucune ambiguïté de méthode ne sont possibles et que
grâce à l'implémentation explicite, toutes les méthodes de même nom sont accessibles.
Car l'oubli du parenthésage externe dans l'instruction " (( IntA )Obj1).meth1(40) " peut
provoquer des incompréhensions dans la mesure où aucune erreur n'est signalé par le
compilateur car ce n'est plus la même méthode qui est appelée.
Plan général:
C'est en fait via ce mot clé delegate que nous allons construire des classes qui ont pour nom :
classes de délégations. Ces classes sont des classes du genre référence, elles sont instanciables
et un objet de classe délégation est appelé un délégué.
Un objet de classe délégation permet de référencer (pointer vers) une ou plusieurs méthodes.
Il s'agit donc de l'extension de la notion de pointeur de méthode de Delphi. Selon les auteurs
une classe de délégation peut être ausi nommée classe déléguée, type délégué voir même tout
simplement délégué. Il est essentiel dans le texte lu de bien distinguer la classe et l'objet
instancié.
Il donc possible de créer un nouveau type de délégué (une nouvelle classe) dans un
programme, ceci d'une seule manière : en utilisant le qualificateur delegate. Ci-dessous nous
déclarons un type délégué (une nouvelle classe particulière) nommé NomTypeDelegue :
Un objet délégué peut donc être instancé à partir de cette "classe" comme n'importe quel autre
objet :
Toutes les méthodes référencées par un même délégué ont la même signature partielle :
même type de retour du résultat,
même nombre de paramètres,
même ordre et type des paramètres,
seul leur nom diffère.
Un type commençant par le mot clef delegate est une classe délégation.
Les fonctions de classe Fonc1 et Fonc11 répondent à la signature partielle du type Deleguer1
static string Fonc1 ( int x ) { ... }
static string Fonc11 ( int x ) { ... }
On peut créer un objet (un délégué) qui va référencer l'une ou l'autre de ces deux fonctions :
Deleguer1 FoncDeleg1 = new Deleguer1 ( Fonc1 ) ;
ou bien
Deleguer1 FoncDeleg1 = new Deleguer1 ( Fonc11 ) ;
On peut maintenant appeler le délégué FoncDeleg1 dans une instruction avec un paramètre
d'entrée de type int, selon que le délégué référence Fonc1 ou bien Fonc11 c'est l'une ou l'autre
des ces fonctions qui est en fait appelée.
class ClasseA {
static string Fonc1 ( int x ) {
return ( x * 10 ) .ToString ( );
}
static string Fonc11 ( int x ) {
return ( x * 100 ) .ToString ( );
}
static void Main ( string [] args ) {
string s = Fonc1 ( 32 );
System .Console.WriteLine ("Fonc1(32) = " + s );
s = Fonc11 ( 32 ); // appel de fonction classique
System .Console.WriteLine ("Fonc11(32) = " + s );
System .Console.WriteLine ("\nLe délégué référence Fonc1 :");
Deleguer1 FoncDeleg1 = new Deleguer1 ( Fonc1 ) ;
Comme une référence de délégué peut pointer (référencer) vers des méthodes de classes
différentes au cours de l'exécution, il est intéressant d'obtenir des informations sur la méthode
de classe actuellement référencée par le délégué.
Nous donnons ci-dessous les deux propriétés publiques qui sont utiles lors de cette recherche
d'informations, elles proviennent de la classe mère Delegate non héritable par programme :
Ci-dessous nous avons extrait quelques informations concernant la propriété Method qui est
elle-même de classe MethodInfo :
Ces propriétés sont des membres de la propriété Method qui est applicable uniquement
lorsque le délégué en cours référence une méthode de classe (qualifiée static).
Nous illustrons dans la figure ci-après, dans le cas d'une méthode de classe, l'utilisation des
propriétés Name, DeclaringType et ReturnType membres de la propriété Method :
Source complet exécutable d'un exemple d'information sur la méthode de classe référencée :
namespace PrDelegate {
class ClasseA {
static string Fonc1 ( int x ) {
return ( x * 10 ) .ToString ( );
}
Outre un méthode de classe, un délégué peut pointer aussi vers une méthode d'instance(une méthode d'un
objet). Le fonctionnement (déclaration, instanciation, utilisation) est identique à celui du référencement d'une
méthode de classe, avec syntaxiquement l'obligation, lors de l'instanciation du délégué, d'indiquer le nom de
l'objet ainsi que le nom de la méthode Obj.Methode (similitude avec le pointeur de méthode en Delphi).
Ci-dessous la syntaxe d'un exemple de déclaration de classe délégation pour une méthode d'instance, nous
devons :
Nous illustrons dans la figure ci-après, dans le cas d'une méthode d'instance, l'utilisation de
membres de la propriété Method et de la propriété Target :
class ClasseA {
public int meth1 ( char x ) {
return x ;
}
servent à faire "pointer" la référence ObjX vers l'objet vers lequel pointe FoncDeleg.Target. La
référence de cet objet est transtypée car ObjX est de type ClasseA, FoncDeleg.Target est de
type Object et le compilateur n'accepterait pas l'affectation ObjX = FoncDeleg.Target. Le test
if (ObjX.Equals(ObjA))... permet de nous assurer que les deux références ObjX et ObjA
pointent bien vers le même objet.
Rappelons qu'un type délégué multicast est une classe qui hérite intrinsèquement de la classe
MulticasteDelegate :
Object _target ;
Int32 _methodPtr ;
class ClasseA {
public int meth100 ( char x ) {
System.Console.WriteLine ("Exécution de meth100('"+x+"')");
return x+100 ;
}
static void Main ( string [] args ) {
ClasseA ObjA = new ClasseA( );
Deleguer FoncDeleg = new Deleguer ( ObjA.meth100 ) ;
}
}
Lors de l'exécution, nous avons vu qu'il y a création d'un ObjA de ClasseA et création d'un objet
délégué FoncDeleg, les propriétés Method et Target sont automatiquement initialisées par le
compilateur :
En fait, ce sont les champs privés qui sont initialisés et les propriétés Method et Target qui sont en
lecture seulement, lisent les contenus respectifs de _methodPtr et de _target; le champ _prev est
pour l'instant mis à null , enfin la méthode meth100(...) est actuellement en tête de liste.
Il est possible d'ajouter une nouvelle méthode meth101(...) au délégué qui va la mettre en tête de
liste à la place de la méthode meth100(...) qui devient le deuxième élément de la liste. C# utilise
l'opérateur d'addition pour implémenter l'ajout d'une nouvelle méthode au délégué. Nous étendons
le programme précédent :
delegate int Deleguer ( char x );
class ClasseA {
public int meth100 ( char x ) {
System.Console.WriteLine ("Exécution de meth100('"+x+"')");
return x+100 ;
}
public int meth101 ( char x ) {
System.Console.WriteLine ("Exécution de meth101('"+x+"')");
return x+101 ;
}
static void Main ( string [] args ) {
ClasseA ObjA = new ClasseA( );
//-- meth100 est en tête de liste :
Deleguer FoncDeleg = new Deleguer ( ObjA.meth100 ) ;
// meth101 est ajoutée en tête de liste devant meth100 :
FoncDeleg = FoncDeleg + new Deleguer ( ObjA.meth101 ) ;
}
}
class ClasseA {
Le parcours manuel de la liste montre bien que ce sont des objets de type Delegate qui sont
stockés et que l'on peut accéder entre autre possibilités, à leurs propriétés :
Les événements
Propriétés et indexeurs
Fenêtres et ressources
Contrôles dans les formulaires
Exceptions
Plan général:
Le modèle de conception de l'observateur (Design Pattern observer) est utilisé par Java et C#
pour gérer un événement. Selon ce modèle, un client s'inscrit sur une liste d'abonnés auprès
d'un observateur qui le préviendra lorsqu'un événement aura eu lieu. Les clients délèguent
ainsi l'interception d'un événement à une autre entité. Java utilise ce modèle sous forme d'objet
écouteur.
Dans l'univers des Design Pattern on utilise essentiellement le modèle observateur dans les cas
suivants :
Abonné à un événement
Code C# :
using System;
using System.Collections;
namespace ExempleEvent {
//--> déclaration du type délégation :(par exemple procédure avec 1 paramètre string )
public delegate void DelegueTruc (string s);
Une fois qu'une classe a déclaré un événement Truc, elle peut traiter cet événement
exactement comme un délégué ordinaire. La démarche est très semblable à celle de Delphi, le
champ Truc vaudra null si le client ObjA de ClassA n'a pas raccordé un délégué à l'événement
Truc. En effet être sensible à plusieurs événements n'oblige pas chaque objet à gérer tous les
événement, dans le cas où un objet ne veut pas gérer un événement Truc on n'abonne aucune
méthode au délégué Truc qui prend alors la valeur null.
Dans l'éventualité où un objet doit gérer un événement auquel il est sensible, il doit invoquer
l'événement Truc (qui référence une ou plusieurs méthodes). Invoquer un événement Truc
consiste généralement à vérifier d'abord si le champ Truc est null, puis à appeler l'événement
(le délégué Truc).
Remarque importante
L'appel d'un événement ne peut être effectué qu'à partir de la classe qui a déclaré cet événement.
Considérons ci-dessous la classe ClassA qui est sensible à un événement que nous nommons
Truc (on déclare la référence Truc), dans le corps de la méthode void DeclencheTruc( ) on
appelle l'événement Truc.
Nous déclarons cette méthode void DeclencheTruc( ) comme virtuelle et protégée, de telle
manière qu'elle puisse être redéfinie dans la suite de la hiérarchie; ce qui constitue un gage
d'évolutivité des futures classes quant à leur comportement relativement à l'événement Truc :
Il nous faut aussi prévoir une méthode publique qui permettra d'invoquer l'événement depuis
une autre classe, nous la nommons LancerTruc.
namespace ExempleEvent {
//--> déclaration du type délégation :(par exemple procédure avec 1 paramètre string)
public delegate void DelegueTruc (string s);
Un événement ressemble à un champ public de la classe qui l'a déclaré. Toutefois l'utilisation
de ce champ est très restrictive, c'est pour cela qu'il est déclaré avec le spécificateur event.
Seulement deux opérations sont possibles sur un champ d'événement qui rapellons-le est un
délégué :
Ajouter une nouvelle méthode (à la liste des méthodes abonnées à l'événement).
Supprimer une méthode de la liste (désabonner une méthode de l'événement).
namespace ExempleEvent {
//--> déclaration du type délégation :(par exemple procédure avec 1 paramètre string)
public delegate void DelegueTruc (string s);
Il faut maintenant définir des gestionnaires de l'événement Truc (des méthodes ayant la même
signature que le type délégation " public delegate void DelegueTruc (string s); ". Ensuite nous
ajouterons ces méthodes au délégué Truc (nous les abonnerons à l'événement Truc), ces méthodes
peuvent être de classe ou d'instance.
Supposons que nous ayons une méthode de classe et trois méthodes d'instances qui vont s'inscrire
sur la liste des abonnés à Truc, ce sont quatre gestionnaires de l'événement Truc :
namespace ExempleEvent {
//--> déclaration du type délégation :(par exemple procédure avec 1 paramètre string)
public delegate void DelegueTruc (string s);
Lorsque nous ajoutons en C# les nouvelles méthodes method100, ... , method103 au délégué Truc,
par surcharge de l'opérateur +, nous dirons que les gestionnaires method100,...,method103,
s'abonnent à l'événement Truc.
Prévenir (informer) un abonné correspond ici à l'action d'appeler l'abonné (appeler la méthode) :
Nous complétons le corps de la méthode " static private void methodUse( ) " par l'abonnement au
délégué des quatre gestionnaires.
Pour invoquer l'événement Truc, il faut pouvoir appeler enfin à titre d'exemple une invocation de
l'événement :
using System;
using System.Collections;
namespace ExempleEvent {
//--> déclaration du type délégation :(par exemple procédure avec 1 paramètre string)
public delegate void DelegueTruc (string s);
Bien que le langage C# autorise les événements à utiliser n'importe quel type délégué, le .NET
Framework applique à ce jour à des fins de normalisation, certaines indications plus
restrictives quant aux types délégués à utiliser pour les événements.
Les indications .NET Framework spécifient que le type délégué utilisé pour un événement doit
disposer de deux paramètres et d'un retour définis comme suit :
un paramètre de type Object qui désigne la source de l'événement,
un autre paramètre soit de classe EventArgs , soit d'une classe qui dérive
de EventArgs, il encapsule toutes les informations personnelles relatives à
l'événement,
enfin le type du retour du délégué doit être void.
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 220
Evénement normalisé sans information :
Si vous utilisez des informations personnelles pour l'événement, vous définirez une
classe MonEventArgs qui hérite de la classe EventArgs et qui contiendra ces informations
personnelles, dans cette éventualité la signature du délégué sera :
Il est conseillé d'utiliser la représentation normalisée d'un événement comme les deux
exemples ci-dessous le montre, afin d'augmenter la lisibilité et le portage source des
programmes événementiels.
using System;
using System.Collections;
namespace ExempleEvent {
Pour mettre en place un événement Truc normalisé sans information spéciale, vous devrez
utiliser les éléments suivants :
1°) la classe System.EventArgs
2°) le type délégation normalisée System.EventHandler
3°) une déclaration d'une référence Truc du type délégation normalisée spécifiée event
4.1°) une méthode protégée qui déclenche l'événement Truc (nom commençant par On: OnTruc)
4.2°) une méthode publique qui lance l'événement par appel de la méthode OnTruc
5°) un ou plusieurs gestionnaires de l'événement Truc
6°) abonner ces gestionnaires au délégué Truc
7°) consommer l'événement Truc
using System;
using System.Collections;
namespace ExempleEvent {
Ces 58 événements sont tous normalisés, certains sont des événements sans information
spécifique, d'autres possèdent des informations spécifiques, ci-dessous un extrait de la liste
des événements de la classe Control, plus particulièrement les événements traitant des
actions de souris :
Nous avons mis en évidence deux événements Click et Paint dont l'un est sans information
(Click), l'autre est avec information (Paint). Afin de voir comment nous en servir nous
traitons un exemple :
Soit un fiche (classe Form1 héritant de la classe Form) sur laquelle est déposé un bouton
poussoir (classe Button) de nom button1 :
Les étapes 1° à 4° ont été conçues et developpées par les équipes de .Net et ne sont plus à
notre charge.
L'étape 7° est assurée par le système d'exploitation qui se charge d'envoyer des messages et de
lancer les événements.
Dans le cas de l'événement Paint, le délégué est du type PaintEventArgs situé dans
System.WinForms :
La classe PaintEventArgs :
Dans le cas de l'événement Click, le délégué est de type Event Handler situé dans System :
En fait l'inspecteur d'objet de C#Builder permet de réaliser en mode visuel la machinerie des
étapes qui sont à notre charge :
le délégué de l'événement, [ public event EventHandler Click; ]
le squelette du gestionnaire de l'événement, [ private void button1_Click(object
sender, System.EventArgs e){ } ]
l'abonnement de ce gestionnaire au délégué. [ this.button1.Click += new
System.EventHandler ( this.button1_Click ); ]
Code C# généré
Voici le code généré par C#Builder utilisé en conception visuelle pour faire réagir button1 aux
deux événements Click et Paint :
public WinForm( ) {
InitializeComponent( );
}
.....
}
static void Main( ) {
Application.Run( new WinForm( ) );
}
Plan général:
1. Les propriétés
1.1 Définition et déclaration de propriété
1.2 Accesseurs de propriété
1.3 Détail et exemple de fonctionnement d'une propriété
Exemple du fonctionnement
Explication des actions
1.4 Les propriétés sont de classes ou d'instances
1.5 Les propriétés peuvent être masquées comme les méthodes
1.6 Les propriétés peuvent être virtuelles et redéfinies comme les méthodes
1.7 Les propriétés peuvent être abstraites comme les méthodes
1.8 Les propriétés peuvent être déclarées dans une interface
1.9 Exemple complet exécutable
1.9.1 Détail du fonctionnement en écriture
1.9.2 Détail du fonctionnement en lecture
2. Les indexeurs
Les propriétés du langage C# sont très proches de celle du langage Delphi, mais elles sont plus
complètes et restent cohérentes avec la notion de membre en C#.
Un champ n'est qu'un emplacement de stockage dont le contenu peut être consulté (lecture du
contenu du champ) et modifié (écriture dans le champ), tandis qu'une propriété associe des
actions spécifiques à la lecture ou à l'écriture ainsi que la modification des données que la
propriété représente.
En C#, une propriété fait systématiquement appel à une ou à deux méthodes internes dont les
noms sont les mêmes pour toutes les propriétés afin de fonctionner soit en lecture, soit en
écriture. On appelle ces méthodes internes des accesseurs; leur noms sont get et set , ci-
dessous un exemple de lecture et d'écriture d'une propriété au moyen d'affectations :
cet accesseur indique que la propriété est en lecture et doit renvoyer un résultat dont le type
doit être le même que celui de la propriété. La propriété propr1 ci-dessous est déclarée en
lecture seule et renvoie le contenu d'un champ de même type qu'elle :
cet accesseur indique que la propriété est en écriture et sert à initialiser ou à modifier la
propriété. La propriété propr1 ci-dessous est déclarée en écriture seule et stocke une donnée
de même type qu'elle dans la variable champ :
Ci-dessous une déclaration d'une propriété en lecture et écriture avec attribut de stockage :
Dans l'exemple précédent, la propriété accéde directement sans modification à la donnée brute
stockée dans le champ, mais il est tout à fait possible à une propriété d'accéder à cette donnée
en en modifiant sa valeur avant stockage ou après récupération de sa valeur.
prop1 = 14 ;
La valeur 14 est passée comme paramètre dans la méthode set à la variable implicite value, le
calcul value+5 est effectué et le résultat 19 est stocké dans l'attribut champ.
int x = propr1 ;
La valeur brute 19 stockée dans l'attribut champ est récupérée par la propriété qui l'utilise
dans la méthode accesseur get en la multipliant par 10, c'est cette valeur modifiée de 190 qui
renvoyée par la propriété.
Une propriété servant à fournir automatiquement le prix d'un article en y intégrant la TVA au
taux de 19.6% et arrondi à l'unité d'euro supérieur :
namespace ProjPropIndex
{
class Class {
static private Double prixTotal ;
static private Double tauxTVA = 1.196 ;
[STAThread]
static void Main ( string [] args ) {
Double val = 100 ;
System .Console.WriteLine ("Valeur entrée :" + val );
prix = val ;
System .Console.WriteLine ("Valeur stockée :" + prixTotal );
val = prix ;
System .Console.WriteLine ("valeur arrondie (lue) : " + val ) ;
System .Console.ReadLine ( );
}
}
}
Résultats d'exécution :
Les propriétés, comme les champs peuvent être des propriétés de classes et donc qualifiées
par les mots clefs comme static, abstract etc ...Dans l'exemple précédent nous avons qualifié
tous les champs et la propriété prix en static afin qu'ils puissent être accessibles à la méthode
Main qui est elle-même obligatoirement static.
Voici le même exemple utilisant une version avec des propriétés et des champs d'instances et
non de classe (non static) :
using System ;
namespace ProjPropIndex
{
class clA {
private Double prixTotal ;
private Double tauxTVA = 1.196 ;
class Class {
[STAThread]
static void Main ( string [] args ) {
clA Obj = new clA ( );
Double val = 100 ;
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 237
System .Console.WriteLine ("Valeur entrée :" + val );
Obj.prix = val ;
// le champ prixTotal n'est pas accessible car il est privé
val = Obj.prix ;
System .Console.WriteLine ("valeur arrondie (lue) : " + val ) ;
System .Console.ReadLine ( );
}
}
}
Résultats d'exécution :
Une propriété sans spécificateur particulier de type de liaison est considérée comme une
entité à liaison statique par défaut.
Dans l'exemple ci-après nous dérivons une nouvelle classe de la classe clA nommée clB, nous
redéclarons dans la classe fille une nouvelle propriété ayant le même nom, à l'instar d'un
champ ou d'une méthode C# considère que nous masquons la propriété mère et nous suggère
le conseil suivant :
[C# Avertissement] Class...... : Le mot clé new est requis sur '...........', car il masque le
membre hérité...... '
Nous mettons donc le mot clef new devant la nouvelle déclaration de la propriété dans la
classe fille. En reprenant l'exemple précédent supposons que dans la classe fille clB, la TVA
soit à 5%, nous redéclarons dans clB une propriété prix qui va masquer celle de la mère :
using System ;
namespace ProjPropIndex
{
class clA {
private Double prixTotal ;
private Double tauxTVA = 1.196 ;
Résultats d'exécution :
1.6 Les propriétés peuvent être virtuelles et redéfinies comme les méthodes
Les propriété en C# ont l'avantage important d'être utilisables dans le contexte de liaison
dynamique d'une manière strictement identique à celle des méthodes en C# , ce qui confère au
langage une "orthogonalité" solide relativement à la notion de polymorphisme.
Une propriété peut donc être déclarée virtuelle dans une classe de base et être surchargée
dynamiquement dans les classes descendantes de cette classe de base.
Dans l'exemple ci-après semblable au précédent, nous déclarons dans la classe mère clA la
propriété prix comme virtual, puis :
Nous dérivons clB, une classe fille de la classe clA possédant une propriété prix
masquant statiquement la propriété virtuelle de la classe clA, dans cette classe clB la
TVA appliquée à la variable prix est à 5% (nous mettons donc le mot clef new devant
la nouvelle déclaration de la propriété prix dans la classe fille clB). La propriété prix
est dans cette classe clB à liaison statique.
Nous dérivons une nouvelle classe de la classe clA nommée clB2 dans laquelle nous
redéfinissons en override la propriété prix ayant le même nom, dans cette classe clB2
la TVA appliquée à la variable prix est aussi à 5%. La propriété prix est dans cette
classe clB2 à liaison dynamique.
Notre objectif est de comparer les résultats d'exécution obtenus lorsque l'on utilise une
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 239
référence d'objet de classe mère instanciée soit en objet de classe clB ou clB2. C'est le
comportement de la propriété prix dans chacun de deux cas (statique ou dynamique) qui nous
intéresse :
using System ;
namespace ProjPropIndex
{
class clA {
private Double prixTotal ;
private Double tauxTVA = 1.196 ;
class Class {
static private Double prixTotal ;
static private Double tauxTVA = 1.196 ;
[STAThread]
static void Main ( string [] args ) {
clA Obj = new clA ( );
Double val = 100 ;
System.Console.WriteLine ("Valeur entrée Obj=new clA :" + val );
Obj.prix = val ;
val = Obj.prix ;
System.Console.WriteLine ("valeur arrondie (lue)Obj=new clA : " + val ) ;
System.Console.WriteLine ("----------------------------------------");
Obj = new clB ( );
val = 100 ;
System.Console.WriteLine ("Valeur entrée Obj=new clB :" + val );
Obj.prix = val ;
val = Obj.prix ;
System.Console.WriteLine ("valeur arrondie (lue)Obj=new clB : " + val ) ;
System.Console.WriteLine ("----------------------------------------");
Obj = new clB2 ( );
val = 100 ;
System.Console.WriteLine ("Valeur entrée Obj=new clB2 :" + val );
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 240
Obj.prix = val ;
val = Obj.prix ;
System.Console.WriteLine ("valeur arrondie (lue)Obj=new clB2 : " + val ) ;
System.Console.ReadLine ( );
}
}
}
Résultats d'exécution :
Nous voyons bien que le même objet Obj instancié en classe clB ou en classe clB2 ne fournit
pas les mêmes résultats pour la propriété prix, ces résulats sont conformes à la notion de
polymorphisme en particulier pour l'instanciation en clB2.
Rappelons que le masquage statique doit être utilisé comme pour les méthodes à bon escient,
plus spécifiquement lorsque nous ne souhaitons pas utiliser le polymorphisme, dans le cas
contraire c'est la liaison dynamique qui doit être utilisée pour définir et redéfinir des
propriétés.
Les propriétés en C# peuvent être déclarées abstract, dans ce cas comme les méthodes elles
sont automatiquement virtuelles sans necéssiter l'utilisation du mot clef virtual.
Comme une méthode abstraite, une propriété abstraite n'a pas de corps de définition pour le ou
les accesseurs qui la composent, ces accesseurs sont implémentés dans une classe fille.
Toute classe déclarant une propriété abstract doit elle-même être déclarée abstract,
l'implémentation de la propriété a lieu dans une classe fille, soit en masquant la propriété de la
classe mère (grâce à une déclaration à liaison statique avec le mot clef new), soit en la
redéfinissant (grâce à une déclaration à liaison dynamique avec le mot clef override) :
Les propriétés en C# peuvent être déclarées dans une interface comme les événements et les
méthodes sans le mot clef abstract, dans ce cas comme dans le cas de propriété abstraites la
déclaration ne contient pas de corps de définition pour le ou les accesseurs qui la composent,
ces accesseurs sont implémentés dans une classe fille qui implémente elle-même l'interface.
Les propriétés déclarées dans une interface lorsqu'elles sont implémentées dans une classe
peuvent être définies soit à liaison statique, soit à liaison dynamique.
Ci dessous une exemple de hiérarchie abstraite de véhicules, avec une interface IVehicule
contenant un événement ( cet exemple est spécifié au chapitre sur les interfaces ) :
interface IVehicule {
....
string TypeEngin { // déclaration de propriété abstraite par défaut
get ;
set ;
}
....
}
Elle remonte à ses définitions successives grâce l'utilisation du mot clef base qui fait référence à la classe mère
de la classe en cours.
.........
.....................
Pour aller chercher la valeur effective, elle remonte à ses définitions successives grâce l'utilisation du mot
clef base qui fait référence à la classe mère de la classe en cours.
valeur transmise à
partir de la classe
Voiture.
L'accesseur get va chercher le résultat dans base.TypeEngin et lui concatène le mot "-voiture".
base.TypeEngin référence ici la propriété dans la classe Terrestre.
2. Les indexeurs
Nous savons en Delphi qu'il existe une notion de propriété par défaut qui nous permet par
exemple dans un objet Obj de type TStringList se nomme strings, d'écrire Obj[5] au lieu de
Obj.strings[5]. La notion d'indexeur de C# est voisine de cette notion de propriété par défaut
en Delphi.
Un indexeur est un membre de classe qui permet à un objet d'être indexé de la même manière
qu'un tableau. La signature d'un indexeur doit être différente des signatures de tous les autres
indexeurs déclarés dans la même classe. Les indexeurs et les propriétés sont très similaires de
par leur concept, c'est pourquoi nous allons définir les indexeurs à partir des propriétés.
Tous les indexeurs sont représentés par l' opérateur [ ] . Les liens sur les propriétés ou les
indexeurs du tableau ci-dessous renvoient directement au paragraphe associé.
Propriété Indexeur
Identifiée et utilisée par son nom. Identifié par sa signature, utilisé par l'opérateur
[ ] . L'opérateur [ ] doit être situé
immédiatement après le nom de l'objet.
Peut être un membre de classe static ou un Ne peut pas être un membre static, est toujours
membre d'instance. un membre d'instance.
L'accesseur set correspond à une méthode L'accesseur set correspond à une méthode
avec un seul paramètre implicite value. pourvue de la même liste de paramètres formels
que l'indexeur plus le paramètre implicite value.
Une propriété Prop héritée est accessible par Un indexeur Prop hérité est accessible par la
la syntaxe base.Prop syntaxe base.[ ]
Les propriétés peuvent être à liaison statique, Les indexeurs peuvent être à liaison statique, à
à liaison dynamique, masquées ou redéfinies. liaison dynamique, masqués ou redéfinis.
Les propriétés peuvent être abstraites. Les indexeurs peuvent être abstraits.
Les propriétés peuvent être déclarées dans Les indexeurs peuvent être déclarés dans une
une interface. interface.
2.1.1 Déclaration
Propriété Indexeur
Déclarée par son nom, avec champ de Déclaré par le mot clef this, avec champ de
stockage : stockage :
private int champ; private int [ ] champ = new int [10];
Déclaration : Déclaration :
class clA { class clA {
private int champ; private int [ ] champ = new int [10];
2.1.3 Paramètres
Propriété Indexeur
Déclaration : Déclaration :
class clA { class clA {
private int champ; private int [ ] champ = new int [10];
interface IVehicule {
....
string TypeEngin { // déclaration de propriété abstraite par défaut
get ;
set ;
}
....
string this [ int k ] { // déclaration d'indexeur abstrait par défaut
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 252
get ;
set ;
}
....
}
interface IVehicule {
event Starting OnStart ; // déclaration événement
string this [ int index] // déclaration Indexeur
{
get ;
set ;
}
string TypeEngin // déclaration propriété
{
get ;
set ;
}
void Stopper ( );
}
class UseVoiture
{
static void Main ( string [] args )
{
// instanciation d'une voiture particulière :
UnVehicule automobile = new Voiture ( );
// utilisation de l'indexeur :
automobile [0] = "Citroen";
automobile [1] = "Renault";
automobile [2] = "Peugeot";
automobile [3] = "Fiat";
for( int i = 0 ; i < 4 ; i ++ )
System .Console.WriteLine ("Marque possible : " + automobile [i] );
System .Console.ReadLine ( );
}
}
Résultats d'exécution :
Plan général:
Les exemples et les traitement qui suivent sont effectués sous l'OS windows avec
NetFramWork 1.1., les paragraphes 1.3, 1.4, ... , 1.10 expliquent le contenu du code
généré automatiquement par Visual Studio ou C# Builder pour développer une
application fenêtrée.
etc ...
L'espace des noms System.Windows.Forms est le domaine privilégié du NetFrameWork
dans lequel l'on trouve des classes permettant de travailler sur des applications fenêtrées.
La classe Form de l'espace des noms System.Windows.Forms permet de créer une fenêtre
classique avec barre de titre, zone client, boutons de fermeture, de zoom...
En C#, Il suffit d'instancier un objet de cette classe pour obtenir une fenêtre classique qui
s'affiche sur l'écran.
Nous constatons que l'exécution par le CLR du fichier _exo.exe a produit le résultat escompté c'est
à dire l'affichage du mot Bonjour suivit de l'exécution de la boucle sur 5 itérations.
Afin que le lecteur soit bien convaincu que nous sommes sous NetFramework et que les fichiers
exécutables ne sont pas du binaire exécutable comme jusqu'à présent sous Windows, mais des
fichiers de code MSIL exécutable par le CLR, nous passons le fichier _exo.exe au désassembleur
ildasm par la commande "ildasm.bat".
Le désassembleur MSIL Disassembler (Ildasm.exe) est un utilitaire inclus dans le kit de
développement .NET Framework SDK, il est de ce fait utilisable avec tout langage de .Net dont
C#. ILDasm.exe analyse toutes sortes d'assemblys .NET Framework .exe ou .dll et présente les
informations dans un format explicite. Cet outil affiche bien plus que du code MSIL (Microsoft
Intermediate Language) ; il présente également les espaces de noms et les types, interfaces
comprises.
Exemple::methodeMain void( )
.method private hidebysig static void Main( ) cil managed
{
.entrypoint
// Code size 51 (0x33)
.maxstack 2
.locals init ([0] int32 i)
.language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-
00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
// Source File 'c:\temp\_exoconsole.cs'
//000007: System.Console.WriteLine("Bonjour");
IL_0000: ldstr "Bonjour"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
//000008: for ( int i=1; i<6; i++ )
IL_000a: ldc.i4.1
IL_000b: stloc.0
IL_000c: br.s IL_0028
//000009: System.Console.WriteLine( "i = "+i.ToString( ) );
IL_000e: ldstr "i = "
IL_0013: ldloca.s i
IL_0015: call instance string [mscorlib]System.Int32::ToString()
IL_001a: call string [mscorlib]System.String::Concat(string,string)
IL_001f: call void [mscorlib]System.Console::WriteLine(string)
//000008: for ( int i=1; i<6; i++ )
IL_0024: ldloc.0
IL_0025: ldc.i4.1
IL_0026: add
IL_0027: stloc.0
IL_0028: ldloc.0
IL_0029: ldc.i4.6
IL_002a: blt.s IL_000e
//000009: System.Console.WriteLine( "i = "+i.ToString( ) );
//000010: System.Console.ReadLine( );
IL_002c: call string [mscorlib]System.Console::ReadLine()
IL_0031: pop
On peut donc de la même façon compiler et exécuter à partir de la console, des programmes
C# contenant des fenêtres, comme en java il est possible d'exécuter à partir de la console des
applications contenant des Awt ou des Swing, idem en Delphi. Nous proposons au lecteur de
savoir utiliser des programmes qui allient la console à une fenêtre classique, ou des
programmes qui ne sont formés que d'une fenêtre classique (à minima).
Ce programme "_exowin.cs" utilise la classe Form et affiche une fenêtre de type Form :
set path=C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322
csc /t:exe /out:_exow.exe _exowin.cs
Cette commande a généré comme précédemment l'exécutable MSIL nommé ici _exow.exe
dans le dossier c:\temp, nous exécutons le programme _exow.exe et nous obtenons l'affichage
d'une fenêtre de console et de la fenêtre fiche :
Si nous ne voulons pas voir apparaître de fenêtre de console mais seulement la fenêtre fiche, il
faut alors changer dans le paramétrage du compilateur l'attribut target. De la valeur csc /t:exe
il faut passer à la valeur csc /t:winexe :
set path=C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322
csc /t:winexe /out:_exow.exe _exowin.cs
Consultons à titre informatif avec ildasm le code MSIL engendré pour la méthode Main( ) :
Nous avons mis en gras et en italique les commentaires d'instructions sources
Exemple::methodeMain void( )
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 35 (0x23)
.maxstack 2
.locals init ([0] class [System.Windows.Forms]System.Windows.Forms.Form fiche)
.language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}',
'{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
// Source File 'c:\temp\_exowin.cs'
//000008: Form fiche = new Form( );
IL_0000: newobj instance void [System.Windows.Forms]System.Windows.Forms.Form::.ctor()
IL_0005: stloc.0
//000009: fiche.Text="Exemple de Form";
IL_0006: ldloc.0
IL_0007: ldstr "Exemple de Form"
IL_000c: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)
//000010: fiche.BackColor=System.Drawing.Color.RosyBrown;
IL_0011: ldloc.0
IL_0012: call valuetype [System.Drawing]System.Drawing.Color
[System.Drawing]System.Drawing.Color::get_RosyBrown()
IL_0017: callvirt instance void
[System.Windows.Forms]System.Windows.Forms.Control::set_BackColor(valuetype
[System.Drawing]System.Drawing.Color)
//000011: Application.Run(fiche);
IL_001c: ldloc.0
IL_001d: call void [System.Windows.Forms]System.Windows.Forms.Application::Run(class
[System.Windows.Forms]System.Windows.Forms.Form)
//000012: }
IL_0022: ret
} // end of method Exemple::Main
Comme les fenêtres dans Windows ne sont pas des objets ordinaires, pour qu'elles
fonctionnent correctement vis à vis des messages échangés entre la fenêtre et le système, il est
nécessaire de lancer une boucle d'attente de messages du genre :
tantque non ArrêtSysteme faire
si événement alors
construire Message ;
si Message ArrêtSysteme alors
reconnaître la fenêtre à qui est destinée ce Message;
distribuer ce Message
fsi
fsi
ftant
La documentation technique indique que l'une des surcharges de la méthode de classe Run de
la classe Application "public static void Run( Form mainForm );" exécute une boucle de
messages d'application standard sur le thread en cours et affiche la Form spécifiée. Nous en
déduisons que notre fenêtre fiche est bien initialisée et affichée par cette méthode Run.
Lorsque nous compilons puis exécutons ce programme la fiche apparaît correctement (titre et
couleur) d'une manière fugace car elle disparaît aussi vite qu'elle est apparue. En effet le
programme que nous avons écrit est correct :
La fugacité de l'affichage de notre fenêtre fiche est donc normale, puisqu'à peine créée la fiche
a été détruite.
Si nous voulons que notre objet de fiche persiste sur l'écran, il faut simuler le comportement de
la méthode classe Run, c'est à dire qu'il nous faut écrire une boucle de messages.
Nous allons utiliser la méthode de classe DoEvents( ) de la classe Application qui existe
depuis Visual Basic 2, et qui permet de traiter tous les messages Windows présents dans la file
d'attente des messages de la fenêtre (elle passe la main au système d'une façon synchrone) puis
revient dans le programme qui l'a invoquée (identique à processMessages de Delphi).
Nous créeons artificiellement une boucle en apparence infinie qui laisse le traitement des
messages s'effectuer et qui attend qu'on lui demande de s'arrêter par l’intermédiaire d’un
booléen stop dont la valeur change par effet de bord grâce à DoEvents :
Choisissons une telle action par exemple lorsque l'utilisateur clique sur le bouton de fermeture
de la fiche, la fiche se ferme et l'événement closed est déclenché, DoEvents( ) revient dans la
boucle d'attente while (!stop) Application.DoEvents( ); au tour de boucle suivant. Si lorsque
l'événement close de la fiche a lieu nous en profitons pour mettre le booléen stop à true, dès le
retour de DoEvents( ) le prochain tour de boucle arrêtera l'exécution de la boucle et le corps
d'instruction de main continuera à s'exécuter séquentiellement jusqu'à la fin (ici on arrêtera le
processus).
On peut aussi vouloir toujours en utilisant la boucle infinie qui laisse le traitement des
messages s'effectuer ne pas se servir d'un booléen et continuer après la boucle, mais plutôt
essayer d'interrompre et de terminer l'application directement dans la boucle infinie sans
exécuter la suite du code. La classe Application ne permet pas de terminer le processus.
Il faut pouvoir détruire le processus en cours (en prenant soin d'avoir tout libéré avant si
nécessaire), pour cela le NetFrameWork dispose d'une classe Process qui permet l'accès à des
processus locaux ainsi que distants, et vous permet de démarrer et d'arrêter des processus
systèmes locaux.
Toutefois la console n'est pas l'outil préférentiel de C# dans le sens où C# est l'outil de
développement de base de .Net et que cette architecture a vocation à travailler essentiellement
avec des fenêtres.
Dans ce cas nous avons tout intérêt à utiliser un RAD visuel C# pour développer ce genre
d'applications (comme l'on utilise Delphi pour le développement d'IHM en pascal objet). Une
telle utilisation nous procure le confort du développement visuel, la génération automatique
d'une bonne partie du code répétitif sur une IHM, l'utilisation et la réutilisation de composants
logiciels distribués sur le net.
RAD utilisables
Visual Studio de microsoft contient deux RAD de développement pour .Net, VBNet
(fondé sur Visual Basic réellement objet et entièrement rénové) et Visual C# (fondé sur
le langage C#), parfaitement adapté à .Net. (prix très réduit pour l'éducation)
Les fiches ou formulaires C# représentent l'interface utilisateur (IHM) d'une application sous
l'apparence d'une fenêtre visuelle. Comme les deux environnements RAD, Visual studio C# de
Microsoft et C# Builder de Borland permettent de concevoir visuellement des applications
avec IHM, nous dénommerons l'un ou l'autre par le terme général RAD C#.
Voici l'apparence d'un formulaire (ou fiche) dans le RAD C# Builder en mode conception :
La fiche elle-même est figurée par l'image ci-dessous retaillable à volonté à partir de cliqué
glissé sur l'une des huits petites "poignées carrées" situées aux points cardinaux de la fiche :
Poignées de manipulation
du composant.
Ces formulaires sont en faits des objets d'une classe nommée Form de l'espace des noms
System.Windows.Forms. Ci-dessous la hiérarchie d'héritage de Object à Form :
System.Object
System.MarshalByRefObject
System.ComponentModel.Component
La classe Form est la classe de base de tout style de fiche (ou formulaire) à utiliser dans votre
application (statut identique à TForm dans Delphi) : fiche de dialogue, sans bordure etc..
Dans un formulaire, le style est spécifié par la propriété FormBorderStyle de la classe Form :
public FormBorderStyle FormBorderStyle {get; set;}
Toutes les propriétés en lecture et écriture d'une fiche sont accessibles à travers l'inspecteur
d'objet qui répercute immédiatement en mode conception toute modification. Certaines
provoquent des changements visuels d'autres non :
WinForm
Le RAD construit automatiquement notre fiche principale comme une classe héritée de la
classe Form et l'appelle WinForm :
public class WinForm : System.Windows.Forms.Form
{ ... }
La classe de notre formulaire s'appelle désormais Form1, mais son aspect visuel est resté le
même :
Après avoir remis grâce à l'inspecteur d'objet, la propriété FormBorderStyle à sa valeur Sizable
et remis le Name à sa valeur initiale WinForm, voyons maintenant en supposant avoir appelé
notre application ProjApplication0 ce que C# Builder a engendré comme code source que
nous trouvons dans l'onglet code pour notre formulaire :
L'intégralité du code
proposé par C# Builder est
sauvegardé dans un fichier
nommé WinForm.cs
La classe WinForm
contient en première
analyse 3 méthodes.
La méthode correspond
public WinForm ( ) { au constructeur d'objet de la classe
... WinForm et que la méthode
}
La classe Application (semblable à TApplication de Delphi) fournit des membres statiques (propriétés et
méthodes de classes) pour gérer une application (démarrer, arrêter une application, traiter des messages
Windows), ou d'obtenir des informations sur une application. Cette classe est sealed et ne peut donc pas être
héritée.
La méthode Run de la classe Application dont voici la signature : Exécute une boucle de messages
public static void Run( ApplicationContext context ); d'application standard sur le thread en
cours, par défaut le paramètre context
écoute l'événement Closed sur la fiche
principale de l'application et dès lors
arrête l'application.
Pour les connaisseurs des Awt et des Swing de Java, cette action C# correspond aux lignes
suivantes :
Java2 avec Awt
class WinForm extends Frame {
public WinForm ( ) {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
}
protected void processWindowEvent(WindowEvent e) {
Lorsque l'on regarde de plus près le code de la classe WinForm situé dans l'onglet code on se
rend compte qu'il existe une ligne en grisé entre la méthode Dispose et la méthode main :
namespace ProjApplication0
{
/// <summary>
/// Description Résumé de Form1.
/// </summary>
public class WinForm : System.Windows.Forms.Form
{
/// <summary>
/// Variable requise par le concepteur.
/// </summary>
private System.ComponentModel.Container components = null;
public WinForm ( )
{
//
// Requis pour la gestion du concepteur Windows Form
//
InitializeComponent( );
//
// TODO: Ajouter tout le code du constructeur après l'appel de InitializeComponent
//
/// <summary>
/// Le point d'entrée principal de l'application.
/// </summary>
[STAThread]
static void Main( )
{
Application.Run(new WinForm ( ));
}
}
}
Essayons de voir comment une manipulation visuelle engendre des lignes de code, pour cela
modifions dans l'inspecteur d'objet deux propriétés FormBorderStyle et BackColor, la
première est mise à None la seconde qui indique la couleur du fond de la fiche est mise à
LightSalmon :
Consultons après cette opération le contenu du nouveau code généré, nous trouvons deux
nouvelles lignes de code correspondant aux nouvelles actions visuelles effectuées (les
nouvelles lignes sont figurées en rouge ) :
#region Code généré par le concepteur Windows Form
/// <summary>
/// Méthode requise pour la gestion du concepteur - ne pas modifier
/// le contenu de cette méthode avec l'éditeur de code.
/// </summary>
private void InitializeComponent( )
{
//
// WinForm
//
Dans le code engendré par Visual studio ou C# Builder, nous avons laissé de côté la méthode
Dispose :
Pour comprendre son utilité, il nous faut avoir quelques lumières sur la façon que
NetFrameWork a de gérer les ressources, rappelons que le CLR exécute et gére le code
administré c'est à dire qu'il vérifie la validité de chaque action avant de l'exécuter. Le code non
administré ou ressource non managée en C# est essentiellement du code sur les pointeurs qui
doivent être déclarés unsafe pour pouvoir être utilisés, ou bien du code sur des fichiers, des
flux , des handles .
classe : System.ComponentModel.Component
Notons que pour le débutant cette méthode ne sera jamais utilisée et peut être omise
puisqu'il s'agit d'une surcharge dynamique de la méthode de la classe mère.
Remarque-2
Il est recommandé par Microsoft, qu'un objet Component libère des ressources
explicitement en appelant sa méthode Dispose sans attendre une gestion automatique de
la mémoire lors d'un appel implicite au Garbage Collector.
Si nous voulons comprendre comment fonctionne le code engendré pour la méthode Dispose,
il nous faut revenir à des éléments de base de la gestion mémoire en particulier relativement à
la libération des ressources par le ramasse-miettes (garbage collector).
Lorsqu'un objet devient inaccessible il est automatiquement placé dans la file d'attente de
finalisation de type FIFO, le garbage collecteur GC, lorsque la mémoire devient trop basse,
effectue son travail en parcourant cette file d'attente de finalisation et en libérant la mémoire
occupée par les objets de la file par appel à la méthode Finalize de chaque objet.
Donc si l'on souhaite libérer des ressources personnalisées, il suffit de redéfinir dans une classe
fille la méthode Finalize( ) et de programmer dans le corps de la méthode la libération de ces
ressources.
~MaClasse( ) {
// libération des ressources personnelles
}
Par exemple, vous pouvez empêcher explicitement la méthode Finalize d'un objet figurant
dans la file d'attente de finalisation d'être appelée, (utilisation de la méthode : public static
void SuppressFinalize( object obj );)
Vous pouvez aussi obliger explicitement la méthode Finalize d'un objet figurant dans la file
d'attente de finalisation mais contenant GC.SuppressFinalize(...) d'être appelée, ( utilisation de
la méthode : public static void ReRegisterForFinalize( object obj ); ).
Il est vivement déconseillé d'appeler une méthode Finalize pour une autre classe que votre classe de base
directement à partir du code de votre application. Pour supprimer correctement des ressources non
managées, il est recommandé d'implémenter une méthode Dispose ou Close publique qui exécute le code
de nettoyage nécessaire pour l'objet.
2- Elle doit également libérer toutes les ressources détenues par ses types de base en appelant la méthode
Dispose de son type parent. La méthode Dispose du type parent doit libérer toutes les ressources qu'il possède et
appeler à son tour la méthode Dispose de son type parent, propageant ainsi ce modèle dans la hiérarchie des
types de base.
3- Pour que les ressources soient toujours assurées d'être correctement nettoyées, une méthode Dispose doit
pouvoir être appelée en toute sécurité à plusieurs reprises sans lever d'exception.
4- Une méthode Dispose doit appeler la méthode GC.SuppressFinalize de l'objet qu'elle supprime.
Ce modèle n'est présenté que pour mémoire afin de bien comprendre le modèle pour une
classe fille qui suit et qui correspond au code généré par le RAD C# .
Voici proposé le modèle de conception simplifié (Design Pattern) d'une classe MaClasseFille
descendante de MaClasseMere, la classe fille contient une ressource de type
System.ComponentModel.Container
System.ComponentModel.Container
La classe Container est l'implémentation par défaut pour l'interface IContainer, une instance s'appelle
un conteneur.
Les conteneurs sont des objets qui encapsulent et effectuent le suivi de zéro ou plusieurs composants
qui sont des objets visuels ou non de la classe System.ComponentModel.Component.
Les références des composants d'un conteneur sont rangées dans une file FIFO, qui définit également
leur ordre dans le conteneur.
La classe Container suit le modèle de conception mentionné plus haut quant à la libération des
ressources managées ou non. Elle possède deux surcharges de Dispose implémentées selon le Design
Pattern : la méthode protected virtual void Dispose( bool disposing); et la méthode public void
Dispose( ). Cette méthode libère toutes les ressources détenues par les objets managés stockés dans la
FIFO du Container. Cette méthode appelle la méthode Dispose( ) de chaque objet référencé dans la
FIFO.
Nous savons maintenant à quoi sert la méthode Dispose dans le code engendré par le
RAD, elle nous propose une libération automatique des ressources de la liste des
composants que nous aurions éventuellement créés :
Supposons que nous avons construit un composant personnel dans une classe UnComposant
qui hérite de la classe Component selon le Design Pattern précédent, et que nous avons défini
cette classe dans le namespace ProjApplication0 :
System.ComponentModel.Component
|__ProjApplication0.UnComposant
Nous voulons construire une application qui est un formulaire et nous voulons créer lors de
l'initialisation de la fiche un objet de classe UnComposant que notre formulaire utilisera.
A un moment donné notre application ne va plus se servir du tout de notre composant, si nous
souhaitons gérer la libération de la mémoire allouée à ce composant, nous pouvons :
Soit attendre qu'il soit éligible au GC, en ce cas la mémoire sera libérée lorsque le GC le
décidera,
namespace ProjApplication0
{
/// <summary>
/// Description Résumé de Form1.
/// </summary>
public class WinForm : System.Windows.Forms.Form
{
/// <summary>
/// Variable requise par le concepteur.
/// </summary>
private System.ComponentModel.Container components = null;
private UnComposant MonComposant = new UnComposant( );
if ( components == null )
components = new Container( );
components.Add ( MonComposant ); Ajout du composant personnel
} dans la Fifo de components.
/// <summary>
/// Nettoyage des ressources utilisées.
/// </summary>
protected override void Dispose (bool disposing)
{
Notre composant personnel est
if (disposing) {
libéré avec les autres
if (components != null) {
components.Dispose( );
}
//-- libération ici d'autres ressources managées...
}
//-- libération ici de vos ressources non managées...
base.Dispose(disposing);
}
/// <summary>
/// Le point d'entrée principal de l'application.
/// </summary>
[STAThread]
static void Main( )
{
Application.Run(new WinForm ( ));
}
}
}
La documentation technique signale que deux utilisations principales du mot clé using sont
possibles :
Directive using :
Crée un alias pour un espace de noms ou importe des types définis dans d'autres
espaces de noms.
Ex: using System.IO ; using System.Windows.Forms ; ...
Instruction using :
Définit une portée au bout de laquelle un objet est supprimé.
<instruction using> ::= using ( <identif. Objet> | <liste de Déclar & instanciation> ) <bloc
instruction>
<bloc instruction> ::= { < suite d'instructions > }
<identif. Objet> ::= un identificateur d'un objet existant et instancié
<liste de Déclar & instanciation> ::= une liste séparée par des virgules de déclaration et
initialisation d'objets semblable à la partie initialisation d'une boucle for.
Dans les deux cas, on utilise une instance (Obj de classeA) ou l'on crée des
instances (Obj1, Obj2 et Obj3 de classeB) dans l'instruction using pour garantir que la
méthode Dispose est appelée sur l'objet lorsque l'instruction using est quittée.
Les objets que l'on utilise ou que l'on crée doivent implémenter l'interface
System.IDisposable. Dans les exemples précédents classeA et classeB doivent
implémenter elles-même ou par héritage l'interface System.IDisposable.
// ....
this.button1 = new System.Windows.Forms.Button ( );
using( button1) {
// code quelconque....
}
// suite du code ....
Nous terminons notre examen du code généré automatiquement par le RAD pour une
application fenêtrée de base, en indiquant la signification de l'attribut (mot entre crochets
[STAThread]) situé avant la méthode main :
/// <summary>
/// Le point d'entrée principal de l'application.
/// </summary>
[STAThread]
static void Main( )
{
Application.Run(new WinForm ( ));
}
Cet attribut placé ici devant la méthode main qualifie la manière dont le CLR exécutera
l'application, il signifie : Single Thread Apartments.
Il s'agit d'un modèle de gestion mémoire où l'application et tous ses composants est gérée dans
un seul thread ce qui évite des conflits de ressources avec d'autres threads. Le développeur n'a
pas à s'assurer de la bonne gestion des éventuels conflits de ressources entre l'application et ses
composants.
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 290
Si on omet cet attribut devant la méthode main, le CLR choisi automatiquement
[MTAThread] Multi Thread Apartments, modèle de mémoire dans lequel l'application et ses
composants sont gérés par le CLR en plusieurs thread, le développeur doit alors s'assurer de la
bonne gestion des éventuels conflits de ressources entre l'application et ses composants.
Sauf nécessité d'augmentation de la fluidité du code, il faut laisser (ou mettre en mode
console) l'attribut [STAThread] :
...
[STAThread]
static void Main( )
{
Application.Run(new WinForm ( ));
}
Plan général:
Car il est essentiel que le lecteur sache par lui-même écrire le code qui sera généré
automatiquement par un RAD, sous peine d'être prisonnier du RAD et de ne pas pouvoir
intervenir sur ce code !
Dans les deux RAD Visual C# et C#Builder la programmation visuelle des contrôles a lieu d'une
façon très classique, par glisser déposer de composants situés dans une palette ou boîte d'outils. Il
est possible de déposer visuellement des composants, certains sont visuels ils s'appellent
contrôles, d'autres sont non visuels, ils s'appellent seulement composants.
C# Builder : Visual C# :
System.ComponentModel.Component
System.Windows.Forms.Menu
System.Windows.Forms.MainMenu
Nous voyons que Visual C# range System.Windows.Forms.MainMenu dans les contrôles alors
que C# Builder le range dans les composants (donc non visuels). Ci-dessous les outils de
composants proposés par les deux RAD :
C# Builder Visual C# :
Pour pouvoir construire une IHM, il nous faut pouvoir utiliser à minima les composants visuels
habituels que nous retrouvons dans les logiciels windows-like. Ici aussi la documentation
technique fournie avec le RAD détaillera les différentes entités mises à disposition de
l'utilisateur.
Il existe des contrôles qui sont des conteneurs visuels, les quatre classes ci-après sont les
principales classe de conteneurs visuels de C# :
System.Windows.Forms.Form
System.Windows.Forms.Panel
System.Windows.Forms.GroupBox
System.Windows.Forms.TabPage
Sur la fiche précédente nous relevons outre le formulaire lui-même, quatre conteneurs visuels
répartis en trois catégories de conteneurs visuels :
System.Windows.Forms.Panel
System.Windows.Forms.TabPage
System.Windows.Forms.GroupBox
System.Windows.Forms.Label
System.Windows.Forms.PictureBox
System.Windows.Forms.Button
System.Windows.Forms.TextBox
public WinForm ( ) {
//
// Requis pour la gestion du concepteur Windows Form
//
InitializeComponent ( );
//
// TODO: Ajouter tout le code du constructeur après l'appel de InitializeComponent
//
}
/// <summary>
/// Nettoyage des ressources utilisées.
/// </summary>
protected override void Dispose ( bool disposing ) {
if ( disposing ) {
if ( components != null) {
components.Dispose ( );
}
}
base .Dispose ( disposing );
}
//
// panel1
//
this.panel1.BackColor = System .Drawing.Color.NavajoWhite ;
this.panel1.Controls.Add (this .button2 );
this.panel1.Controls.Add (this .textBox1 );
this.panel1.Controls.Add (this .label2 );
this.panel1.Location = new System .Drawing.Point ( 32, 40 );
this.panel1.Name = "panel1";
this.panel1.Size = new System .Drawing.Size ( 224, 104 );
this.panel1.TabIndex = 0 ;
//
// button2
//
this.button2.Location = new System .Drawing.Point ( 88, 24 );
this.button2.Name = "button2";
this.button2.Size = new System .Drawing.Size ( 72, 24 );
this.button2.TabIndex = 4 ;
this.button2.Text = "button2";
//
// textBox1
//
this.textBox1.Location = new System .Drawing.Point ( 16, 76 );
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System .Drawing.Size ( 192, 20 );
this.textBox1.TabIndex = 3 ;
this.textBox1.Text = "textBox1";
//
// label2
//
this.label2.BackColor = System .Drawing.SystemColors.Info ;
this.label2.Location = new System .Drawing.Point ( 16, 32 );
this.label2.Name = "label2";
this.label2.Size = new System .Drawing.Size ( 88, 40 );
this.label2.TabIndex = 2 ;
this.label2.Text = "label2";
//
// panel2
//
this.panel2.BackColor = System .Drawing.Color.PaleTurquoise ;
this.panel2.Controls.Add (this .button1 );
this.panel2.Controls.Add (this .pictureBox1 );
this.panel2.Location = new System .Drawing.Point ( 200, 16 );
this.panel2.Name = "panel2";
this.panel2.Size = new System .Drawing.Size ( 208, 112 );
this.panel2.TabIndex = 2 ;
//
// groupBox1
//
this.groupBox1.BackColor = System .Drawing.Color.SkyBlue ;
this.groupBox1.Controls.Add (this .textBox3 );
this.groupBox1.Controls.Add (this .button5 );
this.groupBox1.Location = new System .Drawing.Point ( 8, 160 );
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System .Drawing.Size ( 184, 104 );
this.groupBox1.TabIndex = 4 ;
this.groupBox1.TabStop = false ;
this.groupBox1.Text = "groupBox1";
//
// textBox3
//
this.textBox3.Location = new System .Drawing.Point ( 8, 72 );
this.textBox3.Name = "textBox3";
this.textBox3.Size = new System .Drawing.Size ( 136, 20 );
this.textBox3.TabIndex = 1 ;
this.textBox3.Text = "textBox3";
//
// button5
//
this.button5.Location = new System .Drawing.Point ( 16, 32 );
//
// tabControl1
//
this.tabControl1.Controls.Add (this .tabPage1 );
this.tabControl1.Controls.Add (this .tabPage2 );
this.tabControl1.Location = new System .Drawing.Point ( 224, 144 );
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0 ;
this.tabControl1.Size = new System .Drawing.Size ( 200, 120 );
this.tabControl1.TabIndex = 5 ;
//
// tabPage1
//
this.tabPage1.BackColor = System .Drawing.Color.DarkTurquoise ;
this.tabPage1.Controls.Add (this .textBox2 );
this.tabPage1.Controls.Add (this .button3 );
this.tabPage1.Location = new System .Drawing.Point ( 4, 22 );
this.tabPage1.Name = "tabPage1";
this.tabPage1.Size = new System .Drawing.Size ( 192, 94 );
this.tabPage1.TabIndex = 0 ;
this.tabPage1.Text = "tabPage1";
//
// textBox2
//
this.textBox2.Location = new System .Drawing.Point ( 16, 16 );
this.textBox2.Name = "textBox2";
this.textBox2.Size = new System .Drawing.Size ( 160, 20 );
this.textBox2.TabIndex = 1 ;
this.textBox2.Text = "textBox2";
//
// button3
//
this.button3.Location = new System .Drawing.Point ( 32, 56 );
this.button3.Name = "button3";
this.button3.Size = new System .Drawing.Size ( 128, 24 );
this.button3.TabIndex = 0 ;
this.button3.Text = "button3";
//
// label1
//
this.label1.BackColor = System .Drawing.Color.MediumAquamarine ;
this.label1.Location = new System .Drawing.Point ( 48, 8 );
this.label1.Name = "label1";
this.label1.Size = new System .Drawing.Size ( 88, 16 );
this.label1.TabIndex = 1 ;
this.label1.Text = "label1";
}
#endregion
Le code et les affichages obtenus (le textBox1 est positionné en X=16 et Y=76 sur son parent) :
// Gestionnaire du click sur button1 :
Lorsque l'on clique sur le button2, texteBox1 a pour parent la fiche elle-même :
Le contrôle pictureBox1 permet d'afficher des images : ico, bmp, gif, png, jpg, jpeg
// chargement d'un fichier image dans le pictureBox1 par un click sur button3 :
----->
Nous avons remarqué que C# possède un contrôle permettant l'affichage d'images de différents
formats, qu'en est-il de l'affichage de graphiques construits pendant l'exécution ? Le GDI+
répond à cette question.
Le Graphical Device Interface+ est la partie de NetFrameWork qui fournit les graphismes
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 304
vectoriels à deux dimensions, les images et la typographie. GDI+ est une interface de
périphérique graphique qui permet aux programmeurs d'écrire des applications indépendantes
des périphériques physiques (écran, imprimante,...).
Lorsque l'on dessine avec GDI+, on utilise des méthodes de classes situées dans le GDI+,
donnant des directives de dessin, ce sont ces méthodes qui, via le CLR du NetFrameWork, font
appel aux pilotes du périphérique physique, les programmes ainsi conçus ne dépendent alors
pas du matériel sur lequel ils s'afficheront.
Pour dessiner des graphiques sur n'importe quel périphérique d'affichage, il faut un objet
Graphics. Un objet de classe System.Drawing.Graphics est associé à une surface de dessin,
généralement la zone cliente d'un formulaire (objet de classe Form). Il n'y a pas de
constructeur dans la classe Graphics :
Comme le dessin doit avoir lieu sur la surface visuelle d'un objet visuel donc un contrôle, c'est
cet objet visuel qui fournit le fond, le GDI+ fournit dans la classe
System.Windows.Forms.Control la méthode CreateGraphics qui permet de créer un objet
de type Graphics qui représente le "fond de dessin" du contrôle :
Syntaxe :
public Graphics CreateGraphics( );
Afin de comprendre comment utiliser un objet Graphics construisons un exemple fictif de
code dans lequel on suppose avoir instancié ObjVisuel un contrôle (par exemple : une fiche,
un panel,...), on utilise deux méthodes dessin de la classe Graphics pour dessiner un trait et un
rectangle :
Pen blackPen = new Pen ( Color.Black, 2 ); Création d'un objet de pinceau de couleur noire et
d'épaisseur 2 pixels
fond.DrawLine ( blackPen, 10f, 10f, 50f, 50f ); Utilisation du pinceau blackPen pour tracer une ligne
droite sur le fond d'ObjVisuel entre les deux points
A(10,10) et B(50,50).
Ces quatre instructions ont permis de dessiner le trait noir et le rectangle vert sur le fond du
contrôle ObjVisuel représenté ci-dessous par un rectangle à fond blanc :
Les objets Graphics peuvent donc être libérés par la méthode Dispose( ).
//...
Graphics fond = panelDessin.CreateGraphics ( );
Pen blackPen = new Pen ( Color.Black, 2 );
fond.DrawLine ( blackPen, 10f, 10f, 50f, 50f );
fond.FillRectangle ( Brushes.SeaGreen,5,70,100,50 );
fond.Dispose( );
//...suite du code où l'objet fond n'est plus utilisé
Nous pouvons aussi utiliser l'instruction using déjà vue qui libère automatiquement l'objet par
appel à sa méthode Dispose :
//...
using( Graphics fond = panelDessin.CreateGraphics ( ) ) {
Pen blackPen = new Pen ( Color.Black, 2 );
fond.DrawLine ( blackPen, 10f, 10f, 50f, 50f );
fond.FillRectangle ( Brushes.SeaGreen,5,70,100,50 );
}
//...suite du code où l'objet fond n'est plus utilisé
Nous supposons disposer d'un formulaire nommé WinForm1 contenant un panneau nommé
panelDessin ( private System.Windows.Forms.Panel panelDessin ) et un bouton
nommé buttonDessin ( private System.Windows.Forms.Button buttonDessin )
Faisons apparaître une fenêtre de bloc-note contenant du texte, qui masque partiellement notre
formulaire WinForm1 qui passe au second plan comme ci-dessous :
Si nous nous refocalisons sur le formulaire en cliquant sur lui par exemple, celui-ci repasse au
premier plan, nous constatons que notre dessin est abîmé. Le rectangle vert est amputé de la
partie qui était recouverte par la fenêtre de bloc-note. Le formulaire s'est bien redessiné, mais
Premier pas dans .Net avec C# - ( rév. 05.09.2004 ) page 308
pas nos traçés ;
Il faut cliquer une nouvelle fois sur le bouton pour lancer le redessinement des tracés :
Il est possible de dessiner sur tous les types de conteneurs visuels ci-dessous
un formulaire nommé WinForm et deux panel (panel2 : jaune foncé et panel1 : violet), la
label1 ne sert qu'à afficher du texte en sortie :
Nous écrivons une méthode TracerDessin permettant de dessiner sur le fond de la fiche, sur
le fond des deux panel et d'écrire du texte sur le fond d'un panel. La méthode TracerDessin
est appelée dans le gestionnaire de l'événement Paint du formulaire lors de son redessinement
de la fiche afin d'assurer la persistance de tous les traçés. Voici le code que nous créons :
TracerDessin ( e.Graphics ) ;
/* Explications sur l'appel de la méthode TracerDessin :
Le paramètre e de type PaintEventArgs contient les données relatives à l'événement Paint
en particulier une propriété Graphics qui renvoie le graphique utilisé pour peindre sur la fiche
c'est pourquoi e.Graphics est passé comme fond en paramètre à notre méthode de dessin.
*/
}
Panel2
On dessine deux rectangles sur le fond de la fiche, nous notons que ces deux rectangles ont
une intersection non vide avec le panel2 (jaune foncé)et que cela n'altère pas le dessin du
panel. En effet le panel est un contrôle et donc se redessine lui-même. Nous en déduisons que
le fond graphique est situé "en dessous" du dessin des contrôles.
La première instruction dessine l'ellipse rouge, la seconde écrit le texte "Bonjour" en vert sur
le fond du panel2.
namespace ProjWindowsApplication2
{
/// <summary>
/// Description Résumé de WinForm1.
/// </summary>
public class WinForm1 : System.Windows.Forms.Form
{
/// <summary>
/// Variable requise par le concepteur.
/// </summary>
private System.ComponentModel.Container components = null ;
private System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.Button button1;
private WinForm1( )
{
//
// Requis pour la gestion du concepteur Windows Form
//
InitializeComponent();
//
// TODO: Ajouter tout le code du constructeur après l'appel de InitializeComponent
//
}
/// <summary>
/// Nettoyage des ressources utilisées.
/// </summary>
protected override void Dispose (bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}
1. similitude et différence
Afin de ne pas alourdir l’ouvrage déjà volumineux, nous ne développons pas de cours spécial pour
les exceptions en C# : le langage C# hérite strictement de Java pour la syntaxe et le
fonctionnement de base des exceptions et de la simplicité de Delphi dans les types d’exceptions.
C’est pourquoi nous renvoyons le lecteur au chapitre 9.3.2 (exceptions en java) pour une étude
complète de la notion d’exception, de leur gestion, de la hiérarchie, de l’ordre d’interception et du
redéclenchement d’une exception. Nous figurons ci-dessous un tableau récapitulant les similitudes
dans chacun des trois langages :
Delphi Java C#
try try try
- ... { - ... { - ...
<lignes de code à protéger> <lignes de code à protéger> <lignes de code à protéger>
- ... - ...} - ...}
except catch ( ExxException E ) catch ( ExxException E )
on E : ExxException do begin {- ... {- ...
- ... <lignes de code réagissant à l’exception> <lignes de code réagissant à l’exception>
<lignes de code réagissant à l’exception> - ... - ...
- ... } }
end;
- ...
end ; fonctionnement identique à C# fonctionnement identique à Java
Seul Java possède deux catégories d'exceptions : les exceptions vérifiées et les
exceptions non vérifiées. C# comme Delphi possède un mécanisme plus simple qui
est équivalent à celui de Java dans le cas des exceptions non vérifiées (implicites) (
la propagation de l'exception est implicitement prise en charge par le système d'exécution).
Il est aussi possible de lancer une exception personnalisée (comme si c'était une exception
propre au langage) à n'importe quel endroit dans le code d'une méthode d'une classe, en
instanciant un objet d'exception personnalisé et en le préfixant par le mot clef (raise pour
Delphi et throw pour Java et C#).
Delphi Java C#
Type class MonExcept extends Exception class MonExcept : Exception
MonExcept = class (Exception) { {
.... public MonExcept (String s) public MonExcept (string s) : base(s)
End; { {
super(s); .............
MonExcept hérite par construction de la .......... }
propriété public Message de sa mère, en
lecture et écriture.
} }
} MonExcept hérite par construction de la propriété
public Message de sa mère, en lecture seulement.
Delphi Java C#
Type class MonExcept extends Exception class MonExcept : Exception
MonExcept = class (Exception) { {
.......... public MonExcept (String s) { public MonExcept (string s) : base(s)
End; super(s); {
.......... .............
classA = class } }
Procedure Truc; } }
end;
class classA class classA
Implementation { {
void Truc ( ) throws MonExcept { void Truc ( ) {
Procedure classA.Truc; ….. …..
begin throw new MonExcept ( 'salut' ); throw new MonExcept ( 'salut' );
….. ….. …..
raise MonExcept.Create ( 'salut' ); } }
….. } }
end;
Delphi Java C#
Type class MonExcept extends Exception class MonExcept : Exception
MonExcept = class (Exception) { {
.......... public MonExcept (String s) { public MonExcept (string s) : base(s)
End; super(s); {
.......... .............
classB = class } }
Procedure Meth; } }
end;
class classB class classB
Implementation { {
void Meth ( ) { void Meth ( ) {
end;
3. Un exemple de traitement en C#
<programme>
…<ActionsSurFichier>
…..<AfficheFichier>
……...<ChercheElement>
Les trois blocs du dernier niveau les plus internes <OuvrirFichier>, <LireElement> et
<FermerFichier> peuvent lancer chacun une exception selon le schéma ci-après :
On propose de créer une classe générale d'exception EFichierError héritant de la classe des
Exception, puis 3 classes d'exception héritant de cette classe EFichierError :
Nous proposons par exemple d'intercepter les exceptions dans les deux blocs
<ActionsSurFichier> et <AfficheFichier> :
Le bloc <ActionsSurFichier>
//------------------------------------------------------------------------------
void OuvrirFichier ( ) {
System .Console.WriteLine (" >> Action ouverture...");
GenererIncident ( 1 );
System .Console.WriteLine (" >> Fin ouverture.");
}
void FermerFichier ( ) {
System .Console.WriteLine (" >> Action fermeture...");
GenererIncident ( 3 );
System .Console.WriteLine (" >> Fin fermeture.");
}
void ChercheElement ( ) {
OuvrirFichier ( );
LireElement ( );
FermerFichier ( );
}
//------------------------------------------------------------------------------------------
void ActionsSurFichier ( ) {
System .Console.WriteLine ("Debut du travail sur le fichier.");
try
{
System .Console.WriteLine (".........");
AfficherFichier ( );
}
catch( EOuvertureError E )
{
TraitementSpecif ( E.Message );
}
catch( ELectureError E )
{
TraitementSpecif ( E.Message );
}
catch( EFermetureError E )
{
TraitementSpecif ( E.Message );
}
try {
ChercheElement ( );
}
catch( EFichierError E ) {
TraitementGen ( E.Message );
throw E ;
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 326
Classes, objets et IHM avec
Les exercices utilisent des classes spécifiques au langage C#, si le lecteur veut
traduire ces exemples en code Java ou en code Delphi, il doit soit chercher dans les
packages Java ou Delphi des classes possédant les mêmes fonctionnalités soit les
construire lui-même.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 327
Algorithme
Calcul de la valeur absolue d'un nombre réel
Spécifications de l’algorithme :
lire( x );
si x 0 alors écrire( '|x| =', x)
sinon écrire( '|x| =', -x)
fsi
Implantation en C#
Ecrivez avec les deux instructions différentes "if...else.." et "...?.. : ...", le programme C#
complet correspondant à l'affichage ci-dessous :
class ApplicationValAbsolue {
static void Main(string[ ] args) {
……..
}
}
La méthode Main calcule et affiche la valeur absolue.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 328
Classe C# solution
Remarquons que cette version simple ne protège pas des ereurs de saisie. Pour être plus
robuste le programme devrait intercepter l'exception levée par une éventuelle erreur de saisie
signalée par une exception du type FormatException :
try {
x = Double.Parse( System.Console.ReadLine( ) ) ;
}
catch ( FormatException ) { //...traitement de l'erreur de saisie }
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 329
Algorithme
Algorithme de résolution de l'équation du second degré dans R.
Implantation en C#
Ecrivez le programme C# qui est la traduction immédiate de cet algorithme dans le corps de la
méthode Main.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 330
Conseil :
On utilisera la méthode static Sqrt(double x) de la classe Math pour calculer la racine carré d'un
nombre réel :
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationEqua2 {
static void Main (string[ ] arg) {
double a, b, c, delta ;
double x, x1, x2 ;
System.Console.Write("Entrer une valeur pour a : ") ;
a = Double.Parse( System.Console.ReadLine( ) ) ;
System.Console.Write("Entrer une valeur pour b : ") ;
b = Double.Parse( System.Console.ReadLine( ) ) ;
System.Console.Write("Entrer une valeur pour c : ") ;
c = Double.Parse( System.Console.ReadLine( ) ) ;
if (a ==0) {
if (b ==0) {
if (c ==0) {
System.Console.WriteLine("tout reel est solution") ;
}
else {// c 0
System.Console.WriteLine("il n'y a pas de solution") ;
}
}
else { // b 0
x = -c/b ;
System.Console.WriteLine("la solution est " + x) ;
}
}
else { // a 0
delta = b*b - 4*a*c ;
if (delta < 0) {
System.Console.WriteLine("il n'y a pas de solution dans les reels") ;
}
else { // delta 0
x1 = (-b + Math.Sqrt(delta))/ (2*a) ;
x2 = (-b - Math.Sqrt(delta))/ (2*a) ;
System.Console.WriteLine("il y deux solutions egales a " + x1 + " et " + x2) ;
}
}
}
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 331
Algorithme
Calcul des nombres de Armstrong
Spécifications de l’algorithme :
On sait qu'il n'existe que 4 nombres de Armstrong, et qu'ils ont tous 3 chiffres (ils sont
compris entre 100 et 500).
Si l'on qu'un tel nombre est écrit ijk (i chiffre des centaines, j chiffres des dizaines et k
chiffres des unités), il suffit simplement d'envisager tous les nombres possibles en
faisant varier les chiffres entre 0 et 9 et de tester si le nombre est de Armstrong.
Implantation en C#
Ecrivez le programme C# complet qui fournisse les 4 nombres de Armstrong :
Nombres de Armstrong:
153
370
371
407
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 332
Squelette plus détaillé de la classe C# à implanter :
using System;
namespace CsExosAlgo1
{
class ApplicationArmstrong {
static void Main(string[ ] args) {
int i, j, k, n, somcube;
System.Console.WriteLine("Nombres de Armstrong:");
for(i = 1; i<=9; i++)
for(j = 0; j<=9; j++)
for(k = 0; k<=9; k++)
{
……
}
}
}
}
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationArmstrong {
static void Main(string[ ] args) {
int i, j, k, n, somcube;
System.Console.WriteLine("Nombres de Armstrong:");
for(i = 1; i<=9; i++)
for(j = 0; j<=9; j++)
for(k = 0; k<=9; k++)
{
n = 100*i + 10*j + k;
somcube = i*i*i + j*j*j + k*k*k;
if (somcube == n)
System.Console.WriteLine(n);
}
}
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 333
Algorithme
Calcul de nombres parfaits
Spécifications de l’algorithme :
l'algorithme retenu contiendra deux boucles imbriquées. Une boucle de comptage des nombres
parfaits qui s'arrêtera lorsque le décompte sera atteint, la boucle interne ayant vocation à
calculer tous les diviseurs du nombre examiné d'en faire la somme puis de tester l'égalité entre
cette somme et le nombre.
Algorithme Parfait
Entrée: n N
Sortie: nbr N
Local: somdiv, k, compt N
début
lire(n);
compt 0;
nbr 2;
Tantque(compt < n) Faire
somdiv 1;
Pour k 2 jusquà nbr-1 Faire
Si reste(nbr par k) = 0 Alors // k divise nbr
somdiv somdiv + k
Fsi
Fpour ;
Si somdiv = nbr Alors
ecrire(nbr) ;
compt compt+1;
Fsi;
nbr nbr+1
Ftant
FinParfait
Implantation en C#
Ecrivez le programme C# complet qui produise le dialogue suivant à l’écran (les caractères
gras représentent ce qui est écrit par le programme, les italiques ce qui est entré au clavier) :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 334
Entrez combien de nombre parfaits : 4
6 est un nombre parfait
28 est un nombre parfait
496 est un nombre parfait
8128 est un nombre parfait
class ApplicationParfaits {
static void Main(string[ ] args) {
……..
}
}
La méthode Main calcule et affiche les nombres parfaits.
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationParfaits {
static void Main (string[ ] args) {
int compt = 0, n, k, somdiv, nbr;
System.Console.Write("Entrez combien de nombre parfaits : ");
n = Int32.Parse( System.Console.ReadLine( ) ) ;
nbr = 2;
while (compt != n)
{ somdiv = 1;
k = 2;
while(k <= nbr/2 )
{
if (nbr % k = = 0) somdiv += k ;
k++;
}
if (somdiv = = nbr)
{ System.Console.WriteLine(nbr+" est un nombre parfait");
compt++;
}
nbr++;
}
}
}
}
La saisie de l'entier int n; s'effectue par transtypage grâce à la méthode Parse de la classe Net
Framework Int32 du type int.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 335
Algorithme
Calcul du pgcd de 2 entiers (méthode Euclide)
Spécifications de l’algorithme :
Algorithme Pgcd
Entrée: a,b N* x N*
Sortie: pgcd N
Local: r,t N x N
début
lire(a,b);
Si ba Alors
t a;
a b;
b t
Fsi;
Répéter
r a mod b ;
ab;
br
jusquà r = 0;
pgcd a;
ecrire(pgcd)
FinPgcd
Implantation en C#
Ecrivez le programme C# complet qui produise le dialogue suivant à la console (les caractères
gras représentent ce qui est écrit par le programme, les italiques ce qui est entré au clavier) :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 336
Proposition de squelette de classe C# à implanter :
class ApplicationEuclide {
static void Main(string[ ] args) {
……..
}
static int pgcd (int a, int b) {
……..
}
}
La méthode pgcd renvoie le pgcd des deux entiers p et q .
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationEuclide {
static void Main (string[ ] args) {
System.Console.Write("Entrez le premier nombre : ");
int p = Int32.Parse( System.Console.ReadLine( ) ) ;
System.Console.Write("Entrez le deuxième nombre : ");
int q = Int32.Parse( System.Console.ReadLine( ) ) ;
if (p*q!=0)
System.Console.WriteLine("Le pgcd de "+p+" et de "+q+" est "+pgcd(p,q));
else
System.Console.WriteLine("Le pgcd n'existe pas lorsque l'un des deux nombres est nul !");
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 337
Algorithme
Calcul du pgcd de 2 entiers (méthode Egyptienne)
Spécifications de l’algorithme :
Lire (p, q ) ;
Tantque p q faire
Si p > q alors
pp–q
sinon
qq–p
FinSi
FinTant;
Ecrire( " PGCD = " , p )
Implantation en C#
Ecrivez le programme C# complet qui produise le dialogue suivant à la console (les caractères
gras représentent ce qui est écrit par le programme, les italiques ce qui est entré au clavier) :
class ApplicationEgyptien {
static void main(String[ ] args) {
……..
}
static int pgcd (int p, int q) {
……..
}
}
La méthode pgcd renvoie le pgcd des deux entiers p et q .
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 338
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationEgyptien {
static void Main (string[ ] args) {
System.Console.Write("Entrez le premier nombre : ");
int p = Int32.Parse( System.Console.ReadLine( ) ) ;
System.Console.Write("Entrez le deuxième nombre : ");
int q = Int32.Parse( System.Console.ReadLine( ) ) ;
if ( p*q != 0 )
System.Console.WriteLine("Le pgcd de "+p+" et de "+q+" est "+pgcd(p,q));
else
System.Console.WriteLine("Le pgcd n'existe pas lorsque l'un des deux nombres est nul !");
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 339
Algorithme
Calcul de nombres premiers (boucles while et do…while)
Spécifications de l’algorithme :
Algorithme Premier
Entrée: n N
Sortie: nbr N
Local: Est_premier {Vrai , Faux}
divis,compt N2;
début
lire(n);
compt 1;
ecrire(2);
nbr 3;
Tantque(compt < n) Faire
divis 3;
Est_premier Vrai;
Répéter
Si reste(nbr par divis) = 0 Alors
Est_premier Faux
Sinon
divis divis+2
Fsi
jusquà (divis > nbr / 2)ou (Est_premier=Faux);
Si Est_premier =Vrai Alors
ecrire(nbr);
compt compt+1
Fsi;
nbr nbr+1
Ftant
FinPremier
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 340
Implantation en C#
Ecrivez le programme C# complet qui produise le dialogue suivant à la console (les caractères
gras représentent ce qui est écrit par le programme, les italiques ce qui est entré au clavier) :
Classe C# solution
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 341
Algorithme
Calcul de nombres premiers (boucles for)
début
lire(n);
compt 1;
ecrire(2);
nbr 3;
Tantque(compt < n) Faire
divis 3;
Est_premier Vrai;
Répéter
Si reste(nbr par divis) = 0 Alors
Est_premier Faux
Sinon
divis divis+2
Fsi
jusquà (divis > nbr / 2)ou (Est_premier=Faux);
Si Est_premier =Vrai Alors
ecrire(nbr);
compt compt+1
Fsi;
nbr nbr+2 // nbr impairs
Ftant
FinPremier
Implantation en C#
Ecrivez le programme C# complet qui produise le dialogue suivant à la console (les caractères
gras représentent ce qui est écrit par le programme, les italiques ce qui est entré au clavier) :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 342
Combien de nombres premiers : 5
2
3
5
7
11
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationComptPremiers2 {
static void Main(string[ ] args) {
int divis, nbr, n, compt = 0 ;
bool Est_premier;
System.Console.Write("Combien de nombres premiers : ");
n = Int32.Parse( System.Console.ReadLine( ) ) ;
System.Console.WriteLine( 2 );
//-- primalité uniquement des nombres impairs
for( nbr = 3; compt < n-1; nbr += 2 )
{ Est_premier = true;
for (divis = 2; divis<= nbr/2; divis++ )
if ( nbr % divis == 0 )
{ Est_premier = false;
break;
}
if (Est_premier)
{
compt++;
System.Console.WriteLine( nbr );
}
}
}
}
}
Le fait de n'étudier la primalité que des nombres impairs accélère la vitesse d'exécution du
programme, il est possible d'améliorer encore cette vitesse en ne cherchant que les diviseurs
dont le carré est inférieur au nombre ( test : jusquà (divis2 > nbr )ou (Est_premier=Faux) ).
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 343
Algorithme
Calcul du nombre d'or
et
On montre que la suite (V) tend vers une limite appelée nombre d'or (nbr
d'Or = 1,61803398874989484820458683436564).
Spécifications de l’algorithme :
n,Un ,Un1 ,Un2 : sont des entiers naturels
Vn ,Vn1 , : sont des nombres réels
Classe C# solution
Avec un boucle for :
using System;
namespace CsExosAlgo1
{
class AppliNombredOr {
static void Main(string[ ] args) {
int n, Un, Un1=2, Un2=1 ;
double Vn,Vn1=2, Eps ;
System.Console.Write("Précision du calcul ? : ");
//-- précision demandée (exemple 1e-4 ou 1e-2) :
Eps = Double.Parse( System.Console.ReadLine( ) ) ;
for (n=2; ; n++) //n est le rang du terme courant
{
Un = Un1 + Un2;
Vn =(double)Un / (double)Un1;
if (Math.Abs(Vn - Vn1) <= Eps) break;
else
{
Un2 = Un1;
Un1 = Un;
Vn1 = Vn;
}
}
System.Console.WriteLine("Nombre d'Or = " + Vn+" // rang="+n);
}
}
}
Remarquons que nous proposons une boucle for ne contenant pas de condition de rebouclage
dans son en-tête (donc en apparence infinie), puisque nous effectuerons le test "si |Vn - Vn1|
<= Eps alors Arrêt de la boucle" qui permet l'arrêt de la boucle. Dans cette éventualité , la
boucle for devra donc contenir dans son corps, une instruction de rupture de séquence.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 345
Algorithme
Conjecture de Goldbach
Algorithme Premier
Entrée: nbr N
Local: Est_premier {Vrai , Faux}
divis,compt N2 ;
début
lire(nbr);
divis 3;
Est_premier Vrai;
Répéter
Si reste(nbr par divis) = 0 Alors
Est_premier Faux
Sinon
divis divis+2
Fsi
jusquà (divis > nbr / 2)ou (Est_premier=Faux);
Si Est_premier = Vrai Alors
ecrire(nbr est premier)
Sinon
ecrire(nbr n'est pas premier)
Fsi
FinPremier
En deux étapes :
1. On entre un nombre pair n au clavier, puis on génère tous les couples (a,b) tels que
a + b = n, en faisant varier a de 1 à n/2. Si l'on rencontre un couple tel que a et b
soient simultanément premiers la conjecture est vérifiée.
2. On peut alors, au choix soit arrêter le programme, soit continuer la recherche sur
un autre nombre pair.
Exemple :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 346
Pour n = 10, on génère les couples :
(1,9), (2,8), (3,7), (4,6), (5,5)
on constate que la conjecture est vérifiée, et on écrit :
10 = 3 + 7
10 = 5 + 5
Conseils :
Il faudra traduire cet algorithme en fonction recevant comme paramètre d'entrée le
nombre entier dont on teste la primalité, et renvoyant un booléen true ou false selon
que le nombre entré a été trouvé ou non premier
On écrira la méthode booléenne EstPremier pour déterminer si un nombre est premier
ou non, et la méthode generCouples qui génère les couples répondant à la conjecture.
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationGoldBach {
static void Main(string[ ] args) {
int n;
System.Console.WriteLine("Entrez un nombre pair (0 pour finir) :");
while ( (n = Int32.Parse( System.Console.ReadLine( ) )) !=0 ){
generCouples(n); }
}
static bool EstPremier(int m) {
int k ;
for (k = 2 ; k <= m / 2 ; k++) {
if (m % k == 0) {
return false;
}
}
return true;
}
static void generCouples(int n) {
if (n % 2 ==0) {
for (int a = 1; a <= n/2; a++) {
int b;
b = n - a;
if ( EstPremier(a) && EstPremier(b) ) {
System.Console.WriteLine(n+" = "+a+" + "+b);
}
}
}
else System.Console.WriteLine("Votre nombre n'est pas pair !");
}
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 347
Algorithme
Méthodes d'opérations sur 8 bits
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 348
Implantation en C#
La conception de ces méthodes ne dépendant pas du nombre de bits de la mémoire à opérer on
choisira le type int comme base pour une mémoire. Ces méthodes sont classiquement des
outils de manipulation de l'information au niveau du bit.
Lors des jeux de tests pour des raisons de simplicité de lecture il est conseillé de ne rentrer que
des valeurs entières portant sur 8 bits.
Il est bien de se rappeler que le type primaire int est un type entier signé sur 32 bits
(représentation en complément à deux).
class Application8Bits {
static void Main(string [ ] args){
…….. }
static int BitSET (int nbr, int num) { …….. }
static int BitCLR (int nbr, int num) { …….. }
static int BitCHG (int nbr, int num) { …….. }
static int SetValBit (int nbr, int rang, int val) { …….. }
static int DecalageD (int nbr, int n) { …….. }
static int DecalageG (int nbr, int n) { …….. }
static int BitRang (int nbr, int rang) { …….. }
static int ROL (int nbr, int n) { …….. }
static int ROR (int nbr, int n) { …….. }
}
Classe C# solution
using System;
namespace CsExosAlgo1
{
class Application8Bits
{
// Int32.MAX_VALUE : 32 bit = 2147483647
// Int64.MAX_VALUE : 64 bits = 9223372036854775808
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 349
System.Console.WriteLine("BitCHG(n,3) ="+toBinaryString(r));
t = BitCHG(n,2);// t=13
System.Console.WriteLine("BitCHG(n,2) ="+toBinaryString(t));
System.Console.WriteLine("p = "+p+", q = "+q+", r = "+r+", t = "+t);
n =-2147483648;//1000.....00 entier minimal
System.Console.WriteLine("n=-2^31 : n="+toBinaryString(n));
p=ROL(n,3);// 000...000100 = p=4
System.Console.WriteLine("p = "+p);
n =-2147483648+1;//1000.....01 entier minimal+1
System.Console.WriteLine("n=-2^31+1 : n="+toBinaryString(n));
p=ROL(n,3);// 000...0001100 = p=12
System.Console.WriteLine("p = "+p);
n =3;//0000.....0 11
System.Console.WriteLine("n=3 : n="+toBinaryString(n));
p=ROR(n,1);//100000...001 = p=-2147483647
System.Console.WriteLine("ROR(n,1) = "+p+"= "+toBinaryString(p));
p=ROR(n,2);// 11000...000 = p= -1073741824
System.Console.WriteLine("ROR(n,2) = "+p+"= "+toBinaryString(p));
p=ROR(n,3);// 011000...000 = p= +1610612736 =2^30+2^29
System.Console.WriteLine("ROR(n,3) = "+p+"= "+toBinaryString(p));
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 350
static int DecalageD (int nbr, int n)
{ // décalage sans le signe de n bits vers la droite
return nbr >> n ;
}
} //--Application8Bits
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 351
String avec C#
phrase palindrome (deux versions)
Une phrase est dite palindrome si en éliminant les blancs entre les mots elle représente la même
lecture dans les deux sens :
}
static void Main ( string [ ] args )
{
System .Console.WriteLine ("Entrez une phrase :");
string phrase = System .Console.ReadLine ( );
string strMot = compresser ( phrase );
string strInv = inverser ( strMot );
if( strMot == strInv )
System .Console.WriteLine ("phrase palindrome !");
else
System .Console.WriteLine ("phrase non palindrome !");
System .Console.ReadLine ( );
}
}
Travail à effectuer :
Ecrire les méthode compresser et Inverser , il est demandé d'écrire deux versions de la méthode
Inverser.
La deuxième version de la méthode Inverser modifiera les positions des caractères ayant
des positions symétriques dans la chaîne avec une boucle for à deux indices et en utilisant
un tableau de char.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 352
Classe C# solution
Le code de la méthode compresser :
La méthode compresser élimine les caractères non recevables comme : blanc, virgule, point et
apostrophe de la String s passée en paramètre.
Remarquons que l'instruction strLoc +=s[i] permet de concaténer les caractères recevables de la
chaîne locale strLoc, par balayage de la String s depuis le caractère de rang 0 jusqu'au caractère de
rang s.Length-1.
La référence de String strLoc pointe à chaque tour de la boucle for vers un nouvel objet créé par
l'opérateur de concaténation +
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 353
for ( int i = 0 , j = tChar.Length - 1 ; i < j ; i ++ , j -- )
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 354
String avec C#
convertir une date numérique en lettres
static string mois ( int num ) Si le paramètre est un entier compris entre 1 et 12, elle renvoie
le nom du mois (janvier,….,décembre), sinon elle renvoie ###.
static string jours ( int num ) Si le paramètre est un entier compris entre 1 et 7, elle renvoie
le nom du jour (lundi,….,dimanche), sinon elle renvoie ###.
static string numjour ( int num ) Si le paramètre est un entier compris entre 1 et 31, elle le sous
forme d'une string, sinon elle renvoie ###.
static void scanDate ( string date, out int Reçoit en entrée une date numérique formatée avec des
jour, out int val, out int mois ) séparateurs (/,.-) et ressort les 3 entiers la composant.
static string ConvertDate ( string date ) Reçoit en entrée une date numérique formatée avec des
sépaarteurs (/,.-) et renvoie sa conversion en forme littérale.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 355
Classe C# solution
class Dates
{
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 356
String avec C#
convertir un nombre écrit en chiffres Romains
static int recurRom ( string chiff, int i ) static int iterRom ( string chiff ) {
{ int som = 0, valLettre = 0 ;
int valLettre = tradRom ( chiff[i] ); for( int i = 0 ; i < chiff.Length ; i++ ) {
if( i == chiff.Length - 1 ) valLettre = tradRom ( chiff[i] );
return valLettre; if( i == chiff.Length - 1 )
else break;
if (valLettre < tradRom ( chiff[i + 1] )) if (valLettre < tradRom ( chiff[i + 1] ))
return recurRom ( chiff, ++ i ) - valLettre; som = som - valLettre;
else else
return recurRom ( chiff, ++ i ) + valLettre; som = som + valLettre;
} }
return som + valLettre;
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 357
Algorithme
Tri à bulles sur un tableau d'entiers
Spécifications de l’algorithme :
Algorithme Tri_a_Bulles
local: i , j , n, temp Entiers naturels
Entrée - Sortie : Tab Tableau d'Entiers naturels de 1 à n éléments
début
pour i de n jusquà 1 faire // recommence une sous-suite (a1, a2, ... , ai)
pour j de 2 jusquà i faire // échange des couples non classés de la sous-suite
si Tab[ j-1 ] > Tab[ j ] alors // aj-1et aj non ordonnés
temp Tab[ j-1 ] ;
Tab[ j-1 ] Tab[ j ] ;
Tab[ j ] temp //on échange les positions de aj-1et aj
Fsi
fpour
fpour
Fin Tri_a_Bulles
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationTriInsert {
static int[ ] table ; // le tableau à trier, par exemple 19 éléments
static void AfficherTable ( ) {
// Affichage du tableau
int n = table.Length-1;
for ( int i = 1; i <= n; i++)
System.Console.Write (table[i]+" , ");
System.Console.WriteLine ( );
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 358
System.Console.WriteLine ("Tableau une fois trié :");
AfficherTable ( );
System.Console.Read();
}
static void TriBulle ( ) {
// sous-programme de Tri à bulle : on trie les éléments du n°1 au n°19
int n = table.Length-1;
for ( int i = n; i>=1; i--)
for ( int j = 2; j <= i; j++)
if (table[j-1] > table[j]){
int temp = table[j-1];
table[j-1] = table[j];
table[j] = temp;
}
/* Dans le cas où l'on démarre le tableau à l'indice zéro
on change les bornes des indices i et j:
for ( int i = n; i >= 0; i--)
for ( int j = 1; j <= i; j++)
if ....... reste identique
*/
}
}
}
Tableau initial :
25 , 7 , 14 , 26 , 25 , 53 , 74 , 99 , 24 , 98 , 89 , 35 , 59 , 38 , 56 , 58 , 36 , 91 , 52
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 359
Algorithme
Tri par insertion sur un tableau d'entiers
Spécifications de l’algorithme :
Algorithme Tri_Insertion
local: i , j , n, v Entiers naturels
Entrée : Tab Tableau d'Entiers naturels de 0 à n éléments
Sortie : Tab Tableau d'Entiers naturels de 0 à n éléments (le même tableau)
{ dans la cellule de rang 0 se trouve une sentinelle chargée d'éviter de tester dans la boucle tantque .. faire si
l'indice j n'est pas inférieur à 1, elle aura une valeur inférieure à toute valeur possible de la liste
}
début
pour i de2 jusquà n faire// la partie non encore triée (ai, ai+1, ... , an)
v Tab[ i ] ; // l'élément frontière : ai
ji; // le rang de l'élément frontière
Tantque Tab[ j-1 ]> v faire//on travaille sur la partie déjà triée (a1, a2, ... , ai)
Tab[ j ] Tab[ j-1 ]; // on décale l'élément
j j-1; // on passe au rang précédent
FinTant ;
Tab[ j ] v //on recopie ai dans la place libérée
fpour
Fin Tri_Insertion
Classe C# solution
On utilise une sentinelle placée dans la cellule de rang 0 du tableau, comme le type d'élément du
tableau est un int, nous prenons comme valeur de la sentinelle une valeur négative très grande par
rapport aux valeurs des éléments du tableau; par exemple le plus petit élément du type int, soit la
valeur Integer.MIN_VALUE.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 360
using System;
namespace CsExosAlgo1
{
class ApplicationTriInsert {
static int[] table ; // le tableau à trier, par exemple 19 éléments
/* Tri avec sentinelle :
* dans la cellule de rang 0 se trouve une sentinelle (Int32.MinValue)
* chargée d'éviter de tester dans la boucle tantque .. faire
* si l'indice j n'est pas inférieur à 1, elle aura une valeur
* inférieure à toute valeur possible de la liste.
*/
static void AfficherTable ( ) {
// Affichage du tableau
int n = table.Length-1;
for ( int i = 1; i <= n; i++)
System.Console.Write (table[i]+" , ");
System.Console.WriteLine ( );
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 361
Tableau initial :
25 , 7 , 14 , 26 , 25 , 53 , 74 , 99 , 24 , 98 , 89 , 35 , 59 , 38 , 56 , 58 , 36 , 91 , 52
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 362
Algorithme
Recherche linéaire dans une table non triée
On recherche le rang (la place) de l'élément Elt dans ce tableau. L'algorithme renvoie
le rang (la valeur -1 est renvoyée lorsque l'élément Elt n'est pas présent dans le tableau
t)
i 1;
Tantque (i n) et alors (t[i] Elt) faire
i i+1
finTant;
si i n alors rang i
sinon rang -1
Fsi
i 1;
Tantque (i < n) et (t[i] Elt) faire
i i+1
finTant;
si t[i] = Elt alors rang i
sinon rang -1
Fsi
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 363
i1;
Tantque (i n) et alors (t[i] Elt) faire
i i+1
finTant;
si i n alors rang i
sinon rang -1
Fsi
Traduire chacune des quatre versions sous forme d'une méthode C#.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 364
else System.Console.WriteLine("Elément "+x+" non trouvé !");
System.Console.Read();
}
}
}
Classe C# solution
Les différents sous-programmes C# implantant les 4 versions d'algorithme de recherche linéaire
(table non triée) :
static int RechSeq1( int[] t, int Elt ) static int RechSeq2( int[] t, int Elt )
{ {
int i = 1; int n = t.Length-1; int i = 1; int n = t.Length-1;
while ((i <= n) && (t[i] != Elt)) i++; while ((i < n) & (t[i] != Elt)) i++;
if (i<=n) if (t[i] == Elt)
return i; return i;
else else
return -1; return -1;
} }
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 365
int n = table.Length-1;
for ( int i = 1; i <= table.Length-1; i++)
tableSent[i] = table[i];
}
static int RechSeq1( int[] t, int Elt ) {
// Version Tantque avec "et alors" (opérateur et optimisé)
int i = 1; int n = t.Length-1;
while ((i <= n) && (t[i] != Elt)) i++;
if (i<=n)
return i;
else
return -1;
}
static int RechSeq2( int[] t, int Elt ) {
// Version Tantque avec "et" (opérateur non optimisé)
int i = 1; int n = t.Length-1;
while ((i < n) & (t[i] != Elt)) i++;
if (t[i] == Elt)
return i;
else
return -1;
}
static int RechSeq3( int[] t, int Elt ) {
// Version Tantque avec sentinelle en fin de tableau
int i = 1; int n = t.Length-2;
t[n+1]= Elt ; // sentinelle rajoutée
while ((i <= n) & (t[i] != Elt)) i++;
if (i<=n)
return i;
else
return -1;
}
static int RechSeq4( int[] t, int Elt ) {
// Version Pour avec instruction de sortie break
int i = 1; int n = t.Length-1;
for(i = 1; i <= n ; i++)
if (t[i] == Elt)
break;
if (i<=n)
return i;
else
return -1;
}
static void Main(String[ ] args) {
InitTable ( );
System.Console.WriteLine("Tableau initial :");
AfficherTable (table );
int x = Int32.Parse(System.Console.ReadLine( )), rang;
//rang = RechSeq1( table, x );
//rang = RechSeq2( table, x );
//rang = RechSeq3( tableSent, x );
rang = RechSeq4( table, x );
if (rang > 0)
System.Console.WriteLine("Elément "+x+" trouvé en : "+rang);
else System.Console.WriteLine("Elément "+x+" non trouvé !");
System.Console.Read();
}
}}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 366
Algorithme
Recherche linéaire dans table déjà triée
Objectif : Ecrire un programme C# effectuant une recherche séquentielle
dans un tableau linéaire (une dimension) trié avant recherche.
Spécifications de l’algorithme :
Soit t un tableau d'entiers de 1..n éléments rangés par ordre croissant par exemple.
On recherche le rang (la place) de l'élément Elt dans ce tableau. L'algorithme renvoie
le rang (la valeur -1 est renvoyée lorsque l'élément Elt n'est pas présent dans le tableau
t)
On peut aussi utiliser le fait que le dernier élément du tableau est le plus grand élément et s'en
servir comme une sorte de sentinelle. Ci-dessous deux versions utilisant cette dernière
remarque.
Version Tantque :
Version pour :
si t[n] Elt alors rang -1
pour i 1 jusquà n-1 faire
Sortirsi t[i] Elt //sortie de boucle
fpour;
si t[i] = Elt alors
Renvoyer rang i // retour du résultat et sortie du programme
Fsi
Fsi;
Renvoyer rang -1 // retour du résultat et sortie du programme
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 367
Ecrire chacune des méthodes associées à ces algorithmes (prendre soin d'avoir trié le tableau
auparavant par exemple par une methode de tri), squelette de classe proposé :
Classe C# solution
Les deux méthodes C# implantant les 2 versions d'algorithme de recherche linéaire (table triée) :
static int RechSeqTri1( int[] t, int Elt ) static int RechSeqTri2( int[] t, int Elt )
{ {
int i = 1; int n = t.Length-1; int i = 1; int n = t.Length-1;
if (t[n] >= Elt) { if (t[n] >= Elt) {
while (t[i] < Elt) i++; for (i = 1; i <= n; i++)
if (t[i] == Elt) if (t[i] == Elt)
return i ; return i;
} }
return -1; return -1;
} }
using System;
namespace CsExosAlgo1
{
class ApplicationRechLinTrie {
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 368
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 369
Algorithme
Recherche dichotomique dans une table
Spécifications de l’algorithme :
Soit t un tableau d'entiers de 1..n éléments triés par ordre croissant.
Version itérative
bas, milieu, haut, rang : entiers
bas 1;
haut N;
Rang -1;
repéter
milieu (bas + haut) div 2;
si x = t[milieu] alors
Rang milieu
sinon si t[milieu] < x alors
bas milieu + 1
sinon
haut milieu-1
fsi
fsi
jusquà ( x = t[milieu] ) ou ( bas haut )
Implanter une méthode en C# que vous nommerez RechDichoIter qui recevant en entrée un
tableau et un élément à chercher, renverra le rang de l'élément selon les spécifications ci-haut.
N'oubliez pas de trier le tableau avant d'invoquer la méthode de recherche. Ci-dessous une
suggestion de rédaction de la méthode Main :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 370
TriInsert ( );
System.Console.WriteLine("Tableau trié :");
AfficherTable ( );
int x = Int32.Parse(System.Console.ReadLine( )), rang;
rang = RechDichoIter( table, x );
if (rang 0)
System.Console.WriteLine("Elément "+x+" trouvé en : "+rang);
else System.Console.WriteLine("Elément "+x+" non trouvé !");
System.Console.Read();
}
Classe C# solution
using System;
namespace CsExosAlgo1
{
class ApplicationRechDicho
{
static int[ ] table; // le tableau à examiner cellules de 1 à 19
static void AfficherTable ( )
{
// Affichage du tableau
int n = table.Length-1;
for ( int i = 1; i <= n; i++)
System.Console.Write (table[i]+" , ");
System.Console.WriteLine( );
}
static void InitTable ( )
{
// La cellule de rang zéro est inutilisée (examen sur 19 éléments)
int[ ] tableau = { 0 , 53 , 77 , 11 , 72 , 28 , 43 , 65 , 83 , 39 , 73 ,
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 371
82 , 69 , 65 , 4 , 95 , 46 , 12 , 87 , 75 };
table = tableau;
}
static void TriInsert ( )
{
// sous-programme de Tri par insertion :
int n = table.Length-1;
for ( int i = 2; i <= n; i++) {
int v = table[i];
int j = i;
while (table[ j-1 ] v) {
table[ j ] = table[ j-1 ];
j = j-1;
}
table[ j ] = v ;
}
}
static int RechDichoIter( int[ ] t, int Elt )
{
int n = t.Length-1;
int bas = 1, haut = n, milieu ;
int Rang = -1;
do{
milieu = (bas + haut) / 2;
if ( Elt == t[milieu]) Rang = milieu ;
else if ( t[milieu] < Elt ) bas = milieu + 1 ;
else haut = milieu-1 ;
}
while ( ( Elt != t[milieu] ) & ( bas <= haut ) );
return Rang;
}
static void Main(string[ ] args)
{
InitTable ( );
System.Console.WriteLine("Tableau initial :");
AfficherTable ( );
TriInsert ( );
System.Console.WriteLine("Tableau trié :");
AfficherTable ( );
int x = Int32.Parse(System.Console.ReadLine( )), rang;
rang = RechDichoIter( table, x );
if (rang 0)
System.Console.WriteLine("Elément "+x+" trouvé en : "+rang);
else System.Console.WriteLine("Elément "+x+" non trouvé !");
System.Console.Read();
}
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 372
Classes, objet et IHM
Problème de la référence circulaire
La classe ClasseA possède une référence ObjB à un objet public de classe ClasseB, possède un
attribut Table qui est un tableau de 50 000 entiers, lorsqu'un objet de ClasseA est construit il
incrémente de un le champ static compteur de la classe TestRefCirculaire et instancie la
référence ObjB.
La classe ClasseB possède une référence ObjA à un objet public de classe ClasseA, possède un
attribut Table qui est un tableau de 50 000 entiers, lorsqu'un objet de ClasseB est construit il
incrémente de un le champ static compteur de la classe TestRefCirculaire et instancie la
référence ObjA.
Implémentez ces trois classes en ne mettant dans le corps de la méthode Main qu'une seule
instruction consistant à instancier un objet local de classe ClasseA, puis exécuter le programme et
expliquez les résultats obtenus.
Programme C# solution
namespace ConsoleGarbageRefCirc {
class ClasseA {
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 373
public ClasseB ObjB;
public int[ ]Table = new int[50000];
public ClasseA( ) {
TestRefCirculaire.compteur++;
System.Console.WriteLine("Création objet ClasseA n° "+TestRefCirculaire.compteur);
ObjB = new ClasseB( );
}
}
class ClasseB {
public ClasseA ObjA;
public int[ ]Table = new int[50000];
public ClasseB( ) {
TestRefCirculaire.compteur++;
System.Console.WriteLine("Création objet ClasseB n° "+TestRefCirculaire.compteur);
ObjA = new ClasseA( );
}
}
class TestRefCirculaire {
public static int compteur=0;
[STAThread]
static void Main(string[ ] args) {
ClasseA ObjA = new ClasseA( );
}
}
}
Résultats d'exécution :
Explications :
L' instanciation d'un objetA provoque l'instanciation d'un objetB qui lui même
provoque l'instanciation d'un autre objetA qui à son tour instancie un objetB etc… A
chaque instanciation d'un objet de ClasseA ou de ClasseB, un tableau de 50 000 entiers
est réservé dans la pile d'exécution, nous voyons sur l'exemple que la 7055ème
instanciation a été fatale car la pile a été saturée ! L'instanciation d'un seul objet a
provoqué la saturation de la mémoire car les ClasseA et ClasseB sont liées par une
association de double référence ou référence circulaire, il faut donc faire attention à ce
genre de configuration.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 374
Classes, objet et IHM
Classe de salariés dans une entreprise fictive
Soient les diagrammes de classes suivants censés modèliser le type de salarié employé dans une entreprise. Nous
distinguons deux genres de salariés ceux qui sont mensualisés et ceux qui sont payés à l'heure :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 375
On propose une implémentation partielle des squelettes de ces classes:
using System ;
using System.Collections ;
using System.Threading ;
namespace cci
{
enum CategoriePerso { Cadre_Sup,Cadre,Maitrise,Agent,Autre }
/// <summary>
/// Interface définissant les propriétés de position d'un
/// salarié dans la hiérarchie de l'entreprise.
/// </summary>
interface IPositionHierarchie {
int Indice_Hierarchique {
}
double Coeff_Prime {
}
DateTime IndiceDepui {
}
}
/// <summary>
/// Classe de base abstraite pour le personnel. Cette classe n'est
/// pas instanciable.
/// </summary>
}
///le constructeur de la classe Salarie , payé sans mérite :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 376
public Salarie ( ..... )... {
}
protected double EvaluerPrimeCadreSup ( int coeffMerite ) {
return ( 100 + coeffMerite * 8 ) * FCoeffPrime * FBasePrime + FIndice * 7 ;
}
protected double EvaluerPrimeCadre ( int coeffMerite ) {
return ( 100 + coeffMerite * 6 ) * FCoeffPrime * FBasePrime + FIndice * 5 ;
}
protected double EvaluerPrimeMaitrise ( int coeffMerite ) {
return ( 100 + coeffMerite * 4 ) * FCoeffPrime * FBasePrime + FIndice * 3 ;
}
protected double EvaluerPrimeAgent ( int coeffMerite ) {
return ( 100 + coeffMerite * 2 ) * FCoeffPrime * FBasePrime + FIndice * 2 ;
}
/// propriété abstraite donnant le montant du salaire
/// (virtual automatiquement)
abstract public double MontantPaie {
}
/// propriété identifiant le salarié dans l'entreprise (lecture) :
public int IDentifiant {
}
/// propriété nom du salarié (lecture /ecriture) :
public string Nom {
}
/// propriété prénom du salarié (lecture /ecriture) :
public string Prenom {
}
/// propriété catégorie de personnel du salarié (lecture /ecriture) :
public CategoriePerso Categorie {
}
/// propriété n° de sécurité sociale du salarié (lecture /ecriture) :
public string Insee {
}
/// propriété de point de mérite du salarié (lecture /ecriture) ::
public virtual int Merite {
}
/// propriété classement indiciaire dans la hiérarchie (lecture /ecriture) :
public int Indice_Hierarchique {
//--lors de l'écriture : Maj de la date de détention du nouvel indice
}
/// propriété coefficient de la prime en % (lecture /ecriture) :
public double Coeff_Prime {
}
/// propriété valeur de la prime selon la catégorie (lecture) :
public double Prime {
}
/// date à laquelle l'indice actuel a été obtenu (lecture /ecriture) :
public DateTime IndiceDepuis {
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 377
/// <summary>
/// Classe du personnel mensualisé. Implémente la propriété abstraite
/// MontantPaie déclarée dans la classe de base (mère).
/// </summary>
}
/// propriété de point de mérite du salarié (lecture /ecriture) :
public ... int Merite {
}
}
/// <summary>
/// Classe du personnel horaire. Implémente la propriété abstraite
/// MontantPaie déclarée dans la classe de base (mère).
/// </summary>
}
/// nombre d'heures effectuées (lecture /ecriture) :
public double HeuresTravaillees {
}
/// implémentation de la propriété donnant le montant du salaire (lecture) :
public override double MontantPaie {
}
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 378
Implémenter les classes avec le programme de test suivant :
class ClassUsesSalarie
{
}
[STAThread]
static void Main ( string [] args )
{
SalarieMensuel Employe1 = new SalarieMensuel ( 123456, "Euton" , "Jeanne" ,
CategoriePerso.Cadre_Sup, "2780258123456" ,6,700,0.5,50000 );
SalarieMensuel Employe2 = new SalarieMensuel ( 123457, "Yonaize" , "Mah" ,
CategoriePerso.Cadre, "1821113896452" ,5,520,0.42,30000 );
SalarieMensuel Employe3 = new SalarieMensuel ( 123457, "Ziaire" , "Marie" ,
CategoriePerso.Maitrise, "2801037853781" ,2,678,0.6,20000 );
SalarieMensuel Employe4 = new SalarieMensuel ( 123457, "Louga" , "Belle" ,
CategoriePerso.Agent, "2790469483167" ,4,805,0.25,20000 );
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 379
InfoSalarie ( s );
Console .WriteLine (">>> Promotion indice de " + Employe1.Nom + " dans 2 secondes.");
Thread.Sleep ( 2000 );
Employe1.Indice_Hierarchique = 710 ;
InfoSalarie ( Employe1 );
System .Console.ReadLine ();
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 380
montant prime annuelle : 176900
montant paie mensuelle: 18908,3333333333
=======================================
Employé n°123457: Yonaize / Mah
n°SS : 1821113896452
catégorie : Cadre
indice hiérarchique : 520 , détenu depuis : 15/06/2004 19:24:23
coeff mérite : 5
coeff prime : 0,42
montant prime annuelle : 57200
montant paie mensuelle: 7266,66666666667
… tout le tableau :
coeff mérite : …
montant prime annuelle …
montant paie mensuelle: …
=======================================
…… tous les autres salariés
=======================================
>>> Promotion indice de Euton dans 2 secondes.
Employé n°123456: Euton / Jeanne
n°SS : 2780258123456
catégorie : Cadre_Sup
indice hiérarchique : 710 , détenu depuis : 15/06/2004 19:24:25
coeff mérite : 6
coeff prime : 0,5
montant prime annuelle : 152970
montant paie mensuelle: 16914,1666666667
coeff prime : 0,5
montant prime annuelle : 152970
montant paie mensuelle: 16914,1666666667
coeff prime : 0,6
montant prime annuelle : 182570
montant paie mensuelle: 19380,8333333333
coeff prime : 0,7
montant prime annuelle : 212170
montant paie mensuelle: 21847,5
coeff prime : 0,8
montant prime annuelle : 241770
montant paie mensuelle: 24314,1666666667
coeff prime : 0,9
montant prime annuelle : 271370
montant paie mensuelle: 26780,8333333333
coeff prime : 1
montant prime annuelle : 300970
montant paie mensuelle: 29247,5
-----------------------
coeff mérite : 0
montant prime annuelle : 104970
montant paie mensuelle: 12914,1666666667
coeff mérite : 1
montant prime annuelle : 112970
montant paie mensuelle: 13580,8333333333
coeff mérite : 2
montant prime annuelle : 120970
montant paie mensuelle: 14247,5
coeff mérite : 3
montant prime annuelle : 128970
montant paie mensuelle: 14914,1666666667
coeff mérite : 4
montant prime annuelle : 136970
montant paie mensuelle: 15580,8333333333
coeff mérite : 5
montant prime annuelle : 144970
montant paie mensuelle: 1
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 381
Programme C# solution
using System ;
using System.Collections ;
using System.Threading ;
namespace cci
{
enum CategoriePerso { Cadre_Sup,Cadre,Maitrise,Agent,Autre }
/// <summary>
/// Interface définissant les propriétés de position d'un
/// salarié dans la hiérarchie de l'entreprise.
/// </summary>
interface IPositionHierarchie
{
int Indice_Hierarchique {
get ;
set ;
}
double Coeff_Prime {
get ;
set ;
}
DateTime IndiceDepuis {
get ;
set ;
}
}
/// <summary>
/// Classe de base abstraite pour le personnel. Cette classe n'est
/// pas instanciable.
/// </summary>
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 382
FMerite = Merite ;
FIndice = Indice ;
FCoeffPrime = CoeffPrime ;
FIndiceDetenu = DateTime.Now ;
switch ( FCategorie )
{
case CategoriePerso.Cadre_Sup :
FBasePrime = 2000 ;
break;
case CategoriePerso.Cadre :
FBasePrime = 1000 ;
break;
case CategoriePerso.Maitrise :
FBasePrime = 500 ;
break;
case CategoriePerso.Agent :
FBasePrime = 200 ;
break;
}
}
///le constructeur de la classe employé sans mérite :
public Salarie ( int IDentifiant, string Nom, string Prenom, CategoriePerso Categorie, string Insee ):
this( IDentifiant, Nom, Prenom, Categorie, Insee, 0, 0, 0 ) {
}
protected double EvaluerPrimeCadreSup ( int coeffMerite ) {
return ( 100 + coeffMerite * 8 ) * FCoeffPrime * FBasePrime + FIndice * 7 ;
}
protected double EvaluerPrimeCadre ( int coeffMerite ) {
return ( 100 + coeffMerite * 6 ) * FCoeffPrime * FBasePrime + FIndice * 5 ;
}
protected double EvaluerPrimeMaitrise ( int coeffMerite ) {
return ( 100 + coeffMerite * 4 ) * FCoeffPrime * FBasePrime + FIndice * 3 ;
}
protected double EvaluerPrimeAgent ( int coeffMerite ) {
return ( 100 + coeffMerite * 2 ) * FCoeffPrime * FBasePrime + FIndice * 2 ;
}
/// propriété abstraite donnant le montant du salaire
/// (virtual automatiquement)
abstract public double MontantPaie {
get ;
}
/// propriété identifiant le salarié dans l'entreprise :
public int IDentifiant {
get { return FCodeEmploye ; }
}
/// propriété nom du salarié :
public string Nom {
get { return FNom ; }
set { FNom = value ; }
}
/// propriété prénom du salarié :
public string Prenom {
get { return FPrenom ; }
set { FPrenom = value ; }
}
/// propriété catégorie de personnel du salarié :
public CategoriePerso Categorie {
get { return FCategorie ; }
set { FCategorie = value ; }
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 383
/// propriété n° de sécurité sociale du salarié :
public string Insee {
get { return FInsee ; }
set { FInsee = value ; }
}
/// propriété de point de mérite du salarié :
public virtual int Merite {
get { return FMerite ; }
set { FMerite = value ; }
}
/// propriété classement indiciaire dans la hiérarchie :
public int Indice_Hierarchique {
get { return FIndice ; }
set {
FIndice = value ;
//--Maj de la date de détention du nouvel indice :
IndiceDepuis = DateTime.Now ;
}
}
/// propriété coefficient de la prime en %:
public double Coeff_Prime {
get { return FCoeffPrime ; }
set { FCoeffPrime = value ; }
}
/// propriété valeur de la prime :
public double Prime {
get {
switch ( FCategorie )
{
case CategoriePerso.Cadre_Sup :
return EvaluerPrimeCadreSup ( FMerite );
case CategoriePerso.Cadre :
return EvaluerPrimeCadre ( FMerite );
case CategoriePerso.Maitrise :
return EvaluerPrimeMaitrise ( FMerite );
case CategoriePerso.Agent :
return EvaluerPrimeAgent ( FMerite );
default :
return EvaluerPrimeAgent ( 0 );
}
}
}
/// date è laquelle l'indice actuel a été obtenu :
public DateTime IndiceDepuis {
get { return FIndiceDetenu ; }
set { FIndiceDetenu = value ; }
}
}
/// <summary>
/// Classe du personnel mensualisé. Implémente la propriété abstraite
/// MontantPaie déclarée dans la classe de base (mère).
/// </summary>
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 384
///le constructeur de la classe (salarié au mérite) :
public SalarieMensuel ( int IDentifiant, string Nom, string Prenom, CategoriePerso Categorie,
string Insee, int Merite, int Indice, double CoeffPrime, double RemunerationTotal ):
base ( IDentifiant, Nom, Prenom, Categorie, Insee, Merite, Indice, CoeffPrime ) {
FPrime = this .Prime ;
FRemunerationTotal = RemunerationTotal ;
}
/// implémentation de la propriété donnant le montant du salaire :
public override double MontantPaie {
get { return ( FRemunerationTotal + this .Prime ) / 12 ; }
}
/// propriété de point de mérite du salarié :
public override int Merite {
get { return FMerite ; }
set { FMerite = value ; FPrime = this .Prime ; }
}
}
/// <summary>
/// Classe du personnel horaire. Implemente la propriété abstraite
/// MontantPaie déclarée dans la classe de base (mère).
/// </summary>
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 385
Classes, objet et IHM
Classe de salariés dans un fichier de l'entreprise
Nous reprenons les trois classes de l'exercice précédent définissant un salarié horaire et
mensualisé dans une entreprise. Nous créons un fichier des salariés de l'entreprise, pour cela
nous définissons une classe FichierDeSalaries permettant de gérer le fichier des salariés de
l'entreprise :
using System ;
using System.Collections ;
using System.Threading ;
using System.IO ;
namespace cci
{
class FichierDeSalaries
{
private string Fchemin ;
private ArrayList FListeEmployes ; // liste des nouveaux employés à entrer dans le fichier
private ArrayList indexCadreSup ; // Table d'index des cadres supérieurs du fichier
}
// méthode de création de la table d'index des cadre_sup :
public void CreerIndexCadreSup ( ) {
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 386
}
// méthode convertissant le champ string catégorie en la constante enum associée
private CategoriePerso strToCategorie ( string s ) {
}
// méthode renvoyant un objet SalarieMensuel de rang fixé dans le fichier
private Salarie EditerUnSalarie ( int rang ) {
SalarieMensuel perso ;
...........
perso = new SalarieMensuel ( IDentifiant, Nom, Prenom, Categorie, Insee,
Merite, Indice, CoeffPrime, RemunerationTotal );
...........
return perso ;
}
// méthode affichant sur la console à partir de la table d'index :
public void EditerFichierCadreSup ( )
{
...........
foreach( int ind in indexCadreSup )
{
AfficherUnSalarie ( EditerUnSalarie ( ind ) );
}
...........
}
// méthode affichant sur la console le fichier de tous les salariés :
public void EditerFichierSalaries ( ) {
}
// méthode créant et stockant des salariés dans le fichier :
public void StockerSalaries ( ArrayList ListeEmploy )
{
...........
// si le fichier n'existe pas => création du fichier sur disque :
fichierSortie = File.CreateText ( Fchemin );
fichierSortie.WriteLine ("Fichier des personnels");
fichierSortie.Close ( );
...........
// ajout dans le fichier de toute la liste :
...........
foreach( Salarie s in ListeEmploy )
{
}
...........
}
}
class ClassUsesSalarie
{
/// <summary>
/// Le point d'entrée principal de l'application.
/// </summary>
static void InfoSalarie ( SalarieMensuel empl )
{
FichierDeSalaries.AfficherUnSalarie ( empl );
double coefPrimeLoc = empl.Coeff_Prime ;
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 387
int coefMeriteLoc = empl.Merite ;
//--impact variation du coef de prime
for( double i = 0.5 ; i < 1 ; i += 0.1 )
{
empl.Coeff_Prime = i ;
Console .WriteLine (" coeff prime : " + empl.Coeff_Prime );
Console .WriteLine (" montant prime annuelle : " + empl.Prime );
Console .WriteLine (" montant paie mensuelle: " + empl.MontantPaie );
}
Console .WriteLine (" -----------------------");
empl.Coeff_Prime = coefPrimeLoc ;
//--impact variation du coef de mérite
for( int i = 0 ; i < 10 ; i ++ )
{
empl.Merite = i ;
Console .WriteLine (" coeff mérite : " + empl.Merite );
Console .WriteLine (" montant prime annuelle : " + empl.Prime );
Console .WriteLine (" montant paie mensuelle: " + empl.MontantPaie );
}
empl.Merite = coefMeriteLoc ;
Console .WriteLine ("=======================================");
}
[STAThread]
static void Main ( string [ ] args )
{
SalarieMensuel Employe1 = new SalarieMensuel ( 123456, "Euton" , "Jeanne" ,
CategoriePerso.Cadre_Sup, "2780258123456" ,6,700,0.5,50000 );
SalarieMensuel Employe2 = new SalarieMensuel ( 123457, "Yonaize" , "Mah" ,
CategoriePerso.Cadre, "1821113896452" ,5,520,0.42,30000 );
SalarieMensuel Employe3 = new SalarieMensuel ( 123458, "Ziaire" , "Marie" ,
CategoriePerso.Maitrise, "2801037853781" ,2,678,0.6,20000 );
SalarieMensuel Employe4 = new SalarieMensuel ( 123459, "Louga" , "Belle" ,
CategoriePerso.Agent, "2790469483167" ,4,805,0.25,20000 );
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 388
ListeSalaries.Add ( Employe2 );
ListeSalaries.Add ( Employe3 );
ListeSalaries.Add ( Employe4 );
Fiches.StockerSalaries ( ListeSalaries );
Fiches.EditerFichierSalaries ( );
Fiches.CreerIndexCadreSup ( );
Fiches.EditerFichierCadreSup ( );
System .Console.ReadLine ( );
}
}
Pour tester le programme précédent, on donne le fichier des salariés fichierSalaries.txt suivant :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 389
Hamas
*Cadre_Sup
1750258123456
4
500
15/02/2004 19:52:41
0,7
188300
19191,6666666667
123461
Kong
King
*Cadre
1640517896452
4
305
15/02/2004 19:52:41
0,62
78405
8867,08333333333
123462
Zaume
Philippo
*Maitrise
1580237853781
2
245
15/02/2004 19:52:41
0,8
43935
4911,25
123463
Micoton
Mylène
*Agent
2850263483167
4
105
15/02/2004 19:52:41
0,14
3234
1269,5
===============================
namespace cci
{
class FichierDeSalaries
{
private string Fchemin ;
private ArrayList FListeEmployes ;
private ArrayList indexCadreSup ;
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 390
// méthode static affichant un objet Salarie è la console :
public static void AfficherUnSalarie ( Salarie Employe ) {
if( Employe is SalarieMensuel )
{
SalarieMensuel empl = ( Employe as SalarieMensuel );
Console .WriteLine ("Employé n° + empl.IDentifiant + ": " + empl.Nom + " / " + empl.Prenom );
Console .WriteLine (" n° SS : " + empl.Insee );
Console .WriteLine (" catégorie : " + empl.Categorie );
Console .WriteLine (" indice hiérarchique : " + empl.Indice_Hierarchique + " , détenu depuis : "
+ empl.IndiceDepuis );
Console .WriteLine (" coeff mérite : " + empl.Merite );
Console .WriteLine (" coeff prime : " + empl.Coeff_Prime );
Console .WriteLine (" montant prime annuelle : " + empl.Prime );
Console .WriteLine (" montant paie mensuelle: " + empl.MontantPaie );
}
}
// constructeur de la classeFichierDeSalaries
public FichierDeSalaries ( string chemin, ArrayList Liste ) {
Fchemin = chemin ;
FListeEmployes = Liste ;
StockerSalaries ( FListeEmployes );
}
// méthode de création de la table d'index des cadre_sup :
public void CreerIndexCadreSup ( ) {
// Ouvre le fichier pour le lire
StreamReader fichierEntree = File.OpenText ( Fchemin );
string Ligne ;
int indexLigne = 0 ;
indexCadreSup = new ArrayList ( );
while (( Ligne = fichierEntree.ReadLine ()) != null)
{
indexLigne ++ ;
if("*" + CategoriePerso.Cadre_Sup.ToString () == Ligne )
{
Console .WriteLine ("++> " + Ligne + " : " + indexLigne );
indexCadreSup.Add ( indexLigne - 3 );
}
}
fichierEntree.Close ();
}
// méthode convertissant le champ string catégorie en la constante enum associée
private CategoriePerso strToCategorie ( string s ) {
switch( s )
{
case "*Cadre_Sup":return CategoriePerso.Cadre_Sup ;
case "*Cadre":return CategoriePerso.Cadre ;
case "*Maitrise":return CategoriePerso.Maitrise ;
case "*Agent":return CategoriePerso.Agent ;
case "*Autre":return CategoriePerso.Autre ;
default : return CategoriePerso.Autre ;
}
}
// méthode renvoyant un objet SalarieMensuel de rang fixé dans le fichier
private Salarie EditerUnSalarie ( int rang ) {
int compt = 0 ;
string Ligne ;
int IDentifiant = 0 ;
string Nom = "" , Prenom = "";
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 391
CategoriePerso Categorie = CategoriePerso.Autre ;
string Insee = "";
int Merite = 0, Indice = 0 ;
DateTime delai = DateTime.Now ;
double CoeffPrime = 0, RemunerationTotal = 0, MontantPaie = 0 ;
SalarieMensuel perso ;
StreamReader f = File.OpenText ( Fchemin );
//System .IFormatProvider format = new System .Globalization.CultureInfo ("fr-FR" , true );
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 392
if ( ! File.Exists ( Fchemin ))
{
// création du fichier sur disque :
fichierSortie = File.CreateText ( Fchemin );
fichierSortie.WriteLine ("Fichier des personnels");
fichierSortie.Close ( );
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 393
Classes, objet et IHM
Construction d'un ensemble de caractères
Il est aussi demandé à ce que l'ensemble propose deux événements OnInserer qui se produit
lorsque l'on ajoute un nouvel élément à l'ensemble et OnEnlever qui a lieu lorsque l'on
supprime un élément de l'ensemble.
CollectionBase est une classe de .Net Framework et fournit la classe de base abstract pour une
collection fortement typée :
System.Object
|__System.Collections.CollectionBase
public abstract class CollectionBase : IList, ICollection, IEnumerable
L'interface IList représente une liste d'objets accessibles séparément par indexeur et des méthodes
classiques de gestion de liste dont nous extrayons ci-dessous les principales utiles, à l'exercice à
traiter:
Méthodes publiques
int Add( object valeur ); Ajoute l'élément valeur dans la liste
void Remove(object valeur ); Enlève l'élément valeur dans la liste
IeventEnsemble est une interface qui est donnée pour décrire les deux événements auxquels un
ensemble doit être sensible :
interface IEventEnsemble {
event EventHandler OnInserer;
event EventHandler OnEnlever;
}
Question :
compléter dans le squelette de programme ci-après, la classe ensemble de caractère setOfChar.
using System;
using System.Collections;
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 394
namespace cci
{
interface IEventEnsemble {
event EventHandler OnInserer;
event EventHandler OnEnlever;
}
public setOfChar(char[ ] t) {
….
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 395
}
// initialisation
setOfChar EnsCar1 = new setOfChar();
setOfChar EnsCar,EnsCar2;
// ajout d'éléments
EnsCar1.Add('a');
EnsCar1.Add('b');
EnsCar1.Add('c');
EnsCar1.Add('d');
EnsCar1.Add('e');
Console.WriteLine("card="+EnsCar1.Card+" ; "+EnsCar1.ToString());
EnsCar2=new setOfChar("xyztu#");
Console.WriteLine("card="+EnsCar2.Card+" ; "+EnsCar2.ToString());
EnsCar=EnsCar1+EnsCar2;
EnsCar.OnEnlever += new EventHandler(EnsCarOnEnlever);
Console.WriteLine("card="+EnsCar.Card+" ; "+EnsCar.ToString());
Console.WriteLine();
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 396
for(int i=0; i<EnsCar.Card; i++)
Console.Write( EnsCar[i]+"," );
Console.WriteLine();
// on enlève un élément dans l'ensemble
EnsCar.Remove( 'd' );
Résultats de l'exécution :
public setOfChar(char[ ] t) {
foreach(char car in t)
this.Add(car);
} Le seul constructeur qui a un corps et
permettant de charger l'ensemble avec
public char[ ] ToArray( ) un tableau de caractères.
{
char[ ] t = new char[this.Count];
for(int i=0; i<this.Count; i++)
t[i]=this[i];
return t;
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 397
public override string ToString( ) {
return new string(this.ToArray( ));
}
Si l'on veut éliminer la redondance d'élément (un élément n'est présent qu'une seule fois dans
un ensemble) il faut agir sur la méthode d'ajout (add) et vérifier que l'ajout est possible.
L'ajout est possible si l'élément à ajouter n'est pas déjà contenu dans la liste :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 398
Comme la méthode add renvoie le rang d'insertion de l'élément, nous lui faisons renvoyer la valeur
-1 lorsque l'élément n'est pas ajouté parce qu'il est déjà présent dans la liste :
Nous pouvons aussi prévoir d'envoyer un message à l'utilisateur de la classe sous forme d'une
fenêtre de dialogue l'avertissant du fait qu'un élément était déjà présent et qu'il n'a pas été rajouté.
Nous utilisons la classe MessageBox de C# qui sert à afficher un message pouvant contenir du
texte, des boutons et des symboles :
System.Object
|__ System.Windows.Forms.MessageBox
Voici dans cette éventualité, ce que provoque l'exécution de la sixième ligne du code ci-
dessous sur la deuxième demande d'insertion de l'élément 'd' dans l'ensemble EnsCar1 :
EnsCar1.Add('a');
EnsCar1.Add('b');
EnsCar1.Add('c');
EnsCar1.Add('d');
EnsCar1.Add('e');
EnsCar1.Add('d');
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 399
Classes, objet et IHM
Construction d'une classe d'ensemble générique
Soit à construire une classe setOfObject d'ensemble plus générale que celle de l'exercice
précédent. Nous souhaitons en, effet disposer d'une classe de type ensemble possédant les
fonctionnalités de la classe setOfChar ( ajouter, enlever un élément de l'ensemble, test
d'appartenance d'un élément à l'ensemble, cardinal de l'ensemble, non redondance d'un
élément), qui puisse accueillir des éléments de même type mais que ce type puisse être
n'importe quel type héritant de la classe object.
Cette classe d'ensemble, proposera deux événements OnInserer qui se produit lorsque l'on
ajoute un nouvel élément à l'ensemble et OnEnlever qui a lieu lorsque l'on supprime un
élément de l'ensemble.
Conseils :
Par rapport à l'exercice précédent, il faut faire attention à l'ajout d'un élément du même
type que tous ceux qui sont déjà présents dans l'ensemble et refuser un nouvel élément
qui n'est pas strictement du même type. Il faut donc utiliser le mécanisme de reflexion
de C# (connaître le type dynamique d'un objet lors de l'exécution) contenu dans la
classe Type.
constructeur fonctionnalité
public setOfObject( ) Construit un ensemble vide
(de n'importe quel type)
public setOfObject (object[ ] t) Rempli un ensemble à partir d'un tableau d'object.
(le type des éléments de l'ensemble construit est automatiquement celui du
premier élément du object[ ])
public setOfObject (Array t) Rempli un ensemble à partir d'un tableau de type Array.
(le type des éléments de l'ensemble construit est automatiquement celui du
premier élément du Array )
public setOfObject (ArrayList t) Rempli un ensemble à partir d'un tableau de type ArrayList.
(le type des éléments de l'ensemble construit est automatiquement celui du
premier élément du ArrayList )
public setOfObject (string s) Rempli un ensemble à partir chaîne.
(le type des éléments de l'ensemble construit est automatiquement celui du
premier élément, ici char )
Nous proposons enfin, de prévoir un champ protégé nommé FtypeElement qui contient le type
de l'élément de l'ensemble qui peut varier au cours du temps Car lorsque l'ensemble est vide il
n'a pas de type d'élément c'est l'ajout du premier élément qui détermine le type de l'ensemble et
donc des futurs autres éléments à introduire. Ce champ protégé devra être accessible en lecture
seulement par tout utilisateur de la classe.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 400
Code à compléter :
public class setOfObject : CollectionBase, IEventEnsemble {
public setOfObject() {
}
public setOfObject(object[] t) {
…..
}
public setOfObject(Array t) {
…..
}
public setOfObject(string s) {
…..
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 401
public object this[ int index ] {
get {
return( List[index] );
}
set {
List[index] = value;
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 402
public setOfObject ( ) { Le constructeur vide initialise le
FTypeElement = null; type de l'ensemble.
}
public setOfObject(object[ ] t) {
Le constructeur ajoute tous les
foreach(object elt in t)
objets du tableau t dans l'ensemble.
this.Add(elt);
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 403
public int Add( object value ) {
// ajoute un élément à l'ensemble sinon -1 ou -2
if (this.Count==0)
FTypeElement = value.GetType( );
If (value.GetType( ) ==TypeElement) {
if (!Contains(value) )
return( List.Add( value ) );
else {
MessageBox.Show("L'élément "+value.ToString()+", est déjà présent !",
"Ensemble : insertion impossible (-1)");
return -1;
}
}
else {
MessageBox.Show("L'élément "+value.ToString()+", n'est pas du même type !",
"Ensemble : insertion impossible (-2)");
return -2;
}
}
//-- chargement par ajout élément par élément : On teste sur le type d'élément char.
E1.Add('a');
E1.Add('b');
E1.Add('c');
E1.Add('d');
E1.Add('e');
//-- chargement par string :
E2 = new setOfObject("xyztu#");
EnsCar = E1 + E2 ;
foreach (object elt in EnsCar)
Console.WriteLine(" "+ elt );
Console.WriteLine("type d'élément de
l'ensemble : "+EnsCar.TypeElement);
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 404
Extrait de code de test où E1, E2 et EnsCar sont des setOfObject
//-- chargement par ajout élément par élément : On teste sur le type d'élément int.
E1.Add(45);
E1.Add(-122);
E1.Add(666);
E1.Add(45);
E1.Add(-9002211187785);
E1.Add(12);
Console.WriteLine("type d'élément de
l'ensemble : "+EnsCar.TypeElement);
class UtilisesetOfObject{
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 405
EnsCar=Ensble1+Ensble2;
EnsCar.OnEnlever += new EventHandler(EnsbleOnEnlever);
Console.WriteLine("card(EnsCar)="+EnsCar.Card+" ; ");
Console.WriteLine();
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 406
E1.Add("trois");
E1.Add("quatre");
E1.Add("cinq");
//-- chargement par ArrayList de string : Appel au constructeur :
ArrayList t = new ArrayList( ); public setOfObject(ArrayList t) : this (t.ToArray( ) )
t.Add("six");
t.Add("sept"); qui appelle lui-même :
t.Add("fin."); public setOfObject(object[ ] t)
E2=new setOfObject(t);
}
[STAThread]
public static void Main( ) {
Console.WriteLine("------------ 1 -----------------");
version(1);
Console.WriteLine("----------- 2 ------------------");
version(2);
Console.WriteLine("----------- 3 ------------------");
version(3);
Console.WriteLine("----------- 4 ------------------");
version(4);
Console.WriteLine("----------- 5 ------------------");
version(5);
Console.WriteLine("----------- 6 ------------------");
version(6);
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 407
Classes, objet et IHM
Construction d'un jeu : puzzle genre "taquin"
Soit à construire une interface de jeu du taquin permettant de ranger par ordre alphabétique,
des lettres disposées dans n'importe quel ordre sur un damier 3 x 3 :
Sur les 9 cases une seule est disponible, le jeu consiste à ne déplacer qu'une seule lettre à la
fois et uniquement dans la case restée libre. Par exemple dans la configuration de départ ci-
haut seules les lettres G, E, C , B peuvent être déplacées vers la case centrale restée libre.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 408
L'interface à construire en C# doit permettre à un utilisateur de jouer au taquin dans les
conditions de contraintes du jeu, il utilisera la souris pour cliquer sur une lettre afin de la
déplacer vers la case libre (le programme doit gérer la possibilité pour une case d'être déplacée
ou non et doit tester à chaque déplacement si le joueur a gagné ou non).
L'IHM affichera la liste des lettres lue depuis le coin supérieur gauche jusqu'au coin inférieur droit du tableau (la
case libre sera représentée par un #)
etc …
Au final :
Lorsque le joueur a trouvé la bonne combinaison et rangé les lettres dans le bon ordre, prévoir
de lui envoyer une fenêtre de dialogue de félicitation et un menu lui permettant de rejouer ou
de quitter le jeu:
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 409
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;
namespace WinAppliPuzzle
{
/// <summary>
/// Description résumée de Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form {
private System.Windows.Forms.MainMenu mainMenu1;
private System.Windows.Forms.Label labelHorloge;
private System.Windows.Forms.MenuItem menuItemjeu;
private System.Windows.Forms.MenuItem menuItemson;
private System.Windows.Forms.MenuItem menuItemrelancer;
private System.Windows.Forms.MenuItem menuItemquitter;
private System.Windows.Forms.Timer timerTemps;
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 410
private void DeplaceVers(Button source, int but) {
int T,L;
if(but>0) {
T = source.Location.Y;
L = source.Location.X;
source.Location = Ptrou;
Ptrou = new Point(L,T);
if (Sonok)
MessageBeep(0);
}
}
public Form1( ) {
//
// Requis pour la prise en charge du Concepteur Windows Forms
//
InitializeComponent( );
//
// TODO : ajoutez le code du constructeur après l'appel à InitializeComponent
//
}
/// <summary>
/// Nettoyage des ressources utilisées.
/// </summary>
protected override void Dispose( bool disposing ) {
if( disposing ) {
if (components != null) {
components.Dispose( );
}
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 411
base.Dispose( disposing );
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 412
//
this.menuItemrelancer.Index = 1;
this.menuItemrelancer.Text = "Relancer";
this.menuItemrelancer.Click += new System.EventHandler(this.menuItemrelancer_Click);
//
// menuItemquitter
//
this.menuItemquitter.Index = 2;
this.menuItemquitter.Text = "Quitter";
this.menuItemquitter.Click += new System.EventHandler(this.menuItemquitter_Click);
//
// panelFond
//
this.panelFond.BackColor = System.Drawing.SystemColors.Info;
this.panelFond.Controls.Add(this.buttonH);
this.panelFond.Controls.Add(this.buttonG);
this.panelFond.Controls.Add(this.buttonF);
this.panelFond.Controls.Add(this.buttonE);
this.panelFond.Controls.Add(this.buttonD);
this.panelFond.Controls.Add(this.buttonC);
this.panelFond.Controls.Add(this.buttonB);
this.panelFond.Controls.Add(this.buttonA);
this.panelFond.ImeMode = System.Windows.Forms.ImeMode.NoControl;
this.panelFond.Location = new System.Drawing.Point(8, 32);
this.panelFond.Name = "panelFond";
this.panelFond.Size = new System.Drawing.Size(264, 264);
this.panelFond.TabIndex = 0;
//
// buttonH
//
this.buttonH.BackColor = System.Drawing.Color.Tan;
this.buttonH.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonH.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonH.Location = new System.Drawing.Point(92, 176);
this.buttonH.Name = "buttonH";
this.buttonH.Size = new System.Drawing.Size(80, 80);
this.buttonH.TabIndex = 16;
this.buttonH.Tag = 8;
this.buttonH.Text = "H";
this.buttonH.Click += new System.EventHandler(this.buttons_Click);
this.buttonH.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonG
//
this.buttonG.BackColor = System.Drawing.Color.Tan;
this.buttonG.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonG.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonG.Location = new System.Drawing.Point(8, 176);
this.buttonG.Name = "buttonG";
this.buttonG.Size = new System.Drawing.Size(80, 80);
this.buttonG.TabIndex = 15;
this.buttonG.Tag = 7;
this.buttonG.Text = "G";
this.buttonG.Click += new System.EventHandler(this.buttons_Click);
this.buttonG.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonF
//
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 413
this.buttonF.BackColor = System.Drawing.Color.Tan;
this.buttonF.Cursor = System.Windows.Forms.Cursors.Default;
this.buttonF.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonF.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonF.Location = new System.Drawing.Point(176, 92);
this.buttonF.Name = "buttonF";
this.buttonF.Size = new System.Drawing.Size(80, 80);
this.buttonF.TabIndex = 14;
this.buttonF.Tag = 6;
this.buttonF.Text = "F";
this.buttonF.Click += new System.EventHandler(this.buttons_Click);
this.buttonF.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonE
//
this.buttonE.BackColor = System.Drawing.Color.Tan;
this.buttonE.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonE.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonE.Location = new System.Drawing.Point(92, 92);
this.buttonE.Name = "buttonE";
this.buttonE.Size = new System.Drawing.Size(80, 80);
this.buttonE.TabIndex = 13;
this.buttonE.Tag = 5;
this.buttonE.Text = "E";
this.buttonE.Click += new System.EventHandler(this.buttons_Click);
this.buttonE.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonD
//
this.buttonD.BackColor = System.Drawing.Color.Tan;
this.buttonD.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonD.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonD.Location = new System.Drawing.Point(8, 92);
this.buttonD.Name = "buttonD";
this.buttonD.Size = new System.Drawing.Size(80, 80);
this.buttonD.TabIndex = 12;
this.buttonD.Tag = 4;
this.buttonD.Text = "D";
this.buttonD.Click += new System.EventHandler(this.buttons_Click);
this.buttonD.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonC
//
this.buttonC.BackColor = System.Drawing.Color.Tan;
this.buttonC.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonC.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonC.Location = new System.Drawing.Point(176, 8);
this.buttonC.Name = "buttonC";
this.buttonC.Size = new System.Drawing.Size(80, 80);
this.buttonC.TabIndex = 11;
this.buttonC.Tag = 3;
this.buttonC.Text = "C";
this.buttonC.Click += new System.EventHandler(this.buttons_Click);
this.buttonC.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonB
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 414
//
this.buttonB.BackColor = System.Drawing.Color.Tan;
this.buttonB.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonB.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonB.Location = new System.Drawing.Point(92, 8);
this.buttonB.Name = "buttonB";
this.buttonB.Size = new System.Drawing.Size(80, 80);
this.buttonB.TabIndex = 10;
this.buttonB.Tag = 2;
this.buttonB.Text = "B";
this.buttonB.Click += new System.EventHandler(this.buttons_Click);
this.buttonB.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// buttonA
//
this.buttonA.BackColor = System.Drawing.Color.Tan;
this.buttonA.Font = new System.Drawing.Font("Times New Roman", 27.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.buttonA.ForeColor = System.Drawing.Color.SaddleBrown;
this.buttonA.Location = new System.Drawing.Point(8, 8);
this.buttonA.Name = "buttonA";
this.buttonA.Size = new System.Drawing.Size(80, 80);
this.buttonA.TabIndex = 9;
this.buttonA.Tag = 1;
this.buttonA.Text = "A";
this.buttonA.Click += new System.EventHandler(this.buttons_Click);
this.buttonA.MouseMove += new System.Windows.Forms.MouseEventHandler(this.buttons_MouseMove);
//
// pictureBoxSon
//
this.pictureBoxSon.Image = ((System.Drawing.Image)(resources.GetObject("pictureBoxSon.Image")));
this.pictureBoxSon.Location = new System.Drawing.Point(128, 8);
this.pictureBoxSon.Name = "pictureBoxSon";
this.pictureBoxSon.Size = new System.Drawing.Size(16, 16);
this.pictureBoxSon.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;
this.pictureBoxSon.TabIndex = 1;
this.pictureBoxSon.TabStop = false;
//
// labelModele
//
this.labelModele.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.labelModele.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.labelModele.ForeColor = System.Drawing.Color.Chocolate;
this.labelModele.Location = new System.Drawing.Point(16, 8);
this.labelModele.Name = "labelModele";
this.labelModele.Size = new System.Drawing.Size(88, 16);
this.labelModele.TabIndex = 2;
this.labelModele.Text = "ABCDEFGH#";
this.labelModele.TextAlign = System.Drawing.ContentAlignment.TopCenter;
//
// timerTemps
//
this.timerTemps.Enabled = true;
this.timerTemps.Interval = 1000;
this.timerTemps.Tick += new System.EventHandler(this.timer1_Tick);
//
// labelHorloge
//
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 415
this.labelHorloge.BackColor = System.Drawing.Color.Aqua;
this.labelHorloge.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
this.labelHorloge.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.labelHorloge.Location = new System.Drawing.Point(192, 4);
this.labelHorloge.Name = "labelHorloge";
this.labelHorloge.Size = new System.Drawing.Size(72, 20);
this.labelHorloge.TabIndex = 4;
this.labelHorloge.Text = "00:00:00";
this.labelHorloge.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(280, 305);
this.Controls.Add(this.labelHorloge);
this.Controls.Add(this.labelModele);
this.Controls.Add(this.pictureBoxSon);
this.Controls.Add(this.panelFond);
this.Menu = this.mainMenu1;
this.Name = "Form1";
this.Text = "Mini puzzle type taquin";
this.Load += new System.EventHandler(this.Form1_Load);
this.panelFond.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// Point d'entrée principal de l'application.
/// </summary> Gestionnaire centralisé du
[STAThread] click de souris sur les 8
static void Main( ) { boutons représentant les
Application.Run(new Form1());
lettres dans le tableau.
}
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 416
private void menuItemson_Click(object sender, System.EventArgs e) {
Console.WriteLine(menuItemson.Text);
if(menuItemson.Text.IndexOf("off")>0) {
menuItemson.Text = "Son on";
Sonok = false;
pictureBoxSon.Visible = false;
}
else {
menuItemson.Text = "Son off";
Sonok = true;
pictureBoxSon.Visible = true;
}
}
}
Remarque
System.Object
|__ System.MarshalByRefObject
|__ System.ComponentModel.Component
|__ System.Windows.Forms.Timer
Cette classe très semblable à la classe TTimer de Delphi, implémente une minuterie déclenchant
un événement Tick (événement OnTimer en Delphi)selon un intervalle défini par l'utilisateur
(préconisation Microsoft : un Timer doit être utilisé dans une fenêtre). Ci-dessous le gestionnaire
de l'événement Tick :
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 417
Les valeurs de date de type DateTime sont mesurées en unités de 100 nanosecondes et exprimées
sous forme d'un entier long.
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 418
Bibliographie
Livres papier vendus par éditeur
Livres C# en français
Premier pas dans .Net avec C# - - ( rév. 05.09.2004 ) EXERCICES page 419