Le langage Delphi pour .NET


précédentsommaire

19. Classes

19-1. IL Dasm

IL Dasm est un désassembleur de code IL (Common Intermediate Language). Il permet, entre autre, d'analyser le code généré par le compilateur Delphi .NET. Vous trouverez des informations supplémentaires dans la documentation fournie avec le SDK.
Si vous exécutez dans une console le script Sdkvars.cmd, la variable d'environnement %NetSamplePath% pointera sur le répertoire d'installation du SDK :

 
Sélectionnez

%systemDrive%
cd %NetSamplePath%Tool Developers Guide\docs

Il est possible d'ouvrir un assemblage dans Delphi mais les informations disponibles sont moins détaillées. Cf. menu 'Ouvrir' et menu 'Outils'-'Reflection'.
Dans Delphi, la configuration des entrées du menu Outils est mémorisée dans la clé de registre :
HKEY_CURRENT_USER\Software\Borland\BDS\3.0\Transfer

19-2. Nouveaux spécificateurs de visibilité pour les membres de classe

Projet : ..\Visibilite-Membre\ControleAcces

Delphi pour .NET ajoute 2 spécificateurs de visibilité supplémentaires strict private (privée stricte) et strict protected (protégée stricte) conformes aux spécifications de la CLS de .NET. Ils sont ordonnés ainsi, bien que dans une déclaration de classe cette notion d'ordre ne soit effective :

  • strict private
  • private
  • strict protected
  • protected
  • public

Texte issu de la documentation de DELPHI 2005
Les membres de classes dont la visibilité est strict private ne sont accessibles que dans la classe dans laquelle ils sont déclarés. Ils ne sont pas visibles pour les procédures ou fonctions déclarées dans la même unité.

Les membres de classes dont la visibilité est strict protected sont visibles dans la classe dans laquelle ils sont déclarés et dans toutes les classes dérivées, quel que soit l'endroit où elles sont déclarées.
Le spécificateur de visibilité private traditionnel de Delphi correspond à la visibilité assemblage du CLR. Le spécificateur de visibilité protected de Delphi correspond à la visibilité assemblage ou famille du CLR.

Remarque : Le mot strict est traité comme une directive dans le contexte d'une déclaration de classe. Dans une déclaration de classe, vous ne pouvez pas déclarer un membre nommé strict, mais vous pouvez l'utiliser à l'extérieur d'une déclaration de classe.

Rappel :
Les membres au début de la déclaration d'une classe dont la visibilité n'est pas spécifiée sont par défaut public.

 
Sélectionnez

  TMaClasse = class

  strict private
     // Visible seulement par les instances de la classe MaClasse
    FStrictPrivate: Integer;

  private
     // Visible seulement par les instances de la classe MaClasse ou de l'unité
    Fprivate: Integer;

  strict protected
     // Visible seulement par les instances de la classe MaClasse ou des classes dérivées
    FStrictProtected: Integer;

  protected
     // Visible seulement par les instances de la classe MaClasse, des classes dérivées ou de l'unité
    FProtected: Integer;

  public
     // Visible par tout code qui dispose d'un accès global à cette classe.
    FVisible: Integer;

  published
     // Rend publique cette partie et génère également les informations de types à l'exécution.
     // Entre autres rôles, les informations de types à l'exécution permettent à l'inspecteur
     // d'objets d'accéder aux propriétés et aux événements.
    property Visible: Integer read FVisible;
  end;

Vous pouvez, dans une classe, déclarer un constructeur privé afin d'empêcher la création d'instance de cette classe.

19-3. Champ de classe

Texte issu de la documentation de DELPHI 2005

Les champs de classe sont des champs de données d'une classe accessibles sans une référence d'objet.
La déclaration de bloc class var vous permet d'introduire un bloc de champs de classe dans une déclaration de classe.
Tous les champs déclarés après class var ont des attributs de stockage statique.

Un bloc class var se termine par :

  • Une autre déclaration class var
  • Une déclaration de procédure ou fonction (c'est-à-dire une méthode) (y compris des procédures de classe et des fonctions de classe)
  • Une déclaration de propriété (y compris des propriétés de classe)
  • Une déclaration de constructeur ou de destructeur
  • Un spécificateur de portée de visibilité (public, private, protected, published, strict private et strict protected)

Par exemple :

 
Sélectionnez

   TDBBool = record
    Private
        // Champ privé pour stocker la valeur du type
      FValeur : integer;
      Constructor Create(AValeur: IDBType);
      procedure SetValeur(AValeur: IDBType);

    Public
        // Champs statiques de classe.
      Class var dbFalse: TDBBool;
      Class var dbNull : TDBBool;
      Class var dbTrue : TDBBool;
...

Il est préférable de répéter la déclaration de class var, car dans certains cas vous rencontrerez quelques problèmes :

 
Sélectionnez

 Public 
        // champs de classe, static 
      Class var 
        dbFalse: TDBBool; 
        dbNull : TDBBool; 
        dbTrue : TDBBool; 
      // private 
       FValeur : integer; 

La mise en commentaire de Private provoque un effet de bord.


Le code suivant permet d'accéder aux champs de classe, déclarés ci-dessus, en préfixant le nom du champ avec le nom de la classe propriétaire :

 
Sélectionnez

Begin
  TDBBool.dbNull.FValeur:=cdbNull;
  TDBBool.dbFalse.FValeur:=cdbFalse;
  TDBBool.dbTrue.FValeur:=cdbTrue;
end;

Projet : ..\Classe\VariableClasse\ClassVar

Les variables de classe sont accessibles dans les classes dérivées :

 
Sélectionnez

Type
 TMaClass= Class
   VariableInstance :String;
   class var VariableDeClasse_TMaClass :String;
 end;

 TDescendant= Class(TMaClass)
   class var VariableDeClasse_TDescendant :String;
 end;

var Test          : TMaClass;
    TestDescendant: TDescendant;

begin
 Test.VariableDeClasse_TMaClass:='Test';
 TDescendant.VariableDeClasse_TDescendant:='Descendant';
 TestDescendant.VariableDeClasse_TDescendant:='Autre appel possible';
 
 TDescendant.VariableDeClasse_TMaClass:='Modification';
end.

19-4. Méthode statique de classe

Projet : ..\Classe\MethodeClasse\MethodeClasse

Texte issu de la documentation de DELPHI 2005
Les méthodes statiques de classe sont des méthodes d'une classe accessibles sans une référence d'objet. Elles fonctionnent de la même manière que les méthodes traditionnelles de classe sous Delphi Win32.

Contrairement aux méthodes de classe Delphi Win32, les méthodes statiques de classe n'ont pas de paramètre Self (c'est une exigence du CLR) et ne peuvent donc accéder à aucun membre de classe. En dehors des champs de classe. En outre, contrairement à Delphi Win32, les méthodes statiques de classe ne peuvent pas être déclarées virtuelles (Virtual).

Pour rendre une méthode de classe statique, ajoutez le mot static à sa déclaration, par exemple

 
Sélectionnez

Type
Type
 TMaClass= Class
   VariableInstance :String;
   class var VariableDeClasse :String;
   Class procedure Affiche; static; // Méthode statique de classe 
   Class procedure Affiche2; // Méthode de classe 
   procedure Traitement;
 end;

Class procedure TMaClass.Affiche;
begin
 VariableDeClasse :='Affiche : une seule occurence pour cette variable de classe';

 // La ligne suivante est gérée par le compilateur et n'est pas possible.
 //VariableInstance :='Plusieurs occurences pour cette variable d''instance.';

 Writeln(VariableDeClasse);
 // Writeln(Self.ClassName); // E2003 : Identificateur non déclaré
end;

Class procedure TMaClass.Affiche2;
begin
 //VariableDeClasse :='Affiche2 : une seule occurence pour cette variable de classe';
 Writeln(VariableDeClasse);

 if assigned(Self) // Self existe si une instance existe !?
  then Writeln(Self.ClassName);
end;

procedure TMaClass.Traitement;
begin
  Writeln('Dans la méthode traitement');
end;

Vous pouvez appeler une méthode statique de classe par l'intermédiaire du type de la classe (c'est-à-dire sans référence d'objet), par exemple

 
Sélectionnez

var Test : TMaClass;

begin
  // Appel de méthode de classe.
  //Test:=TMaClass.Create;  // Self accessible dans Affiche2
 Test.Affiche;
 Test.Affiche2;
 Readln;
end.

Les méthodes de classe sans le mot-clé static sont aussi autorisées pour les classes mais pas pour les enregistrements (Record).

19-5. Propriété de classe

Projet : ..\Classe\ProprieteDeClasse\ProprieteDeClasse

Les propriétés de classe sont des propriétés d'une classe accessibles sans une référence d'objet.
Une propriété de classe ne peut pas être publiée et ne peut pas avoir de définition de valeur stockée ou par défaut. De plus le champ associé à la propriété de classe doit être au minimum un champ de classe et les possibles accesseurs de propriétés de classes doivent être déclarés comme méthodes statiques de classe.
Le non-respect de ces deux règles provoquera l'erreur de compilation suivante :

 
Sélectionnez

E2355 : L'accesseur de la propriété de classe doit être un champ de classe ou une méthode statique de classe.
 
Sélectionnez

type
 TMaClass = class
  strict private
   class var FY : Integer;
   class var FX : Integer;

  strict protected
  // Remarque : les accesseurs des propriétés de classe doivent être déclarés comme méthodes de classe statiques.
   class function GetX: Integer; static;
   class procedure SetX(val: Integer); static;

  public
   class property X: Integer read GetX write SetX;
   class property Y: Integer read FY write FY;
 end;

class function TMaClass.GetX: Integer;
begin
 Result:=FX;
end;

class procedure TMaClass.SetX(val: Integer);
begin
 FX:=val;
end;

L'accés aux propriétés de classe se fait de la maniére suivante :

 
Sélectionnez

begin
 TMaClass.X:=9;
 TMaClass.y:=7;
 Writeln(TMaClass.X.ToString+' '+TMaClass.Y.ToString);
 Readln;
end.

19-6. Constructeur d'instance de type référence

Projet : ..\Classe\Constructeur\Constructeur

Sous Delphi .NET, à la différence du C#, le code du constructeur doit comporter explicitement l'appel du constructeur de la classe ancêtre :

 
Sélectionnez

type
  MaClasse=Class
   UnChamp :Integer;
   Constructor Create(I:Integer);
  end;

Constructor MaClasse.Create(I:Integer);
begin
 UnChamp:=I;
end;

provoque l'erreur de compilation :

 
Sélectionnez

E2304 : 'Self' n'est pas initialisé. Un constructeur hérité doit être appelé.

L'appel doit donc être :

 
Sélectionnez

Constructor MaClasse.Create(I:Integer);
begin
 inherited Create;
 UnChamp:=I;
end;

A la différence de Delphi Win32, il n'est pas possible d'appeler un constructeur en utilisant une référence d'objet :

 
Sélectionnez

 Objet:=MaClasse.Create(10);
 Objet.S:='Modification';
 
 Objet:=Objet.Create(5); // Invalide
 Objet.Create(5); // Invalide

L'appel des deux dernières instructions provoque l'erreur de compilation :

 
Sélectionnez

E2382 : Impossible d'appeler des constructeurs utilisant des variables d'instance.

Les valeurs des différents champs d'une classe sont initialisées avec des zéros.
Dans le cas où vous ne déclareriez pas de constructeur, le compilateur en ajoutera un. Une classe a donc toujours au moins un constructeur Create sans paramètre.

Sous IL Dasm les constructeurs d'instance sont nommés .ctor.

19-6-1. "Constructeur de copie"

Projet : ..\Classe\ConstructeurCopie\ConstructeurCopie

Sous .NET on utilise la méthode MemberwiseClone pour recopier un objet membre par membre dans un autre objet. Cette méthode permet uniquement un clonage partiel, l'interface ICloneable devant être utilisée pour une copie complète. Cette méthode permet donc de créer des instances sans appel de constructeur.
L'utilisation de la méthode MemberwiseClone doit se faire au sein d'une méthode membre d'une classe. Sinon son utilisation directement dans le code provoquera l'erreur suivante:

 
Sélectionnez

M2:=M1.MemberwiseClone as MaClassDerive;
 
Sélectionnez

E2363 : Seules les méthodes des types descendants peuvent accéder au symbole protégé 
[mscorlib]Object.MemberwiseClone au travers des limites d'assemblage.
 
Sélectionnez

type
  MaClass=Class
   public
    age : integer;
    name : string;
  end;

 MaClassDerive =class(MaClass)
  function Clone: MaClassDerive;
 end;


function MaClassDerive.Clone: MaClassDerive;
begin
  Result := MemberwiseClone as MaClassDerive;
end;

var M1,M2,M3 : MaClassDerive;

Begin
 // Crée une instance de MaClassDerive et renseigne ces champs.
 M1:= MaClassDerive.Create;
 M1.age:= 42;
 M1.name:='Sam';

 M3:=M1;

 // Effectue une copie partielle de M1 vers M2.
 M2:=M1.Clone;
 if M2=M3
  then Writeln('M2 et M3 sont des objets identiques.')
  else Writeln('M2 et M3 ne sont pas des objets identiques.');

 readln;
end.

Dans le cas où un objet contiendrait d'autres objets, le clonage partiel copie les références de ces objets et ne crée pas d'objets distincts.
La copie complète quant à elle clone l'objet et tous ses membres. Dans ce cas il y a bien création d'objets distincts.

19-6-2. Création d'objet de la FCL

Pour créer une instance d'une classe de la Framework Class Library on utilise le constructeur conventionnel Create de Delphi.
Par exemple pour appeler le constructeur par défaut de la classe Regex (par défaut en C# le constructeur porte le même nom que la classe) public Regex(string pattern), on utilisera l'appel suivant :

 
Sélectionnez

uses ...
  System.Text.RegularExpressions; // Espace de nommage de la classe RegEx


Var RegExp : System.Text.RegularExpressions.Regex;

Function ValidMail(Adresse : String):Boolean;
begin
  // FCL : Appel du constructor par défaut '.ctor'
 Regexp:= Regex.Create('^([\w]+)@([\w]+)\.([\w]+)$');
... 

Bien que la classe RegEx ne déclare pas de constructeur nommé Create, le code source précédent est traduit en code IL suivant:

 
Sélectionnez

...
  // Appel d'un des .ctor de la classe
 IL_0005:  newobj     instance void [System]System.Text.RegularExpressions.Regex::.ctor(string)
...  

Dans les cas où on souhaite créer un objet de durée de vie très brève, on peut éviter les variables intermédiaires par la construction suivante : Result:=Regex.Create('\d+').Split(ChainePasseeEnParametre);

19-7. Constructeur de classe

Projet : ..\Classe\ConstructeurClasse\ConstructeurClasse

Texte issu de la documentation de DELPHI 2005 :
Un constructeur de classe s'exécute avant le référencement ou l'utilisation d'une classe. Le constructeur de classe est déclaré strict private et il ne peut être appelé dans le code. Il ne peut y avoir qu'un constructeur de classe déclaré dans une classe et il ne peut pas avoir de paramètres. Les descendants peuvent déclarer leurs propres constructeurs de classe, mais ils n'appellent pas inherited dans le corps d'un constructeur de classe. En fait, vous ne pouvez pas appeler directement un constructeur de classe ni y accéder de quelque façon que ce soit (par exemple en prenant son adresse). Le compilateur génère automatiquement le code permettant d'appeler les constructeurs de classe.

Il n'existe aucune garantie quant au moment de l'exécution d'un constructeur de classe ; tout ce que l'on sait, c'est qu'il s'exécute avant l'utilisation de la classe. Pour pouvoir "utiliser" une classe sur la plate-forme .NET, cette classe doit résider dans le code exécuté. Par exemple, si une classe est d'abord référencée dans une instruction if et si le test de l'instruction if ne renvoie jamais la valeur true pendant l'exécution, la classe ne sera jamais chargée ni compilée par le compilateur JIT. Dans ce cas, le constructeur de classe n'est donc pas appelé.

 
Sélectionnez

  MaClasse=Class
   UnChamp : Integer;
   S       : String;
   Constructor Create;
   Class Constructor CreateClass;
   // E2359 : Plusieurs constructeurs de classe dans la classe MaClasse : CreateClass et CreateClass2
   //Class Constructor CreateClass2;
  end;

Constructor MaClasse.Create;
begin
 inherited;
 S:='Initialisation';
 Writeln(#9+#9+#9+'Appel du constructeur d''instance MaClasse.Create');
end;

Class Constructor MaClasse.CreateClass;
begin
 Writeln(#9+#9+'Appel du constructeur de classe MaClasse.CreateClass');

 // Erreurs Possibles 
 // inherited; Erreur de compilation E2075 : Forme d'appel de méthode autorisée seulement dans méthodes de type dérivé.
 // S:='Initialisation'; E2124 : Le membre d'instance 'S' est inaccessible ici.
 //                      Vous essayez de référencer un membre instance depuis une procédure class.
end;

var Objet : MaClasse ;
begin
 Writeln('Début d''exécution du code.');
 //Première référence d'un objet la classe dans le code, pas d'appel du constructeur de classe.
 Writeln(#13#10+'Référence de la classe dans le code : Objet:=Nil');
 Objet:=Nil;

 Writeln('Création d''une instance.');
 Objet:=MaClasse.Create;
 Writeln(#13#10+'Affiche Objet.S = '+Objet.S);

 Readln;
end.

Le code précédent affiche :

 
Sélectionnez

Début d'exécution du code.

Référence de la classe dans le code : Objet:=Nil
Création d'une instance.
                Appel du constructeur de classe MaClasse.CreateClass
                        Appel du constructeur d'instance MaClasse.Create
                        
Affiche Objet.S = Initialisation

L'appel de constructeurs de classe est non-déterministe, vous ne pouvez donc pas supposer qu'un constructeur sera appelé avant un autre.
Par exemple, l'ajout d'une métaclasse modifie l'ordre d'appel du constructeur de classe :

 
Sélectionnez

type
 ...
  TMetaClass= Class of MaClasse; 
...
var Objet : MaClasse ;
    M: TMetaClass;
begin
 Writeln('Début d''exécution du code.');
 //Première référence d'un objet la classe dans le code, pas d'appel du constructeur de classe.
 Writeln(#13#10+'Référence de la classe dans le code : Objet:=Nil');
 Objet:=Nil;

 // Modifie l'ordre d'appel du constructeur de classe
 M:=Maclasse;

 Writeln('Création d''une instance.');
 Objet:=MaClasse.Create;
 Writeln(#13#10+'Affiche Objet.S = '+Objet.S);

 Readln;
end.

Le code précédent affiche :

 
Sélectionnez

                Appel du constructeur de classe MaClasse.CreateClass
Début d'exécution du code.

Référence de la classe dans le code : Objet:=Nil
Création d'une instance.
                        Appel du constructeur d'instance MaClasse.Create

Affiche Objet.S = Initialisation

Vous pouvez utiliser un constructeur de classe pour initialiser les champs statiques (variable de classe) d'une classe.

Sous IL Dasm les constructeurs de classe sont nommés .cctor.

Nous verrons dans la section Record que la gestion des constructeurs de type valeur est légèrement différente.
Pour aller plus loin recherchez 'beforefieldinit' dans les documents suivants :

C:\Program Files\Microsoft.NET\SDK\v1.1\Tool Developers Guide\docs\Partition I Architecture.doc
C:\Program Files\Microsoft.NET\SDK\v1.1\Tool Developers Guide\docs\Partition II Metadata.doc

19-8. Classe scellée (sealed)

Projet : ..\Classe\Sealed\ClasseSealed

Cette nouvelle fonctionnalité permet à Delphi pour .NET de déclarer une classe qui ne peut pas être étendue par le biais de l'héritage. Cela s'applique à tous les langages .NET susceptibles d'utiliser la classe sealed.
Une fois une classe déclarée scellée (sealed) elle ne peut plus être dérivée comme le montre l'exemple suivant :

 
Sélectionnez

program ClasseSealed;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TMaClass= class (TObject)
   Champ1 : integer ;
   procedure FaitqqChose; virtual;
  end;

  TSealedClass= class sealed (TMaClass)
   Champ2 : integer ;
   procedure FaitqqChose; override;
  end;


  TImpossibleClass= class (TSealedClass)
   Champ3 : integer ;
   procedure FaitqqChose; override;
  end;

procedure TmaClass.FaitqqChose;
begin
 Writeln('Champ1 ', Champ1);
end;

procedure TSealedClass.FaitqqChose;
begin
 Writeln('Champ2 ', Champ2);
end;

procedure TImpossibleClass.FaitqqChose;
begin
 Writeln('Champ3 ', Champ3);
end;

var
 MonInstance: TSealedClass;

begin
  MonInstance:=TSealedClass.Create;
end.

L'ajout du mot clé sealed dans la déclaration de classe TSealedClass= class (TMaClass) provoque l'erreur de compilation suivante pour la déclaration de la classe TImpossibleClass :

 
Sélectionnez

E2353 : Impossible d'étendre la classe sealed 'TSealedClass' 

A noter que les enregistrements (Record), de type valeur, sont scellés implicitement.

19-9. Classe abstraite

Projet : ..\Classe\Abstract\ClasseAbstract

Cette nouvelle fonctionnalité permet à Delphi pour .NET de déclarer la totalité d'une classe comme classe abstraite (abstract) ce qui implique que cette classe ne peut pas être instanciée.
La syntaxe de déclaration est la suivante :

 
Sélectionnez

program Abstract;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TAbstractClass= class abstract (TObject)
   ChampExistant : integer ;

   procedure FaitqqChose;
  end;

procedure TAbstractClass.FaitqqChose;
begin
 Writeln('ChampExistant ',ChampExistant);
end;

var
  MonInstance:TAbstractClass ;

begin
  MonInstance:=TAbstractClass.Create; // Impossible !
end.

La compilation de ce code, incluant une création d'une instance de classe abstraite, provoque l'erreur suivante :

 
Sélectionnez

E2402 : Construction de l'instance de la classe abstraite 'TAbstractClass'.

Il n'est pas possible de déclarer une classe abstract sealed :

 
Sélectionnez

type
  TAbstractClass= class abstract sealed (TObject)
   ChampExistant : integer ;

   procedure FaitqqChose;
  end;

Provoque l'erreur suivante :

 
Sélectionnez

E2383 : ABSTRACT et SEALED ne peuvent être utilisés ensemble.

19-10. Méthode final

Projet : ..\Classe\MethodeFinal\MethodeFinal

Cette nouvelle fonctionnalité permet à Delphi pour .NET d'utiliser le concept de méthode virtuelle finale. Quand le mot clé final est appliqué à une méthode virtuelle, aucune classe dérivée ne peut redéfinir cette méthode.
L'usage du mot clé final est une décision de conception importante qui permet de définir l'utilisation de la classe. Il peut aussi donner au compilateur .NET JIT des conseils qui lui permettent d'optimiser le code produit.

 
Sélectionnez

program MethodeFinal;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TMaClass= class (TObject)
   Champ1 : integer ;
   procedure FaitqqChose; virtual;
  end;

  TDescendantClass= class (TMaClass)
   Champ2 : integer ;
   procedure FaitqqChose; override; Final;
  end;

  TClassRedeclareMethodeFinal= class (TDescendantClass)
   Champ3 : integer ;
   procedure FaitqqChose; override;   //Erreur
                                        // E2352 : Impossible de redéfinir une méthode finale 
  end;

procedure TMaClass.FaitqqChose;
begin
 Writeln('Champ1 ',Champ1);
end;

procedure TDescendantClass.FaitqqChose;
begin
 Writeln('Champ2 ',Champ2);
end;

procedure TClassRedeclareMethodeFinal.FaitqqChose;
begin
 Writeln('Champ3 ',Champ3);
end;

var
  MonInstance: TClassRedeclareMethodeFinal;

begin
  MonInstance:=TClassRedeclareMethodeFinal.Create;
end.

L'ajout du mot clé final à la déclaration de la méthode FaitqqChose; override de la classe TDescendantClass provoque l'erreur de compilation suivante pour la déclaration de la classe TClassRedeclareMethodeFinal :

 
Sélectionnez

E2352 : Impossible de redéfinir une méthode finale.

Projet : ..\Methode Virtual et dynamic\MethodeVirtualDynamic

Sous Delphi Win32, la seule différence entre Virtual et Dynamic est l'optimisation du code, sous .NET cette différence n'existe plus. Vous pouvez le vérifier en contrôlant sous IL DASM le code généré du projet. Il semble donc préférable de ne pas utiliser Dynamic sous .NET.
Je remercie Nono40 pour cette information.

19-11. Méthode imbriquée

Projet : ..\Procedure_Imbriquee\Procedure_imbriquee

Sous Delphi il est possible de déclarer des procédures et/ou fonctions imbriquées. Dans ce cas ces procédures doivent avoir accès à tous les paramètres passés à la fonction externe aussi bien qu'à toutes les fonctions locales définies dans la fonction externe. En code natif, ceci est mis en oeuvre en utilisant les cadres de pile (Stack frames). Les cadres de pile ne sont pas disponibles sous .NET, c'est pourquoi une autre manière est utilisée pour passer des paramètres de et vers des fonctions imbriquées.

Puisque le CLR de .NET ne supporte pas les fonctions imbriquées, le compilateur traduit et compile la fonction imbriquée comme une méthode privée de la classe courante, mais il déforme son nom. Il ajoute "@", puis le numéro de la fonction imbriquée, suivi de "$" et du nom de la fonction externe, puis un autre "$" et enfin le nom de la fonction imbriquée.

 
Sélectionnez

uses
  SysUtils;

function Principale(Value: Integer): Integer;
var
  I: Integer;

  procedure Imbriquee;
  begin
    I := Value + 100;
  end;

begin
  I:=10;
  Imbriquee;
  Result := I;
end;

begin
 Principale(10);
end.
Image non disponible
Résultat de la compilation d'une procédure imbriquée

Le nom de la procédure imbriquée nommée Imbriquee est transformé en @1$Principale$Imbriquee.

Puisque la fonction imbriquée est compilée en tant que méthode privée, le compilateur doit créer un enregistrement et le passer à la fonction imbriquée. Le compilateur générera un record nommé $Unnamed et ajoutera le numéro de la fonction imbriquée. L'enregistrement contiendra toutes les variables qui devront être accessibles à la fonction imbriquée.
Cet enregistrement est passé par référence à la fonction imbriquée et le compilateur doit résoudre tous les symboles référencés dans la fonction imbriquée en utilisant les champs du record.

 
Sélectionnez

procedure Procedure_imbriquee.@1$Principale$Imbriquee([In] var $frame_Principale: $Unnamed2);
begin
 $frame_Principale.I := ($frame_Principale.Value + 100)
end;

function Procedure_imbriquee.Principale(Value: Integer): Integer;
var unnamed1: $Unnamed2;
begin
 unnamed1.Value := Value;
 unnamed1.I := 10;
 Procedure_imbriquee.@1$Principale$Imbriquee(@(unnamed1));
 result := unnamed1.I
end;

En regardant le code généré sous Reflector associé à son Add-in pour Delphi (Merci à Piotrek), il apparaît que ce type d'appel à un coût d'exécution conséquent, parce que les valeurs sont copiées des paramètres vers un enregistrement provisoire qui est ensuite passé en tant que paramètre à la fonction imbriquée, puis au retour les valeurs sont recopiées de l'enregistrement provisoire vers les paramètres. Puisque le CLR de .NET ne gère pas les paramètres const, le compilateur doit manipuler tous les paramètres par l'intermédiaire de cet enregistrement provisoire.

Il est donc préférable de ne pas utiliser de procédure imbriquée sous .NET.

19-12. Surcharge de propriété indexée

Projet : ..\Classe\SurchagePropriete\SurchageProprieteIndexee

Il est possible de surcharger la définition d'une propriété afin d'utiliser un ou plusieurs index de types différents. Si une classe a une propriété par défaut, vous pouvez accéder à cette propriété en utilisant la forme abrégée object[index] équivalente à object.property[index]. Par exemple, StringArray.Strings[7] peut s'abréger en StringArray[7]. Une classe ne peut avoir qu'une seule propriété par défaut.

 
Sélectionnez

type
 TMaClass = class
  FList : Array[1..10] of String;

  function GetItem(Index: string): string; overload;
  procedure SetItem(Index:string; Value: string); overload;

  function GetItem(Index: Integer): string; overload;
  procedure SetItem(Index: Integer; Value: string); overload;

  property Item[Index: string]: string
    read GetItem write SetItem; default;

  property Item[Index: integer]: string
    read GetItem write SetItem; default;
end;

Les deux propriétés sont déclarées avec le mot-clé default sinon il n'est pas possible de compiler cet exemple. Il semblerait que la surcharge ne soit possible que pour la propriété par défaut d'une classe.

Voici un exemple d'appel :

 
Sélectionnez

var Propriete : TMaClass;

begin
 Propriete:=TMaClass.Create;
 Propriete[1]:='Premier';
 Affiche(Propriete);

 Writeln('Accès integer '+Propriete[1]);
 Writeln('Accès String '+Propriete['Premier']);
 readln;

 Propriete['Premier']:='Deux';
 ...
end; 

19-13. Métaclasses

Projet : ..\Classe\MetaClasse\Meta, unité uMetaClasses

Sur les notions des métaclasses, vous pouvez consulter l'article les références de classe ou métaclasses sous Delphi Win32.

Il y a deux utilisations possibles des méthodes de classe, une correspondante aux méthodes static du CLR/C# et une autre pour le polymorphisme. Dans ce dernier cas les constructeurs doivent obligatoirement être déclarés virtual pour implémenter le polymorphisme.

 
Sélectionnez

  TUneClasseDeBase = class(TObject)
  private
  protected
  public
     // On déclare une méthode de classe qui
     // agit sur une classe et pas sur une instance de classe
    //Class Procedure AfficheMonNomDeClass; Virtual;
    Class Procedure AfficheMonNomDeClass;

    // Cette function doit être redéclarée dans les classes dérivée.
    function MonTraitement: String; Virtual; Abstract;
    Constructor Create; //Virtual; 
    Destructor Destroy;Override;
  end;

Provoquera l'erreur suivante

 
Sélectionnez

E2391 : Les appels de constructeur potentiellement polymorphiques doivent être virtuels.

Delphi .NET peut utiliser les classes du CLR ( Common Language Runtime), mais toutes les classes déclarées sous Delphi .NET sont légèrement différentes des classes du CLR. Le compilateur ajoute des métaclasses afin de supporter les caractéristiques du langage Delphi.

Projet : ..\Classe\MetaClasse\Meta1

Les classes Delphi .NET sont automatiquement compatible .NET. Nous commencerons par définir une classe simple appelée TTest :

 
Sélectionnez

type
  TTest = class
  public
    constructor Create; Virtual;
    procedure Test;
  end;

constructor TTest.Create;
begin
  inherited; //le constructeur hérité doit être appelé
  WriteLn('ctor');
end;

procedure TTest.Test;
begin
  WriteLn('Test');
end;

var
  UnTest  : TTest;

begin
 UnTest:=TTest.Create;
end;

Le code IL généré ne sera pas équivalent à un code C#. Le compilateur Delphi .NET ajoute la déclaration de l'espace de nom Borland.Delphi.System et pour chaque type de classe (TMyClass) déclarée dans le code source de Delphi, le compilateur créera les métaclasses correspondantes (@MetaTMyClass) héritées de @Borland.Delphi.System.@TClass pour implémenter le fonctionnement de "class of object" de Delphi, tels que les constructeurs virtuels et les méthodes virtuelles de classe. Toutes les métaclasses de Delphi héritent de TClass.

Note :
Les descendants de @TClass (les métaclasses de Delphi) ne sont pas conformes vis à vis de la CLS (Common Language Specification) et ne sont pas prévus pour être employés en dehors d'une application Delphi .NET, c'est à dire manipulable par d'autres langages comme le C# par exemple.

Dans cet exemple le compilateur ajoute @MetaTTest et ce en tant que définition de classe imbriquée.

Image non disponible
Métaclasse ajoutée par le compilateur à la classe TTest

Projet : ..\Classe\MetaClasse\Meta2

Ajoutons la déclaration d'une métaclasse et du code associé :

 
Sélectionnez

type
  TTestClass = class of TTest; // Déclaration d'une métaclasse

procedure TestIt(C: TTestClass);
begin
  C.Create.Test; // Appel de constructeur polymorphique
end;

begin
  TestIt(TTest); // Manipule une classe
end;

La déclaration de la métaclasse TTestClass est vue comme une classe dérivée de Borland.Delphi.TAliasTypeBase :

Image non disponible
Code compilé pour la métaclasse TTesTClasse

Voici la déclaration de TAliasTypeBase dans l'unité Borland.Delphi.System :

 
Sélectionnez

 // used to declare class reference type name.
  TAliasTypeBase = class
  end;

L'appel de la méthode TestIt sera compilé ainsi

 
Sélectionnez

procedure TestIt(&class: Meta2.TTest/@MetaTTest );
begin
  &class.@Create.Test;
end;
Image non disponible
Code généré

La méthode @TClass.@Create renvoie une instance de sa classe simplement en appelant son constructeur. Ici la 'classe containeur' est la classe TTest.

Toutes les fois qu'une instance d'objet ou un type de classe est assigné à une variable ou à un paramètre référence de classe, le compilateur choisira la métaclasse appropriée au lieu de passer l'instance réelle de l'objet.

Au lieu de construire une instance de la métaclasse chaque fois qu'elle est référencée, le compilateur définira une instance statique de la métaclasse, chaque métaclasse possède un champ statique nommé @Instance de type @TClass. Pour la plupart des appels de méthode de classe et des assignations de référence de classe, cette instance statique @Instance sera passée comme paramètre Self de l'appel de méthode de classe. Ceci permet à TObjectHelper.ClassParent de renvoyer l'ancêtre réel TClass sans devoir construire une instance de System.Type, et sans devoir rechercher la TClass appropriée.

Image non disponible
Champ @Instance ajouté par le compilateur

Voici la définition de la classe TObjectHelper :

 
Sélectionnez

  TObjectHelper = class helper for TObject
  public
    procedure Free;
    function ClassType: TClass;
    class function ClassName: string;
    class function ClassNameIs(const Name: string): Boolean;
    class function ClassParent: TClass;
    class function ClassInfo: System.Type;
    class function InheritsFrom(AClass: TClass): Boolean;
    class function MethodAddress(const AName: string): TMethodCode;
    class function MethodName(ACode: TMethodCode): string;
    function FieldAddress(const AName: string): TObject;
    procedure Dispatch(var Message);
  end;

Je vous rappelle que la classe TObject est un alias et est déclarée ainsi TObject = System.Object.
Alors que sous Win32 elle est déclarée de la manière suivante TObject = class.

Les méthodes AfterConstruction and BeforeDestruction ne sont plus supportées.

Projet : ..\Classe\MetaClasse\Meta3

Allons plus loin en ajoutant un autre constructeur à la classe TTest.

 
Sélectionnez

type
  TTest = class
  ...
  public
    ...
    constructor CreateMeta(const I: Integer);
    ...
  end;

procedure TestIt2(C: TTestClass);
begin
  C.CreateMeta(100).Test;
end;

Nous pouvons voir clairement le besoin de créer un descendant de @TClass pour chaque nouvelle classe Delphi .NET: nous avons ajouté un autre constructeur avec un paramètre. Le compilateur a étendu la classe @MetaTTest en ajoutant CreateMeta(const I: integer) ce qui appelle le constructeur TTest.CreateMeta(Integer) et renvoie l'instance de la classe créée.

Image non disponible
Extension de la métaclasse imbriquée

La classe @TClass sous Delphi .NET ajoute également des méthodes pour obtenir les informations de classe utilisées par le système de réflexion de .NET :

 
Sélectionnez

  _TClass = class;

  TClass = class of TObject;

  _TClass = class
  strict protected
    FInstanceTypeHandle: System.RuntimeTypeHandle;
    FInstanceType: System.Type;
    FClassParent: _TClass;
  protected
    procedure SetInstanceType(ATypeHandle: System.RuntimeTypeHandle);
    procedure SetDelegator(ATypeDelegator: System.Type);
  public
    constructor Create; overload;
    constructor Create(ATypeHandle: System.RuntimeTypeHandle); overload;
    constructor Create(AType: System.Type); overload;
    function ClassParent: TClass;
    function InstanceTypeHandle: System.RuntimeTypeHandle;
    function InstanceType: System.Type;
    function Equals(AObj: TObject): Boolean; override;
    function GetHashCode: Integer; override;
  end;

Cette classe ne peut être manipulée dans Delphi 2005, tout comme les méthodes visibles dans IL Dasm précédées d'un arrobas '@'.

Les membres qui prennent ou renvoient System.Type et System.RuntimeTypeHandle ont été ajoutées pour supporter le système de réflexion de .NET. La classe _TClass, déclarée dans l'unité Borland.Delphi.System, est identique à la classe @TClass.

Si un type de classe est importé du CLR (et n'est pas une classe générée par Delphi), il n'aura pas de métaclasse. Quand une classe importée par le CLR est employée dans une expression de référence de classe de Delphi, le compilateur construira une instance générique de TClass, passant le 'type CLR' au constructeur. La classe générique TClass peut simuler le fonctionnement des métaclasses de Delphi pour les classes du CLR, mais pas aussi efficacement que les métaclasses native du compilateur.

Si vous récupérez le type, via System.Type, à partir d'une classe Delphi et que vous l'employez dans une expression de référence de classe de Delphi, vous perdez le comportement spécifiques de Delphi fourni par les métaclasses de Delphi associées aux classes Delphi.

La classe TClass emploie System.RuntimeTypeHandle pour identifier le type d'instance d'un objet. Les RuntimeTypeHandles ont une utilisation mémoire plus efficace que les instances de System.Type, en particulier quand on ne compte pas les utiliser très souvent.

19-13-1. System.Reflection

Vous trouverez ici le détail du principe du système de réflexion (RTTI).
Personnellement la traduction du terme reflection en anglais par réflexion me semble inadéquate, le sens de réflecteur étant perdu, i.e ce que permet un miroir. Ici le terme de réflexion laisse penser que le code dispose d'une intelligence autonome, une espèce de 'code sujet', alors qu'il ne s'agit que d'un mécanisme de description à l'aide de méta-données. Le terme de RTTI est certes inélégant mais bien moins présomptueux ;-)

Introduction au système de réflexion sous .NET

19-13-2. Opérateur TypeOf

Projet : ..\Classe\MetaClasse\Meta4

Delphi pour .NET introduit un nouvel opérateur : TypeOf. Il renvoie une instance de System.Type qui décrit le type de l'instance passée comme paramètre à TypeOf.
Le problème avec l'opérateur TypeOf est qu'il renvoie uniquement des instances de la métaclasse générique TClass. La raison est que l'instance passée comme paramètre à l'opérateur TypeOf peut ne pas être une classe de Delphi .NET; ce peut être une classe du CLR, dans ce cas il n'y a aucune métaclasse de Delphi .NET.

 
Sélectionnez

var
  MetaClasse_TTest   : TTestClass;
  MetaClass_TClass : TClass;
  unObjet: TObject;
Begin
 // MetaClasse_TTest := TypeOf(TTest);   // Types incompatibles
 MetaClasse_TTest := TTest;              // Valid
 MetaClass_TClass:= MetaClasse_TTest;    // Valid
 // MetaClasse_TTest:=MetaClass_TClass;  // Types incompatibles

 unObjet:= MetaClasse_TTest.Create;
 Writeln('unObjet.ClassName='+unObjet.ClassName);
 Readln;
end.

Ce code affiche bien le résultat attendu, à savoir créer un objet de la classe contenue dans la métaclasse utilisée lors de la création de l'instance :

 
Sélectionnez

unObjet.ClassName=TTest

L'utilisation de l'opérateur TypeOf se fait de la manière suivante :

 
Sélectionnez

var   maClassType : System.Type;
...
begin
  //Récupére un System.Type
 MaClassType:=TypeOf(MetaClasse_TTest);
 Writeln('maClassType.FullName= '+maClassType.FullName);

  //Récupére une métaclasse à partir d'un System.type
 MetaClass_TClass:= TTestClass(maClassType); // identique à TClass(maClassType);
 //MetaClasse_TTest:= TClass(maClassType); //Types Incompatibles

 Writeln('Nom de la classe contenue dans MetaClass_TClass = '+ TypeOf(MetaClass_TClass).FullName);
 WriteLn('MetaClass_TClass.ClassName= ' + MetaClass_TClass.ClassName);
 readln;
end.

affiche le résultat suivant, meta4 étant le nom du fichier .dpr :

 
Sélectionnez

maClassType.FullName= meta4.TTest+@MetaTTest
Nom de la classe contenue dans MetaClass_TClass = meta4.TTest+@MetaTTest
MetaClass_TClass.ClassName= TTest

Comme expliqué précédemment, on obtient bien un nom de métaclasse +@MetaTTest, le signe + (TTest+@MetaTTest) indique une classe imbriquée, c'est une convention de nommage sous MS .NET.

Observons maintenant la manipulation de classes du CLR :

 
Sélectionnez

  // Manipule la classe TObject
 MetaClass_TClass:= TObject;
 Writeln('Nom de la classe [TObject] contenue dans MetaClass_TClass = '+ TypeOf(MetaClass_TClass).FullName);
 WriteLn('MetaClass_TClass.ClassName= ' + MetaClass_TClass.ClassName);

  // Manipule une classe du CLR
  // TypeInfo=TTypeInfo=System.Type
 MetaClass_TClass:=TClass(TypeInfo(System.Int32)); // identique à Integer
 Writeln('Nom de la classe [CLR] contenue dans MetaClass_TClass = '+ TypeOf(MetaClass_TClass).FullName);
 WriteLn('MetaClass_TClass.ClassName=' + MetaClass_TClass.ClassName);

  // Manipule un Record
 MetaClass_TClass:=TClass(TypeInfo(Currency));
 Writeln('Nom de la classe [Currency] contenue dans MetaClass_TClass = '+ TypeOf(MetaClass_TClass).FullName);
 WriteLn('MetaClass_TClass.ClassName= ' + MetaClass_TClass.ClassName);
 Readln;

affiche le résultat suivant :

 
Sélectionnez

Nom de la classe [TObject] contenue dans MetaClass_TClass = Borland.Delphi.@TClass
MetaClass_TClass.ClassName= TObject

Nom de la classe [CLR] contenue dans MetaClass_TClass = Borland.Delphi.@TClass
MetaClass_TClass.ClassName=Int32

Nom de la classe [Currency] contenue dans MetaClass_TClass = Borland.Delphi.@TClass
MetaClass_TClass.ClassName= Currency

On s'aperçoit que le nom de la métaclasse affichée pour ces classes correspond à la classe générique _TClass ! Dans ce cas la métaclasse Delphi n'existe pas.
La classe Currency, déclarée sous Delphi, est un Record (type valeur).
Il est possible d'obtenir via le système de réflexion de .NET le type d'une classe contenue dans une chaîne de caractère :

 
Sélectionnez

Var  
 NomQualifié:String;
 LAssembly: System.Reflection.Assembly;

...
begin
  // Obtenir le nom de la métaclasse
 NomQualifié := TypeOf(MetaClasse_TTest).FullName;
 MetaClasse_TTest:=Nil;
 // Obtenir une classe à partir d'un nom de classe contenue dans une chaîne de caractéres
 // pour l'assigner à une référence de classe.
 MetaClasse_TTest:= TTestClass(System.Type.GetType(NomQualifié, False));  // sans le nom complet renvoie Nil
  // Identique à
  //    MetaClasse_TTest:=TTestClass(System.Type.GetType('meta4.TTest', False));
  // Ou
  //    maClassType:= &Type.GetType('meta4.TTest');
  //    Writeln('FullName maClassType '+MaClassType.FullName);
 Writeln('FullName de MetaClasse_TTest aprés récupération du type via une chaîne de caractéres : '
  +TypeOf(MetaClasse_TTest).FullName);
 Readln;

 // Affiche les infos d'assembly
 LAssembly:=lAssembly.GetAssembly(TypeOf(MetaClasse_TTest));
 WriteLn('FullName= ' + lAssembly.GetCallingAssembly.FullName);
 Readln;

end.

Affiche le résultat suivant :

 
Sélectionnez

FullName de MetaClasse_TTest aprés récupération du type via une chaîne de caractéres : meta4.TTest+@MetaTTest

FullName= meta4, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

L'accès aux informations sur les classes se fait à partir du nom d'assemblage vers les classes, l'inverse étant impossible. On ne peut donc retrouver, facilement, le nom de l'assemblage d'une classe si on ne connaît que le nom de cette classe.

19-14. Destructeur

A la différence de Win32, la gestion de la mémoire y étant déterministe c'est à dire que l'on sait quand sera appelé le code de libération de la mémoire, sous .NET ce n'est plus le cas. Consultez l'article Destructeurs d'objet et Finaliseurs sous .NET qui traite dans le détail les problématiques liées à la destruction d'objet sous .NET.

20. Record

Projet : ..\Record\Record0

Un enregistrement (Record) en Delphi .NET est un type par valeur alloué dans la pile à la différence d'une classe qui est un type par référence alloué dans le tas managé. Lors d'une utilisation en tant que paramètre d'une méthode il est copié par valeur, attention donc aux enregistrements de taille importante.

Les Record, dérivé de System.ValueType, sont désormais considérés comme des objets et peuvent donc :

  • posséder des méthodes, des méthodes de classe et des propriétés,
  • implémenter des interfaces,
  • et autoriser la surcharge d'opérateurs.

Mais ils ne peuvent être dérivés comme une classe.

 
Sélectionnez


type
    // Il existe une classe DBNull dans le framework
    // Utiliser la classe DBNull.
   TDBBool = record
     Strict private
      Class Constructor Create;

     Private
        // Champ privé pour stocker la valeur du type
      FValeur : integer;
      Class Var FCValeur:Integer;
      Function GetValeur:Integer;
      Procedure SetValeur(value:Integer);
      //  E2398 : Les méthodes de classe dans les types enregistrements doivent être statiques
      // Class Procedure TestErreur;
     Class Procedure Test;Static;

     Public
        // E2394 : Les constructeurs sans paramètre ne sont pas autorisés dans les types enregistrement
        //Constructor Create;
       Constructor Init;
       Constructor InstanceCreate(AValeur: Integer);

       property Valeur : Integer read GetValeur write SetValeur;
    end;

Dans cette déclaration il y a 2 points intéressants à connaître. Premièrement les méthodes de classe nécessitent le mot-clé static, ici son oubli provoquera l'erreur de compilation suivante :

 
Sélectionnez

E2398 : Les méthodes de classe dans les types enregistrement doivent être statiques

Deuxièmement la présence d'un constructeur sans paramètre est possible mais il ne doit pas porter le nom Create. Si vous déclarez un constructeur d'instance sans paramètre nommé Create, sa présence provoquera l'erreur de compilation suivante :

 
Sélectionnez

E2394 : Les constructeurs sans paramètre ne sont pas autorisés dans les types enregistrement

Alors que la présence de la déclaration Constructor Init ne gêne en rien la compilation !

 
Sélectionnez

var UnDBBool,
    DeuxDBBool,
    TroisDBBool : TDBBool;

begin
   // méthode de classe
  TroisDBBool.Test;

   // Pas d'appel de constructeur
   // DeuxDBBool.FValeur initialisé à zéro
  UnDBBool.Valeur:=-1;
  Writeln('DeuxDBBool='+DeuxDBBool.FValeur.ToString+' UnDBBool='+UnDBBool.FValeur.ToString);
  Readln;

   // Test d'assignation
  DeuxDBBool:=UnDBBool;
  Writeln('DeuxDBBool='+DeuxDBBool.FValeur.ToString+' UnDBBool='+UnDBBool.FValeur.ToString);
  Readln;

   //Visualise l'égalité des 2 variables
  DeuxDBBool.Valeur:=-2;
  Writeln('DeuxDBBool='+DeuxDBBool.FValeur.ToString+' UnDBBool='+UnDBBool.FValeur.ToString);
  Readln;
end.

Dans ce code on constate qu'une instance de type Record ne nécessite pas, tout comme une instance de type integer, d'appel explicite à un constructeur, que les champs sont initialisés à zéro et qu'une assignation d'une instance de type Record dans une seconde instance de même type crée bien deux instances distinctes.

Projet : ..\Record\Record1

Le code suivant ne comporte pas de constructeur de type.

 
Sélectionnez

type
   TDBBool2 = record
     Private
      FValeur : integer;
      Function GetValeur:Integer;
      Procedure SetValeur(value:Integer);

     Public
       property Valeur : Integer read GetValeur write SetValeur;
    end;

Function TDBBool2.GetValeur:Integer;
begin
 Result:=FValeur;
end;

Procedure TDBBool2.SetValeur(value:Integer);
begin
 FValeur:=Value;
end;

var Autre : TDBBool2;

begin
  Autre.Valeur:=12;
end.

Projet : ..\Record\ConstructeurClasse\ConstructeurClasse

Le CLR gère la construction des instances de type valeur différemment des instances de type référence. Notamment pour des raisons de performances, il ne génère pas de constructeur par défaut et n'essaie pas d'appeler un constructeur, même un constructeur sans paramètre. Par contre il initialise les champs à zéro ou à nil.
L'appel d'un constructeur doit être explicite si vous souhaitez initialiser les champs d'une instance. Afin d'éviter toute confusion Delphi .NET, tout comme le C#, interdit la déclaration d'un constructeur sans paramètre pour les types valeur.

20-1. Destructeur

La déclaration d'un destructeur dans un Record provoquera l'erreur de compilation suivante :

 
Sélectionnez

x1025 : Fonctionnalité de langage non supportée : 'destructor' 

Par contre, à partir du moment où un Record peut implémenter une interface, vous pouvez utiliser le pattern Dispose.

20-2. Quand utiliser un Record

Voici les situations où il est préférable d'utiliser un record :

  • Vous voulez que votre type ressemble et se comporte comme un type primitif.
  • Vous créez beaucoup d'instances d'une durée de vie brève. Par exemple dans une boucle.
  • Les instances que vous créez sont peu passées comme paramètre.
  • Vous ne voulez pas dériver d'autres types ou laisser d'autres types dériver de votre type.
  • Vous voulez que d'autres types opèrent une copie de vos données (passage par valeur).

20-3. Quand ne pas utiliser un Record

  • La taille du record (la somme des tailles de ses membres) est importante. La raison est qu'au-delà d'une taille particulière, le passage par paramètre devient prohibitif. Microsoft recommande que la taille d'un struct devrait idéalement être en dessous de 16 bytes, mais elle est peut être supérieure. Au cas où votre record aurait des membres de types référence, assurez-vous que vous n'incluez pas la taille des instances des types référence, mais juste la taille des références.
  • Vous créez des instances, les insérez dans une collection, et modifiez des éléments dans cette collection au sein d'une boucle. Ceci aura comme conséquence de nombreuses opérations de boxing/unboxing comme lorsque les collections du Framework (FCL) opèrent sur System.Object. Chaque ajout comportera une opération de boxing, et chaque modification impliquera une opération d'unboxing suivi d'une opération de boxing.

Pour réinitialiser un type valeur (Record, tableau, ...), l'équivalent d'un FillChar, utilisez l'appel suivant :
Initialize(MonRecord); // Code IL initobj, le constructeur n'est pas appelé.

21. Surcharge d'opérateurs dans les classes et les enregistrements

Projet : ..\operateur\operateur

Texte issu de la documentation de DELPHI 2005
Delphi .NET autorise la surcharge de certaines fonctions (ou "opérateurs") dans les déclarations de classe et d'enregistrement. Le nom de la fonction opérateur correspond à une représentation symbolique dans le code source. Par exemple, l'opérateur Add correspond au symbole +. Le compilateur génère un appel à la surcharge appropriée, en faisant correspondre le contexte (c'est-à-dire le type de retour et le type des paramètres utilisés dans l'appel) à la signature de la fonction opérateur.
Le tableau suivant énumère les opérateurs Delphi pouvant être surchargés :

Opérateur Catégorie Signature de déclaration Mappage de symbole
Implicit Conversion Implicit(a : type): resultType; implicit typecast
Explicit Conversion Explicit(a: type): resultType; explicit typecast
Negative Unaire Negative(a: type): resultType; -
Positive Unaire Positive(a: type): resultType; +
Inc Unaire Inc(a: type): resultType; Inc
Dec Unaire Dec(a: type): resultType Dec
LogicalNot Unaire LogicalNot(a: type): resultType; not
BitwiseNot Unaire BitwiseNot(a: type): resultType; not
Trunc Unaire Trunc(a: type): resultType; Trunc
Round Unaire Round(a: type): resultType; Round
Equal Comparaison Equal(a: type; b: type): Boolean; =
NotEqual Comparaison NotEqual(a: type; b: type): Boolean; <>
GreaterThan Comparaison GreaterThan(a: type; b: type) Boolean; >
GreaterThanOrEqual Comparaison GreaterThanOrEqual(a: type; b: type): resultType; >=
LessThan Comparaison LessThan(a: type; b: type): resultType; <
LessThanOrEqual Comparaison LessThanOrEqual(a: type; b: type): resultType; <=
Add Binaire Add(a: type; b: type): resultType; +
Subtrac Binaire Subtract(a: type; b: type): resultType; -
Multiply Binaire Multiply(a: type; b: type): resultType; *
Divide Binaire Divide(a: type; b: type): resultType; /
IntDivide Binaire IntDivide(a: type; b: type): resultType; div
Modulus Binaire Modulus(a: type; b: type): resultType; mod
ShiftLeft Binaire ShiftLeft(a: type; b: type): resultType; shl
ShiftRight Binaire ShiftRight(a: type; b: type): resultType; shr
LogicalAnd Binaire LogicalAnd(a: type; b: type): resultType; et
LogicalOr Binaire LogicalOr(a: type; b: type): resultType; ou
LogicalXor Binaire LogicalXor(a: type; b: type): resultType; xor
BitwiseAnd Binaire BitwiseAnd(a: type; b: type): resultType; et
BitwiseOr Binaire BitwiseOr(a: type; b: type): resultType; ou
BitwiseXor Binaire BitwiseXor(a: type; b: type): resultType; xor

Aucun autre opérateur ne peut être définis sur une classe ou un enregistrement.
A la différence du C#, le code suivant n'est pas possible, les opérateurs true et false n'étant pas implémentés :

 
Sélectionnez

  class operator True(a: type;): resultType;

Les méthodes d'opérateurs surchargés ne peuvent pas être référencées par nom dans le code source :

 
Sélectionnez

   y := x + x;   // Appelle TMaClasse.Add(a, b: TMaClasse): TMaClasse
   y := x add x ; // Interdit !
   y := TMaClasse.add(x, x); // Interdit !

Pour accéder à une méthode d'opérateur spécifique d'une classe spécifique, vous devez utiliser des transtypages explicites sur tous les opérandes :

 
Sélectionnez

  A,Z : integer
Begin
   A:= Z+TMaClasse(x);

Il n'existe aucune hypothèse concernant les propriétés distributives ou commutatives de l'opération. Pour les opérateurs binaires, le premier paramètre est toujours l'opérande gauche et le second paramètre l'opérande droit. En l'absence de parenthèses explicites, l'associativité est supposée être de gauche à droite.

En règle générale, les opérateurs ne doivent pas modifier leurs opérandes. A la place, ils renvoient une nouvelle valeur, construite en effectuant l'opération sur les paramètres.

Les opérateurs surchargés sont utilisés le plus souvent dans les enregistrements (c'est-à-dire les types de valeur). Très peu de classes du framework .NET ont des opérateurs surchargés, mais la plupart des types de valeur ( i.e des enregistrements) en ont.

21-1. Déclaration des surcharges d'opérateurs

Les surcharges d'opérateurs sont déclarées dans des classes ou des enregistrements avec la syntaxe suivante :

 
Sélectionnez

type
  typeName = [class | record]
      class operator Signature_de_déclaration (a: type): Type_résultant;
      liste d'opérateur	
  end;

L'implémentation des opérateurs surchargés doit aussi inclure la syntaxe class operator :

 
Sélectionnez

 class operator Name._de_type. Signature _de_déclaration (a: type): Type_résultant;

Exemples d'opérateurs surchargés pour un enregistrement :

 
Sélectionnez

type
    // Il existe une classe DBNull dans le framework
    // Utiliser la classe DBNull
   TDBBool = record
   Strict private
        // Champ privé pour stocker la valeur du type
      FValeur : integer;

        // champs de classe, static
      Class var FdbFalse : TDBBool;
      Class var FdbNull  : TDBBool;
      Class var FdbTrue  : TDBBool;

      Class Constructor CCreate;

         // Type interne et Privé
        type
           DBType = -1..1;  // Etendue du type

     Private
      // Le constructeur ne doit pas être accessible
      // Private pour TDBBool.dbTrue:=TDBBool.Create(0);
      Constructor Create(AValeur: DBType);

        // Constantes internes et Privées
      const
        // Valeurs du type autorisé
        // On ne reprend pas à l'identique les valeurs du type Boolean de Delphi
        // Le zéro permet les tests x>0 pour true et x<0 pour false, voir aussi l'opérateur LogicalNot
       cdbNull   : integer =0;
       cdbFalse  : Integer =-1;
       cdbTrue   : Integer =1;

     Public
       // Examine la valeur d'un DBBool.
       // Renvoie true si la valeur de Fvaleur correspond à la valeur testé
      Function IsNull  : Boolean;
      Function IsFalse : Boolean;
      Function IsTrue  : Boolean;

        // Surcharge d'opérateur
      Class operator Implicit(Avalue: Boolean): TDBBool;
      Class operator Implicit(x: TDBBool): String;
      Class operator Explicit(x: TDBBool): Boolean;
      Class operator Equal(x,y : TDBBool):TDBBool; //  =
      Class operator NotEqual(x,y : TDBBool):TDBBool; // <>
      Class operator LogicalNot(x : TDBBool):TDBBool; // not
      Class operator LogicalAnd(x,y : TDBBool):TDBBool; // and
      Class operator LogicalOr(x,y : TDBBool):TDBBool; // or

      { Pas d'opérateur True ou False !}
       // Surcharge des méthodes liées aux opérateurs
        // Convertit la valeur spécifiée en sa représentation String équivalente.
      Function ToString: String;Override;
      Function Equals(x: TObject):Boolean;Override;
      Function GetHashCode:Integer;Override;

       // Propriétées en ReadOnly
      Class Property dbFalse : TDBBool Read FdbFalse;
      Class Property dbNull  : TDBBool Read FdbNull;
      Class Property dbTrue  : TDBBool Read FdbTrue;
    end;

Voici l'implémentation d'un opérateur surchargé, qui ne modifie par ses opérandes mais renvoie une nouvelle valeur :

 
Sélectionnez

// Opérateur ET ( AND ) logique. Renvoie
    //  dbFalse si l'une des opérandes est égale à dbFalse,
    //  dbNull si l'une des opérandes est égale à dbNull,
    //  sinon renvoie dbTrue.
   Class operator TDBBool.LogicalAnd(x,y : TDBBool):TDBBool;
   var Resultat:Integer;
   begin
     If x.FValeur < y.FValeur
      then Resultat:=x.FValeur
      else Resultat:=y.FValeur;
     Result:=TDBBool.Create(Resultat);
   end;

22. Initialisation et finalisation d'unité

Projet : ..\Initialisation-Unite\Initialisation

Comme nous l'avons vu au début de cet article sous .NET tout est objet, l'équipe de Delphi .NET a donc du adapter la notion d'unité pour continuer de l'utiliser dans ce nouvel environnement.
Une fois encore on retrouve les problématiques de manipulation déterministe de code, par l'utilisation des sections Initialization et Finalization d'une unité mais aussi par l'ordre d'exécution de la clause Uses. Sous .NET l'initialisation des unités ne doit pas dépendre des effets liés à l'initialisation d'unités dépendantes.

Voici le corps d'un programme principal référençant une unité qui utilise les parties Initialization et Finalization :

 
Sélectionnez

program Initialisation;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  UInit in 'UInit.pas';

begin
 Writeln(VariableGlobale.ToString);
 Readln;
end.

et voici le corps de l'unité :

 
Sélectionnez

unit UInit;

interface
var
 VariableGlobale : Integer;

 Procedure PrcPublic;

implementation

var  VariablePrivee : Integer;

Procedure PrcPublic;
begin
 VariableGlobale:=10;
end;

Procedure PrcPrivee;
begin
  VariableGlobale:=VariableGlobale;
end;

// La partie Initialization est identique au bloc begin .. end.
Initialization 
 PrcPublic;
 PrcPrivee;

Finalization
 PrcPrivee;
end.

On voit donc ici un code des plus banal, par contre du coté du code généré par le compilateur il en va autrement :

Image non disponible
Code généré par le compilateur

On peut voir que

  • le code du programme Initialisation est défini comme une classe. Elle contient un constructeur de classe et une méthode de classe de même nom que le nom du programme,
  • l'unité UInit est aussi définie comme une classe et qu'ici aussi elle contient un constructeur de classe, une méthode de classe de même nom que le nom de l'unité, une méthode Finalization et enfin les 2 procédures déclarées, transformées en méthode statique de cette classe. Ainsi déclarées (static) elles sont accessibles par toutes les classes,
  • la variable VariableGlobale devient une variable de classe.

Voyons le détail de ce constructeur de classe :

Image non disponible
Constructeur de classe de l'unité

La section Finalization est traité comme un délégué (@FinalizeHandler est un descendant de System.MulticastDelegate). Comme nous pouvons le voir il n'y a aucun code supplémentaire pour l'initialisation et la finalisation d'une unité. Juste une organisation différente.

Ce constructeur de classe appel la méthode UInit qui elle contient le code de la section Initialization de l'unité :

Image non disponible
Méthode contenant le code des diverses initialisations

La méthode Finalization contient bien le code de la section Finalization :

Image non disponible
La section Finalization

Le code du constructeur de classe de la 'classe' Initialisation (le programme) appelle tous les constructeurs de classe des classes utilisées (les unités), le CLR déterminant leur exécution :

Image non disponible
'Le constructeur du programme'

23. Attributs .NET

La notion d'attribut sous .NET est un nouveau concept de programmation permettant d'associer des informations sur les éléments d'un langage.
Extrait de "Formation à C#", chez MS-Press, page 128:

 
Sélectionnez

...
Les attributs fournissent un moyen générique d'associer des données (annotations) à vos types définis (sous C# ou Delphi).
Les attributs permettent de définir des informations de niveaux conception (par exemple une documentation), des informations 
de niveau exécution ( par exemple le nom d'une colonne de base de données associée à un champ), ou même des caractéristiques 
de comportement (par exemple pour savoir si un certain membre est capable de participer à une transaction). 
Les possibilités sont infinies, ce qui est le but recherché.
..."

Lorsque l'on parle de caractéristiques de comportement ce n'est pas l'attribut lui même qui modifie le comportement de l'élément auquel il est associé mais des processus capables d'effectuer des traitements spécifiques en fonction de la présence ou non d'un attribut personnalisé donné.

Extrait du SDK .NET 1.1 FR :

 
Sélectionnez

...
Lorsque vous compilez votre code pour le runtime, il est converti en langage MSIL (Microsoft Intermediate Language), 
puis il est placé dans un fichier exécutable portable avec les métadonnées générées par le compilateur. Les attributs vous 
permettent de placer dans les métadonnées des informations descriptives supplémentaires, qui peuvent être extraites à l'aide 
des services du système de réflexion du runtime. Le compilateur crée des attributs lorsque vous déclarez des instances de 
classes spéciales dérivées de System.Attribute.
..."

Un attribut dérive de la classe System.Attribut, le type équivalent sous Delphi est TCustomAttribute déclaré dans System.pas.

On peut placer un attribut sur les éléments suivants :

  • un assemblage,
  • un type valeur (Record),
  • une classe,
  • un événement,
  • une propriété,
  • une méthode,
  • un champ,
  • un retour de fonction,
  • un paramètre de méthode, ...

Voir aussi :
Liste des éléments de l'application auxquels un attribut peut être appliqué. Local.
Extension des métadonnées à l'aide des attributs. Local.
Aide de Delphi 2005 : Utilisation des attributs personnalisés .NET.

23-1. Création

Projet : ..\Attribut\Attribut

Créons un attribut qui permette de savoir :

  • si la classe ou un membre de cette classe est documenté
  • si ces mêmes éléments ont-été testés
  • quel est le chemin du référentiel contenant les informations de documentation et de test ?
 
Sélectionnez

     // Déclaration du nouvel attribut
  TValidationAttribute = class(TCustomAttribute)
  private
   FDocumentee     : Boolean;
   FTestee         : Boolean;
   FCheminDocument : String;
  public
    constructor Create(ADocumentee,ATestee: Boolean);
    property Documentee : Boolean read FDocumentee;
    property Testee     : Boolean read FTestee;
    property CheminDocument : String read FCheminDocument;
  end;

Ici les propriétés sont en lecture seule, mais ce n'est pas une obligation.

Une fois cet attribut déclaré, on l'utilise de la manière suivante :

 
Sélectionnez

   // Déclaration d'une nouvelle classe utilisant l'attribut TValidationAttribute
   
  [TValidationAttribute(False,False)] // Attribut sur la classe
  TMaClasse=class
   UnChamp : Integer;

   // Attribut sur une méthode
  [TValidationAttribute(True,False,CheminDocument='C:\Référentiel\Delphi\PrjAttribut\PrcFaitQQChose')]
   procedure FaitQQChose;
   Function Traitement:Integer;
   constructor Create(I : Integer);
  end;

C'est tout !
Vous pouvez construire l'exécutable, le compilateur effectuant le travail d'association entre la classe et votre attribut.

Voir aussi :
La hiérarchie des attributs. Local.

Il est préconisé de nommer les attributs en les suffixant par Attribute. Certains attributs du langage tel que [Serializable] se nomment SerializableAttribute dans le SDK. Ces attributs, très utilisés par le CLR et la FCL, sont compilés en étant compressé (1 bit) dans les méta-données.

Note :
Les types de paramètres pour une classe d'attribut sont limités aux types suivants :

  • Boolean,
  • Byte,
  • Char,
  • Double,
  • Float,
  • Integer,
  • Int64,
  • Shortint,
  • String,
  • System.Type,
  • TObject,
  • Un type énumération, à condition qu'il soit ainsi que les types imbriqués d'accès public. Un tableau unidimensionnel contenant n'importe quel type listé ci-dessus et si son index de base est zéro.

23-2. Association

Dans les 2 exemples de code précédents, vous pouvez remarquer que le constructeur déclare 2 paramètres :

 
Sélectionnez

  constructor Create(ADocumentee,ATestee: Boolean);

Correspondant à l'utilisation suivante, où nous utilisons les paramètres de position qui sont obligatoires :

 
Sélectionnez

  [TValidationAttribute(False,False)] // Attribut sur la classe
  TMaClasse=class

Par contre la syntaxe suivante utilise les paramètres de position mais également des paramètres nommés qui eux sont optionnels. Ils évitent de devoir construire autant de constructeurs qu'il y a de combinaisons possibles.

 
Sélectionnez

   // Attribut sur une méthode
 [TValidationAttribute(True,False,CheminDocument='C:\Référentiel\Delphi\PrjAttribut\PrcFaitQQChose')] 
  procedure FaitQQChose;

Il est possible d'associer plusieurs attributs à un élément, par exemple :

 
Sélectionnez

  [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode=True)]
  [FileIOPermission(SecurityAction.LinkDemand, Unrestricted=True)]
  TIniFile = class(TCustomIniFile)

L'écriture suivante est permise :

 
Sélectionnez

[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode=True),
 FileIOPermission(SecurityAction.LinkDemand, Unrestricted=True)]
TIniFile = class(TCustomIniFile)

23-3. Limiter les associations à certains éléments

Projet : ..\Attribut\Attribut2

Il peut être intéressant de limiter ou préciser avec quels éléments l'attribut peut être associé. La classe AttributeUsageAttribute (local) permet ces précisions.
Le code suivant limite l'usage de l'attribut TValidationAttribute pour les classes et les méthodes uniquement :

 
Sélectionnez

   // Limite l'usage de l'attribut TValidationAttribute pour les classes et les méthodes
  [AttributeUsageAttribute(AttributeTargets.Class or AttributeTargets.Method, AllowMultiple=True)]
   // Déclaration du nouvel attribut
  TValidationAttribute = class(TCustomAttribute)

On peut voir que cette limitation se fait grâce à un attribut du SDK !
Une fois ceci fait, l'association de l'attribut TValidationAttribute sur le membre unChamp :

 
Sélectionnez

  [TValidationAttribute(False,False)]  // Attribut sur la classe
  TMaClasse=class
   [TValidationAttribute(False,False)] // Attribut sur un champ
   UnChamp : Integer;

provoquera l'erreur de compilation suivante :

 
Sélectionnez

E2325 : L'attribut 'TValidationAttribute' n'est pas correct pour cette cible.

Le paramètre AllowMultiple autorise l'usage multiple d'un même attribut sur une même cible :

 
Sélectionnez

  [TValidationAttribute(False,False)]  // Attribut sur la classe
  [TValidationAttribute(False,False)]  
  TMaClasse=class
   UnChamp : Integer;

Si AllowMultiple est à True, l'usage multiple est possible mais n'a aucun sens dans cet exemple.
Il reste un paramètre optionnel qui est Inherited. Il indique si l'attribut concerné peut être hérité par des classes dérivées ou certains types de membres dérivés. Il évite donc la redéclaration de l'attribut sur la classe ou le membre surchargé. Il est très peu utilisé.

Par défaut un attribut est initialisé avec ValidOn à All, AllowMultiple à False et Inherited à True.

En utilisant la fonction Chercher sur le caractère '[', voici quelques exemples d'utilisation d'attributs sous Delphi .NET, provenant de différents fichiers sources :

 
Sélectionnez

[DllImport('user32.dll', SetLastError=true)]
function CreateDirectory(name: string ; sa : SecurityAttribute):Boolean; external;
...
[ReflectionPermission(SecurityAction.Assert, MemberAccess=True, TypeInformation=True)]
constructor TMethodMap.Create(ClassType: System.Type);
...
 [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi, Pack=1)]
  ObjAttrDesc = packed record
    iFldNum    : Word;                   { Field id }
    [MarshalAs(UnmanagedType.LPSTR)]
    pszAttributeName: string; // PChar;  { Object attribute name }
  end;
...  
function ExtractIconEx(lpszFile: string; nIconIndex: Integer;
  [out] phiconLarge, [out] phiconSmall: array of HICON; nIcons: UINT): UINT; overload;
...
 // Ici il ne s'agit pas d'un attribut mais d'une syntaxe commune à 
 // Delphi Win32 et Delphi .NET concernant le GUID d'un objet COM.
  IInterfaceList = interface
    ['{285DEA8A-B865-11D1-AAA7-00C04FB17A72}']

Note:
L'usage de l'attribut [Assembly : ...] est une exception à la syntaxe d'utilisation, elle permet au compilateur de reconnaître la portée de cet attribut sur l'assemblage et non pas sur la ou les classes déclarées dans ce même assemblage.

23-4. Utilisation

Un attribut est une information inerte dans le code. Il ne peut lui même déclencher une action, il faut donc, en utilisant le mécanisme de réflexion, rechercher sa présence et exécuter une action s'il est présent ou non, l'attribut n'est instancié qu'à partir de ce moment.

Voyons comment rechercher les attributs associés à une classe :

 
Sélectionnez

Procedure GetAllAttribut(NomClasse:String);
var ClassType  : System.Type;
    Attributs  : array of TObject;
    unAttribut : TObject;
begin
 ClassType := System.Type.GetType(NomClasse, False);
 Attributs := ClassType.GetCustomAttributes(False);
 
 Writeln('Attributs de la classe : '+NomClasse);
 for unAttribut in Attributs do
  Writeln(unAttribut.GetType.FullName);
end;

On doit d'abord obtenir le type de la classe concernée et ensuite récupérer tous les attributs associés à la classe dans un tableau. Une fois ceci fait, il est possible d'énumérer et de manipuler chaque attribut.

Il est possible de rechercher un attribut spécifique associé à une classe :

 
Sélectionnez

procedure GetDetailUnAttribut(NomClasse:String);
var ClassType  : System.Type;
    Attributs  : array of TObject;
    Validation : TValidationAttribute;

begin
 ClassType := System.Type.GetType(NomClasse, False);
 Attributs := ClassType.GetCustomAttributes(TypeOf(TValidationAttribute),false);

 if Length (Attributs) = 0
  then  Writeln('Attribut de validation inexistant pour la classe '+NomClasse)
   else
    begin
     Writeln('Propriétés d''une instance de TValidationAttribute sur la classe : '+NomClasse);
     Validation := TValidationAttribute(Attributs[0]);

     Writeln('Documentée : '+Validation.Documentee.ToString);
     Writeln('Testée     : '+Validation.Testee.ToString);
     Writeln('Chemin des documents dans le référentiel : '+#13#10+Validation.CheminDocument);
  end;
end;

La recherche d'attributs sur d'autres éléments est aussi possible, consulter le SDK .NET pour plus d'informations.
Voir la classe Type, plus précisément les méthodes GetFields, GetMethod, GetProperties, ...

23-5. Les spécificateurs d'attribut

Projet : ..\Attribut\Attribut3

Dans certain cas le compilateur ne peut pas deviner, d'après le contexte, à quoi doit s'appliquer l'attribut. Dans ce cas vous pouvez forcer cette information en précisant la cible de l'association, par exemple dans l'exemple suivant le compilateur ne peut déterminer si l'attribut s'associe à la méthode ou au résultat de la méthode :

 
Sélectionnez

   [TValidationAttribute(False,False)]
   Function Traitement:Integer;

Voici la liste des spécificateurs d'attribut utilisable :

  • assembly pour un assemblage ( global).
  • module pour un module ( global).
  • type pour une classe ou un record. La syntaxe '[type:' ne semble pas être valide. La position de l'association semble être suffisante.
  • method pour une méthode.
  • property pour une propriété.
  • event pour un événement.
  • field pour un champ.
  • param pour un paramètre au sein d'un entête de fonction ou pour une suite de variable de la clause var d'un bloc. La syntaxe '[param:' ne semble pas être valide. La position de l'association semble être suffisante.
  • return pour un retour de fonction. La syntaxe '[Result:' est valide et semble être identique.

Pour préciser la cible de l'association utilisons une spécification d'attribut :

 
Sélectionnez

    // Précise que l'attribut porte sur le résultat et pas sur la méthode.
   [return:TValidationAttribute(False,False)]
   Function Traitement:Integer;

Voici quelques possibilités d'écriture :

 
Sélectionnez

  [TValidationAttribute(False,False)] 
  TMaClasse=class
   [Field:TValidationAttribute(False,False)] 
   UnChamp : Integer;

   [Method:TValidationAttribute(True,False,CheminDocument='C:\Référentiel\Delphi\PrjAttribut\PrcFaitQQChose')]
   procedure FaitQQChose;

   [return:TValidationAttribute(False,False)]
   Function Traitement:Integer;
   
   constructor Create(  [TValidationAttribute(False,False)]   I : Integer);
   
   [property:TValidationAttribute(False,False)]
   property s:integer read unchamp;
  end;

24. Espaces de nommage

Dans Delphi, une unité est le conteneur de base des types. Le CLR de Microsoft introduit une autre couche d'organisation appelée espace de nommage. Dans l'environnement .NET, un espace de nommage est un conteneur conceptuel de types. Un espace de nommage est un conteneur hiérarchique d'unités Delphi, il peut lui-même être imbriqué pour former une hiérarchie de contenance. Il permet ainsi de lever les ambiguïtés sur les types/classes ayant le même nom et de différencier les unités de même nom.

Dans Delphi 2005, un fichier projet (program, library ou package) introduit implicitement son propre espace de nommage, appelé espace de nommage par défaut du projet. Une unité peut être un membre de l'espace de nommage par défaut du projet ou peut se déclarer explicitement elle-même comme membre d'un autre espace de nommage. Dans les deux cas, l'unité déclare son appartenance à un espace de nommage dans l'entête d'unité.
Par exemple la syntaxe suivante :

 
Sélectionnez

 unit Developpez.Tools.TClasse;

déclare explicitement l'espace de nommage Developpez.Tools, l'identificateur le plus à droite et le point (.TClasse) sont supprimés de la déclaration du nom. Cela simplifie les appels d'assemblage créés avec Delphi à partir d'autre langage.

24-1. Exemples

Créons 3 unités factices afin de visualiser cette notion. La première contient une classe :

 
Sélectionnez

unit Developpez.Tools.TClasse;

interface
type
 TClasse=Class
  ChampInt :Integer;
 end;
implementation
end.

La seconde contient un enregistrement :

 
Sélectionnez

unit Developpez.Tools.TRecord;

interface
type
 TRecord=Record
  ChampInt :Integer;
 end;

implementation
end.

Et enfin la troisième qui contient une classe. Cette unité est dite générique ce qui signifie qu'elle n'est pas rattachée à un espace de nom. Ce type d'unité permet, entre autre, d'utiliser du code devant fonctionner sur différentes plate-formes :

 
Sélectionnez

unit UGeneric;

interface
type
  TMultiPlateforme=Class
    TestPascal:String; 
  end;

implementation
end.

Projet : ..\EspaceNommage\DemoEspaceNommage.bdsproj

Compilons le programme factice suivant :

 
Sélectionnez

program DemoEspaceNommage;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Developpez.Tools.TRecord in 'Developpez.Tools.TRecord.pas',
  Developpez.Tools.TClasse in 'Developpez.Tools.TClasse.pas',
  UGeneric;

var     UnRecord  : TRecord;
        UneClasse : TClasse;
        Cible     : TMultiPlateforme;
begin
 UnRecord.ChampInt:=10;
 UneClasse:=TClasse.Create;
 UneClasse.ChampInt:=10;
 Cible:= TMultiPlateforme.Create;
end.

et visualisons le résultat de la compilation dans IL Dasm

Image non disponible
Le code compilé du programme DemoEspaceNommage.exe

Nous retrouvons donc nos 3 unités et nous nous apercevons que

  • les deux unités de l'espace de nommage Developpez.Tools sont effectivement regroupées au sein d'un espace de nommage ainsi que les classes déclarées dans chaque unité,
  • l'unité UGeneric est vue comme un espace de nommage séparé qui semble indépendant de l'espace de nommage du programme,
  • le programme principal est bien un espace de nommage à part entière,
  • l'espace de nommage Developpez.Tools.Units, imbriqué dans Developpez.Tools, contient les classes 'Unité' (cf. Initialisation et finalisation d'unité).

Pour l'unité UGeneric il est possible de la rattacher à l'espace de nommage du programme. Il suffit de la renommer en DemoEspaceNommage.UGeneric.pas mais ce faisant elle cesse d'être une unité générique ! Consulter le programme Demo2EspaceNommage pour plus de détails.

Les exemples de packages suivants vous proposent différents scénarios (non exhaustif) :

  • Developpez.Tools.Dpk
  • Developpez.Dpk
  • DeveloppezV2.Dpk
  • DeveloppezV3.Dpk

Le package DeveloppezV3.Dpk déclarant l'espace de nommage Developpez ressemble à ceci :

Image non disponible
Contenu de l'espace de nommage 'Developpez'.

Voici trois recommandations concernant les espaces de nommage :

  1. Utilisez toujours des packages.
  2. Référencez les types par leur nom complet (Fully Qualified Name).
  3. Vérifier la duplication de types avec l'utilitaire PEVerify.

La suite Namespaces in Delphi 2005

Voir aussi :
Indications concernant l'attribution d'un nom aux espaces de noms. Local.
Aide de Delphi 2005 : utilisation des espaces de nommage avec Delphi.

Note : Le mot clé Namespaces cité dans l'aide en ligne de Delphi ne semble pas implémenté.

25. Assemblage

Projet : ..\Assemblage\Developpez.Outils.TDBBool

Sous .NET les seuls composants générés pour la plate-forme sont les modules et les assemblages (assembly). Un module (.netmodule) n'est pas déployable, par contre un assemblage peut l'être et ce sous la forme d'une DLL ou d'un exécutable (différent de Win32). La différence entre une DLL et un EXE sous .NET est simplement la présence d'un code pour l'exécution (méthode Main).

L'entête d'un .netmodule est différent de celle d'une unité .dcuil, on ne peut donc malheureusement pas générer de module avec Delphi 2005 ni avec Visual Studio. Un assemblage est une agrégation physique ou logique de modules, au vu de la limite précédente j'aborderais ici uniquement le regroupement physique.
Il faut savoir qu'entre 2 assemblages, seuls les données membres de type Public sont accessibles; Entre 2 modules seules les données membres de type Public et Protected sont accessibles.

Voici les possibilités de génération avec le C# en ligne de commande :

 
Sélectionnez

csc /t:module	--> .netmodule
csc /t:library	--> .DLL
csc /t:exe	--> .EXE

Pour Delphi Borland recommande d'utiliser un package pour créer un assemblage.
Exemple
A voir aussi :
Aide de Delphi 2005 : liaison des unités Delphi dans une application.

Programmation à l'aide d'assemblies. Local.

Les Copies fantômes (Shadow Copy). Local. : Si un fichier exécutable est en cours d'utilisation, les mises à jour apportées au fichier exécutable sont stockées dans un répertoire de copies fantômes. Les utilisateurs existants continuent d'utiliser le fichier exécutable d'origine jusqu'à ce qu'il termine et les nouveaux utilisateurs utilisent la copie fantôme du fichier exécutable.

25-1. Assemblage partageable et nom fort

Si votre assemblage doit être partagé, i.e inséré dans le GAC, vous devez spécifier un nom fort. Si vous déployez votre assemblage dans le même répertoire que le programme, la clé forte n'est pas nécessaire.
Pour garantir l'unicité le nom fort est constitué :

  • du nom du fichier d'assemblage sans extension,
  • du numéro de version de l'assemblage,
  • des informations facultatives de culture de l'assemblage,
  • et de la signature de l'assemblage.

Pour signer un assemblage on utilise le programme sn.exe du SDK (Strong Name). Il permet de générer une paire de clés privée/publique pouvant être utilisée pour la signature.
La clé publique est celle qui est liée à l'assemblage, la clé privée ne devant pas être divulguée vous devez donc vous assurer de sa confidentialité. Cette clé permet entre autre au CLR de reconnaître une modification malveillante dans un assemblage.

 
Sélectionnez

 sn -k nom_assemblage.snk

L'extension .snk est une convention du SDK. Une fois ce fichier de signature créé vous pouvez le référencer dans l'attribut [assembly: AssemblyKeyFile('')].

Au niveau professionnel ce point vous amènera à revoir vos méthodes d'organisation du cycle de développement.
Cf. l'attribut [assembly: AssemblyDelaySign()] :
Quand cet attribut est utilisé sur un assemblage, l'espace réservé pour la signature est rempli ultérieurement par un outil de signature tel que l'utilitaire Sn.exe. La temporisation de signature est utilisée quand l'auteur de l'assemblage n'a pas accès à la clé privée qui doit être utilisée pour générer la signature, comme dans [assembly:AssemblyDelaySign(true)].

25-2. Définition d'attributs spécifiques

Votre assemblage (package, Dll ou exécutable) doit définir des attributs dédiés afin d'autoriser son partage.

 
Sélectionnez

 // Nom du package, DLL ou executable
[assembly: AssemblyTitle('Developpez.Outils.TDBBool')]

 // Numéro de version de l'assembly
[assembly: AssemblyVersion('1.0.0.1')]

 // Code de la culture de l'assemblage : 
 //  la langue, la sous-langue, le pays et/ou la région,...
[assembly: AssemblyCulture('fr-FR')]

 // Nom du fichier de signature,
 // par défaut dans le même répertoire que l'assembly.
[assembly: AssemblyKeyFile('Developpez.Outils.TDBBool.snk')]
...

Voir aussi :
Définitions des attributs assemblyxxxx. Local.
Définition des codes d'information de culture. Local.

25-3. Ajout de l'assemblage dans le GAC

Pour ajouter un assemblage signé dans le référentiel .NET, il s'agit en fait d'une recopie respectant certaines règles, vous pouvez utiliser soit une opération Drag&Drop dans l'explorateur de fichier soit l'utilitaire gacutil.exe du SDK :

 
Sélectionnez

GacUtil /i nom_de_l_assemblage

La convention de nommage des sous-répertoires du GAC est la suivante, version\langue\TokenCléPublique, en cas d'absence de code langue, des caractères de soulignement sont utilisés.
Il existe un cache local dans le profil de l'utilisateur qui est principalement utilisé par les applications WEB.

25-4. Suppression de l'assemblage du GAC

Pour supprimer un assemblage vous pouvez le faire directement dans l'explorateur de fichier ou avec GacUtil :

 
Sélectionnez

GacUtil /u nom_de_l_assemblage

25-5. Visualisation du nouvel assemblage dans l'EDI

Pour le visualiser dans l'EDI via le menu 'Projets->Ajouter une référence', vous devez ajouter une entrée dans la clé de registre suivante :

 
Sélectionnez

Windows Registry Editor Version 5.00 

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders\UN_NOM_DE_CLE] 
@="C:\\Nom du répertoire de vos assemblages" 

Il n'est pas nécessaire de quitter et de redémarrer L'EDI. Vous pouvez désormais ajouter votre nouvel assemblage dans vos projets.

Si votre assemblage contient un objet COM, vous devez effectuer une opération supplémentaire, identique à regsvr32 :
regasm /tlb AssemblageCOM.dll

26. Divers

Vous trouverez ici des informations diverses qui faute de temps ne sont pas suffisamment développées mais qui m'ont parues intéressantes à vous communiquer.

26-1. Les tableaux

Type tableau statique
Sur la plate-forme .NET, le type tableau statique est implémenté en utilisant le type System.Array. La disposition en mémoire est donc déterminée par le type System.Array.

Type tableau dynamique
Sur la plate-forme .NET, les tableaux dynamiques et les paramètres tableaux ouverts sont implémentés en utilisant le type System.Array. Comme avec les tableaux statiques, la disposition en mémoire est déterminée par le type System.Array.

Vous trouverez les déclarations des tableaux dynamiques les plus utilisés dans l'unité Borland.Vcl.Types.pas :

 
Sélectionnez

  TIntegerDynArray      = array of Integer;
  TCardinalDynArray     = array of Cardinal;
  TWordDynArray         = array of Word;
  TSmallIntDynArray     = array of SmallInt;
  TByteDynArray         = TBytes;
  TShortIntDynArray     = array of ShortInt;
  TInt64DynArray        = array of Int64;
  TLongWordDynArray     = array of LongWord;
  TSingleDynArray       = array of Single;
  TDoubleDynArray       = array of Double;
  TBooleanDynArray      = array of Boolean;
  TStringDynArray       = array of string;
  TWideStringDynArray   = array of WideString;

De nombreuses méthodes du FCL renvoient un tableau dynamique, telle que la méthode Split de la classe Regexe : public string[] Split(string input);
Voici comment utiliser cette méthode sous Delphi .NET :

 
Sélectionnez

Function Splitter(Chaine:String):TStringDynArray;
Begin
 Regexp:=Regex.Create('\d+');
 Result:=Regexp.Split(Chaine);
end;

La déclaration suivante n'étant pas autorisée Function Splitter(Chaine:String): Array of String;, on doit donc déclarer un type.

Aide de Delphi 2005 : Les tableaux multidimensionnels alloués dynamiquement.

26-2. Types caractères et chaînes

Sous .NET les types Char et String sont implémentés différemment.
Pour rappel les types PChar et PWideChar ne sont plus supportés dans du code managé sous .NET.

26-2-1. Types caractères

Extrait de l'aide en ligne de Delphi:
Sur la plate-forme Win32, les variables de type Char, AnsiChar ou d'un intervalle du type Char sont stockées sous la forme d'un octet non signé (8 bits). Le type WideChar est stocké sous la forme d'un mot non signé (16 bits).
Sur la plate-forme .NET, le type Char correspond au type WideChar.

"Set of char" de Win32 définit un ensemble sur l'étendue entière du type Char. Puisque Char est un type dimensionné en octet dans Win32, cela définit un ensemble de taille maximum contenant 256 éléments. Dans .NET, Char est un type dimensionné en mot, et cette étendue (0..65535) dépasse la capacité du type ensemble.

Projet : ..\DotNetString\SetOfChar

Voici un exemple :

 
Sélectionnez

type
 EnsembleChar1= set of char;      // Provoque l'avertissement (W1050)
                                  // Le compilateur traite l'expression en "set of AnsiChar".

 EnsembleChar3= set of Widechar;  // Provoque l'avertissement (W1050)
                                  // Le compilateur traite l'expression en "set of AnsiChar".

 EnsembleChar2= set of Ansichar;  // Pas d'avertissement.

var SetChar: EnsembleChar1;

begin
 SetChar:=['a'];
end.

Voir aussi :
System.Char. Local.

26-2-2. Types chaînes

Extrait du SDK .NET :

 
Sélectionnez

Une chaîne est une collection séquentielle de caractères Unicode, servant généralement à représenter du texte, 
tandis qu'un String est une collection séquentielle d'objets System.Char représentant une chaîne. La valeur de 
String correspond au contenu de la collection séquentielle et elle est immuable.

Un String est appelé « immuable » parce que sa valeur ne peut pas être modifiée après sa création. Les méthodes 
qui semblent modifier une instance de String retournent en fait un nouveau String contenant la modification. 

Extrait de l'aide en ligne de Delphi :
Sur la plate-forme Win32, AnsiString, appelé parfois chaîne longue, est le type le mieux adapté aux utilisations les plus diverses. WideString est le type de chaîne privilégié de la plate-forme .NET.
Sur la plate-forme .NET, le type string correspond à la classe System.String (local). Vous pouvez utiliser des chaînes de caractères mono-octets sur la plate-forme .NET, mais vous devez les déclarer explicitement comme appartenant au type AnsiString.
Sur la plate-forme Win32, vous pouvez utiliser la directive {$H-} pour transformer string en ShortString. La directive {$H-} est dépréciée sur la plate-forme .NET.


Une String est un type référence particulier sous .NET. Vous n'avez pas à appeler son constructeur explicitement. Le type String qui est intégré au CLR pour des raisons de performance construit les objets String de manière spéciale. Une simple affectation S:='test' est traduit en code IL non pas par un appel à newobj mais par ldstr "test".
Je ne peux que vous recommander la lecture de l'ouvrage de Jeffrey Ritcher cité plus loin qui aborde dans le détail les problématiques liées aux traitements des chaînes sous .NET.
Sinon une lecture attentive des informations disponible dans le SDK, par exemple : Intern (local) ou les différentes méthodes de comparaison qui comportent quelques finesses d'utilisation.

26-2-3. StringBuilder

Projet : ..\DotNetString\StrBuilder

A la différence du type String qui est immuable, la Classe System.Text.StringBuilder (local) est une chaîne mutable et permet d'améliorer les performances lors de manipulation de chaînes, par exemple dans une boucle.
En interne un StringBuilder manipule un tableau de char, sans provoquer d'allocation de nouveau objet sur le tas, et renvoie via l'appel de la méthode ToString une référence sur ce tableau.

Voici un exemple d'utilisation :

 
Sélectionnez

Function Splitter(Chaine:String):String;
var StrItere : String;
    ChaineSB : StringBuilder;
Begin
 Regexp:=Regex.Create('\d+');
  //Construit le StringBuilder, préallocation de la taille
 ChaineSB:=StringBuilder.Create(80);

 For StrItere in Regexp.Split(Chaine) Do
   //Ajoute un mot à la chaine existante
  ChaineSB.Append(StrItere+' ');

  // Renvoi d'un type chaine par le StringBuilder.
 Result:=ChaineSB.ToString;
end;

Projet : ..\DotNetString\TestConcatenation

L'exemple TestConcatenation propose une simple concaténation de chaînes. Il permet de visualiser les temps de réponse de différentes solutions.

Un enchaînement d'opérations intéressant :

 
Sélectionnez

uses
  SysUtils,
  System.Text;

var
  StrBuild : System.Text.StringBuilder;
  Chaine   : String;

begin
  StrBuild:=StringBuilder.Create(30);
   //Formate une chaine
  Chaine:=StrBuild.AppendFormat('{0} {1}','Jeffrey','Richter').ToString;
  Writeln(Chaine);

   // Remplace un caractère
  Chaine:=StrBuild.Replace(' ','-').ToString;
  Writeln(Chaine);

   // Efface des caractères
  Chaine:=StrBuild.Remove(4,3).ToString;
  Writeln(Chaine);
  Readln;

   // Raz du contenu
  StrBuild.Remove(0,StrBuild.Length);

   // Une seule opération
   // Ici chaque méthode renvoie une référence au même objet StringBuilder
  Chaine:=StrBuild.AppendFormat('{0} {1}','Jeffrey','Richter').Replace(' ','-').Remove(4,3).ToString;
  Writeln(Chaine);
  Readln;
end.

Définition des codes d'information de culture. Local.

26-3. les fichiers

Consultez l'article utilisation des fichiers typés sous Delphi .NET de Nono40.

26-4. Exceptions

Le principe des exceptions est identique à celui sous Win32 par contre sous .NET de nouvelles exceptions sont déclarées :
Les exceptions sous .NET. Local.

La hiérarchie des exceptions, chaque entrée peut en contenir d'autres. Local.
Classes d'exceptions communes. Local.

26-5. Les messages d'erreurs dus à l'absence du Framework .NET

Si le runtime .NET n'est pas installé sur une machine sur laquelle on exécute un programme compilé en mode managé, le système affichera un message selon la plate-forme.
Sous MS-DOS (Delphi 2005)

 
Sélectionnez

This program cannot be run in DOS mode.

ou (MS-C#)

 
Sélectionnez

This program must be run in Win32.

Sous Windows98

Image non disponible
RunTime .NET absent

27. Liens

Documents sur Delphi pour .NET :
Quelques chapitres sur le livre Delphi 8 pour .NET.
Cours sur DotNet, notamment ASP ( lien direct sur un fichier au format .Pdf, 13 Mo ).
Conversion du C# vers Delphi .NET
Un chapitre complet du livre "Essential Delphi 8 for .NET" par Marco Cantù.
Documentation complète des ajouts dans Delphi 2005.

Rubrique Livre .NET
Je vous recommande la lecture des 3 livres suivants qui m'a permis de vous donner de nombreuses informations qu'il aurait été difficile de retrouver à moins d'une lecture passionnante du SDK.
.NET de Dick lantim chez Eyrolles.
Formation à C# de Tom Archer chez MS-Press.
Programmer Microsoft .NET Framework de Jeffrey Ritcher chez Dunod/MS press.

Outils :
John Colibri:
Générateur de .BAT DCCIL : un outil qui génère le fichier batch pour compiler les projets Delphi 7, Delphi 8 et Delphi 2005 avec le compilateur en ligne DCCIL.
Le fichier .BDSPROJ pour de Delphi .Net : présentation du contenu du fichier de configuration .BDSPROJ utilisé par Delphi 7, Delphi 8 et Delphi 2005 et analyse du fichier .XML en Delphi.

Une console intégrée à l'explorer (Merci à Pascal Jankowski).

Sources :
Code source .NET (1).
Code source .NET (2).
Delphi2005 CD partner.

Aide à la migration :
Borland Delphi 2005 Migration to .NET using VCL for .NET
Table de correspondance Microsoft Win32 vers le framework .NET.

Annex D, Class Library Design Guidelines, dans le fichier %NetSamplePath%Tool Developers Guide\docs\Partition V Annexes.doc

Site :
Delphi.Net Basics.


précédentsommaire

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Laurent Dardenne. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.