XIX. Classes▲
XIX-A. IL Dasm▲
IL Dasm est un désassembleur de code IL (Common Intermediate Language). Il permet, entre autres, 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 :
%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
XIX-B. Nouveaux spécificateurs de visibilité pour les membres de classe▲
Projet : ..\Visibilite-Membre\ControleAcces
Delphi pour .NET ajoute deux 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 pas 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.
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'instances de cette classe.
XIX-C. 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 :
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 :
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 :
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 :
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
.
XIX-D. 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
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 occurrence 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 occurrence 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
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).
XIX-E. 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 :
E2355 : L'accesseur de la propriété de classe doit être un champ de classe ou une méthode statique de classe.
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 :
begin
TMaClass.X:=9
;
TMaClass.y:=7
;
Writeln(TMaClass.X.ToString+' '
+TMaClass.Y.ToString);
Readln;
end
.
XIX-F. 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 :
type
MaClasse=Class
UnChamp :Integer
;
Constructor
Create(I:Integer
);
end
;
Constructor
MaClasse.Create(I:Integer
);
begin
UnChamp:=I;
end
;
provoque l'erreur de compilation :
E2304 : 'Self' n'est pas initialisé. Un constructeur hérité doit être appelé.
L'appel doit donc être :
Constructor
MaClasse.Create(I:Integer
);
begin
inherited
Create;
UnChamp:=I;
end
;
À la différence de Delphi Win32, il n'est pas possible d'appeler un constructeur en utilisant une référence d'objet :
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 :
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.
XIX-F-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:
M2:=M1.MemberwiseClone as
MaClassDerive;
E2363 : Seules les méthodes des types descendants peuvent accéder au symbole protégé
[mscorlib]Object.MemberwiseClone au travers des limites d'assemblage.
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.
XIX-F-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 :
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:
...
// 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);
XIX-G. 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é.
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 :
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 :
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 :
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
XIX-H. 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 :
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 :
E2353 : Impossible d'étendre la classe sealed 'TSealedClass'
À noter que les enregistrements (Record), de type valeur, sont scellés implicitement.
XIX-I. 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 :
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 :
E2402 : Construction de l'instance de la classe abstraite 'TAbstractClass'.
Il n'est pas possible de déclarer une classe abstract sealed :
type
TAbstractClass= class
abstract
sealed
(TObject)
ChampExistant : integer
;
procedure
FaitqqChose;
end
;
Provoque l'erreur suivante :
E2383 : ABSTRACT et SEALED ne peuvent être utilisés ensemble.
XIX-J. 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.
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 :
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.
XIX-K. 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 œuvre 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.
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
.
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.
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.
XIX-L. 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.
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 :
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
;
XIX-M. 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.
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
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 :
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.
Projet : ..\Classe\MetaClasse\Meta2
Ajoutons la déclaration d'une métaclasse et du code associé :
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 :
Voici la déclaration de TAliasTypeBase dans l'unité Borland.Delphi.System :
// used to declare class reference type name.
TAliasTypeBase = class
end
;
L'appel de la méthode TestIt sera compilé ainsi
procedure
TestIt(&class
: Meta2.TTest/@MetaTTest );
begin
&class
.@Create.Test;
end
;
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.
Voici la définition de la classe TObjectHelper :
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.
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.
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 :
_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écifique 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.
XIX-M-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étadonnées. Le terme de RTTI est certes inélégant, mais bien moins présomptueux ;-)
Introduction au système de réflexion sous .NET
XIX-M-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.
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 :
unObjet.ClassName=TTest
L'utilisation de l'opérateur TypeOf se fait de la manière suivante :
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 :
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 :
// 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 :
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 chaine de caractère :
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 chaine 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 chaine 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 :
FullName de MetaClasse_TTest après récupération du type via une chaine 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.
XIX-N. Destructeur▲
À 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.
XX. 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.
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 deux 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 :
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 :
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 !
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.
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.
XX-A. Destructeur▲
La déclaration d'un destructeur dans un Record provoquera l'erreur de compilation suivante :
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.
XX-B. 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).
XX-C. 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 doive 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é.
XXI. 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éfini sur une classe ou un enregistrement.
À la différence du C#, le code suivant n'est pas possible, les opérateurs true et false n'étant pas implémentés :
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 :
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 :
A,Z : integer
Begin
À:= 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. À 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.
XXI-A. 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 :
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 :
class
operator Name._de_type. Signature _de_déclaration (a: type
): Type_résultant;
Exemples d'opérateurs surchargés pour un enregistrement :
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ée
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 :
// Opérateur ET ( AND ) logique. Renvoie
// dbFalse si l'une des opérandes est égale à dbFalse,
// dbNull si l'un des opérandes est égal à 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
;
XXII. 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 :
program
Initialisation;
{$APPTYPE CONSOLE}
uses
SysUtils,
UInit in
'UInit.pas'
;
begin
Writeln(VariableGlobale.ToString);
Readln;
end
.
et voici le corps de l'unité :
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 côté du code généré par le compilateur il en va autrement :
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 deux 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 :
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é :
La méthode Finalization contient bien le code de 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 :
XXIII. 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:
...
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 :
...
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.
XXIII-A. 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 ?
// 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 :
// 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étadonné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.
XXIII-B. Association▲
Dans les 2 exemples de code précédents, vous pouvez remarquer que le constructeur déclare 2 paramètres :
constructor
Create(ADocumentee,ATestee: Boolean
);
Correspondant à l'utilisation suivante, où nous utilisons les paramètres de position qui sont obligatoires :
[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.
// 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 :
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode=True
)]
[FileIOPermission(SecurityAction.LinkDemand, Unrestricted=True
)]
TIniFile = class
(TCustomIniFile)
L'écriture suivante est permise :
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode=True
),
FileIOPermission(SecurityAction.LinkDemand, Unrestricted=True
)]
TIniFile = class
(TCustomIniFile)
XXIII-C. 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 :
// 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 :
[TValidationAttribute(False
,False
)] // Attribut sur la classe
TMaClasse=class
[TValidationAttribute(False
,False
)] // Attribut sur un champ
UnChamp : Integer
;
provoquera l'erreur de compilation suivante :
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 :
[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és. 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 :
[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.
XXIII-D. 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 :
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 :
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…
XXIII-E. Les spécificateurs d'attribut▲
Projet : ..\Attribut\Attribut3
Dans certains 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 :
[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 :
// 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 :
[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
;
XXIV. 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 :
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.
XXIV-A. Exemples▲
Créons trois unités factices afin de visualiser cette notion. La première contient une classe :
unit
Developpez.Tools.TClasse;
interface
type
TClasse=Class
ChampInt :Integer
;
end
;
implementation
end
.
La seconde contient un enregistrement :
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 autres, d'utiliser du code devant fonctionner sur différentes plateformes :
unit
UGeneric;
interface
type
TMultiPlateforme=Class
TestPascal:String
;
end
;
implementation
end
.
Projet : ..\EspaceNommage\DemoEspaceNommage.bdsproj
Compilons le programme factice suivant :
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
Nous retrouvons donc nos trois 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 :
Voici trois recommandations concernant les espaces de nommage :
- Utilisez toujours des packages.
- Référencez les types par leur nom complet (Fully Qualified Name).
- 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é.
XXV. 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 deux assemblages, seuls les données membres de type Public sont accessibles; Entre deux 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 :
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
À 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.
XXV-A. 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 autres au CLR de reconnaître une modification malveillante dans un assemblage.
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)].
XXV-B. 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.
// 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.
XXV-C. 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 :
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.
XXV-D. Suppression de l'assemblage du GAC▲
Pour supprimer un assemblage vous pouvez le faire directement dans l'explorateur de fichier ou avec GacUtil :
GacUtil /u nom_de_l_assemblage
XXV-E. 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 :
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
XXVI. Divers▲
Vous trouverez ici des informations diverses qui faute de temps ne sont pas suffisamment développées, mais qui m'ont paru intéressantes à vous communiquer.
XXVI-A. 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 :
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, telles que la méthode Split de la classe Regexe : public string[] Split(string input);
Voici comment utiliser cette méthode sous Delphi .NET :
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.
XXVI-B. Types caractères et chaines▲
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.
XXVI-B-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 :
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.
XXVI-B-2. Types chaines▲
Extrait du SDK .NET :
Une chaine 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 chaine. 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 chaine longue, est le type le mieux adapté aux utilisations les plus diverses. WideString est le type de chaine 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 chaines de caractères monooctets 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 chaines 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.
XXVI-B-3. StringBuilder▲
Projet : ..\DotNetString\StrBuilder
À la différence du type String qui est immuable, la Classe System.Text.StringBuilder (local) est une chaine mutable et permet d'améliorer les performances lors de manipulation de chaines, par exemple dans une boucle.
En interne un StringBuilder manipule un tableau de char, sans provoquer d'allocation de nouvel 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 :
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 chaines. Il permet de visualiser les temps de réponse de différentes solutions.
Un enchaînement d'opérations intéressant :
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
.
XXVI-C. les fichiers▲
Consultez l'article utilisation des fichiers typés sous Delphi .NET de Nono40.
XXVI-D. 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.
XXVI-E. 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)
This program cannot be run in DOS mode.
ou (MS-C#)
This program must be run in Win32.
Sous Windows98
XXVII. 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.