Le langage Delphi pour .NET

Laurent Dardenne

Vous trouverez dans cet article les bases sur les nouveautés du langage Delphi sous .NET qui vous permettront de comprendre au mieux ces possibilités.

J'ai abordé ce sujet plus sous l'angle du comment ça marche que sous l'angle du comment utiliser ces nouveautés, je laisse donc à autrui le soin de traiter cet autre aspect des choses.

La connaissance du langage Delphi est un prérequis.

Je tiens à remercier Pierre Castelain pour ses corrections orthographiques et relectures attentives. ♪

- Titre : Le langage Delphi pour .NET

- Auteur : Laurent Dardenne

- Parution : 29 mars 2005

- Mise à jour : 6 mars 2017

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

Sommaire

- I. Public concerné

- I-A. Les sources

- II. Aperçu de la plate-forme .NET

- II-A. Quelques détails sur le framework .NET

- III. Introduction

- IV. Les pointeurs n'existent plus sous DotNet ?

- V. Type primitif

- VI. Value Types et Reference Types

- VI-A. Principales fonctionnalités des types valeur

- VI-B. Un peu de pratique

- VII. Conversion

- VII-A. Boxing

- VII-B. UnBoxing

- VII-C. L'allocation mémoire

- VIII. Passage de paramètres

- VIII-A. Paramètre sans type

- IX. Déclarations de types imbriqués dans les classes

- IX-A. Visibilité des types imbriqués.

- X. Alias de type

- XI. Identificateurs étendus

- XII. Itérateurs

- XIII. Directives de compilation

- XIII-A. Autobox

- XIII-B. Experimental

- XIV. Compilation conditionnelle

- XV. Commutateurs de la ligne de commande de l'IDE

- XVI. Ce qui est déconseillé

- XVII. Les délégués

- XVII-A. La diffusion multiple

- XVII-B. Gestionnaire d'événements sous la VCL

- XVII-C. Gestionnaire d'événements sous Windows Form

- XVIII. Syntaxe d'assistance de classe ( Class Helper )

- XIX. Classes

- XIX-A. IL Dasm

- XIX-B. Nouveaux spécificateurs de visibilité pour les membres de classe

- XIX-C. Champ de classe

- XIX-D. Méthode statique de classe

- XIX-E. Propriété de classe

- XIX-F. Constructeur d'instance de type référence

- XIX-F-1. « Constructeur de copie »

- XIX-F-2. Création d'objet de la FCL

- XIX-G. Constructeur de classe

- XIX-H. Classe scellée (sealed)

- XIX-I. Classe abstraite

- XIX-J. Méthode final

- XIX-K. Méthode imbriquée

- XIX-L. Surcharge de propriété indexée

- XIX-M. Métaclasses

- XIX-M-1. System.Reflection

- XIX-M-2. Opérateur TypeOf

- XIX-N. Destructeur

- XX. Record

- XX-A. Destructeur

- XX-B. Quand utiliser un Record

- XX-C. Quand ne pas utiliser un Record

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

- XXI-A. Déclaration des surcharges d'opérateurs

- XXII. Initialisation et finalisation d'unité

- XXIII. Attributs .NET

- XXIII-A. Création

- XXIII-B. Association

- XXIII-C. Limiter les associations à certains éléments

- XXIII-D. Utilisation

- XXIII-E. Les spécificateurs d'attribut

- XXIV. Espaces de nommage

- XXIV-A. Exemples

- XXV. Assemblage

- XXV-A. Assemblage partageable et nom fort

- XXV-B. Définition d'attributs spécifiques

- XXV-C. Ajout de l'assemblage dans le GAC

- XXV-D. Suppression de l'assemblage du GAC

- XXV-E. Visualisation du nouvel assemblage dans l'EDI

- XXVI. Divers

- XXVI-A. Les tableaux

- XXVI-B. Types caractères et chaines

- XXVI-B-1. Types caractères

- XXVI-B-2. Types chaines

- XXVI-B-3. StringBuilder

- XXVI-C. les fichiers

- XXVI-D. Exceptions

- XXVI-E. Les messages d'erreurs dus à l'absence du Framework .NET

- XXVII. Liens

I - Public concerné

Testé sous le framework .NET 1.1 et Delphi 2005.
Version 1.0

Concernant l'EDI de Delphi 2005 vous pouvez consulter l'article de SJRD : Prise en main de Delphi 2005.

Cet article contient des liens nommés Local. Ils pointent sur des URL locales de la documentation de Delphi 2005 et de Microsoft. Leur utilisation nécessitera l'installation du SDK .NET v1.1 en français sur le poste.

Ils sont uniquement accessibles Offline, les liens 'ms-help' exécutent en local l'aide en ligne du SDK.

I-A - Les sources

Les fichiers sources des différents exemples :
FTP.
HTTP.

L'article au format .PDF.

II - Aperçu de la plate-forme .NET

Extrait du SDK :
"Le .NET Framework est une nouvelle plate-forme informatique qui simplifie le développement d'applications dans l'environnement fortement distribué d'Internet. Le .NET Framework est conçu pour remplir les objectifs suivants :

- Fournir un environnement cohérent de programmation orientée objet que le code objet soit stocké et exécuté localement, exécuté localement, mais distribué sur Internet ou exécuté à distance.

- Fournir un environnement d'exécution de code qui minimise le déploiement de logiciel et de conflits de versioning.

- Fournir un environnement d'exécution de code qui garantit l'exécution sécurisée de code y compris le code créé par un tiers d'un niveau de confiance moyen ou un tiers inconnu.

- Fournir un environnement d'exécution de code qui élimine les problèmes de performance des environnements interprétés ou écrits en scripts.

- Fournir au développeur un environnement cohérent entre une grande variété de types d'applications comme les applications Windows et les applications Web.

- Générer toutes les communications à partir des normes d'industries pour s'assurer que le code basé sur le .NET Framework peut s'intégrer à n'importe quel autre code.

Le .NET Framework contient deux composants principaux : le Common Language Runtime et la bibliothèque de classes du .NET Framework. Le Common Language Runtime est la base du .Net Framework. Le runtime peut être considéré comme un agent qui manage le code au moment de l'exécution, fournit des services essentiels comme la gestion de la mémoire, la gestion des threads, et l'accès distant. Il applique également une stricte sécurité des types et d'autres formes d'exactitude du code qui garantissent un code sécurisé et robuste. En fait, le concept de gestion de code est un principe fondamental du runtime. Le code qui cible le runtime porte le nom de code managé, tandis que le code qui ne cible pas le runtime porte le nom de code non managé. La bibliothèque de classes, l'autre composant principal du .NET Framework, est une collection complète orientée objet, de types réutilisables que vous pouvez utiliser pour développer des applications allant des traditionnelles applications à ligne de commande ou à interface graphique utilisateur (GUI, Graphical User Interface) jusqu'à des applications qui exploitent les dernières innovations fournies par ASP.NET, comme les services Web XML et Web Forms.
…"
Lire la suite. Local.

II-A - Quelques détails sur le framework .NET

Le Runtime
Il se situe dans le répertoire C:\WINDOWS\Microsoft.NET\Framework.

Le SDK
Il se situe dans C:\Program Files\Microsoft.NET\SDK\v1.1\, n'hésitez pas à le parcourir il contient évidemment de nombreux outils, sources et informations diverses.

Le GLobal Assembly Cache (GAC)
Le GAC remplace selon Microsoft la registry pour la centralisation des informations d'assemblage de .NET.
Il se situe dans le répertoire %Windir%\assembly sa visualisation dans l'exploreur se fait d'une manière différente des autres répertoires, ici l'exploreur charge la DLL Shfusion.dll qui est une extension du shell Windows (local) permettant de visualiser son contenu. Bien évidemment il est possible de le visualiser en 'mode fichier' dans une console Windows.

Attention la manipulation du GAC doit se faire au travers d'outils appropriés, voir le cache de l'assembly global. Local.

À noter que certaines clés de registre référencent des entrées du GAC :
HKEY_CLASSES_ROOT\Installer\Assemblies et HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer\Assemblies.

Vous trouverez dans cette clé HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework le répertoire d'installation du SDK et du Runtime

Recopiez, dans le répertoire contenant le SDK .NET, le script « …\Microsoft.NET\SDK\v1.1\Bin\sdkvars.bat » dans le répertoire C:\Windows. Vous pourrez ainsi déclarer les variables d'environnement selon les besoins.

III - Introduction

De nouvelles fonctionnalités ont été ajoutées au langage Delphi pour .NET afin de supporter des concepts et des fonctionnalités de programmation propre à la plate-forme .NET, et ce tout en respectant la CLS (Common Language Specification).

Cette adaptation pouvant laisser croire qu'il s'agit d'un nouveau langage alors qu'en fait ce qui est nouveau c'est la plate-forme .NET, c'est une évidence qu'il est bon de rappeler, votre premier objectif étant d'abandonner certaines pratiques et habitudes liées à la programmation sous Win32, qui à mon sens sont un frein à la compréhension de .NET. N'essayez donc pas de faire du WIN32 sous .NET, mais ceci est plus facile à dire qu'a faire ;-).

Selon moi, un des points importants est qu'étant donné que sous .NET tout est objet vous devez 'penser' objet, plus encore que sous Win32.
Bien évidemment les problématiques telles que la gestion des threads ne changent pas ( .NET s'appuyant sur WIN32 ), seule leur implémentation diffère.

J'aborderais ici ces nouveautés du langage ainsi que les différences de comportement les plus sensibles. Ces quelques informations permettront aux débutants sous Delphi pour .NET, d'appréhender au mieux les bases de .NET et du langage Delphi pour .NET. Pour simplifier, j'ai principalement utilisé des applications console dans les exemples proposés.
Cette introduction n'aborde pas les problématiques de migration de code Win32 vers .NET, consultez à ce sujet l'ouvrage d'Olivier Dahan ou les articles appropriés sur le site de Borland US.

De nombreuses informations issues du SDK .NET émailleront cet article, autant puiser à la source.

IV - Les pointeurs n'existent plus sous DotNet ?

Sous Dot Net la manipulation d'adresse n'est plus possible dans du code managé étant donné que l'organisation de la mémoire est désormais du domaine exclusif du Garbage Collector (ramasse-miettes) et plus du programmeur.
Cette prise en charge de la mémoire par le Garbage Collector implique une réorganisation périodique de la mémoire, on ne peut donc plus avoir de référence fixe sur des zones mémoire.

La manipulation de pointeur reste possible, mais dans ce cas votre code est considéré comme unsafe, non sécurisé. Ceci pouvant avoir comme conséquence de limiter ou d'interdire l'intégration de ce code dans d'autres applications .NET.

Voir aussi
Garbage Collector : Le GC est un sous-ensemble de la CLR. Local.

Le Common Language Runtime (CLR) gère la mémoire, l'exécution des threads, l'exécution du code, la vérification de la sécurité du code, la compilation et d'autres services du système. Ces fonctionnalités font partie intégrante du code managé qui s'exécute sous le Common Language Runtime. Local.

L'article sur les pointeurs sous .NET par Nono40.

Certains liens peuvent ne pas s'afficher directement, dans ce cas valider à nouveau l'URL dans votre browser.

V - Type primitif

Les types primitifs sous .NET sont Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Char, Double et Single.
Ils correspondent directement aux types qui existent dans le Framework .NET

Type Delphi

Mappage de types .NET

Boolean

Boolean

Shortint

SByte

Byte

Byte

Smallint

Int16

Word

UInt16

Longint

Int32

Integer

Int32

Longword

UInt32

Int64

Int64

Cardinal

UInt32

Char

Char

Single

Single

Double

Double

VI - Value Types et Reference Types

Les allocations de blocs de données, par exemple, se font désormais soit dans la pile soit sur le tas (Managed Heap). Si on ne manipule plus d'adresse comment dans ce cas passer en paramètre un objet sans avoir à recopier l'intégralité de son contenu sur la pile ?

Sous .Net les données sont de deux types :

1. Type valeur
Les types valeur contiennent directement leurs données; les instances de types valeur sont allouées sur la pile.

2. Type référence
Les types référence stockent une référence à l'adresse mémoire de la valeur et sont alloués sur le tas managé.

Je tiens à remercier Emerica qui m'a proposé ce graphique, m'évitant ainsi des explications des plus hasardeuses sur le sujet et confirmant par là même qu'un dessin vaut mieux qu'un long discours.

Hiérarchie des types sous Dot NET.

Les variables de type valeur stockent des données alors que les variables de type référence stockent les références aux données. Les types référence sont également considérés comme des objets comme on peut le voir sur le schéma précédent.

On voit donc que la notion d'indirection persiste sous .Net, mais n'a plus rien à voir avec les pointeurs présent sous Win32, Win16 ou MS-DOS. La possibilité d'interpréter des données pointées, c'est-à-dire la possibilité de transtyper des données, est également affectée. On retrouve donc la notion de typage fort, chère au Pascalien.

Il faut savoir que sous .NET tout est objet. La classe System.Object prend en charge toutes les classes de la hiérarchie des classes du .NET Framework et fournit des services de bas niveau aux classes dérivées. Il s'agit de la classe de base fondamentale parmi toutes les classes du .NET Framework. Elle constitue la racine de la hiérarchie des types.

Voir Aussi
Vue d'ensemble du système de type commun. Local.
Qu'est-ce que la spécification CLS (Common Language Specification) ? Local.

VI-A - Principales fonctionnalités des types valeur

- Une variable d'un type valeur contient toujours une valeur de ce type.

- L'assignation d'un type valeur à une variable entraîne la création d'une copie de la valeur assignée alors que l'assignation d'un type référence à une variable crée une copie de la référence, mais pas de l'objet référencé.

- Tous les types valeur sont dérivés implicitement de la classe .NET System.Object.

- Les variables d'un type valeur sont généralement des types primitifs et assignées sur la pile.

- Contrairement aux types référence, il n'est pas possible de dériver un nouveau type à partir d'un type valeur existant. En revanche, comme les types référence, les types Record peuvent implémenter des interfaces.

- Un type valeur ne peut pas contenir de valeur null/NIL, à la différence d'un type référence.

- Chaque type valeur possède implicitement un constructeur par défaut qui initialise la valeur par défaut du type associé.

- Puisqu'un type de valeur contient une copie de ses données, la modification d'un type de valeur modifiera seulement cet exemple particulier.

- Cependant, la modification d'un type de référence modifiera toutes les instances qui ont une référence à ce type.

Si vous souhaitez créer un nouveau type par valeur, vous devez le dériver de System.ValueType, c'est-à-dire à partir d'un Record sous Delphi .NET. Les classes étant utilisées pour créer de nouveau type par référence.

VI-B - Un peu de pratique

Projet : ..\TypeValue-Ref\ValeurReference

Nous aborderons plus avant le détail des nouvelles fonctionnalités associées au type de donnée Record.
Chargez le projet et regardons ici la modification d'un type valeur :

Type
monRecord=Record
À :Integer ;
B :Integer ;
Constructor Create(p1,p2:integer);
end ;

Constructor monRecord.Create(p1,p2:integer);
begin
A:=p1;
B:=p2;
end;

var
RInstance1, RInstance2 : monRecord;

Begin
RInstance1:= monRecord.Create(5, 3) ; // Création d'un objet.
// L'assignation directe des champs par RInstance1.A 9 est possible.
RInstance2:= RInstance1; // Recopie de l'objet RInstance1 dans RInstance2
RInstance1.A:= 7; //  Le champ RInstance2.A reste égale à 2.

Writeln('RInstance1.A=',RInstance1.A);
Writeln('RInstance2.A=',Rinstance2.A);
Readln;
End;

On constate la recopie membre par membre de RInstance1 vers RInstance2. Dans cet exemple nous avons bien deux instances différentes.
Passons à la modification d'un type référence :

Type
maClasse=Class
À :Integer ;
B :Integer ;
Constructor Create(p1,p2:integer);
end ;

Constructor maClasse.Create(p1,p2:integer);
begin
inherited Create;
A:=p1;
B:=p2;
end;

var
CInstance1, CInstance2 : maClasse;

Begin

CInstance1:= maClasse.Create(2, 9) ; // Création d'un objet
CInstance2:= CInstance1; // copie la référence d'Instance1 dans CInstance2
CInstance1.A:= 1; //  Le champ Instance2.A dans ce cas est aussi égal à 1.

Console.Writeline; // Appel d'une classe du Framework .NET
// Alias FCL ( Framework Classe Library)
Writeln('CInstance1.A=',CInstance1.A);
Writeln('CInstance2.A=',Cinstance2.A);
Console.readline;
End;

Dans cet exemple on constate que CInstance1 référence bien CInstance2, nous avons bien deux instances identiques.
Sous Delphi .NET on voit donc qu'une classe est un type référence alors qu'un record est un type valeur.

Allons plus loin en mixant les deux types et essayons de récupérer une référence d'une variable de type valeur.
Ajoutons d'abord la déclaration d'une variable de type TObject qui permet de référencer n'importe quel objet :

var
OTemp: TObject;

Ensuite

OTemp:=RInstance1;     // Récupére une référence sur RInstance1
RInstance1.A:=20;      // Modifie la variable référencée
Writeln('OTemp.A transtypé en monRecord, référence RInstance1.A=',monRecord(OTemp).A);
Writeln('RInstance1.A référencé par OTemp=',RInstance1.A);
Writeln;
Readln;

Nous nous apercevons que le résultat ne correspond pas à nos attentes. Ici l'instruction OTemp:=RInstance1; n'affecte pas l'adresse( la référence ) de la variable RInstance1, mais effectue une copie complète de l'objet RInstance1 dans OTemp avec allocation de la mémoire nécessaire !

Continuons notre démarche, essayons de modifier une variable de type valeur en lui assignant un type valeur :

Writeln('Assignation d''un type valeur avec une variable de type référence'+#13#10);
RInstance1:=MonRecord(OTemp);
Writeln('RInstance1.A est réassigné avec OTemp=',RInstance1.A);
Writeln('OTemp.A=',MonRecord(OTemp).A);
Writeln;
Readln;

Ici aussi il y a deux variables distinctes.
Un type valeur ne pouvant tout simplement pas se transformer en un type référence !
Par contre il est possible de convertir un type valeur en type référence, et inversement, en effectuant une conversion que l'on nomme Boxing et unBoxing.

Dans le source vous trouverez le dernier cas de figure qui concerne l'assignation de type référence qui lui ne pose pas de problème particulier de compréhension.

Par contre vous trouverez cette suite d'instructions qui est plus du 'bricolage' qu'autre chose :

try
OTemp:=Rinstance1;
CInstance1:=maClasse(OTemp);
Writeln('Test de bricolage ',CInstance1.A);
Console.readline;
except
On E: NullReferenceException do
Console.WriteLine('Erreur d''exécution la variable CInstance1 contient une référence nulle !');
end;
end;

Et ceci pour mettre en évidence qu'une variable instanciée de type référence peut se retrouver avec une assignation NULL, assignation qui provoquera l'exception NullReferenceException.

Voir aussi : Système de type commun. Local.

VII - Conversion

Certaines traductions d'ouvrage sur .NET associent le terme boxing à une notion de compartiment.

VII-A - Boxing

Projet : ..\Boxing-unBoxing\Boxing

Le boxing est une conversion explicite ( attention implicite en C# ) d'un type valeur en type objet ou bien vers n'importe quel type interface implémenté par ce type valeur. Lorsqu'une conversion boxing est appliquée à une variable de type valeur, une instance d'un objet est allouée et la valeur de cette variable est copiée dans le nouvel objet.

Examinons ce procédé dans l'exemple suivant :

var VarTypeValeur : integer;     // Type valeur
VarTypeReference : TObject;  // Type objet ( référence )

Begin
VarTypeValeur:= 123;
VarTypeReference:=TObject(VarTypeValeur);  // Boxing explicite
VarTypeValeur:= 456;                       // Change le contenu de i

Writeln('Valeur de VarTypeValeur de type valeur = ', VarTypeValeur);
Writeln('Valeur de VarTypeReference de type object (référence ) = ', VarTypeReference);

L'instruction VarTypeReference:=TObject(VarTypeValeur) réalise explicitement une conversion boxing sur la variable VarTypeValeur. Il ne s'agit pas ici d'un simple transtypage, mais d'une opération à part entière masquée par le compilateur et prise en charge par le CLR.
Le boxing permet donc à chaque chose d'être vue comme un objet (un objet sous .NET étant un type référence).

À partir du moment où tout est objet on peut donc appeler les méthodes de System.Object sur les variables VarTypeValeur et VarTypeReference et particulièrement la méthode GetType qui renvoie de manière littérale le type de l'objet interrogé.

Ajoutons ces quelques lignes au début du programme précédent :

VarTypeReference:=TObject.Create; // L'appel de GetType nécessite l'appel du constructeur

// Gettype renvoie une valeur de type System.Type
Writeln('Nom complet du type de VarTypeValeur= ', VarTypeValeur.GetType);
Writeln('Nom complet du type de VarTypeReference= ', VarTypeReference.GetType);

L'affichage nous renvoie :

Nom complet du type de VarTypeValeur= System.Int32
Nom complet du type de VarTypeReference= System.Object

Ajoutons de nouveau les lignes d'affichage du type, mais cette fois-ci après les modifications et l'affichage du contenu des variables.

Nom complet du type de VarTypeValeur= System.Int32
Nom complet du type de VarTypeReference= System.Int32

La variable VarTypeReference est désormais de type System.Int32, le boxing modifie donc le contenu et le type de l'objet cible.
Je vous laisse vérifier ce que donne l'instruction suivante :

VarTypeReference:=TObject(321.1);

Note :

var w : Word;    // Type valeur
i : integer; // Type valeur
o : TObject; // Type objet ( référence )

Begin
i:= 123;
o:=i;
...

Le code ci-dessus provoquera une erreur de compilation sur la seconde ligne à cause du typage fort de Delphi. Dans ce cas présent, le programmeur peut faire deux choses :
- soit expliciter le transtypage de la variable I vers un TObject :

o := TObject(I);

- soit ajouter la directive du compilateur AUTOBOX, directive qui est à utiliser avec précaution.

L'opération suivante n'est plus autorisée par le compilateur : Word(i) := w;

Voir aussi
Conversion Boxing. Local.

VII-B - UnBoxing

Projet : ..\Boxing-unBoxing\unBoxing

Il s'agit de l'opération inverse au boxing.
L'unboxing est une conversion explicite du type objet en un type valeur ou bien d'un type interface en un type valeur qui implémente l'interface.

Une conversion unboxing comprend les étapes suivantes :

- Vérification de l'instance de l'objet pour s'assurer qu'il s'agit bien d'une valeur convertie du type valeur spécifié.

- Copie de la valeur de l'instance dans la variable de type valeur.

Pour qu'une conversion unboxing vers un type valeur donné soit exécutée correctement, la valeur de l'argument source doit être une référence à un objet existant qui a été créé en effectuant une conversion d'une valeur du même type valeur.
Si l'argument source a la valeur nulle/Nil ou est une référence à un objet non pris en charge, une exception InvalidCastException est levée.

Examinons ce procédé dans l'exemple suivant :

var j,i : integer;
box : TObject;

begin
i:=312;            // Un type valeur
box:=TObject(i);   // Boxing explicite
j:=integer(box);   // Unboxing explicite

Ajoutons une variable de type Byte et le code suivant :

var
h   : byte;
j,i : integer;
box : TObject;

begin
...
H:=byte(box);
Writeln('Nom complet du type de K= ', H.GetType);
Writeln('Valeur de h, de type valeur, = ', h);

L'assignation de H à partir de la variable box provoque l'erreur InvalidCastException à l'exécution.
Pourtant ce transtypage est sensé, mais il faut savoir que l'opération de unboxing doit se faire vers un type identique à celui utilisé lors de l'opération de boxing.

L'opération de transtypage doit donc se faire sur deux niveaux :

- le premier pour récupérer la valeur du type compartimenté.

- le second vers le type final tout en respectant les conventions de conversion de Delphi pour .NET.
Par exemple si le type contenu dans la variable box est de type double cette opération ne peut se faire qu'en utilisant l'instruction Trunc

var h   : Byte;
box : TObject;
K   : Double;

begin
...
H:=byte(integer(box));
...

K:=3.12;
box:=TObject(K);   // Boxing explicite
h:=byte(Trunc(Double(box)));     // Transtypage des types réels en entiers nécessite Trunc

Voir aussi
Conversion unBoxing. Local.

VII-C - L'allocation mémoire

Projet : ..\AllocationArrayList\AllocationArrayList

À la différence de Win32, et en dehors de la création d'objets par son constructeur, il n'est plus nécessaire d'allouer explicitement de la mémoire pour manipuler un type enregistrement ou un type de base (nous verrons plus tard les constructeurs de type).

L'exemple suivant, en utilisant le boxing, nous le confirme :

//AllocationArrayList
uses
System.Collections;

Type
Data=Record
X,Y : Integer;
end;

var D   : Data;
Tab : Arraylist;
i   : Integer;

begin
Tab:=Arraylist.Create;

For i:=0 to 10 do
begin
D.X:=i;
D.Y:=D.X*5;
Tab.Add(D); // Ajout d'un record dans la liste,
// ne nécessite pas d'allocation mémoire pour les multiples instances de D.
// Box implicite
end;

For i:=0 to 10 do
Writeln('X= '+Data(Tab[i]).X.ToString+' Y= '+Data(Tab[i]).Y.ToString);

Readln;
end.

L'allocation est réalisée lors de l'appel de la méthode Tab.Add, par une opération de boxing qui comme nous l'avons vu :
Lorsqu'une conversion boxing est appliquée à une variable de type valeur, une instance d'un objet est allouée et la valeur de cette variable est copiée dans le nouvel objet.

VIII - Passage de paramètres

Projet : ..\Passage_Parametre\Parametre

Les type valeur et type référence se comportent différemment dans un appel de méthode selon qu'ils sont passés soit en paramètre par valeur soit en paramètre par référence.

L'utilisation de var ou out permet de spécifier le passage de paramètre par référence, le mot clé out permettant de passer en paramètre une valeur qui n'a pas été initialisée.
L'utilisation de const ou l'absence de mot clé permet de spécifier le passage de paramètre par valeur.

Le tableau suivant récapitule les cas :

1. Passage de types valeur par valeur.
Les modifications intervenant au sein de la méthode n'affectent en rien la valeur d'origine de la variable.

2. Passage de types valeur par référence.
La valeur du paramètre est modifiée après l'appel de la méthode.

3. Permutation de types valeur (deux entiers).
Vous devez passer les paramètres à la méthode Swap par référence

4. Passage de types référence par valeur.
La tentative de réassignation du paramètre à un emplacement de mémoire différent s'effectue uniquement au sein de la méthode et n'affecte pas la variable d'origine

5. Passage de types référence par référence.
Toutes les modifications qui interviennent dans la méthode affecteront les variables d'origine dans le programme appelant.

6. Permutation de types référence (deux chaines).
La permutation de chaines est un bon exemple de passage de paramètres de type référence par référence. Les deux chaines sont permutées au sein de la méthode, ainsi que dans l'appelant.

On voit donc que l'initialisation d'une variable globale, si toutefois cette notion a encore un sens sous .NET, se fera ainsi :

Procedure Init_Variable( Out uneRéference: unType);

VIII-A - Paramètre sans type

Projet : ..\Passage_Parametre\ParametreSansType

La déclaration d'un paramètre sans type dans un entête de méthode reste possible sous Delphi .NET . Dans ce cas la variable est vue comme une référence de type TObject à l'intérieur de la méthode. Un transtypage sera donc nécessaire pour la manipuler. De plus il est possible de lui assigner une nouvelle référence, mais la classe de cette nouvelle référence doit être une classe identique ou dérivée, mais pas une classe ancêtre. Ceci étant dû à mon avis au typage fort de .NET.

procedure PrcAvecParametreSansType(var X);
begin
Writeln('Type de la variable X : '+TypeOf(x).ToString);
end;

IX - Déclarations de types imbriqués dans les classes

Vous pouvez consulter l'article suivant concernant les déclarations de type imbriqué.

IX-A - Visibilité des types imbriqués.

Les membres (d'une classe ou d'un enregistrement) des types imbriqués peuvent avoir des visibilités différentes de la classe qui les héberge.
Déclarons une classe imbriquée comme ceci :

type
TForm1 = class(TForm) //Déclaration de classe qui héberge une déclaration de classe imbriqué.
private
{ Déclarations privées}
public
Type
TSousClasse = Class (Tobject) // Classe imbriquée
Private
MembrePrivé : Integer;
Public
MembrePublic : Integer;
End;
{ Déclarations publiques}
public
SousClasse : TSousClasse;
end;

Bien que TSousClasse soit public, de même que son instance (SousClasse), il ne sera pas possible d'accéder en dehors de l'unité à TForm1.SousClasse.MembrePrivé.

procedure TForm2.Button1Click(Sender: TObject);
Var i:Integer;
begin
i:=Form1.SousClasse.MembrePublic; // Compile
i:=Form1.SousClasse.MembrePrivé; // Ne compile pas
end;

Je remercie Nono40 pour cet exemple.

X - Alias de type

Projet : ..\Type\Type_type

Delphi permettait déjà de créer des alias de type tels que

type TMaString = string;

autorisant la déclaration de variable suivante :

var
Chaine1: TMaString;
Chaine2: string;

Ici Chaine1 et Chaine2 sont du même type.
Si on souhaite distinguer le type exact de ces deux variables, cela n'est pas possible, car TMaString est un nom de type différent de string, mais si on veut créer un autre type, vu comme tel par le compilateur, il faut utiliser la syntaxe suivante :

type TMaString = type string;

Dans ce cas le passage en paramètre par référence d'une variable de ce type provoquera une erreur de compilation :

type
TMaString1 = string;
TMaString2 = type string;

var
Chaine: string;
Chaine1: TMaString1;
Chaine2: TMaString2;

procedure Affiche(S:TMaString2);
begin
end;

procedure Affiche_Var(Var S:TMaString2);
begin
end;

begin
Chaine:='Type Chaine classique';
Chaine1:=Chaine;
// l'affectation ne pose pas de problème
Chaine2:=Chaine;

// Le passage en paramètre à une procédure const ou par valeur ne pose pas de problème
Affiche(Chaine1);

// Le passage en paramètre à une procédure var, provoque l'erreur (E2033) :
// Les types des paramètres var originaux et formels doivent être identiques.
Affiche_Var(Chaine1);

Le code précédent provoque l'erreur suivante :

E2033 : Les types des paramètres var originaux et formels doivent être identiques.

XI - Identificateurs étendus

Texte issu de la documentation de DELPHI 2005 :

Lors de la programmation avec Delphi pour .NET, il se peut que vous rencontriez des identificateurs CLR (c'est-à-dire des types ou des méthodes de classe) de même nom qu'un mot clé Delphi. Par exemple, une classe peut posséder une méthode appelée begin. La classe CLR Type de l'espace de nommage System constitue un autre exemple. Type est un mot clé du langage Delphi qui ne peut être utilisé comme nom d'identificateur.

En revanche, vous pouvez sans problème qualifier l'identificateur à l'aide de sa spécification complète de l'espace de nommage. Pour utiliser la classe Type, par exemple, vous devez utiliser son nom complet :

var
TMyType : System.Type; // L'utilisation de l'espace de nommage qualifié
// évite toute ambiguïté avec le mot clé Delphi.

Comme alternative abrégée, Delphi pour .NET propose l'opérateur perluète (&) afin de résoudre les ambiguïtés entre identificateurs CLR et mots-clé du langage Delphi. Si vous rencontrez une méthode ou un type portant le même nom qu'un mot clé Delphi alors que vous avez indiqué le signe & au début de l'identificateur, vous pouvez omettre la spécification de l'espace de nommage.

Par exemple, le code suivant utilise un « et commercial » pour distinguer la classe CLR Type du mot clé Delphi type.

var
TMyType : &Type; // Le mot commence par '&' ce qui le distingue du mot clé.

Un autre exemple dans un contexte de conversion de code C# vers Delphi.

XII - Itérateurs

Vous pouvez consulter l'article suivant concernant les itérateurs.

var I, odd, even : integer;
arr  : Array[] of integer;

Begin
odd:= 0;
even:=0;
arr:= New(array[] of integer,(0,1,2,5,7,8,11));

For I in arr Do
begin
WriteLn('I= '+ intToStr(I));
if (I mod 2 = 0)
then
begin
inc(even);
WriteLn('Trouvé '+ intToStr(even)+' '+intToStr(I));
end
else inc(odd);
end;
WriteLn('Trouvé '+ intToStr(odd)+' chiffre impair et '+intToStr(even)+' chiffre pair.') ;
Readln;

Attention ce qui différencie une boucle classique d'un itérateur est qu'on manipule via la variable d'indice I le contenu de la collection.
La variable I ne contient pas le nombre d'itérations.
Cette syntaxe peut prêter à confusion dans les premiers temps.

Rappel des différentes possibilités :

// Tableau
const
MyArr : Array[0..3] of char = ('1','2','3','4');
var
C : Char;
S : String;
begin
for C in MyArr do
S := S + C;
ShowMessage(S);
end;

// Ensemble
const
MySet: set of char = ['1','2','3','4'];
var
C : Char;
S : String;
begin
for C in MySet do
S := S + C;
ShowMessage(S);
end;

// Chaine de caractère (Strings)
const
S : String = 'C''est une chaine intéressante';
var
C : Char;
cnt: Integer;
begin
cnt := 0;
for C in S do
if C = 'e' then
Inc (cnt);
ShowMessage(Format('Votre chaine contient %d lettres "e"',[cnt]));
end;

// Collection prédéfinie TStringLists
var
S : String;
begin
for S in Memo1.Lines do
ListBox1.Items.Add(S);
end;

Projet : ..\class-iterateur\class_iterateur

L'itération sur une classe demande un peu plus de code :

type
TPeopleEnumerator = class; // Forward déclaration

// Objet manipulé
TPerson = class
Name: String;
DateOfBirth: TDateTime;
Gender : Char;
constructor Create (AName: String; ADateOfBirth: TDateTime; AGender: Char);
end;

// Liste d'objets, conteneur pour le type TPerson
TPeopleList = class
strict private
List : TList;
function GetCount: Integer;
function GetPerson(idx: Integer): TPerson;
procedure SetPerson(idx: Integer; const Value: TPerson);
public
Constructor Create;
function Add (Person: TPerson): Integer;
procedure Delete (idx: Integer);
property Person [idx: Integer] : TPerson read GetPerson write SetPerson; default;
property Count: Integer read GetCount;
//Méthode nécessaire pour la prise en charge de l'itération
Function GetEnumerator: TPeopleEnumerator;
end;

// Afin que la collection soit énumérable, elle doit implémenter une suite de méthodes
// comme indiqué ci-dessous.

// Énumérateur pour la classe conteneur.
TPeopleEnumerator = class
strict private
CurrentPos: Integer;
PeopleList : TPeopleList;
public
constructor Create (AList : TPeopleList);
function GetCurrent: TPerson;
//Membre de classe nécessaire pour la prise en charge de l'itération
function MoveNext :Boolean;    // Méthode publique
property Current : TPerson read GetCurrent; // Propriété en Read Only
end;

Une fois les méthodes implémentées, l'itération de la classe devient possible :

//Exemple d'énumération de la classe TPeopleList
// Le membre PeopleList appartient à la Forme principale
Procedure TForm1.Button1Click(Sender: TObject);
var Person : TPerson; // Doit être une variable locale
begin
// Ajout d'élément
PeopleList.Add(TPerson.Create('Daniel',StrToDate('15/03/1973'),'M'));
PeopleList.Add(TPerson.Create('Monique',StrToDate('05/05/1973'),'F'));

// Itération sur la classe
for Person in PeopleList do
ListBox1.Items.Add(Person.Name);
end;

La variable utilisée pour l'itération doit être de même type que celle déclarée dans la classe cible et cette variable doit être locale à la méthode ou à la procédure.

XIII - Directives de compilation

Vous pouvez consulter l'article suivant concernant les nouvelles directives.

XIII-A - Autobox

{$AUTOBOX ON}

La directive AUTOBOX génère un typage faible. L'employer peut faciliter l'écriture, mais peut également amener plus d'erreurs de programmation.
Son utilisation force le boxing c'est-à-dire qu'elle évite au programmeur les opérations de transtypage nécessaires sans sa présence.

XIII-B - Experimental

Il s'agit d'une directive de conseil. Vous pouvez utiliser cette directive pour désigner des unités qui sont dans un état de développement instable. Le compilateur émet un avertissement quand il construit une application qui utilise l'unité.

unit Borland.Vcl.NetControl platform experimental;

XIV - Compilation conditionnelle

Nom

Définition

VER170

Version du compilateur Delphi 2005. La documentation est erronée.

CLR

Indique que le code sera compilé pour la plate-forme .NET.

WIN32

WIN32 Indique que le système d'exploitation est Win32 API.
Utilisez WIN32 pour distinguer les diverses plates-formes Windows, comme Windows 32 et 64 bits.
En général, ne limitez pas le code à WIN32 sauf si vous êtes certain qu'il ne fonctionnera pas en WIN64.
Utilisez à la place MSWINDOWS.

XV - Commutateurs de la ligne de commande de l'IDE

Un nouveau commutateur, debugger=[borwin32|bordonet], vous permet de sélectionner le débogueur Win32 de Borland ou le débogueur .NET en ligne de commande du SDK, lorsque vous déboguez à partir de la ligne de commande.

Lors de l'utilisation du commutateur -LU sur la plate-forme .NET, vous pouvez faire une référence au package avec ou sans l'extension .dll.
Si vous omettez l'extension .dll, le compilateur recherchera le package sur le chemin de recherche de l'unité et sur le chemin de recherche du package. Néanmoins, si la spécification de package contient une lettre de lecteur ou le caractère séparateur de chemin, le compilateur suppose alors que le nom de package est le nom de fichier complet (y compris l'extension .dll). Dans le dernier cas, si vous spécifiez un chemin relatif ou complet, mais omettez l'extension .dll, le compilateur ne sera pas capable de localiser le package.

Voir aussi

XVI - Ce qui est déconseillé

Il n'est plus possible, dans du code managé, d'utiliser les déclarations ou procédures suivantes :
pointeurs, type procédure, exitproc, absolute, @, les types pchar et variants…

Pour la manipulation de délégués l'opérateur @ reste possible, mais c'est une exception.

Les procédures de manipulation de mémoire : move, fillchar, getmem, freemem…
Et bien évidemment la directive asm, l'accès aux registres du microprocesseur n'est plus possible dans du code managé ( .NET ou JAVA ).

Et ceci pour la simple raison qu'ils sont considérés comme type ou code non sécurisé ( unsafe ).
Les types pointeur et pointeur de fonction ne sont pas conformes CLS. Certains types de données ne sont plus supportés et sont soit émulés soit remplacés par d'autres types natifs de .NET.

Le tableau suivant montre la correspondance entre les types réels fondamentaux et les types .NET Framework.
Mappage des types réels .NET fondamentaux

Type

Mappage .NET

Real48

Deprecated

Single

Single

Double

Double

Extended

Double

Comp

Deprecated

Currency

Réimplémenté comme un type de valeur en utilisant le type Decimal du .NET Framework

Si votre composant doit être entièrement utilisable par des composants écrits dans d'autres langages, les types exportés de votre composant doivent exposer uniquement les fonctionnalités de langage qui font partie de la spécification CLS (Common Language Specification).

Voir aussi
Deprecated Language Features
Aide de delphi 2005 :Portage d'applications VCL vers Delphi 2005

XVII - Les délégués

Projet : ..\Delegues\Delegate1

Un délégué en Delphi .NET est similaire à un pointeur de fonction ou de procédure en WIN32. Avec un délégué, vous pouvez encapsuler une référence à une méthode à l'intérieur d'un objet delegate. Ce dernier peut ensuite être transmis au code, qui peut appeler la méthode référencée sans savoir, au moment de la compilation, quelle méthode sera appelée. Toutefois, à la différence des pointeurs de procédure ou de fonctions, les délégués sont orientés objet, indépendants des types et sécurisés.
Une de leur utilisation concerne les fonctions de rappel (Callback).

En Delphi .NET les types procédure sont donc implémentés comme des types délégués. Leur déclaration se fait par l'ajout de of object au nom de type. Exemple :

TMonDelegue= Procedure(AClient:TClient) of Object;

Les déclarations suivantes sont toutes compilées comme des délégués :

Type
TMonDelegue  = Procedure of Object;
TMonDelegue2 = Procedure;
TMonDelegueF = Function:Boolean;
var
unDelegue : procedure(L :integer);

L'utilisation se fait ensuite de la manière suivante :

TCompte=Class
Clients:Array[1..5] of TClient;
Constructor Create;
// Déclaration
procedure Callback(UnDelegue:TMonDelegue);
end;
...
// Implémentation
procedure TCompte.Callback(UnDelegue:TMonDelegue);
var i: integer;
begin
For i:=1 to 5 do
//Callback
UnDelegue(Clients[i]);
end;
...
// Main
Procedure AfficheClient(UnClient:TClient);
begin
Writeln('Procédure de callback appelée pour le client nommé : ',UnClient.Nom);
end;

begin
UnCompte:=TCompte.Create;
// Appel
UnCompte.Callback(AfficheClient);

Il est tout à fait possible de manipuler ce délégué par l'intermédiaire d'une variable typée :

Var TestDelegue:TMonDelegue;
..
// Autre construction possible
TestDelegue:=AfficheClient;
UnCompte.Callback(TestDelegue);

Projet : ..\Delegues\Delegate2

Ensuite il est possible de visualiser la différence qui ne semble pas évidente entre une déclaration d'un type procédure sous Win32 et d'un délégué sous Delphi .NET:

// Assignation obligatoire, sinon exception NullReferenceException lors de l'appel de GetType
TestDelegue:=AfficheClient;
Writeln('Ancêtre de TestDelegue = ',typeof(TestDelegue).BaseType);

L'instruction suivante provoque l'erreur de compilation suivante :

Pas assez de paramètres originaux (E2035)

La syntaxe correcte ici se fait par l'utilisation de l'opérateur @ qui semblait déconseillé. Il s'agit ici de la seule exception.
Il permet d'obtenir l'adresse du délégué. Si vous n'employez pas cette syntaxe, le compilateur pensera que vous essayez d'appeler le délégué, d'où l'erreur précédente.

Writeln('Ancêtre de TestDelegue = ',typeof(@TestDelegue).BaseType);

// Ici le compilateur reconnaît un accès à un membre de classe.
Writeln('Type de TestDelegue = ',TestDelegue.GetType);

L'affichage de la classe ancêtre de TestDelegue nous renvoie System.MultiCastDelegate qui est elle-même dérivée de la classe System.Delegate.
En quoi notre procédure est-elle à diffusion multiple (multicast) ?

XVII-A - La diffusion multiple

Projet : ..\Delegues\Delegate3

Cet exemple nous permet d'aborder une nouveauté fort appréciable du langage Delphi pour .NET.
On peut désormais par un appel de procédure, notre délégué utilisé précédemment, enchaîner l'appel de plusieurs procédures et modifier ce chaînage au gré des besoins !
Nous avons vu jusqu'ici l'information multi, portée par le nom de classe; en associant cette mécanique aux événements (messages) on retrouve l'information de diffusion (cast), on peut donc désormais déclencher simplement des actions multiples à la suite de la réception d'un message.

Sous Delphi .NET, la déclaration d'une propriété est obligatoire pour implémenter ces fonctionnalités.
Voici la déclaration d'un délégué single-cast :

Property MonDelegue:TMonDelegue read FMonDelegue write FMonDelegue;

La déclaration d'un délégué multicast nécessite l'utilisation de nouveaux mots clés, concernant les propriétés, qui sont add et remove.
Leur présence signale au compilateur qu'il autorise le chaînage :

Property MonDelegue:TMonDelegue add FMonDelegue remove FMonDelegue;

La différence entre les deux déclarations est que pour la première il n'y a qu'un seul appel possible, il s'agit du dernier délégué ajouté à 'la liste de délégués' via la méthode include. Voyons dans le détail ce comportement.
On doit donc dans un premier temps modifier la classe TCompte et le prototype de sa procédure Callback :

TCompte=Class
Clients:Array[1..5] of TClient;
// Pour le multicast doit être une propriété :
FMonDelegue:TMonDelegue;
Constructor Create;
procedure Callback;
// la syntaxe : Property MonDelegue:TMonDelegue read FMonDelegue write FMonDelegue;
// ne permet pas la composition de délégué. Il n'y a qu'un seul appel qui est le dernier déclaré
Property MonDelegue:TMonDelegue add FMonDelegue remove FMonDelegue;
end;

Ensuite dans l'implémentation on doit appeler le délégué via la propriété :

procedure TCompte.Callback;
var i: integer;
begin
if assigned(FMonDelegue) then
For i:=1 to 5 do
FMonDelegue(Clients[i]);
end;

Et pour finir l'ajout d'un délégué dans la liste se fait par l'appel de la procédure dédiée Include.

Include(UnCompte.MonDelegue,@Traite);
Include(UnCompte.MonDelegue,@AfficheClient);
UnCompte.Callback;

La suppression d'un délégué dans la liste des délégués se fait par l'appel de la procédure dédiée Exclude.

Exclude(UnCompte.MonDelegue,@Traite);
Exclude(UnCompte.MonDelegue,@AfficheClient);
Include(UnCompte.MonDelegue,@Fin);
UnCompte.Callback;

Il reste possible d'assigner une méthode à délégué comme ceci :

UnCompte.FMonDelegue:=Traite;
UnCompte.Callback;

mais dans ce cas le compilateur crée un nouvel objet délégué et lui affecte la méthode indiquée. La suppression de l'ancienne référence contenue dans FMonDelegue étant prise en charge par le Garbage Collector.
La suppression complète peut se faire par l'affectation de NIL au délégué :

// Identique à System.Delegate.RemoveAll(System.Delegate(@FMultiEvent), Nil);
UnCompte.FMonDelegue:=Nil;
UnCompte.Callback;

XVII-B - Gestionnaire d'événements sous la VCL

Projet : ..\Delegues\VCL\Delegues

Vous trouverez dans cet exemple une utilisation d'un délégué au sein d'une Fenêtre VCL (TForm).

Il est possible de retrouver le contenu de la liste des délégués :

type
//Tableau Dynamique de Délégués
TDelegateDynArray = array of System.Delegate;
...

procedure TForm1.Button1Click(Sender: TObject);
var TabDelegue : TDelegateDynArray;
unDelegue  : System.Delegate;

begin
if Not assigned(FMultiEvent)
then
begin
Log.Lines.Add('La liste des délégués est vide');
Exit;
end;
TabDelegue:=System.Delegate(@FMultiEvent).GetInvocationList;
For unDelegue in TabDelegue do
begin
// Obtient le type du Delegué, ici : TNotifyEvent
//  log.Lines.Add('Array :'+Deleg.ToString);
Log.Lines.Add('Array :'+' '+unDelegue.Target.ToString+'.'+unDelegue.Method.Name);
end;
end;

Il est tout à fait possible de tester si deux listes de délégués contiennent ou non des méthodes identiques.

Consultez la documentation du SDK à propos des tests d'égalité sur des instances de délégués : Delegate.Equals …

Voir aussi :
Délégué EventHandler. Local
Classe EventArgs. Local

XVII-C - Gestionnaire d'événements sous Windows Form

La gestion des événements sous Windows Form est différente, on doit utiliser un prototype de délégué spécifique :

unit Borland.Vcl.NetControlWrapper platform experimental;
...
TNetEventHandler = procedure (Sender: System.Object; AArgs: System.EventArgs);
...

Notez que la convention de nommage, préconisé par Microsoft, est d'ajouter le suffixe EventHandler. Libre à vous de la suivre.
L'entête de la méthode du délégué contient un premier paramètre de type System.Object et un deuxième de type System.EventArgs. Par convention, le premier paramètre est toujours l'instance qui a appelé le gestionnaire d'événement, le second contient des arguments décrivant l'événement.

La classe System.EventArgs ne contient pas de donnée d'événement; elle est utilisée par des événements qui ne passent pas d'informations d'état à un gestionnaire d'événements lorsqu'un événement est déclenché. Si le gestionnaire d'événements nécessite des informations d'état, l'application doit dériver une classe à partir de cette classe pour contenir les données.

type
MyEventArgs=Class(EventArgs)

UserPrintPageEventArgs = class(EventArgs)
Public
MySpecialValue :String;
end;

TSampleEventHandler = procedure(Sender: TObject; Args: MyEventArgs);

Lorsque le délégué est appelé, le gestionnaire a accès à Args.MySpecialValue.

Voir aussi :
Didacticiel sur les événements. Local
System.EventArgs. Local
Délégué EventHandler. Local

XVIII - Syntaxe d'assistance de classe ( Class Helper )

Texte issu de la documentation de DELPHI 2005
Une assistance de classe est un type qui, lorsqu'il est associé à une autre classe, introduit des méthodes et propriétés supplémentaires qui peuvent être utilisées dans le contexte de la classe associée (ou ses descendants). Les assistances de classes permettent d'étendre une classe sans avoir recours à l'héritage.

Les assistances de classes permettent d'étendre une classe, mais elles ne doivent pas être considérées comme un outil de conception à utiliser pour développer un nouveau code. Elles ne doivent être utilisées que pour la liaison entre le langage et la RTL de la plate-forme.

On peut voir ci-dessous l'utilisation parcimonieuse des class Helper dans les sources du runtime de Delphi 2005 .NET.

...\Borland\BDS\3.0\source\dotNet\rtl\
Borland.Delphi.System.pas :   TObjectHelper = class helper for TObject
Borland.Vcl.Classes.pas)  :   TPersistentHelper = class helper (TObjectHelper) for TPersistent
Borland.Vcl.Classes.pas   :   TComponentHelper = class helper (TPersistentHelper) for TComponent
Borland.Vcl.SysUtils.pas  :   ExceptionHelper = class helper (TObjectHelper) for Exception
Borland.Vcl.TypInfo.pas   :   TTypeInfoHelper = class helper for TTypeInfo
Borland.Vcl.TypInfo.pas   :   TPropInfoHelper = class helper for TPropInfo
Borland.Vcl.Variants.pas  :   VariantHelper = class helper for Variant
Borland.Vcl.Variants.pas  :   OleVariantHelper = class helper for OleVariant

Toutes les classes concernées sont des classes spécifiques à Delphi et mappées sur les classes correspondantes du Framework .NET. Par contre ces classes du Framework ne déclarent pas certains membres présents dans la classe Delphi.

Le class Helper permet donc ici d'ajouter ces membres manquants afin de préserver la compatibilité du code existant.

ExceptionHelper = class helper (TObjectHelper) for Exception
private
class function CreateMsg(const Msg: string): Exception;
function GetHelpContext: Integer;
procedure SetHelpContext(AHelpContext: Integer);
public
/// Doc: The help context return zero(0) if exception's helplink property cannot be parsed into an integer.
property HelpContext: Integer read GetHelpContext write SetHelpContext;

// constructor Create(const Msg: string) is provided by the CLR class
class function CreateFmt(const Msg: string; const Args: array of const): Exception;
class function CreateHelp(const Msg: string; AHelpContext: Integer): Exception;
class function CreateFmtHelp(const Msg: string; const Args: array of const;
AHelpContext: Integer): Exception;

// System.Exception.HResult is protected.  This helper exposes
// the HResult for read-only access by non-descendents
function HResult: Integer;
end;
ExceptionClass = class of Exception;

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.

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.

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.

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

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 :

Code compilé pour la métaclasse TTesTClasse

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;

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.

Champ @Instance ajouté par le compilateur

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.

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 :

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

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

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

Méthode contenant le code des diverses initialisations

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

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 :

'Le constructeur du programme'

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

Le code compilé du programme DemoEspaceNommage.exe

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 :

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

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.

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

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

RunTime .NET absent

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.