Introduction : modification des niveaux de visibilités
1. Les classes C# : des nouveaux types
Rappelons les classiques modificateurs de visibilité des variables et des méthodes dans les langages orientés objets, dont C# dispose :
Modification de visibilité (modularité public-privé)
par défaut (aucun mot clef) les variables et les méthodes d'une classe non précédées d'un mot clef sont private et ne sont visibles que dans la classe seulement. public les variables et les méthodes d'une classe précédées du mot clef public sont visibles par toutes les classes de tous les modules. private les variables et les méthodes d'une classe précédées du mot clef private ne sont visibles que dans la classe seulement. protected les variables et les méthodes d'une classe précédées du mot clef protected sont visibles par toutes les classes inclues dans le module, et par les classes dérivées de cette classe. internal
les variables et les méthodes d'une classe précédées du mot clef protected sont visibles par toutes les classes incluses dans le même assembly.
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.
Tableau des limitations des niveaux de visibilité fourni par microsoft :
![]()
1. Les classes : des nouveaux types
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# |
Unit Biblio;
interface // les déclarations des classes implementation // les implémentations des classes end. |
package
Biblio; // les déclarations et implémentation des classes si pas de nom de package alors automatiquement dans : package java.lang; |
namespace Biblio { // les déclarations et implémentation
des classes
|
En C#, nous n'avons pas comme en Delphi, une partie déclaration
de la classe et une partie implémentation séparées l'une
de l'autre. La classe avec ses attributs et ses méthodes sont déclarés
et implémentés à un seul endroit comme en Java.
Delphi | Java |
C# |
interface uses biblio; type Exemple = class x : real; y : integer; function F1(a,b:integer): real; procedure P2; end; implementation |
import
biblio;
class Exemple { ...........code de P2 } } |
using biblio
{ void P2( ) }
|
1.2 Une classe est un type en C#
Comme en Delphi et Java, une classe C# peut être considérée comme un nouveau type dans le programme et donc des variables d'objets peuvent être déclarées selon ce nouveau "type".
Une déclaration de programme comprenant 3 classes :
Delphi | Java |
C# |
interface
type Un = class ... end; Deux = class Appli3Classes = class
implementation procedure Appli3Classes.main;
end. |
class
Appli3Classes { Un x; Deux y; public static void main(String [ ] arg) { Un x; Deux y; ... } } class Un { ... } class Deux { ... } |
class Appli3Classes
{ Un x; Deux y; static void Main(String [ ] arg) { Un x; Deux y; ... } } class Un { ... } class Deux
|
1.3 Toutes les classes ont le même ancêtre - héritage
Comme en Delphi et Java, toutes les classes C# dérivent automatiquement d'une seule et même classe ancêtre : la classe Object. En C# le mot-clef pour indiquer la dérivation (héritage) à partir d'une autre classe est le symbole deux points ':', lorsqu'il est omis c'est donc que la classe hérite automatiquement de la classe Object :
Les deux déclarations de classe ci-dessous sont équivalentes :
Delphi | Java |
C# |
type Exemple = class ( TObject ) ...... end; |
class Exemple extends
Object { ....... } |
class Exemple : Object { ....... } |
type Exemple = class ...... end; |
class Exemple { ....... } |
class Exemple { ....... } |
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 les
méthodes de la classe ClasseMere.
Comparaison héritage :
Delphi | Java |
C# |
type ClasseMere = class // champs de ClasseMere // méthodes de ClasseMere end; ClasseFille = class ( ClasseMere ) |
class ClasseMere
{ // champs de ClasseMere // méthodes de ClasseMere } class ClasseFille extends ClasseMere { // hérite des champs de ClasseMere // hérite des méthodes de ClasseMere } |
class ClasseMere { // champs de ClasseMere // méthodes de ClasseMere } class ClasseFille : ClasseMere |
La visibilité et la protection des classes en Delphi est apportée par le module Unit où toutes les classes sont visibles dans le module en entier et dès que la unit est utilisée les classes sont visibles partout. Il n'y a pas de possibilité d'imbriquer une classe dans une autre.
En C#, nous avons la possibilité d'imbriquer des
classes dans d'autres classes (classes internes), par conséquent
la visibilité de bloc s'applique aussi aux classes.
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.
Mots clefs pour la protection des classes et leur visibilité
:
Attention
Par défaut dans une classe tous les membres sans qualificateur de visibilité (classes internes inclues)
sont private.
C# | Explication |
mot clef abstract : abstract class ApplicationClasse1 { ... } |
classe abstraite non instanciable. Aucun objet ne peut être créé. |
mot clef public : public class ApplicationClasse2 { ... } |
classe visible par n'importe quel programme d'un autre namespace |
mot clef protected : protected class ApplicationClasse3 { ... } |
classe visible seulement par toutes les autres classes héritant
de la classe conteneur de cette classe. |
mot clef internal : internal class ApplicationClasse4 { ... } |
classe visible seulement par toutes les autres classes du même
assembly. |
mot clef private : private class ApplicationClasse5 { ... } |
classe visible seulement par toutes les autres classes du même
namespace où elle est définie. |
pas de mot clef : class ApplicationClasse6 { ... } - sauf si c'est une classe interne |
qualifiée public -si c'est une classe interne elle est alors qualifiée private. |
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. Les mot clef 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.
1.5 Exemple de classes imbriquées dans une autre classe
Dans le premier contexte, ces six classes sont utilisées en étant intégrées (imbriquées) à une classe publique.
La classe ApplicationClasses :
C# | Explication |
![]() namespace Exemple { public class ApplicationClasses { abstract class ApplicationClasse1 { ... } public class ApplicationClasse2 { ... } protected class ApplicationClasse3 { ... } internal class ApplicationClasse4 { ... } private class ApplicationClasse5 { ... } class ApplicationClasse6 { ... } } } |
Ces 6 "sous-classes" sont visibles ou non, à partir de l'accès à la classe englobante "ApplicationClasses", elles peuvent donc être utilisées dans tout programme qui utilise la classe "ApplicationClasses". Par défaut la classe ApplicationClasse6 est ici private. |
Un programme utilisant la classe ApplicationClasses
:
C# | Explication |
![]() namespace Exemple { class AppliTestClasses |
Le programme de gauche "class AppliTestClasses" utilise la classe précédente ApplicationClasses et ses sous-classes. La notation uniforme de chemin de classe est standard. Seule la classe interne ApplicationClasse2 est visible et permet d'instancier un objet. |
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# | Explication |
![]() namespace Exemple { abstract class ApplicationClasse1 { ... } public class ApplicationClasse2 { ... } protected class ApplicationClasse3 { ... } internal class ApplicationClasse4 { ... } private class ApplicationClasse5 { ... } class ApplicationClasse6 { ... } } |
Une classe dans un espace de nom ne peut pas être qualifiée
protected ou private (si elle est imbriquée
comme ci-haut cela est possible): La classe ApplicationClasse1 est ici abstraite et public, donc visible. La classe ApplicationClasse2 est public, donc visible. La classe ApplicationClasse4 n'est visible que dans le même assembly. Par défaut la classe ApplicationClasse6 est ici public, donc visible. |
Un programme AppliTestClasses utilisant ces 4 classes :
C# dans deux namespace différents |
Explication |
![]() using Exemple; namespace Essai |
Le programme de gauche "class AppliTestClasses"
utilise les classes qui composent le namespace Exemple. Cette classe AppliTestClasses est définie dans un autre namespace dénommé Essai qui est supposé ne pas faire partie du même assembly que le namespace Exemple. Elle ne voit donc pas la classe ApplicationClasse4 (visible dans le même assembly seulement). Si l'on veut instancier des objets seules les classes ApplicationClasse2 et ApplicationClasse6 sont de bonnes candidates, car la classe ApplicationClasse1 bien qu'elle soit visible est abstraite. |
C# dans le même namespace |
Explication |
![]() namespace Exemple { class AppliTestClasses{ ApplicationClasse1 a1; ApplicationClasse2 a2; ApplicationClasse4 a4; ApplicationClasse6 a6; } } |
Le programme de gauche "class AppliTestClasses" utilise les classes qui composent le namespace Exemple. La classe AppliTestClasses est définie dans le même namespace Exemple que les 4 autres classes. Toutes les classes du même namespace sont visibles entre elles. Ici les toutes les 4 classes sont visibles pour la classe AppliTestClasses. |
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. |
Le mot clef abstract est utilisé pour représenter une classe ou une méthode abstraite. Quel est l'intérêt de cette notion ? Avoir des modèles génériques permettant de définir ultérieurement des actions spécifiques.
Une méthode déclarée en abstract dans une classe mère :
|
Exemple de méthode abstraite :
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) :
class Serpent : Etre_Vivant {
} class Oiseau : Etre_Vivant { class Homme : Etre_Vivant { |
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 :
abstract class
Etre_Vivant { public abstract void SeDeplacer( ); } class Serpent : Etre_Vivant { class Oiseau : Etre_Vivant { class Homme : Etre_Vivant { |
abstract class Etre_Vivant { protected abstract void SeDeplacer( ); } class Serpent : Etre_Vivant { class Oiseau : Etre_Vivant { class Homme : Etre_Vivant { |
Comparaison de déclaration d'abstraction de méthode en Delphi et C# :
Delphi | C# |
type Etre_Vivant = class procedure SeDeplacer;virtual; abstract ; end; Serpent = class ( Etre_Vivant ) Oiseau = class ( Etre_Vivant ) Homme = class ( Etre_Vivant )
|
abstract class
Etre_Vivant { public abstract void SeDeplacer( ); } class Serpent : Etre_Vivant { class Oiseau : Etre_Vivant { class Homme : Etre_Vivant { |
1.8 Classe abstraite, Interface
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.
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.
Rappel classes abstraites-interfaces
- 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.
- Une interface ne peut pas contenir des méthodes déjà implémentées.
- Une interface doit contenir des méthodes non implémentées.
- Une interface est héritable.
- On peut contsruire une hiérarchie d'interfaces.
- 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 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, private.Contrairement à Java, une classe abstraite C# qui implémente une interface doit obligatoirement déclarer toutes les méthodes de l'interface, celles qui ne sont pas implémentées dans la classe abstraite doivent être déclarées abstract. C'est le cas dans l'exemple ci-dessous pour la méthode abstraite Démarrer , nous proposons deux écritures possibles pour cette hiérarchie de classe :
C#
méthode abstraite sans corps
C#
méthode virtuelle à corps vide
interface IVehicule{
void Demarrer( );
void RépartirPassager( );
void PériodicitéMaintenance( );
}
abstract class Terrestre : IVehicule {
public abstract void Demarrer( );
public void virtual RépartirPassager( ){...}
public void virtual PériodicitéMaintenance( ){...}
}
class Voiture : Terrestre {
override public void Demarrer( ){...}
}
abstract class Marin : IVehicule {
public abstract void Demarrer( );
public void virtual RépartirPassager( ){...}
public void virtual PériodicitéMaintenance( ){...}
}
class Voilier : Marin {
override public void Demarrer( ){...}
}
class Croiseur : Marin {
override public void Demarrer( ){...}
}
interface IVehicule{
void Demarrer( );
void RépartirPassager( );
void PériodicitéMaintenance( );
}
abstract class Terrestre : IVehicule {
public virtual void Demarrer( ){}
public void virtual RépartirPassager( ){...}
public void virtual PériodicitéMaintenance( ){...}
}
class Voiture : Terrestre {
override public void Demarrer( ){...}
}
abstract class Marin : IVehicule {
public virtual void Demarrer( ) {}
public void virtual RépartirPassager( ){...}
public void virtual PériodicitéMaintenance( ){...}
}
class Voilier : Marin {
override public void Demarrer( ){...}
}
class Croiseur : Marin {
override public void Demarrer( ){...}
}
Les méthodes RépartirPassagers, PériodicitéMaintenance et Demarrer sont implantées virtual , soit comme des méthodes à liaison dynamique, afin de laisser la possibilité pour des classes enfants de redéfinir ces méthodes. Soit à titre de comparaison, les deux mêmes écritures en Java de ces classes :
Java
méthode abstraite sans corps
Java
méthode virtuelle à corps vide
![]()
![]()
2. Les objets : des références ou des valeurs
Les classes sont des descripteurs d'objets, les objets sont les agents
effectifs et "vivants" implantant les actions d'un programme. Les objets
dans un programme ont une vie propre :
C'est dans le segment de mémoire du CLR de .NetFramework que s'effectue
l'allocation et la désallocation d'objets.
Objets type valeur
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
Objets type référence
Le principe d'allocation et de représentation des objets type référence en C# est identique à celui de Delphi il s'agit de la référence, qui est une encapsulation de la notion de pointeur. Dans .NET Framework 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 catégories d'objets :
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 programme
C# les types références du développeur représentent
près de 99% des objets du programme).
2.1 Modèle de la référence en C#
Rappelons que dans le modèle de la référence chaque
objet (représenté par un identificateur de variable) est caractérisé
par un couple (référence, bloc de données). Comme en Delphi, C# décompose l'instanciation
(allocation) d'un objet en deux étapes :
Delphi | C# |
type Un = class ...... end; // la déclaration :
// la création : |
class Un { ... } // la déclaration :
.... // la création : |
Après exécution du pseudo-programme précédent, les variables x et y contiennent chacune une référence (adresse mémoire) vers un bloc objet différent:
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 :
Attitude à rapprocher pour comparaison, à celle dont Delphi
gère les objets dans une pile d'exécution de type LIFO et un
tas :
Attention à l'utilisation de l'affectation entre variables d'objets dans le modèle de représentation par référence. L'affectation x = y ne recopie pas le bloc objet de données de y dans celui de x, mais seulement la référence (l'adresse) de y dans la référence de x. Visualisons cette remarque importante :
Situation au départ, avant affectation
Situation après l'affectation " x = y "
En C#, la désallocation étant automatique, le bloc de
données objet qui était référencé par
y avant l'affectation, n'est pas perdu, car le garbage collector
se charge de restituer la mémoire libérée au segment
de mémoire du CLR
2.2 Les constructeurs d'objets références ou valeurs
Un constructeur est une méthode spéciale d'une classe
dont la seule fonction est d'instancier un objet (créer le
bloc de données). Comme en Delphi une classe C# peut posséder
plusieurs constructeurs, il est possible de pratiquer des initialisations
d'attributs dans un constructeur. Comme toutes les méthodes, un constructeur
peut avoir ou ne pas avoir de paramètres formels.
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; } |
|
Exemple de constructeur avec instructions :
C# | Explication |
class Un { public Un ( ) { a = 100 } int a; } |
Le constructeur public Un sert ici à initialiser à 100 la valeur de l'attribut "int a" de chaque objet qui sera instancié. |
Exemple de constructeur avec paramètre :
C# | Explication |
class Un { public Un (int b ) { a = b; } int a; } |
Le constructeur public Un sert ici à initialiser la valeur de l'attribut "int a" de chaque objet qui sera instancié. Le paramètre int b contient cette valeur. |
Exemple avec plusieurs constructeurs :
C# | Explication |
class Un { public Un (int b ) { a = b; } public Un ( ) { a = 100; } public Un (float b ) { a = (int)b; } int a; } |
La classe Un possède 3 constructeurs servant
à initialiser chacun d'une manière différente le seul
attribut int a.
|
Exemple avec un appel à un constructeur de la même
classe:
C# | Explication |
class Un { int a; public Un (int b ) { a = b; } public Un ( ) { a = 100; } public Un (float b ) { a = (int)b; } public Un (int x , float y ) : this(y) { a += 100; } } |
La classe Un possède 3 constructeurs servant
à initialiser chacun d'une manière différente le seul
attribut int a. Soit le dernier constructeur : public Un (int x , float y ) : this(y) { a += 100; } Ce constructeur appelle tout d'abord le constructeur Un (float y ) par l'intermédiaire de this(y), puis il exécute le corps de méthode soit : a += 100; Ce qui revient à calculer : a = (int)y + 100 ; |
Comparaison Delphi - C# pour la déclaration de constructeurs
Delphi | C# |
Un = class a : integer; public constructor creer; overload; constructor creer (b:integer); overload; constructor creer (b:real); overload; end; implementation constructor Un.creer; begin
constructor Un.creer(x:integer; y:real);
begin |
class Un { int a; public Un ( ) { a = 100; } public Un (int
b ) public Un (float
b ) public Un (int x
, float y ) : this(y)
|
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#.
2.3 Utilisation du constructeur d'objet automatique (par défaut)
Le constructeur d'objet par défaut de toute classe C# qu'elle soit de type valeur ou de type référence, comme nous l'avons signalé plus haut est une méthode spéciale sans paramètre, l'appel à cette méthode spéciale afin de construire un nouvel objet répond à une syntaxe spécifique par utilisation du mot clef new.
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 :
Exemple : (deux façons équivalentes de créer un objet x de classe Un)
Cette instruction crée dans le segment de mémoire,
un nouvel objet de classe Un dont la
référence (l'adresse) est mise dans la variable x, si x est de type référence,
ou bien l'objet est directement créé dans la pile et mis dans
la variable x, si x est de type valeur. 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( ) ;
|
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
{ ...
}
class UnAutre
{
// la déclaration :
Un x , y ;
....
// la création :
x = new Un( );
y = new Un( );
}
Un programme de 2 classes, illustrant l'affectation de références :
C# | Explication |
class AppliClassesReferences { public static void Main(String [] arg) { Un x,y ; x = new Un( ); y = new Un( ); System.Console.WriteLine("x.a="+x.a); System.Console.WriteLine"y.a="+y.a); y = x; x.a =12; System.Console.WriteLine("x.a="+x.a); System.Console.WriteLine("y.a="+y.a); } } class Un { int a=10; } |
Ce programme C# contient deux classes :
class AppliClassesReferences La classe AppliClassesReferences est une classe exécutable
car elle contient la méthode main. C'est donc cette méthode
qui agira dès l'exécution du programme.
|
Détaillons les instructions | Que se passe-t-il à l'exécution ? |
Un x,y ; x = new Un( ); y = new Un( ); |
Instanciation de 2 objets différents x et y de type Un. |
System.Console.WriteLine("x.a="+x.a); System.Console.WriteLine("y.a="+y.a); |
Affichage de : x.a = 10 y.a = 10 |
y = x; | La référence de y est remplacée par celle de x dans la variable y (y pointe donc vers le même bloc que x). |
x.a =12; System.Console.WriteLine("x.a="+x.a); System.Console.WriteLine("y.a="+y.a); |
On change la valeur de l'attribut a de x, et
l'on demande d'afficher les attributs de x et de y : x.a = 12 y.a = 12 Comme y pointe vers x, y et x sont maintenant le même objet sous deux noms différents ! |
2.4 Utilisation d'un constructeur d'objet personnalisé
L'utilisation d'un constructeur personnalisé d'une classe est semblable à celle du constructeur par défaut de la classe. La seule différence se trouve lors de l'instanciation : il faut fournir des paramètres effectifs lors de l'appel au constructeur.
Syntaxe
Exemple avec plusieurs constructeurs :
une classe C# | Des objets créés |
class Un { int a ; public Un (int b ) { a = b ; } public Un ( ) {
public Un (float
b ) { public Un (int x
, float y ) : this(y) |
Un obj1 = newUn( ); Un obj2 = new Un( 15 ); int k = 14; Un obj4 = new Un( 3.25f ); float r = -5.6; int x = 20; |
2.5 Le mot clef this - cas de la référence seulement
Il est possible de dénommer dans les instructions d'une méthode de classe, un futur objet qui sera instancié plus tard. Le paramètre ou (mot clef) this est implicitement présent dans chaque objet instancié et il contient la référence à l'objet actuel. Il joue exactement le même rôle que le mot clef self en Delphi. Nous avons déjà vu une de ses utilisations dans le cas des constructeurs.
C# | C# équivalent |
class Un { public Un ( ) { a = 100; } int a; } |
class Un { public Un ( ) { this.a = 100; } 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.
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 ( ) { a = 100; } public void methode1(Un x) { System.Console.WriteLine("champ a = " + x.a); } public void methode2( int b ) { a += b; methode1(this); } int a; } |
La methode1(Un x) reçoit
un objet de type Exemple en paramètre et imprime son champ int
a.
La methode2( int b ) reçoit
un entier int b qu'elle additionne au champ int a de l'objet,
puis elle appelle la méthode1 avec comme paramètre l'objet
lui-même.
|
Comparaison Delphi - C# sur cet exemple (similitude complète)
Delphi | C# |
Un = class a : integer; public constructor creer; procedure methode1( x:Un ); procedure methode2 ( b:integer ); end; implementation constructor Un.creer; begin
|
class Un { public Un ( ) { a = 100; } public void methode1(Un x) { System.Console.WriteLine("champ a ="+x.a); } public void methode2( int b ) { a += b; methode1(this); } int a; }
|
Cas où le this sert à outrepasser le masquage de visibilité :
C# | Explications |
class Un { int a; public void methode1(float a) { a = this.a + 7 ; } } |
La methode1(float
a) possède un paramètre float a dont le nom masque
le nom du champ int a.
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. |
Comparaison Delphi - C# sur ce second exemple (similitude complète aussi)
Delphi | C# |
Un = class a : integer; public procedure methode( a:real ); end; implementation procedure Un.methode( a:real );begin
|
class Un { int a; public void methode(float a) { a = this.a + 7 ; } }
|
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 :
3.1 Variables dans une classe en général
Rappelons qu'en C#, nous pouvons déclarer dans un bloc (for, try,...) de nouvelles variables à la condition qu'elles n'existent pas déjà dans le corps de la méthode où elles sont déclarées. Nous les dénommerons : variables locales de méthode.
Exemple de variables locales de méthode :
class Exemple { void calcul ( int x, int y ) {int a = 100; for ( int i = 1; i<10; i++ ) {char carlu; System.Console.Write("Entrez un caractère : "); carlu = (char)System.Console.Read(); int b =15; a =.... ..... } } } |
La définition int
a = 100; est locale à la méthode en général
La définition int i = 1; est locale à la boucle for. Les définitions char carlu
et int b sont locales au 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.
Exemple de attributs de classe :
class AppliVariableClasse
{ float r ; void calcul ( int x,
int y ) |
Les variables float
r , long y et int x sont des attributs de classe (ici en
fait plus précisément, des variables d'instance).
Conseil : regroupez les variables de classe au début de la classe afin de mieux les gérer.
|
Les attributs de classe peuvent être soit de la catégorie
des variables de classe, soit de
la catégorie des variables d'instance.
3.2 Variables et méthodes d'instance
C# se comporte comme un langage orienté objet classique
vis à vis de ses variables et de ses méthodes. A chaque
instanciation d'un nouvel objet d'une classe donnée, la machine CLR
enregistre le p-code des méthodes de la classe dans la zone de
stockage des méthodes, elle alloue dans le segment de mémoire
autant d'emplacements mémoire pour les variables
que d'objet créés. C# dénomme cette catégorie
les variables et les méthodes d'instance.
une classe C# | Instanciation de 3 objets |
class AppliInstance
{ int x ; int y ; } |
AppliInstance
obj1 = new AppliInstance( ); AppliInstance obj2 = newAppliInstance( ); AppliInstance obj3 = newAppliInstance( ); |
Segment de mémoire associé à ces 3 objets
si la classe AppliInstance
est de type référence :
Segment de mémoire associé à ces 3 objets si la classe
AppliInstance était de type valeur
(pour mémoire):
struct AppliInstance
{
int x ;
int y ;
}
![]()
Un programme C# à 2 classes illustrant l'exemple précédent (classe référence):
Programme C# exécutable
class AppliInstance
{ public int x = -58 ;
public int y = 20 ;
}
class Utilise
{ public static void Main( ) {
AppliInstance obj1 = new AppliInstance( );
AppliInstance obj2 = new AppliInstance( );
AppliInstance obj3 = new AppliInstance( );
System.Console.WriteLine( "obj1.x = " + obj1.x );
}
}
3.3 Variables et méthodes de classe - static
Variable de classe
On identifie une variable ou une méthode de classe en précédant sa déclaration du mot clef static. Nous avons déjà pris la majorité de nos exemples simples avec de tels composants.
Voici deux déclaration de variables de classe :
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.
une classe C# | Instanciation de 3 objets |
class AppliInstance { static int x ; int y ; } |
AppliInstance
obj1 = new AppliInstance( ); AppliInstance obj2 = newAppliInstance( ); AppliInstance obj3 = newAppliInstance( ); |
Voici une image du segment de mémoire associé à ces 3 objets :
Exemple de variables de classe :
class ApplistaticVar
{ public static int x =15 ; } class UtiliseApplistaticVar { int a ; void f( ) { a = ApplistaticVar.x ; ..... } } |
La définition "static int x =15 ;"
crée une variable de la classe ApplistaticVar,
nommée x.
L'instruction "a = ApplistaticVar.x
;" utilise la variable x comme variable de classe ApplistaticVar
sans avoir instancié un objet de cette classe. |
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 |
class Exemple {public static int x ; int y ; public void f1(int a) { x = a; y = a; } public static void g1(int a) { x = a; } } class Utilise { public static void Main( ) { Exemple obj = new Exemple( ); obj.f1(10); System.Console.WriteLine("<f1(10)>obj.x="+obj.x); obj.g1(50); System.Console.WriteLine("<g1(50)>obj.x="+obj.x); } } |
public void f1(int
a) { x = a; //accès à la variable de classe y = a ; //accès à la variable d'instance } public static void g1(int a) Après exécution on obtient : <f1(10)>obj.x = 10
|
1) - Les méthodes et les variables de classe sont précédées obligatoirement du
mot clef static. Elles jouent un rôle semblable à celui qui est attribué aux variables et aux sous-routines globales dans un langage impératif classique. |
C# | Explications |
class Exemple1 { int a = 5; static int b = 19; void m1( ){...} static void m2( ) {...} } |
La variable a dans int
a = 5; est une variable d'instance.
La variable b dans static int b = 19; est une variable de classe. La méthode m2 dans static void
m2( ) {...} est une méthode de classe. |
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 { public static int b = 19; public static void m2( ) {...} } class UtiliseExemple { Exemple2.b = 53; Exemple2.m2( ); ... } |
Dans la classe Exemple2,
int b est une variable de classe, m2 une méthode de
classe.
La classe UtiliseExemple fait appel à
la méthode m2 directement avec le nom de la classe, il en est de
même avec le 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 ; public int y = 20 ; ... } class Utilise { public static void main(String [] arg) { AppliStatic obj1 = new AppliStatic( ); AppliStatic obj2 = new AppliStatic( ); AppliStatic obj3 = new AppliStatic( ); obj1.y = 100; obj1.x = 101; System.Console.WriteLine("obj1.x="+obj1.x); System.Console.WriteLine("obj1.y="+obj1.y); System.Console.WriteLine("obj2.x="+obj2.x); System.Console.WriteLine("obj2.y="+obj2.y); System.Console.WriteLine("obj3.x="+obj3.x); System.Console.WriteLine("obj3.y="+obj3.y); AppliStatic.x = 99; System.Console.WriteLine(AppliStatic.x="+obj1.x); } } |
Dans la classe AppliStatic
x est une variable de classe, et y une variable d'instance.
La classe Utilise crée 3 objets (obj1,obj2,obj3) de classe AppliStatic. L'instruction obj1.y = 100; est un accès au champ y de l'instance obj1. Ce n'est que le champ x de cet objet qui est modifié,les champs x des objets obj2 et obj3 restent inchangés Il y a deux manières d'accéder à la variable static x, :
Dans les deux cas cette variable x est modifiée globalement et donc tous les champs x des 2 autres objets, obj2 et obj3 prennent la nouvelle valeur. |
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
{ static int x = -58 ; int y = 20 ; void f1(int a)
class Utilise public static void Main( )
{ obj1.x = 101; |
Nous reprenons l'exemple
précédent en ajoutant à la classe AppliStatic une méthode
interne f1 : void f1(int a) { AppliStatic.x = a; y = 6 ; } Cette méthode accède à la variable de classe comme un champ d'objet. Nous rajoutons à la classe Utilise,
un méthode static (méthode de classe) notée f2:
Nous avons donc quatre manières d'accéder à la variable static x, :
|
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.