Les tableaux sous Delphi .NET
Date de publication : 01/12/2005 , Date de mise à jour : 01/12/2005
Par
Laurent Dardenne (Contributions)
Ce tutoriel aborde dans le détail la manipulation des tableaux sous .NET notamment les tableaux multidimensionnels alloués dynamiquement sous Delphi .NET.
Je tiens à remercier Olivier Dahan (Merlin) pour ses remarques pertinentes.
1. Public concerné
1-1. Les sources
2. Les bases
2-1. Du nouveau
2-2. Contrôle des indices hors limites
3. Les tableaux SZ statiques et dynamiques
3-1. Création de tableau
3-1-1. Notes sur la notion de type
3-1-1-1. Exemples
3-2. L'assignation de tableau
3-2-1. Tableaux de constante
3-3. L'initialisation de tableau
3-4. Tableau avec un indice négatif
3-4-1. Problématiques d'itération
4. Tableaux multidimensionnels alloués dynamiquement
4-1. Notes
4-2. Manipulation de tableau de tableaux
4-2. Manipulation de tableau multidimensionnel
4-3. La méthode CreateInstance
5. Remarque sur l'instruction initialize
6. Quelques fonctions utiles
7. Classes apparentées
7-1. BitArray
7-2. Les champs de bits
7-3. ArrayList
8. Conclusion
1. Public concerné
Testé sous le framework .NET 1.1 et Delphi 2005 + update 3.
Version 1.0
Ce tutoriel contient quelques exemples de code C#, vous trouverez les bases de la conversion du C# vers Delphi .NET ici.
1-1. Les sources
2. Les bases
Vous trouverez dans le tutoriel Les tableaux dynamiques, les bases sur la gestion des tableaux sous Pascal/Delphi.
Un tableau est un type structuré qui permet de mémoriser plusieurs valeurs dans une seule structure, sous Delphi .NET les tableaux statiques et dynamiques sont implémentés en utilisant le type System.Array du framework.
Extrait de l'aide du framework concernant la définition de la classe System.Array:
La classe Array est la classe de base pour les implémentations du langage qui prennent en charge
les tableaux. Cependant, seuls les compilateurs et le système peuvent dériver explicitement de la
classe Array.
Les utilisateurs doivent employer les constructions de tableaux fournies par le langage. |
L'allocation mémoire de tableaux dynamiques, comme indiqué dans ce chapitre du tutoriel déjà cité, est désormais impossible, ceci étant due à une gestion mémoire différente sous .NET. Vous trouverez quelques informations sur le sujet en consultant l'aide en ligne locale de Delphi 2005.
Consultez le site de Microsoft pour le détail de la classe System.Array. Les chapitres suivant reprennent par l'exemple l'utilisation des méthodes les plus courantes.
2-1. Du nouveau
Le framework distingue 2 types de tableaux :
- les tableaux SZ, unidimensionnel et débutant à un indice zéro
- et les autres, c'est à dire :
- les tableaux unidimensionnel mais débutant à un indice différent de zéro (un indice négatif est aussi possible),
- les tableaux multidimensionnels,
- les tableaux de tableaux réguliers,
- et les jagged array ou ragged array qui sont des tableaux de tableaux irréguliers c'est-a-dire qui n'ont pas le même nombre d'éléments dans toutes les directions.
Note : Un tableau de tableaux contient des pointeurs sur d'autres tableaux alors qu'un tableau multidimensionnel n'en contient pas.
Cette distinction des tableaux SZ est confirmée par le code suivant, issu du fichier source ...\sscli_20021101\sscli\clr\src\bcl\system\array.cs.
public abstract class Array : ICloneable, IList
{
...
public virtual IEnumerator GetEnumerator()
{
int lowerBound = GetLowerBound(0);
if (Rank == 1 && lowerBound == 0)
return new SZArrayEnumerator(this);
else
return new ArrayEnumerator(this, lowerBound, Length);
}
... |
Voir The Shared Source CLI qui contient les codes source de l'implémentation du runtime pour le Common Language Infrastructure.
 Tableau rectangulaire[ 6, 10 ]
 Tableau de tableaux référençant quatre tableaux horizontaux
 |
Notez que toutes les dimensions d'un tableau multidimensionnel seront de la même taille. Mais dans un tableau jagged Array, il est possible de référencer des tableaux de différentes tailles.
|
Sous .NET le type tableau SZ (unidimensionnel) débute à zéro et est optimisé. Il est recommandé de privilégier les tableaux débutant avec un index zéro notamment pour faciliter la portabilité. Il faut savoir que les tableaux de tableaux ne sont pas compatibles avec la CLS (SDK 1.1) car il ne permet pas qu'un objet System.Array soit un élément de tableau.
La zone mémoire allouée pour un tableau contient des informations supplémentaires qui sont :
- le nombre de dimensions (Rank),
- la borne inférieure du tableau (LowerBound),
- la longueur de chaque dimension (GetLength), i.e le nombre d'éléments contenus dans la dimension spécifiée.
2-2. Contrôle des indices hors limites
Le CLR propose nativement le contrôle d'accès des indices hors limites, la directive de Vérification des limites {$R+} ou {$RANGECHECKS ON} semble donc redondante sous Delphi .NET et ce pour les tableaux uniquement.
Projet : ..\Tableaux\Range_Error
Const
Nbitem=9;
type
TArrayStatic = Array[0..Nbitem] of Byte;
var
TableauStatic:TArrayStatic;
begin
{$R-}
I:=10;
try
TableauStatic[I]:=0;
except
On E:ERangeError do
Writeln('Avec R+ : ERangeError '+E.Message);
On E:System.IndexOutOfRangeException do
Writeln('Avec R- : System.IndexOutOfRangeException '+E.Message);
end; |
Cette remarque ne concerne pas les chaînes de caractères, type System.String, bien que ce type soit implémenté comme un tableau de caractères.
{$R-}
Chaine:='1234';
Caractere:=Chaine[5];
Chaine[5]:='A';
Writeln(Caractere);
Writeln(Chaine); |
Extrait de la documentation du SDK concernant l'exception System.IndexOutOfRangeException:
Les instructions MSIL (Microsoft Intermediate Language) suivantes lèvent IndexOutOfRangeException :
- ldelem.<type> : load an element of an array
- ldelema : load address of an element of an array
- stelem.<type> : store an element of an array |
 |
Le code déclaré unsafe n'effectue pas ce contrôle.
|
3. Les tableaux SZ statiques et dynamiques
Delphi propose 2 types de tableau :
- les tableaux statiques; Ils ne sont jamais à Nil.
- les tableaux dynamiques; Ils peuvent être à Nil.
La différence concerne la définition de la taille dans la déclaration du type. Celle d'un tableau dynamique pouvant évoluer au grès des besoins pendant l'exécution du programme, les tableaux dynamiques n'ont donc pas de taille ni de longueur fixe.
Un tableau dynamique est managé par le runtime Delphi .Net et offre syntaxiquement plus de possibilité qu'un tableau en code natif .NET.
3-1. Création de tableau
Sous .NET une des évolutions concerne l'allocation dynamique de tableau par l'utilisation du mot clé NEW.
Voici un exemple d'utilisation de cette instruction :
Projet : ..\Tableaux\LesBases1
type
TArrayDynamiqueSimple = Array of Byte;
var
TableauDynamique: TArrayDynamiqueSimple;
begin
TableauDynamique := New(TArrayDynamiqueSimple, 256);
end. |
Ici l'instruction NEW crée un tableau unidimensionnel basé zéro et initialise toutes les valeurs à zéro.
Dans ce cas l'instruction New génère l'appel au code IL (Intermediate Language) newarr. Voici le résultat sous Il-Dasm :
...
IL_0006: ldc.i4 0x100
IL_000b: newarr [mscorlib]System.Byte
IL_0010: stloc.0
... |
La construction interne d'un tableau statique est identique.
Projet : ..\Tableaux\LesBases11
Const Nbitem=9;
type
TArrayStatic = Array[0..Nbitem] of Byte;
var
TableauStatic:TArrayStatic;
begin
Writeln('TArrayStatic');
ShowInfoTableau(TableauStatic);
Writeln;
end. |
Ici c'est le compilateur qui prend en charge la création (l'allocation mémoire) du tableau lors de l'initialisation du programme, c'est à dire avant l'instruction Writeln('TArrayStatic').
3-1-1. Notes sur la notion de type
Traduction de Array type compatibility differences between Delphi 7 and Delphi 8 for .NET.
Delphi 7 ne me permet pas d'assigner une variable déclarée en tant que "array of byte"
à une autre variable déclarée en tant que "array of byte" mais Delphi 8 l'accepte, quelle est la différence ?
Le problème est le type d'identité. Delphi considère le nom d'un type comme son identité.
Si deux tableaux de même "forme" ont des noms de type différent, alors Delphi ne les considère pas comme étant de
type équivalent. Ceci facilite pour le programmeur la définition des domaines de type distinct qui s'avèrent être implémenté
en utilisant une structure identique. Ceci signifie également que deux variables de type "Array of foo" ne sont pas
considérées compatibles parce qu'elles ne partagent pas le même nom de symbole.
"Array of foo" est un type anonyme, et chaque type anonyme est unique.
L'autre genre de type d'identité est l'identité structurelle : si deux tableaux ont la même "forme"
(le même nombre et longueur de dimensions et du même type d'élément), alors ils sont
considérés comme des types équivalents. C'est la vision du monde, propre au langage C.
Le CLR .NET emploie l'identité de nom pour tous les types excepté le type Array. Dans la CLR, les types tableaux
peuvent ne pas être nommés du tout. Le seul genre d'identité pour les tableaux est structurelle. C'est une entorse dans la prétention du CLR d'être un système de type indépendant du langage.
CLR exige que les langages implémentent l'identité structurelle pour les types tableaux.
Les règles d'identité de type de Delphi ont été un peu adaptées pour se conformer à la CLR.
Delphi utilisera de nouveau la comparaison structurelle d'identité pour des types tableau
si la comparaison nommée d'identité échoue. Ceci s'applique seulement au type tableau.
3-1-1-1. Exemples
La documentation nous indique :
Des tableaux sont compatibles pour l'affectation uniquement s'ils ont le même type. Comme le langage Delphi
utilise des équivalences de nom pour les types, le code suivant ne se compile pas : |
var
Int1: array[1..10] of Integer;
Int2: array[1..10] of Integer;
...
Int1 := Int2; |
Pour que l'affectation fonctionne, déclarez les variables comme suit :
var Int1, Int2: array[1..10] of Integer; |
ou
type IntArray = array[1..10] of Integer;
var
Int1: IntArray;
Int2: IntArray; |
 |
Sous Delphi Win32 c'est effectivement le cas en revanche sous Delphi .NET la déclaration de variables de même type sur une même ligne ou pas n'a aucun impact.
|
Voici un autre cas concernant la notion de types. Sous Win32 un tableau statique à plusieurs dimensions est un tableau de tableaux.
Par exemple :
TArrayStatic2D =Array[0..1,0..1] of byte; |
est équivalent, d'après la documentation Delphi, à
TArrayStatic2Dv2 = Array[0..1] of Array[0..1] of Byte; |
mais pas à
TArrayDynamique2D = Array of Array of Byte; |
En revanche sous .NET l'affichage du type de donnée, par GetType.ToString, d'une variable du type
- TArrayStatic2D renvoi System.Byte[,],
- TArrayStatic2Dv2 renvoi System.Byte[][].
Le premier étant crée par le code Il newobj et le second par newarr (SZArray), on peut donc en déduire que sous .NET ils ne sont pas équivalents.
Notez que sous .NET les instructions suivantes génèrent le même code IL :
var T2D: TArrayStatic2D;
begin
T2D[0,0]:=10;
T2D[0][0]:=10; |
3-2. L'assignation de tableau
Projet : ..\Tableaux\LesBases2
Si on fusionne le code des exemples précédents (LesBases1 et LesBases11), l'instruction d'affectation suivante est considérée par le compilateur comme une recopie de tableau :
Const Nbitem=9;
type
TArrayStatic = Array[0..Nbitem] of Byte;
TArrayDynamiqueSimple = Array of Byte;
var
TableauStatic:TArrayStatic;
TableauDynamique: TArrayDynamiqueSimple;
begin
TableauDynamique := New(TArrayDynamiqueSimple, 15);
TableauStatic:=TableauDynamique;
end. |
Pour ces 2 déclarations de type similaire le compilateur génère, lors de l'affectation TableauStatic:=TableauDynamique, un appel à System.Array.Copy(sourceArray,destinationArray,longueur).
Ce que confirme la visualisation sous IL-Dasm :
IL_0006: ldc.i4.s 10
IL_0008: newarr [mscorlib]System.Byte
IL_000d: stloc.0
IL_000e: ldc.i4.s 10
IL_0010: newarr [mscorlib]System.Byte
IL_0015: stloc.1
... // code de préparation d'appel
IL_0024: call void [mscorlib]System.Array::Copy(class [mscorlib]System.Array,
class [mscorlib]System.Array,
int32)
... |
Ici cette recopie est nécessaire car la variable TableauStatic est un tableau de type statique. Un tableau statique n'étant pas redimensionnable le compilateur doit donc créer une nouvelle référence de tableau, de la taille de TableauDynamique, puis y recopier le contenu de TableauDynamique et enfin affecter en interne la référence du nouveau tableau à TableauStatic. L'ancienne référence étant prise en charge par le Garbage Collector.
Voici un autre exemple qui met en évidence le coût du redimensionnement d'un tableau :
Procedure CreeDynamique;
begin
TableauDynamic:=New(TArrayDynamiqueSimple,10);
end;
procedure AffecteDynamiqueSetLength;
begin
SetLength(TableauDynamic, 15);
end; |
La méthode SetLength qui permet le redimensionnement d'un tableau dynamique, n'existe pas nativement sous .NET. Dans ce cas le compilateur génère ici aussi la création d'une nouvelle référence de tableau impliquant les mêmes opérations que précédemment.
Dans le cas où le tableau est un tableau statique il n'y pas d'appel à System.Array.Copy.
 |
Si vous souhaitez approfondir cet aspect, consultez le projet ..\Tableaux\Recapitulatif. Vous y trouverez de nombreuses méthodes constituées d'une seule instruction portant sur un tableau de type statique puis de type dynamique. Cette construction facilitant la comparaison, sous IL-DASM, du code IL généré par le compilateur Delphi .NET.
|
Après l'affectation TableauStatic:=TableauDynamique, l'appel à TObject.equals(TableauStatic,TableauDynamique) nous confirme cette recopie en retournant False.
Pour rappel voici le code de la méthode Equals de la classe Object :
public class Object
{
...
public static bool Equals(Object objA, Object objB) {
if (objA==objB) {
return true;
}
if (objA==null || objB==null) {
return false;
}
return objA.Equals(objB);
}
public static bool ReferenceEquals (Object objA, Object objB) {
return objA == objB;
}
... |
Voyons maintenant l'affectation de 2 tableaux dynamique.
Projet : ..\Tableaux\LesBases21
type
TArrayDynamiqueSimple = Array of Byte;
var
Tb1,Tb2 : TArrayDynamiqueSimple;
begin
...
Tb1:= New(TArrayDynamiqueSimple, 4);
Tb2:= New(TArrayDynamiqueSimple, 7);
Tb1:=Tb2; |
Dans ce cas l'appel à TObject.equals(Tb1,Tb2) renvoie True. La variable Tb1 étant de type tableau dynamique, le compilateur lui affecte la référence de la variable Tb2, ces deux variables référencent désormais la même zone mémoire.
Pour obtenir deux références différentes de tableaux alloués dynamiquement mais de contenu identique on utilisera la méthode Clone.
Projet : ..\Tableaux\LesBases22
type
TArrayDynamiqueObjet = Array of TObject;
TArrayDynFileStream= Array of System.IO.FileStream;
var
TabObjet,TFS: system.Array;
begin
TabObjet:=TArrayDynFileStream(TFS.Clone); |
La déclaration des variables TabObjet et TFS de type System.Array facilite l'écriture en évitant le transtypage suivant :
TabObjet:=TArrayDynamiqueObjet(system.Array(TFS).Clone); |
L'appel de Clone peut peut être remplacé par l'instruction Delphi native Copy :
TabObjet:=Copy(TArrayDynFileStream(TFS)); |
Dans ce cas le compilateur génère un appel à Borland.Delphi.Units.System::@DynArrayCopy qui ne fait qu'appeler en interne la méthode Array.Clone.
 |
La copie des valeurs d'un tableau contenant un type de base, des entiers par exemple, ne pose pas de problème en revanche la copie d'un tableau d'objets, contenant d'autres objets, ne recopiera que les références des objets imbriqués. Il s'agit donc d'une copie partielle du tableau.
|
3-2-1. Tableaux de constante
Projet : ..\Tableaux\ConstTab
Pour terminer, l'assignation d'une constante de type tableau statique dans une variable de tableau dynamique est possible, dans ce cas on peut modifier le tableau de constante :
program ConstTab;
{$APPTYPE CONSOLE}
uses
SysUtils;
Const
nbItem=9;
CstTableauStatique : Array[0..nbItem] of Byte=(1, 2, 3, 4, 5, 9, 12, 15, 27, 33);
type
TArrayDynamiqueSimple = Array of Byte;
var
TableauDynamique : TArrayDynamiqueSimple;
I :Byte;
begin
{$R+}
Writeln('CstTableauStatique[3]= ',CstTableauStatique[3]);
Writeln('Assignation : TableauDynamique:=CstTableauStatique');
TableauDynamique:=CstTableauStatique;
Writeln('Assignation : TableauDynamique[3]:=65');
TableauDynamique[3]:=65;
Writeln('CstTableauStatique[3]= ',CstTableauStatique[3]);
Writeln('TableauDynamique[3]= ',TableauDynamique[3]);
readln;
end. |
Pour cette affectation le compilateur ne génère pas d'appel à System.Array.Copy ce qui peut avoir quelques effets de bord.
Vous pouvez consultez le chapitre 8 du tutoriel sur le langage Delphi pour .NET concernant le passage de paramètres.
Note :
Projet : ..\Tableaux\ConstTabW32
Sous Delphi Win32 cette opération est aussi possible en utilisant l'opérateur @.
3-3. L'initialisation de tableau
Projet : ..\Tableaux\LesBases23
Ce projet utilise différentes constructions pour initialiser un tableau.
Const Nbitem=9;
type
TArrayStatic = Array[0..Nbitem] of Byte;
TArrayDynamiqueSimple = Array of Byte;
var
I : Byte;
Tableau : TArrayStatic;
begin
for I:=0 to Nbitem do
begin
Tableau[I]:=I;
end;
For I in Tableau do
begin
Tableau[I] := I;
end;
Initialize(Tableau);
&Array.Clear(Tableau,0,Length(Tableau));
For I := 0 to Pred(Length(Tableau)) do
begin
Tableau[I] := I;
end;
end. |
L'utilisation d'une boucle est à privilégier par rapport à l'appel d'un itérateur car elle génère moins de code.
En ce qui concerne la remise à zéro du contenu d'un tableau, l'utilisation des instructions Initialize ou Array.Clear dépendra du contexte. La première appelant le code IL newarr, la seconde itérant sur le tableau.
Le choix sera donc, à priori, entre un traitement plus rapide avec une sollicitation de la mémoire managée plus importante (avec de possibles appels internes au Garbage Collector) et un traitement moins rapide mais sans surcharge du Garbage Collector.
Reprenons le même exemple mais en modifiant le type de la variable Tableau de TArrayStatic en TArrayDynamiqueSimple :
 |
Dans ce cas l'instruction Initialize n'est plus compilée avec le code newarr mais ldnull.
|
La construction avec une liste de valeur constante est aussi possible :
type
TArrayDynamiqueSimple = Array of Byte;
var
Simple: TArrayDynamiqueSimple;
begin
Simple:=TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
end. |
Cette approche génère, dans le code compilé, 20 octets par valeur à insérer, la construction suivante étant préférable :
Const
nbItem=9;
type
TArrayDynamiqueSimple = Array of Byte;
var
Simple: TArrayDynamiqueSimple;
begin
Simple := New(TArrayDynamiqueSimple, 10);
for I:=0 to nbItem do
Simple[I]:=I;
end. |
Il va sans dire que si vous utilisez des variables dans l'appel de la méthode Create cette astuce ne s'applique pas.
Revenons sur la syntaxe NEW, elle autorise deux formes :
- l'une prend le type d'élément et la taille du tableau (syntaxe que nous avons déjà vue),
- tandis que l'autre prend le type d'élément et une liste d'initialiseurs de tableaux.
Projet : ..\Tableaux\LesBases12
type
TArrayDynamiqueSimple = Array of Byte;
var
TableauDynamique: TArrayDynamiqueSimple;
begin
TableauDynamique := New(TArrayDynamiqueSimple, 256);
Write('Valeur de TableauDynamique[0] =',TableauDynamique[0]);
Writeln(' Nombre d''élément=', System.Array(TableauDynamique).GetLength(0).ToString);
TableauDynamique := New(array[] of byte,(255));
Write('Valeur de TableauDynamique[0] =',TableauDynamique[0]);
Writeln(' Nombre d''élément=', System.Array(TableauDynamique).GetLength(0).ToString);
Readln;
end. |
Le résultat étant successivement le suivant :
Valeur de TableauDynamique[0] =0 Nombre d'élément=256
Valeur de TableauDynamique[0] =255 Nombre d'élément=1 |
Il est possible d'utiliser des initialiseurs pour des objets plus complexes :
C: array [] of TPoint;
begin
C := New(array[] of TPoint, ((X:1;Y:2), (X:3;Y:4)) ); |
3-4. Tableau avec un indice négatif
Projet : ..\Tableaux\TableauNegatif
Il reste une autre possibilité de créer un tableau unidimensionnel qui passe par l'utilisation de la méthode System.Array.CreateInstance qui permet de créer un tableau avec 1 indice différent de zéro.
Extrait du SDK .NET 1.1 :
Contrairement à la plupart des classes, Array fournit la méthode CreateInstance plutôt que des constructeurs
publics, permettant ainsi l'accès à liaison tardive. |
var
Simple: TArrayDynamiqueSimple;
LowerBounds,
Lengths : TIntegerDynArray;
begin
LowerBounds:= New(TIntegerDynArray,1);
Lengths:=New(TIntegerDynArray,1);
LowerBounds[0]:=1;
Lengths[0]:=10;
Simple:=System.Array.CreateInstance(typeof(Byte),Lengths,LowerBounds) as TArrayDynamiqueSimple;
end. |
On peut donc utiliser les initialiseurs et réduire la taille du code :
Simple:=System.Array.CreateInstance(Typeof(Byte),
New(array[] of integer,(10)),
New(array[] of integer,(1))) as TArrayDynamiqueSimple; |
Le code précédent est une démonstration du pourquoi faire simple quand on peut faire compliquer...
 |
La méthode CreateInstance est une des méthodes utilisées avec les tableaux multidimensionnels alloués dynamiquement.
|
En revanche cette syntaxe peut être utile si on souhaite créer un tableau avec des indices négatif, ce que l'instruction New ne permet pas pour un tableau SZ:
TableauDynamiqueSimple := New(TArrayDynamiqueSimple,-256);
Writeln('TableauDynamiqueSimple négatif');
ShowInfoTableau(TableauDynamiqueSimple); |
Dans ce cas les informations affichées sont les suivantes :
Nombre total d'éléments dans toutes les dimensions = 0
Nombre de dimension = 1
Nombre d'éléments(0) = 0
GetLowerBound(0) = 0
GetUpperBound(0) = -1 |
En utilisant CreateInstance :
LowerBounds:= TArrayDynamiqueInteger.Create(-256);
Lengths:=TArrayDynamiqueInteger.Create(256);
TableauDynamiqueObjet:=System.Array.CreateInstance(typeof(Byte),Lengths,LowerBounds);
Writeln('TableauDynamiqueSimple négatif');
ShowInfoTableau(TableauDynamiqueObjet); |
Dans ce cas les informations affichées sont les suivantes :
Nombre total d'éléments dans toutes les dimensions = 256
Nombre de dimension = 1
Nombre d'éléments(0) = 256
GetLowerBound(0) = -256
GetUpperBound(0) = -1 |
On s'aperçoit aussi que le type du tableau n'est plus :
mais
Ce tableau n'est donc plus un Tableau SZ ni CLS-Compliant.
3-4-1. Problématiques d'itération
Sur ce type de tableau les syntaxes d'itération suivantes ne fonctionnent plus :
For I:=Low(Simple) to High(Simple) do writeln('Simple[',I,']',simple[I]);
For I in Simple do writeln('Simple[',I,']',simple[I]);
For I:=-255 to -1 do
writeln('Simple[',I,'], writeln(Simple[I]); |
Pour résoudre le problème du calcul de l'indice on utilisera :
For I:=System.Array(Simple).GetLowerBound(0) to System.Array(Simple).GetUpperBound(0) do |
Note : Vous ne pouvez pas non plus transmettre un tableau multidimensionnel alloué dynamiquement à la fonction Low ou High. Si vous essayez de le faire, une erreur de compilation se produira.
Pour résoudre le problème d'accès à la valeur l'indicée on utilisera :
System.Array(Simple).SetValue(TObject(Byte(I)),I);
writeln('Simple[',I,']',System.Array(Simple).GetValue(I)); |
4. Tableaux multidimensionnels alloués dynamiquement
Pour déclarer un tableau dynamique multidimensionnel, on utilise :
- soit une construction de type array of array of type,
- soit array[,] of type.
Pour créer un tableau irrégulier (jagged Array ou ragged Array) on utilisera la première méthode.
La construction de type array[0..X,0..Y] of type est dédié aux tableaux multidimensionnels statiques.
La création et l'assignation de tableau multidimensionnel diffère peu de ce que nous avons vu jusqu'ici, bien que l'opérateur NEW ait été conçu principalement pour ce type de tableau.
4-1. Notes
Avant d'allez plus loin revenons sur une autre problèmatique de type spécifique à Delphi .NET.
Projet : ..\Tableaux\TypeTableau
Les déclarations des exemples suivants ne sont possibles que sous Delphi .NET :
type
TArray2D = Array[,] of Integer;
var
Y : array[,] of Integer; |
Autant le code suivant compile :
type
TArrayDynamiqueSimple = Array of Byte;
TArrayDynamiqueMultiple = Array of Array of Byte;
var
TableauDynamique: TArrayDynamiqueSimple;
TableauDynamiqueMultiple : TArrayDynamiqueMultiple;
begin
TableauDynamique := New(TArrayDynamiqueSimple, 256);
TableauDynamiqueMultiple := New(TArrayDynamiqueMultiple, 2,2); |
Autant celui-ci ne compile pas :
type
TArray2D = Array[,] of Integer;
var
TableauMultiple : TArray2D;
begin
TableauMultiple := New(TArray2D, 1,2); |
Pourtant d'après la documentation, l'instruction New attend 2 paramètres le premier étant le type d'élément, si dans le premier cas les types TArrayDynamiqueSimple et TArrayDynamiqueMultiple sont acceptés, dans le second cas le type TArray2D ne l'est pas.
Pour le premier cas les types sont considérés comme des tableaux unidimensionnels, l'un contenant des byte l'autre des pointeurs sur des tableaux unidimensionnels.
La variable TableauMultiple n'est pas créée par le compilateur ce n'est donc pas un tableau statique. Dans le premier cas le code IL généré pour les 2 tableaux est newarr, dans le second l'instruction New génére l'appel au code IL newobj.
4-2. Manipulation de tableau de tableaux
Projet : ..\Tableaux\LesBases3
Les tableaux de tableaux peuvent être créés en utilisant l'instruction New :
type
TArrayDynamiqueSimple = Array of Byte;
TArrayDynamiqueMultiple = Array of TArrayDynamiqueSimple;
TArrayDynamique3D = Array of TArrayDynamiqueMultiple;
TArrayDynamique4D = Array of TArrayDynamique3D;
var
TableauDynamiqueMultiple : TArrayDynamiqueMultiple;
TableauDynamique3 : TArrayDynamique3D;
TableauDynamique4 : TArrayDynamique4D;
begin
TableauDynamiqueMultiple := New(TArrayDynamiqueMultiple, 2,2);
TableauDynamique3 := New(TArrayDynamique3D,3,2,8);
TableauDynamique4 := New(TArrayDynamique4D,7,2,8,10); |
Ces tableaux sont initialisés à zéro.
On peut aussi les créer de cette maniére :
TableauDynamiqueMultiple:=TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15));
TableauDynamique3:=TArrayDynamique3D.Create(
(TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(71, 72, 73, 74, 75, 79, 88, 85))),
(TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(71, 72, 73, 74, 75, 79, 88, 85)))
);
TableauDynamique4 := TArrayDynamique4D.Create(
TArrayDynamique3D.Create(
(TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(71, 72, 73, 74, 75, 79, 88, 85))),
(TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(71, 72, 73, 74, 75, 79, 88, 85)))),
TArrayDynamique3D.Create(
(TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(71, 72, 73, 74, 75, 79, 88, 85))),
(TArrayDynamiqueMultiple.Create(
TArrayDynamiqueSimple.Create(1, 2, 3, 4, 5, 9, 12, 15),
TArrayDynamiqueSimple.Create(71, 72, 73, 74, 75, 79, 88, 85)))),
); |
Ce qui confirme qu'un array of array attend bien des types référence et non des types valeur.
Mais le code généré est très bavard et l'imbrication d'appel alourdi la lecture du code. Etant donné que les valeurs sont connues à la compilation autant utiliser des tableaux de constantes, dans ce cas la taille du code généré est divisée par 4 :
Const
Nbitem=8;
cstArray1:Array[0..Nbitem-1] of byte=(1, 2, 3, 4, 5, 9, 12, 15);
cstArray2:Array[0..Nbitem-1] of byte=(71, 72, 73, 74, 75, 79, 88, 85);
...
begin
TableauDynamiqueMultiple:=TArrayDynamiqueMultiple.Create(cstArray1,cstArray2);
TableauDynamique3:=TArrayDynamique3D.Create(
TArrayDynamiqueMultiple.Create(cstArray1,cstArray2),
TArrayDynamiqueMultiple.Create(cstArray1,cstArray2)); |
La construction suivante est aussi possible mais nécessite un peu plus de code que la version précédente :
TableauDynamiqueSimple:= New(array[] of Byte,(5, 3, 2, 4, 1, 9, 12, 15));
TableauDynamiqueMultiple:=TArrayDynamiqueMultiple.Create(
TableauDynamiqueSimple,
TableauDynamiqueSimple);
TableauDynamiqueMultiple:=TArrayDynamiqueMultiple.Create(
New(array[] of Byte,cstArray1),
New(array[] of Byte,cstArray1)); |
le code suivant est identique et compatible Delphi Win32 et Delphi .NET :
SetLength(TableauDynamiqueMultiple, 2);
SetLength(TableauDynamiqueMultiple[0], 8);
SetLength(TableauDynamiqueMultiple[1], 8);
TableauDynamiqueMultiple[0,0] := 5;
TableauDynamiqueMultiple[0,1] := 3;
...
TableauDynamiqueMultiple[0,7] := 15;
TableauDynamiqueMultiple[1,0] := 5;
TableauDynamiqueMultiple[1,1] := 3;
...
TableauDynamiqueMultiple[1,7] := 15;
for i:=0 to 1 do
for j:=0 to 2 do
writeln(TableauDynamiqueMultiple[i,j]);
Readln;
end. |
4-2. Manipulation de tableau multidimensionnel
Projet : ..\Tableaux\LesBases31
Les tableaux multidimensionnels peuvent être créés en utilisant les 2 autres syntaxes de New :
var
a : array [,,] of integer;
b : array [,] of integer;
c : array [,] of TPoint;
begin
a := New(array[3,5,7] of integer);
b := New(array[,] of integer, ((1,2,3), (4,5,6)));
c := New(array[,] of TPoint, (((X:1;Y:2), (X:3;Y:4)), ((X:5;Y:6), (X:7;Y:8))));
end. |
Voici un autre exemple.
Créons un tableau puis initialisons ces cellules une à une :
type
TArrayDynamiqueInteger = Array of Integer;
TArray2D = Array[,] of byte;
var
Tableau2D : TArray2D;
LowerBounds,
Lengths : TArrayDynamiqueInteger;
i,j : Integer;
N : Byte;
begin
{$R-}
Tableau2D := New(array[,] of Byte, ((1,2,3), (4,5,6)));
ShowInfoTableau(Tableau2D);
For N in Tableau2D do
Write(N);
Writeln;
I:=2;
J:=3;
Tableau2D := New(array[I,J] of Byte);
ShowInfoTableau(Tableau2D);
For N in Tableau2D do
Write(N);
Writeln;
readln;
end. |
4-3. La méthode CreateInstance
La méthode CreateInstance peut aussi être utilisée pour créer un tableau multidimensionnel débutant à indice différent de zéro ou non :
LowerBounds:= TArrayDynamiqueInteger.Create(1,2);
Lengths:=TArrayDynamiqueInteger.Create(4,8);
Tableau2D:=System.Array.CreateInstance(typeof(Byte),Lengths,LowerBounds) as TArray2D;
Writeln('Tableau à 2 dimensions');
ShowInfoTableau(Tableau2D);
For N in Tableau2D do
Write(N);
Writeln; |
 |
L'obligation de déclarer la directive $R- pour les tableaux multidimensionnels semble être un bug.
|
5. Remarque sur l'instruction initialize
Pour un tableau statique du type
type
TArrayStatic = Array[0..9] of Byte; |
Initialize(array) est traduit en un appel à l'instruction IL newarr (création d'un nouveau tableau).
Pour un tableau dynamique du type
type
TArrayDynamique= Array of Byte; |
Initialize(array) est traduit en un appel aux instructions IL ldnull puis stloc.
ldnull place une référence nulle (type O) sur la pile. Ceci est employé pour initialiser des références avant qu'elles ne soient créées ou lorsqu'elles sont supprimées.
stloc place une valeur de la pile vers une variable locale.
Dans ce contexte, l'instruction Initialize(array) est donc identique à l'affectation suivante référence:=nil.
Note : L'instruction Finalyze(array) n'est pas implémentée sous Delphi .NET.
 |
Attention à ne pas confondre l'instruction Delphi Initialize avec l'appel de méthode System.Array.Initialize.
|
6. Quelques fonctions utiles
Unité : ..\Tableaux\UInfoArray
La fonction suivante nous permet de déterminer si un tableau est un tableau SZ ou non. Pour ce faire on teste si le nombre de dimension est égal à 1 et si la borne inférieure du tableau est 0 :
Function IsSZArray(Origine:System.Array):Boolean;
Begin
Result:=(Origine.Rank=1) And (Origine.GetLowerBound(0)=0);
end; |
Ici on teste si le tableau Origine à une dimension égale à 1 et une borne inférieure égale à zéro.
Certains choix dans la fonction précédente nous incite à déterminer si un tableau est un tableau de tableau :
Function IsArrayOfArray(Origine:System.Array):Boolean;
var Tableau: System.Array;
begin
Result:=False;
If Not (Origine.GetType.GetElementType=TypeOf(System.Object)) then
if Origine.GetType.GetElementType.IsArray then
Result:=True;
end; |
L'appel de la méthode Origine.GetType renvoie le type de la classe d'origine ( System.Array) puis l'appel de GetElementType renvoie le type de l'objet auquel le tableau fait référence. Ici il y a 2 appels imbriqués.
TypeOf(System.Object) renvoie le type d'après un nom de classe. Pour plus d'informations consulter le tutoriel introduction au système de réflexion sous .NET (C#).
Nativement il n'est pas possible de connaître le niveau d'imbrication d'un tableau de tableau, le code suivant permet d'obtenir cette information :
Function GetNestedLevel(Origine:System.Array):Byte;
type
TableauD2 =Array[0..0,0..0] of TObject;
TableauD3 =Array[0..0,0..0,0..0] of TObject;
begin
Result:=1;
While IsArrayOfArray(Origine) do
begin
case Origine.Rank of
1: Origine:=System.Array(Origine[Origine.GetLowerBound(0)]);
2: Origine:=System.Array(TableauD2(Origine)[Origine.GetLowerBound(0),
Origine.GetLowerBound(1)]);
3: Origine:=System.Array(TableauD3(Origine)[Origine.GetLowerBound(0),
Origine.GetLowerBound(1),
Origine.GetLowerBound(2)]);
end;
Inc(Result);
end;
end; |
Pour déterminer si un objet est du type tableau on peut utiliser la fonction IsArray du système de réflexion. Extrait du SDK .NET 1.1 :
Il se peut que Type.IsArray et Type.GetElementType ne retournent pas les résultats attendus avec Array,
car si un tableau est casté en type Array, le résultat est un objet, et non pas un tableau.
Cela signifie que typeof(System.Array).IsArray retourne false et que typeof(System.Array).GetElementType
retourne une référence nil. |
On utilisera donc une variable intermédiaire pour récupérer l'information :
Function IsAnArray(Origine:TObject):Boolean;
var TabType :System.Type;
begin
TabType:=Origine.GetType;
Result:=TabType.IsArray;
end; |
L'affichage du type d'un tableau simple renvoie typeArray [], celui d'un tableau multidimensionnel renvoie typeArray [,] ([,,], [,,,], ...) et celui d'un tableau de tableau typeArray [][] ([][][], ...).
Les procédures suivantes nous permettront d'afficher la plupart des informations pertinentes concernant une variable de type tableau :
Procedure ShowBound(Origine:System.Array);
var S : String;
I : Byte;
begin
with Origine do
for i:=0 to Rank-1 do
begin
Writeln('Type d''élément du tableau : '+ GetType.GetElementType.ToString);
Writeln('Taille fixe : '+IsFixedSize.ToString);
Writeln('Nombre de dimension = '+Rank.ToString);
S:=I.ToString;
Writeln('Nombre d''éléments('+S+') = '+GetLength(I).ToString);
Writeln('GetLowerBound('+S+') = '+GetLowerBound(I).ToString);
Writeln('GetUpperBound('+S+') = '+GetUpperBound(I).ToString);
end;
end;
Procedure ShowInfoTableau(Origine:System.Array);
begin
with Origine do
begin
Writeln('Variable tableau '+IsAnArray(Origine).ToString);
Writeln('Niveau d''imbrication '+GetNestedLevel(Origine).ToString);
Writeln('Tableau de tableau : '+ IsArrayOfArray(Origine).ToString);
Writeln('Type du tableau : '+GetType.ToString);
Writeln('CLS-Compliant '+ Boolean(GetLowerBound(0)=0).ToString);
Writeln('Tableau simple : '+Boolean(IsSZArray(Origine)).ToString);
Writeln('Taille : '+ IntToStr(SizeOf(Origine) ));
Writeln('nombre total d''éléments dans toutes les dimensions = '+Length.ToString);
ShowBound(Origine)
end;
end; |
Sachez qu'un tableau ne peut être en lecture seul (IsReadOnly) sauf si vous passez par l'usage d'une propriété, le fichier source array.cs précédemment cité nous le confirme :
public abstract class Array : ICloneable, IList
{
...
public virtual bool IsReadOnly {
get { return false; }
}
... |
Sachez aussi que la méthode IsFixedSize renvoie toujours True.
public abstract class Array : ICloneable, IList
{
...
public virtual bool IsFixedSize {
get { return true; }
}
... |
Projet : ..\Tableaux\LesBases13
Pour terminer la fonction SizeOf sur un type ou une variable tableau renvoi la valeur 4, la taille d'un pointeur sur Intel 32 bits. Pour obtenir la taille d'un tableau utilisez Length.
7. Classes apparentées
7-1. BitArray
Projet : ..\Tableaux\BitArray1
Cette classe gère un tableau compact de valeurs de bit représentées par des Boolean, où true indique que le bit est activé (1) et false indique que le bit est désactivé (0).
7-2. Les champs de bits
Cette possibilité n'est pas construite autour d'un tableau, je la signale juste à titre indicatif.
Voir le détail dans ce tutoriel.
7-3. ArrayList
Implémente l'interface IList à l'aide d'un tableau dont la taille augmente dynamiquement selon les besoins.
La classe Borland.Vcl.Classes.TList est mappée sur System.Collections.ArrayList
8. Conclusion
Toutes les syntaxes liées à la gestion des tableaux Win32, sauf Finalyze(array), peuvent être utilisées sous Delphi .NET.
On utilisera les méthodes ou les syntaxes dédiées à .NET pour
- optimiser le code,
- manipuler des types de tableaux que ne permet Delphi Win32,
- bien évidemment, le développement de logiciels qui ne seront pas portés sous Win32.
J'aborderais plus tard la copie complète d'un tableau d'objet contenant d'autres objets (deep copy).
 
|