2.3. C# polymorphisme de méthode 



Plan général:   ...........retour au plan général

1. Le polymophisme de méthodes en C#

    Rappel des notions de base

    1.1 Surcharge et redéfinition en C#

    1.2 Liaison statique et masquage en C#
    1.3 Liaison dynamique en C#
    1.4 Comment opère le compilateur

    Résumé pratique du polymorphisme en C#


2. Accès à la super classe en C#

2.1 Le mot clef base
2.2 Initialiseur de constructeur this et base
2.3 Comparaison C#, Delphi et Java sur un exemple
2.4 Traitement d'un exercice complet



1. Le polymorphisme de méthode en C#

Rappel essentiel sur les notions de bases

Nous avons vu au chapitre précédent le polymorphisme d'objet, les méthodes peuvent être elles aussi polymorphes. Nous avons vu comment Delphi mettait en oeuvre le polymorphisme d'objet et de méthode, nous voyons ici comment C# hérite une bonne part de ses comportements de Delphi et de la souplesse de Java pour l'écriture.


Polymorphisme par héritage de méthode 


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 substituées pour obtenir des implémentations différentes.

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 et redéfinition

En informatique ce vocable s'applique aux méthodes selon leur degré d'adaptabilité,  nous distinguons alors deux dénominations : 

 


A- Surcharge


La surcharge de méthode (polymorphisme statique de méthode) est une fonctionnalité classique des langages très évolués et en particulier des langages orientés objet; elle consiste dans le fait qu'une classe peut disposer de plusieurs méthodes ayant le même nom, mais avec des paramètres formels différents ou éventuellement un type de retour différent. On dit alors que ces méthodes n'ont pas la même signature.

On appelle signature d'une méthode 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 )
    { a = b;    }

    public Un (float b )
    { a = (int)b;    } 

    public Un (int x , float y ) : this(y)
    { a += 100;    }
 }

Mais cette surcharge est possible aussi pour n'importe quelle méthode de la classe autre que le constructeur. Ci-dessous deux exemplaires surchargés de la méthode ouvrir dans la classe PortesEtFenetres :



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 :
Le polymorphisme statique (ou surcharge) de C# est syntaxiquement semblable à celui de Java.

 
Programme C# exécutable Explications
class Un
{ int a;
  public Un (int b ) 
  { a = b; }
  void f ( )
  { a *=10;  }
  void f ( int x )
  { a +=10*x; }
  int f ( int x, char y )
  { a = x+(int)y;
     return a;  }
}
class AppliSurcharge
{
   public static void main(String [ ] arg) {
   Un obj = new Un(15);
   System.Console.WriteLine("<création> a ="+obj.a);
   obj.f( );
   System.Console.WriteLine("<obj.f()> a ="+obj.a);
   obj.f(2);
   System.Console.WriteLine("<obj.f()> a ="+obj.a);
   obj.f(50,'a');
   System.Console.WriteLine("<obj.f()> a ="+obj.a);
   }
}
La méthode f de la classe Un est surchargée trois fois : 

void f ( )
  { a *=10;  }

void f ( int x )
  { a +=10*x; }

int f ( int x, char y )
  { a = x+(int)y;
     return a;  }

La méthode f de la classe Un peut donc être appelée par un objet instancié de cette classe sous l'une quelconque des trois formes :

obj.f( ); pas de paramètre => choix : void f ( )

obj.f(2); paramètre int => choix : void f ( int x )

obj.f(50,'a'); deux paramètres, un int un char => choix : int f ( int x, char y )

 

Comparaison Delphi - C# sur la surcharge :
Delphi C#
 Un = class
    a : integer;
  public
    constructor methode( b : integer );
    procedure f;overload;
    procedure f(x:integer);overload;
    function f(x:integer;y:char):integer;overload;
 end;

implementation

constructor Un.methode( b : integer ); begin
  a:=b
end;

procedure Un.f; begin
  a:=a*10;
end;

procedure Un.f(x:integer); begin
  a:=a+10*x
end;
function Un.f(x:integer;y:char):integer; begin
  a:=x+ord(y);
  result:= a
end;
 

procedure Main; 
var obj:Un;
begin
  obj:=Un.methode(15);
  obj.f;
  Memo1.Lines.Add('obj.f='+inttostr(obj.a));
  obj.f(2);
  Memo1.Lines.Add('obj.f(2)='+inttostr(obj.a));
  obj.f(50,'a');
 Memo1.Lines.Add('obj.f(50,''a'')='+inttostr(obj.a));
end;

class Un
{
   int a;
  public Un (int b ) 
  { a = b;
   }
 
 
public void f ( )
  { a *=10; 
   
}
  public void f ( int x )
  { a +=10*x;
   }

  public int f ( int x, char y )
  { a = x+(int)y;
     return a; 
   }

}
 

 

class AppliSurcharge
{
   public static void Main(String [ ] arg) {
   Un obj = new Un(15);
   System.Console.WriteLine("<création> a ="+obj.a);
   obj.f( );
   System.Console.WriteLine("<obj.f()> a ="+obj.a);
   obj.f(2);
   System.Console.WriteLine("<obj.f()> a ="+obj.a);
   obj.f(50,'a');
   System.Console.WriteLine("<obj.f()> a ="+obj.a);
   }
}
 
 
 


B- 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 :



Redéfinition et répartition des méthodes en C#

La redéfinition de méthode peut être selon les langages :
  • précoce
et/ou
  • tardive

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 mot 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.


1.2 Liaison statique et masquage en C#

Toute méthode C# qui n'est précédée d'aucun des deux qualificateurs virtual ou override est à liaison statique.

Le compilateur détermine l'adresse exacte de la méthode et lie la méthode au moment de la compilation.

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.

Comparaison masquage en Delphi et C# :
Delphi C#
type
 ClasseMere = class 
   x : integer;
   procedure f (a:integer);
 end;

ClasseFille = class ( ClasseMere )
  y : integer;
   procedure f (a:integer);//masquage
 end;

implementation

procedure ClasseMere.f (a:integer); begin...
end;

procedure ClasseFille.f (a:integer); begin...
end;
 

public class ClasseMere
{
  int x = 10;

  public  void f ( int a) 
  { x +=a; }
 
}


public class ClasseFille : ClasseMere
{
   int y = 20;
  public  void f ( int a) //masquage
  { x +=a*10+y; }
 
}
 

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.

Dans l'exemple précédent nous instancions la variable ClasseMere  M en un objet de classe ClasseFille (polymorphisme implicite d'objet) soient les instructions

ClasseMere  M ;
M = new ClasseFille ( ) ;

Une erreur courante  est de croire que dans ces conditions, dans l'instruction M.meth(10) c'est la méthode meth(int a) de la classe ClasseFille (en particulier si l'on ne connaît que Java qui ne pratique pas le masquage) :


Que fait alors le compilateur C# dans ce cas ? : il réalise une liaison statique

  • Lors de la compilation de l'instruction M.meth(10), c'est le code de la méthode meth(int a) de la classe ClasseMere qui  est lié, car la référence M a été déclarée de type ClasseMere et peu importe dans quelle classe elle a été instanciée ( avec comme paramètre par valeur 10; ce qui donnera la valeur 11 au champ x de l'objet M).
  • Lors de la compilation de l'instruction F.meth(10), c'est le code de la méthode meth de la classe ClasseFille comme dans l'exemple précédent (avec comme paramètre par valeur 10; ce qui donnera la valeur 101 au champ x de l'objet F).


Voici la bonne configuration de laison effectuée lors de la compilation :



Afin que le programmeur soit bien conscient d'un effet de masquage d'une méthode héritée par une méthode locale, le compilateur C# envoie, comme le compilateur Delphi, un message d'avertissement indiquant une possibilité de manque de cohérence sémantique ou un masquage

S'il s'agit d'un masquage voulu, le petit plus apporté par le langage C# est la proposition que vous fait le compilateur de l'utilisation optionnelle du mot clef new qualifiant la nouvelle méthode masquant la méthode parent. Cette écriture améliore la lisibilité du programme et permet de se rendre compte que l'on travaille avec une liaison statique. Ci-dessous deux écritures équivalentes du masquage de la méthode meth de la classe  ClasseMere :

masquage C#  masquage C# avec new
public class ClasseMere
{
  int x = 10;
  public  void meth ( int a) //liaison statique
  { x +=a; }
}

public class ClasseFille : ClasseMere
{
  public void meth ( int a) //masquage
  { x +=a*10+y; }
}
public class ClasseMere
{
  int x = 10;
  public  void meth ( int a) //liaison statique
  { x +=a; }
}

public class ClasseFille : ClasseMere
{
  public new void meth ( int a) //masquage
  { x +=a*10+y; }
}


L'exemple ci-dessous récapitule les notions de masquage et de surcharge en C# :

public class ClasseMere {
  int x = 1;
  public  void meth1 ( int a) {  x += a ; }
  public  void meth1 ( int a , int b) {  x += a*b ; }
}

public class ClasseFille : ClasseMere {
  public  new void meth1 ( int a) {  x += a*100 ; }
  public  void meth1 ( int a , int b , int c) {  x += a*b*c ; }
}


public class UtiliseMereFille {
 public  static void Main (string [ ] args) {
  ClasseMere M ;
  ClasseFille F ;
  M = new ClasseFille ( ) ;
  F = new ClasseFille ( ) ;
  M.meth1 (10) ; <--- meth1(int a) de ClasseMere
  M.meth1 (10,5) ; <--- meth1(int a, int b) de ClasseMere
  M.meth1 (10,5,2) ; <--- erreur! n'existe pas dans ClasseMere .
  F.meth1 (10) ; <--- meth1(int a) de ClasseFille
  F.meth1 (10,5) ; <--- meth1(int a, int b) de ClasseFille
  F.meth1 (10,5,2) ; <--- meth1(int a, int b, int c) de ClasseFille
}


1.3 Liaison dynamique (ou redéfinition) en C#

Dans l'exemple ci-dessous la classe ClasseFille qui hérite de la classe ClasseMere, redéfini la méthode f de sa classe mère :

Comparaison redéfinition Delphi et C# :
Delphi C#
type
 ClasseMere = class 
   x : integer;
   procedure f (a:integer);virtual;//autorisation
   procedure g(a,b:integer);
 end;

ClasseFille = class ( ClasseMere )
  y : integer;
   procedure f (a:integer);override;//redéfinition
   procedure g1(a,b:integer);
 end;

implementation

procedure ClasseMere.f (a:integer); begin...
end;
procedure ClasseMere.g(a,b:integer); begin...
end;
procedure ClasseFille.f (a:integer); begin...
end;
procedure ClasseFille.g1(a,b:integer); begin...
end;
 

class ClasseMere
{
  int x = 10;

  public virtual void f ( int a) 
  { x +=a; }
  void g ( int a, int b) 
  { x +=a*b; }
}

class ClasseFille extends ClasseMere
{
   int y = 20;
  public override void f ( int a) //redéfinition
  { x +=a; }
  void g1 (int a, int b) //nouvelle méthode
  { ...... }
}
 
 
 
 

 


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;

  public virtual void f ( int a)
  { x +=a; }
  public virtual void g ( int a, int b) 
  { x +=a*b; }
}

class ClasseFille : ClasseMere
{
   int y = 20;
  public override void f ( int a) //redéfinition
  { x +=a; }
  public virtual void g (char b) //surcharge de g
  { x +=b*y; }
}


1.4 Comment opère le compilateur C#

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 :
 


Soit à partir de l'exemple l'exemple précédent  les instructions suivantes :
ClasseFille obj = new ClasseFille( );
obj.g(-3,8);
obj.g('h');

Le compilateur Java applique la démarche d'analyse décrite, à l'instruction "obj.g(-3,8);". Ne trouvant pas dans ClasseFille de méthode ayant la bonne signature (signature = deux entiers) , le compilateur remonte dans la classe mère ClasseMere et trouve une méthode " void g ( int a, int b) " de la classe ClasseMere ayant la bonne signature (signature = deux entiers), il procède alors à l'appel de cette méthode sur les paramètres effectifs (-3,8).

Dans le cas de l'instruction obj.g('h'); , le compilateur trouve immédiatement dans ClasseFille la méthode " void g (char b) " ayant la bonne signature, c'est donc elle qui est appelée sur le paramètre effectif 'h'.

Le compilateur consulte les méta-données (informations de description) de l'assemblage en cours ( applicationXXX.exe ), plus particulièrement les métadonnées de type qui sont stockées au fur et à mesure dans de nombreuses tables.

Nous figurons ci-dessous deux tables de définition importantes relativement au polymorphisme de méthode MethodDef et TypeDef utilisées par le compilateur.


 


Résumé pratique sur le polymorphisme en C#


La surcharge (polymorphisme statique) consiste à proposer différentes signatures de la même méthode.
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).
Le masquage  ne se produit que dans l'héritage d'une classe, par redéfinition (liaison statique) de la méthode mère par une méthode fille (ayant la même signature).
Toute méthode est considérée à liaison statique sauf si vous la déclarez autrement. 



2. Accès à la super classe en C#


2.1 Le mot clef ' base '

Nous venons de voir que le compilateur s'arrête dès qu'il trouve une méthode ayant la bonne signature dans la hiérarchie des classes, il est des cas où nous voudrions accéder à une méthode de la classe mère alors que celle-ci est redéfinie dans la classe fille. C'est un problème analogue à l'utilisation du this lors du masquage d'un attribut.

classe mère
classe fille
class  ClasseA
{
  public 
int  attrA ;
  private 
int  attrXA ;

  public  void 
meth01 ( )  {
    
attrA = 57 ;
  }
}

class  ClasseB : ClasseA
{
  public new  void 
meth01 ( )  {
    
attrA = 1000 ;
  }
  public void 
meth02 ( )  {
    
meth01 ( );
  }
}

La méthode meth02 ( ) invoque la méthode meth01 ( ) de la classe ClasseB. Il est impossible directement de faire appel à la méthode meth01 ( ) de la classe mère ClasseA car celle-ci est masquée dans la classe fille.



Il existe en C# un mécanisme déclenché par un mot clef qui permet d'accéder à la classe mère (classe immédiatement au dessus): ce mot est base.

Le mot clef base est utilisé pour accéder à tous les membres visibles de la classe mère à partir d'une classe fille dérivée directement de cette classe mère ( la super-classe en Java). 

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 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.


2.2 Initialiseur de constructeur this et base


Semblablement à Delphi et à Java, tous les constructeurs d'instance C# autorisent l'appel d'un autre constructeur d'instance immédiatement avant le corps du constructeur, cet appel est dénommé l'initialiseur du constructeur, en Delphi cet appel doit être explicite, en C# et en Java cet appel peut être implicite.

Rappelons que comme en Java où dans toute classe ne contenant aucun constructeur, en C# un constructeur sans paramètres par défaut est implicitement défini :

vous écrivez votre code comme ceci :
il est complété implicitement ainsi :

class  ClasseA {

  public 
int  attrA ;
  
public  string  attrStrA ;

}

class  ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

   public ClasseA ( ) {
   
}

}


Remarque :
Lors de l'héritage d'une classe fille, différement à 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 exemple une classe ClasseA possédant 2  constructeurs :

class ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

  public  
ClasseA
( )  {  /* premier constructeur */
    
attrA = 57 ;
  }
 public  ClasseA string  s )  {  /* second constructeur */
     attrStrA = s +"...1..." ;
  }
}

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 :

Initialiseur implicite
Initialiseur explicite équivalent

class   ClasseB : ClasseA {

  /* premier constructeur */
  public  ClasseB ( )  {  
     
attrStrA = "..." ;
  }

 /* second constructeur */ 
  public  
ClasseB
string  s )  {
     attrStrA = s ;
  }
}

class  ClasseB : ClasseA {

  /* premier constructeur */
  public  ClasseB ( )  : base( )  {  
     
attrStrA = "..." ;
  }

 /* second constructeur */ 
  public  
ClasseB
string  s ) : base( ) {
     attrStrA = s ;
  }
}

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 */ >>

Remarque :
De même pour une classe fille,  C# comme Java, tout constructeur de la classe fille appelle implicitement et automatiquement le constructeur par défaut (celui sans paramètres) de la classe mère.


Exemple  :

vous écrivez votre code comme ceci :
il est complété implicitement ainsi :

class  ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;
}

class ClasseB : ClasseA {

}

Le constructeur de ClasseA sans paramètres est implicitement déclaré par le compilateur.

class  ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

   public ClasseA ( ) {
   
}
}
class ClasseB : ClasseA {
   public ClasseB ( ): base( ) {
  }
}

Si la classe mère ne possède pas de constructeur par défaut, le compilateur engendre un message d'erreur :

vous écrivez votre code comme ceci :
il est complété implicitement ainsi :

class ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

  public  ClasseA ( int  a ) {
  }

class ClasseB : ClasseA {
  public  ClasseB ( ) {

     //.....
  }
}

La classe de base ClasseA ne comporte qu'un seul constructeur explicite à un paramètre. Le constructeur sans paramètres n'existe que si vous le déclarez explicitement, ou bien si la classe ne possède pas de constructeur explicitement déclaré.

class ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;
   
public  ClasseA ( int  a ) {
   }
}

class ClasseB : ClasseA {
   public ClasseB ( ): base( ) {

    // ....
 
}
}

L'initialiseur implicite base( ) renvoie le compilateur chercher dans la classe de base un constructeur sans paramètres.

Or il n'existe pas dans la classe de base (ClasseA) de 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'

Remarques :
Donc sans initialiseur explicite, tout objet de classe fille ClasseB est à minima et par défaut, instancié comme un objet de classe de base ClasseA.

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 n° du constructeur invoqué ainsi que sa classe  :

class ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

  public  
ClasseA
( )  {  /* premier constructeur */
    
attrA = 57 ;
  }
  public  ClasseA string  s )  {  /* second constructeur */
     attrStrA = s +"...classeA1..." ;
  }
}

Ci-dessous la ClasseB écrite de deux façons équivalentes :

avec initialiseurs implicites-explicites
avec initialiseurs explicites équivalents

class ClasseB : ClasseA {

 
 /* premier constructeur */
  public  ClasseB ( )  {
    
attrA = 100+attrA  ;               
  }

 /* second constructeur */
 public  
ClasseB
string  s )  {
     attrStrA = attrStrA +s+"...classeB2..." ;
  }

/* troisième constructeur */
 public  
ClasseB
( int x , string  ch ) : this( ch )  {
     attrStrA = attrStrA+"...classeB3..."  ;
  }


/* quatrième constructeur */
 public  
ClasseB
( char x , string  ch ) : base( ch )  {
     attrStrA = attrStrA+"...classeB4..."  ;
  }

}

class ClasseB : ClasseA {

 
 /* premier constructeur */
  public  ClasseB ( ) : base( )  {
    
attrA = 100+attrA  ;           
  }

 /* second constructeur */
 public  
ClasseB
string  s ): base( )   {
     attrStrA = attrStrA +s+"...classeB2..." ;
  }

/* troisième constructeur */
 public  
ClasseA
( int x , string  ch ) : this( ch )  {
     attrStrA = attrStrA+"...classeB3..."  ;
  }


/* quatrième constructeur */
 public  
ClasseB
char x , string  ch ) : base( ch )  {
     attrStrA = attrStrA+"...classeB4..."  ;
  }

}

Créons quatre objet 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 ( )  {
    
attrA = 100+attrA  ;               
}


ClasseB ObjetB= new ClasseB( );
System.Console.WriteLine(ObjetB.attrA);
C# sélectionne la signature du premier constructeur de la ClasseB (le constructeur sans paramètres).

C# appelle d'abord implicitement le constructeur sans paramètre de la classe mère ( : base( ) )

public  ClasseA ( )  {
    
attrA = 57 ;
  }

Le champ attrA vaut 57,

puis C# exécute le corps du constructeur :
attrA = 100+attrA  ;

attrA vaut 100+57 = 157
public  ClasseB string  s )  {
     attrStrA = attrStrA +s+"...classeB2..." ;
}


public  
ClasseB
( int x , string  ch ) : this( ch )  {
     attrStrA = attrStrA+"...classeB3..."  ;
}


ObjetB= new ClasseB(x,"aaa");
System.Console.WriteLine(ObjetB.attrStrA);
C# sélectionne la signature du troisième constructeur de la ClasseB (le constructeur avec paramètres : int x , string  ch).

C# appelle d'abord explicitement le constructeur local de la classeB avec un paramètre de type string ( le second constructeur de la ClasseB )

s = "aaa" ;
public  ClasseB string  s )  {
     attrStrA = attrStrA +s+"...classeB2..." ;
}

Le champ attrStrA vaut "aaa...classeB2...",

puis C# exécute le corps du constructeur :
attrStrA = attrStrA+"...classeB3..."  ;

attrStrA  vaut "aaa...classeB2......classeB3..."
public  ClasseB ( char x , string  ch ) : base( ch )  {
     attrStrA = attrStrA+"...classeB4..."  ;
}


ObjetB= new ClasseB((char)x,"bbb");
System.Console.WriteLine(ObjetB.attrStrA);
C# sélectionne la signature du quatrième constructeur de la ClasseB (le constructeur avec paramètres : char x , string  ch).

C# appelle d'abord explicitement le constructeur de la classe mère (de base) classeA avec un paramètre de type string ( ici le second constructeur de la ClasseA )

s = "bbb" ;
public  ClasseA string  s )  {
     attrStrA = s +"...classeA1..." ;
}

Le champ attrStrA vaut "bbb...classeA1..."

puis C# exécute le corps du constructeur :
attrStrA = attrStrA+"...classeB4..."  ; 

attrStrA  vaut "bbb...classeA1......classeB4..."

La dernière instanciation : ObjetB= new ClasseB("ccc"); est strictement identique à la première mais avec appel au second constructeur.


2.3 Comparaison de construction C#, Delphi et Java


Exemple classe mère :
C# Java
class ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

  public  
ClasseA
( )  {  
    
attrA = 57 ;
  }
  public  ClasseA string  s )  {
     attrStrA = s +"...classeA1..." ;
  }
}
class ClasseA {
  public 
int  attrA ;
  
public  String  attrStrA = "" ;

  public  
ClasseA
( )  {  
    
attrA = 57 ;
  }
  public  ClasseA ( String  s )  {
     attrStrA = s +"...classeA1..." ;
  }
}


C#
Delphi
class ClasseA {
  public 
int  attrA ;
  
public  string  attrStrA ;

  public  
ClasseA
( )  {  
    
attrA = 57 ;
  }
  public  ClasseA string  s )  {
     attrStrA = s +"...classeA1..." ;
  }
}

  ClasseA  = class
  public 
     attrA : integer ;
   
attrStrA: string   ;
   
constructor  Creer;overload;
    constructor  Creer(s:string); overload;
 end;
 
constructor  
ClasseA.Creer
 begin  
    
attrA := 57 ;
 end;
constructor  ClasseA.Creer(s:string);
 begin  
    
 attrStrA := s +'...classeA1...' ;
 end;

Exemple classe fille :
C# Java

class ClasseB : ClasseA {

 /* premier constructeur */
  public  ClasseB ( )  {
    
attrA = 100+attrA  ;               
  }


 /* second constructeur */
 public  
ClasseB
string  s )  {
     attrStrA = attrStrA +s+"...classeB2..." ;
  }


/* troisième constructeur */
 public  
ClasseB
( int x , string  ch ) : this( ch )  {
     attrStrA = attrStrA+"...classeB3..."  ;
  }



/* quatrième constructeur */
 public  
ClasseB
( char x , string  ch ) : base( ch )  {
     attrStrA = attrStrA+"...classeB4..."  ;
  }


}

class ClasseB extends ClasseA {

 
 /* premier constructeur */
  public  ClasseB ( )  {
    
super( )  ;
    attrA
= 100+attrA  ;               
  }

 /* second constructeur */
 public  
ClasseB
( String  s )  {
    super( )  ;
    attrStrA = attrStrA +s+"...classeB2..." ;
  }

/* troisième constructeur */
 public  
ClasseB
( int x , String  ch )   {
    this( ch ) ;
    attrStrA = attrStrA+"...classeB3..."  ;
  }


/* quatrième constructeur */
 public  
ClasseB
( char x , String  ch )   {
   super( ch )  ;
   attrStrA
= attrStrA+"...classeB4..."  ;
  }

}


C#
Delphi

class ClasseB : ClasseA
{

 

/* premier constructeur */
 public  ClasseB ( ) 
{

    
attrA = 100+attrA  ;               
}


 /* second constructeur */
 public  
ClasseB
string  s )
 {

     attrStrA = attrStrA +s+"...classeB2..." ;
  }


/* troisième constructeur */
 public  
ClasseB
( int x , string  ch ) : this( ch ) 
{

     attrStrA = attrStrA+"...classeB3..."  ;
 }



/* quatrième constructeur */
 public  
ClasseB
( char x , string  ch ) : base( ch ) 
 {

     attrStrA = attrStrA+"...classeB4..."  ;
 }


}
 ClasseB  = classClasseA )
 public 
  constructor  Creer;overload;
  constructor  Creer(s:string); overload;
  constructor  Creer(x:integer;ch:string); overload;
  constructor  Creer(x:char;ch:string); overload;
 end;
 /* premier constructeur */ 
constructor  
ClasseB.Creer;
 begin  
    inherited ;

   attrA := 100+attrA  ;
 end;

 /* second constructeur */
constructor  ClasseB.Creer(s:string);
 begin  
   
inherited Creer ;  
   attrStrA
:= attrStrA +s+'...classeB2...' ;
 end;


/* troisième constructeur */
constructor  ClasseB.Creer(x:integer;ch:string);
begin  
  
Creer( ch ) ;  
   attrStrA := attrStrA+'...classeB3...'  ;

 end;


/* quatrième constructeur */
constructor  ClasseB.Creer(x:integer;ch:string);
begin  
  
inherited  Creer( ch ) ;  
   attrStrA := attrStrA+'...classeB4...'  ;

 end;



2.4 Traitement d'un exercice complet

soit une hiérarchie de classe de véhicules :

syntaxe de base :

class Vehicule {

}

class Terrestre :Vehicule {

}

class Marin :Vehicule {

}

class Voiture : Terrestre {

}

class Voilier : Marin {

}

class Croiseur : Marin {

}

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 futurs 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#

Une implémentation de la classe Voiture avec des méthodes non virtuelles (Version-1) :

abstract class Vehicule  {
        public abstract void Demarrer( );
        public void RépartirPassagers( ){}
        public void PériodicitéMaintenance( ){}
}

La méthode Démarrer de la classe Vehicule est abstraite et donc automatiquement virtuelle (à liaison dynamique).

Les méthodesRépartirPassagers et PériodicitéMaintenance sont concrètes mais avec un corps vide.

Ces deux méthodes sont non virtuelles (à liaison statique)

abstract class Terrestre : Vehicule  {
        public new void RépartirPassagers( ){
       //...}
        public new void PériodicitéMaintenance( ){
        //...}
}


La classe Terrestre est abstraite car elle n'implémente pas la méthode abstraite Démarrer.

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  {
        public override void  Demarrer( ){
        //...}
}  
La classe Voiture est la seule à être instanciable car toutes 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.

La même implémentation de la classe Voiture avec des méthodes virtuelles (Version-2):

abstract class Vehicule  {
        public abstract void Demarrer( );
        public virtual void RépartirPassagers( ){}
        public virtual void PériodicitéMaintenance( ){}
}

La méthode Démarrer de la classe Vehicule est abstraite et donc automatiquement virtuelle (à liaison dynamique).

Les méthodesRépartirPassagers et PériodicitéMaintenance sont concrètes mais avec un corps vide.

Ces deux méthodes sont  maintenant virtuelles (à liaison dynamique)

abstract class Terrestre : Vehicule  {
        public override void RépartirPassagers( ){
       //...}
        public override void PériodicitéMaintenance( ){
        //...}
}


La classe Terrestre est abstraite car elle n'implémente pas la méthode abstraite Démarrer.

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  {
        public override void  Demarrer( ){
        //...}
}  
La classe Voiture est la seule à être instanciable car toutes 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.

Supposons que les méthodes non virtuelles RépartirPassagers et PériodicitéMaintenance sont implantées complètement dans la classe Vehicule, puis reprenons la classe Terrestre en masquant ces deux méthodes :

abstract class Vehicule  {
        public abstract void Demarrer( );
        public void RépartirPassagers( ){
        //....}
        public void PériodicitéMaintenance( ){
        //....}
}

abstract class Terrestre : Vehicule  {
        public new void RépartirPassagers( ){
       //...}
        public new void PériodicitéMaintenance( ){
        //...}
}

Question 

Nous voulons qu'un véhicule Terrestre répartisse ses passagers ainsi :
1°) d'abord comme tous les objets de classe Vehicule,
2°) ensuite qu'il rajoute un comportement qui lui est propre

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.