IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

20. Tranches (slices) et autres fonctionnalités des tableaux

Nous avons vu dans le chapitre sur les tableauxTableaux comment les éléments sont groupés comme une collection dans un tableau. Ce chapitre a été volontairement bref, laissant à ce chapitre-ci la plupart des fonctionnalités des tableaux.

Avant d'aller plus loin, voici quelques définitions brèves de certains termes qui se trouvent être proches dans leur signification :

  • Tableau : l'idée générale d'un groupe d'éléments qui sont situés les uns à côté des autres et qui sont accédés par des indices ;
  • Tableau de taille fixe (tableau statique) : tableau avec un nombre fixe d'éléments. Ce type de tableau stocke lui-même ses éléments ;
  • Tableau dynamique : tableau qui peut perdre ou gagner des éléments. Ce type de tableau donne accès à des éléments qui sont stockés par l'environnement d'exécution du D ;
  • Tranches (slices) : autre nom des tableaux dynamiques.

Quand j'écrirai slice, je désignerai précisément une tranche. Quand j'écrirai tableau, je désignerai soit une tranche, soit un tableau à taille fixe, sans distinction.

20-1. Tranches

Les tranches sont la même chose que les tableaux dynamiques. Elles sont appelées tableaux dynamiques parce qu'elles sont utilisées comme des tableaux, et sont appelées tranches parce qu'elles donnent un accès à des portions d'autres tableaux. Elles permettent d'utiliser ces portions comme si elles étaient des tableaux à part entière.

Les tranches sont définies avec un intervalle correspondant aux indices du tableau duquel on veut « extraire » la tranche :

 
Sélectionnez
indice_de_debut .. un_apres_l_indice_de_fin

Dans la syntaxe permettant d'écrire un intervalle, l'indice de début fait partie de l'intervalle, mais l'indice de fin est hors de l'intervalle :

 
Sélectionnez
/* ... */ = JoursDesMois[0 .. 3];   // 0, 1, et 2 sont inclus ; mais pas 3

Les intervalles de nombres sont différents des intervalles de Phobos. Les intervalles de Phobos sont en relation avec les interfaces de classes et de structures. Nous verrons cela dans des chapitres ultérieurs.

Par exemple, on peut extraire des tranches du tableau JoursDesMois pour utiliser ses parties à travers quatre tableaux plus petits.

 
Sélectionnez
int[12] JoursDesMois =
      [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
 
int[] premierTrimestre   = JoursDesMois[0 .. 3];
int[] deuxiemeTrimestre  = JoursDesMois[3 .. 6];
int[] troisiemeTrimestre = JoursDesMois[6 .. 9];
int[] quatriemeTrimestre = JoursDesMois[9 .. 12];

Les quatre variables dans le code ci-dessus sont des tranches ; elles donnent accès aux quatre parties d'un tableau déjà existant. Un point important qui vaut la peine d'être noté est que ces tranches ne stockent pas leurs propres éléments. Elles donnent simplement accès aux éléments du vrai tableau. Modifier un élément d'une tranche modifie l'élément du tableau. Pour voir cela, modifions les premiers éléments de chaque tranche et affichons le tableau :

 
Sélectionnez
premierTrimestre[0]   = 1;
deuxiemeTrimestre[0]  = 2;
troisiemeTrimestre[0] = 3;
quatriemeTrimestre[0] = 4;
 
writeln(JoursDesMois);

Sortie :

 
Sélectionnez
[1, 28, 31, 2, 31, 30, 3, 31, 30, 4, 30, 31]

Chaque tranche modifie son premier élément, et l'élément correspondant dans le tableau est affecté.

Nous avons vu plus tôt que les indices valides des tableaux vont de 0 à la taille du tableau moins un. Par exemple, les indices valides d'un tableau à trois éléments sont 0, 1 et 2. De manière similaire, l'indice de fin dans la syntaxe de la tranche est l'indice de l'élément qui est juste après le dernier élément auquel la tranche donne accès. Par exemple, une tranche de tous les éléments d'un tableau à trois éléments serait tableau[0..3]

Évidemment, l'indice de début ne peut pas être plus grand que l'indice de fin :

 
Sélectionnez
int[3] tableau = [ 0, 1, 2 ];
int[] tranche = tableau[2 .. 1];   // ← ERREUR lors de l'exécution

Il est correct d'avoir l'indice de début et l'indice de fin ayant la même valeur. Dans ce cas, la tranche est vide. En supposant qu'indice est valide :

 
Sélectionnez
int[] tranche = unTableau[indice .. indice];
writeln("La taille de la tranche : ", tranche.length);

Sortie :La taille de la tranche : 0

20-2. $, à la place de tableau.length

Quand on accède à un élément, $ est un raccourci pour désigner la taille du tableau :

 
Sélectionnez
writeln(array[array.length - 1]);   // le dernier élément
writeln(array[$ - 1]);              // la même chose

20-3. .dup pour copier

La propriété .dup (comme « dupliquer ») crée un nouveau tableau depuis les copies des éléments d'un tableau existant :

 
Sélectionnez
double[] array = [ 1.25, 3.75 ];
double[] theCopy = array.dup;

Par exemple, définissons un tableau qui contient le nombre de jours pour chaque mois d'une année bissextile. Une méthode est de faire la copie d'un tableau d'une année non bissextile et d'incrémenter l'élément qui correspond à février.

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[12] JoursDesMois =
         [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
 
   int[] anneeBissextile = JoursDesMois.dup;
 
   ++anneeBissextile[1];   // incrémente le nombre de jours de février
 
   writeln("Année non bissextile : ", JoursDesMois);
   writeln("Année bissextile     : ", anneeBissextile);
}

Sortie :

 
Sélectionnez
Année non bissextile : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
Année bissextile      : [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

20-4. Affectation

Jusqu'à maintenant, nous avons vu que l'opérateur d'affectation modifie les valeurs des variables. C'est la même chose avec les tableaux de taille fixe :

 
Sélectionnez
int[3] a = [ 1, 1, 1 ];
int[3] b = [ 2, 2, 2 ];
 
a = b;      // les éléments de 'a' deviennent 2
writeln(a);

Sortie : [2, 2, 2]

L'opération d'affectation a un sens complètement différent pour les tranches : après affectation, la tranche donne accès à des nouveaux éléments.

 
Sélectionnez
int[] impairs = [ 1, 3, 5, 7, 9, 11 ];
int[] pairs = [ 2, 4, 6, 8, 10 ];
 
int[] tranche;   // ne donne accès à aucun élément pour le moment
 
tranche = impairs[2 .. $ - 2];
writeln(tranche);
 
tranche = pairs[1 .. $ - 1];
writeln(tranche);

Ci-dessus, la tranche ne donne accès à aucun élément lors de sa définition. Ensuite, elle est utilisée pour donner accès à certains éléments de impairs et ensuite à certains éléments de pairs :

Sortie :

 
Sélectionnez
[5, 7]
[4, 6, 8]

20-5. Agrandir une tranche plus longue peut finir le partage

Comme la longueur d'un tableau à taille fixe ne peut pas être changée, l'idée de fin de partage ne concerne que les tranches.

Il est possible d'accéder aux mêmes éléments par plus d'une tranche. Par exemple, on accède aux deux premiers des huit éléments ci-dessous à travers trois tranches :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[] tranche = [ 1, 3, 5, 7, 9, 11, 13, 15 ];
   int[] moitie = tranche[0 .. $ / 2];
   int[] quart = tranche[0 .. $ / 4];
 
   quart[1] = 0;   // modification via une tranche
 
   writeln(quart);
   writeln(moitie);
   writeln(tranche);
}

L'effet de la modification du second élément de quart se remarque sur toutes les tranches :

 
Sélectionnez
[1, 0]
[1, 0, 5, 7]
[1, 0, 5, 7, 9, 11, 13, 15]

Vu sous cet angle, les tranches donnent un accès partagé aux éléments. Ce partage pose la question de ce qu'il arrive quand un nouvel élément est ajouté à une des tranches. Comme les tranches donnent accès au même élément, il pourrait ne plus y avoir de place pour ajouter des éléments à une tranche sans rentrer en conflit avec les autres.

D répond à cette question en finissant le partage s'il n'y a pas de place pour le nouvel élément : la tranche qui n'a pas de place pour grandir abandonne le partage. Quand cela arrive, tous les éléments existants de cette tranche sont copiés à un nouvel endroit automatiquement et la tranche commence à donner accès à ces nouveaux éléments.

Pour voir ceci en action, ajoutons un élément à quart avant de modifier son second élément :

 
Sélectionnez
quart ~= 42;  // Cette tranche abandonne le partage parce qu'il
              // n'y a pas de place pour le nouvel élément.
 
quart[1] = 0; // Pour cette raison, la modification
              // n'affecte pas les autres tranches

La sortie du programme montre que la modification apportée à la tranche quart n'affecte pas les autres :

 
Sélectionnez
[1, 0, 42]
[1, 3, 5, 7]
[1, 3, 5, 7, 9, 11, 13, 15]

Augmenter la taille de la tranche de façon explicite lui fait également abandonner le partage :

 
Sélectionnez
++quart.length;       // Abandonne le partage

ou

 
Sélectionnez
quart.length += 5;    // Abandonne le partage

En revanche, raccourcir une tranche n'affecte pas le partage. Raccourcir la tranche veut simplement dire que celle-ci donne maintenant accès à moins d'éléments :

 
Sélectionnez
int[] a = [ 1, 11, 111 ];
int[] d = a;
 
d = d[1 .. $];   // raccourcir par la gauche
d[0] = 42;       // modifier l'élément depuis la tranche
 
writeln(a);      // afficher l'autre tranche

Comme on peut le voir sur la sortie, la modification depuis d est vue depuis a ; le partage est toujours là.

 
Sélectionnez
[1, 42, 111]

Réduire la longueur de différentes manières ne termine pas le partage non plus :

 
Sélectionnez
d = d[0 .. $ - 1];         // raccourcir par la droite
--d.length;                // idem
d.length = d.length - 1;   // idem

Le partage des éléments est toujours là.

20-6. capacity pour savoir si le partage va être terminé

Il y a des cas où les tranches continuent à partager des éléments même après qu'un élément leur a été ajouté. Ceci arrive quand l'élément est ajouté à la tranche la plus longue et qu'il y a de la place après son dernier élément :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[] tranche = [ 1, 3, 5, 7, 9, 11, 13, 15 ];
   int[] half = tranche[0 .. $ / 2];
   int[] quarter = tranche[0 .. $ / 4];
 
   tranche ~= 42;      // ajout à la tranche la plus longue...
   tranche[1] = 0;     // ... et modification d'un élément.
 
   writeln(quarter);
   writeln(half);
   writeln(tranche);
}

Comme on peut le remarquer sur la sortie, même si l'élément ajouté augmente la taille de la tranche, le partage n'a pas été terminé et la modification est reflétée sur toutes les tranches.

 
Sélectionnez
[1, 0]
[1, 0, 5, 7]
[1, 0, 5, 7, 9, 11, 13, 15, 42]

La propriété capacity des tranches détermine si le partage sera fini si un élément est ajouté à une certaine tranche. (capacity est en fait une fonction, mais cette distinction n'a pas d'importance dans cette discussion.)

capacity a deux sens :

  • quand sa valeur est 0, cela veut dire que ce n'est pas la tranche originale la plus grande. Dans ce cas, ajouter un nouvel élément déplacerait de toute façon les éléments de la tranche et le partage prendrait fin ;
  • quand sa valeur n'est pas nulle, cela veut dire que c'est la tranche originale la plus large. Dans ce cas, capacity dénote le nombre total d'éléments que la tranche peut contenir sans avoir besoin d'être copiée. Le nombre de nouveaux éléments qui peuvent être ajoutés à cette tranche avant qu'elle ait besoin d'être copiée peut être calculé en soustrayant la taille de la tranche à sa capacité. Il n'y a pas d'espace pour un nouvel élément si la taille de la tranche vaut sa capacité.

En conséquence, un programme qui a besoin de déterminer si le partage sera fini devrait utiliser un schéma dans ce style :

 
Sélectionnez
if (tranche.capacity == 0) {
   /* Ses éléments seraient déplacés si un élément de plus
    * était ajouté à la tranche */
 
   // ...
 
} else {
   /* Cette tranche peut avoir de la place pour de nouveaux éléments avant
    * de nécessiter un déplacement. Calculons combien d'éléments on peut
    * ajouter avant déplacement de la tranche : */
   auto combienDeNouveauxElements = tranche.capacity - tranche.length;
 
   // ...
}

20-7. Opérations sur tous les éléments

Cette fonctionnalité concerne et les tranches, et les tableaux à taille fixe.

Les caractères [] écrits après le nom d'un tableau veulent dire : « tous ses éléments ». Cette fonctionnalité simplifie le programme quand certaines opérations ont besoin d'être appliquées à tous les éléments d'un tableau.

Le compilateur D que j'ai (NDT L'auteur) utilisé lors de l'écriture de ce chapitre (dmd 2.056) ne prend pas en charge cette caractéristique complètement. Pour cette raison, j'ai utilisé uniquement des tableaux à taille fixe dans quelques-uns des exemples qui suivent.

 
Sélectionnez
import std.stdio;
 
void main()
{
   double[3] a = [ 10, 20, 30 ];
   double[3] b = [  2,  3,  4 ];
 
   double[3] resultat = a[] + b[];
 
   writeln(resultat);
}

Sortie :

 
Sélectionnez
[12, 23, 34]

L'opération addition dans ce programme est appliquée aux éléments correspondant aux deux tableaux dans cet ordre : d'abord, les premiers éléments sont ajoutés, ensuite les seconds éléments sont ajoutés, etc. Une précondition naturelle est que les tailles des deux tableaux doivent être égales.

L'opérateur peut être l'un des opérateurs arithmétiques +, -, /, % et ^^ ; l'un des opérateurs binaires ^, & et | ou l'un des opérateurs unaires - et ~ qui sont écrits devant un tableau. Nous verrons certains de ces opérateurs dans des chapitres ultérieurs.

Les versions d'affectation de ces opérateurs peuvent aussi être utilisées : =, +=, -=, *=, /=, %=, ^^=, ^=, &= et |=.

Cette fonctionnalité n'est pas uniquement valable entre deux tableaux ; en plus d'un tableau, une expression qui est compatible avec les éléments peut aussi être utilisée. Par exemple, l'opération suivante divise tous les éléments d'un tableau par 4 :

 
Sélectionnez
double[3] a = [ 10, 20, 30 ];
a[] /= 4;
 
writeln(a);

Sortie : [2.5, 5, 7.5]

Pour affecter tous les éléments à une valeur spécifique :

 
Sélectionnez
a[] = 42;
writeln(a);

Sortie : [42, 42, 42]

Cette fonctionnalité nécessite une attention particulière quand elle est utilisée avec les tranches. Même s'il n'y a pas de différence apparente dans les valeurs des éléments, les deux expressions suivantes ont un sens très différent :

 
Sélectionnez
tranche2 = tranche1;      // ← tranche2 donne maintenant accès
                          //   aux mêmes éléments que tranche1
 
tranche3[] = tranche1;    // ← les valeurs des éléments
                          //   de tranche3 changent

L'affectation à tranche2 lui fait partager les mêmes éléments que tranche1. En revanche, comme tranche3[] fait référence à tous les éléments de tranche3, les valeurs de ses éléments deviennent les mêmes que celles des éléments de tranche1. L'effet de la présence ou de l'absence des crochets ne peut pas être ignoré.

On peut voir un exemple de cette différence dans le programme suivant :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double[] tranche1 = [ 1, 1, 1 ];
   double[] tranche2 = [ 2, 2, 2 ];
   double[] tranche3 = [ 3, 3, 3 ];
 
   tranche2 = tranche1;      // ← tranche2 commence à partager
                             //   les mêmes éléments que tranche1
 
   tranche3[] = tranche1;    // ← Les valeurs des éléments de
                             //   tranche3 changent
 
    writeln("tranche1 avant : ", tranche1);
    writeln("tranche2 avant : ", tranche2);
    writeln("tranche3 avant : ", tranche3);
 
    tranche2[0] = 42;      // ← La valeur d'un élément qu'elle
                           //   partage avec tranche1 change
 
    tranche3[0] = 43;      // ← la valeur d'un élément auquel
                           //   tranche3 seule procure un accès change
 
   writeln("tranche1 après : ", tranche1);
   writeln("tranche2 après : ", tranche2);
   writeln("tranche3 après : ", tranche3);
}

La modification depuis tranche2 affecte tranche1 également :

 
Sélectionnez
tranche1 avant : [1, 1, 1]
tranche2 avant : [1, 1, 1]
tranche3 avant : [1, 1, 1]
tranche1 après : [42, 1, 1]
tranche2 après : [42, 1, 1]
tranche3 après : [43, 1, 1]

Le danger ici est que le bogue éventuel peut ne pas être remarqué jusqu'à ce que la valeur d'un élément partagé soit changée.

20-8. Tableaux multidimensionnels

Jusqu'à maintenant nous avons utilisé les tableaux avec des types fondamentaux comme int ou double uniquement. Le type d'élément peut en fait être n'importe quel autre type, notamment d'autres tableaux. Cela permet au programmeur de définir des conteneurs complexes comme des tableaux de tableaux. Les tableaux de tableaux sont appelés tableaux multidimensionnels.

Tous les éléments de tous les tableaux que nous avons définis jusqu'à présent ont été écrits dans le code source de gauche à droite. Pour aider à comprendre l'idée de tableau bidimensionnel, définissons cette fois-ci un tableau de haut en bas :

 
Sélectionnez
int[] array = [
            10,
            20,
            30,
            40
            ];

Comme vous devez vous en souvenir, la plupart des espaces dans le code source sont là pour rendre le code lisible et ne changent pas sa signification. Le tableau ci-dessus aurait pu être défini sur une seule ligne et aurait eu, le cas échéant, la même signification.

Remplaçons maintenant chaque élément de ce tableau par un autre tableau :

 
Sélectionnez
/* ... */ array = [
                     [ 10, 11, 12 ],
                     [ 20, 21, 22 ],
                     [ 30, 31, 32 ],
                     [ 40, 41, 42 ]
                  ];

On a remplacé des éléments de type entier avec des éléments de type int[]. Pour rendre le code conforme à la syntaxe permettant de définir les tableaux, on doit maintenant indiquer le type des éléments comme int[] à la place de int :

 
Sélectionnez
int[][] array = [
                   [ 10, 11, 12 ],
                   [ 20, 21, 22 ],
                   [ 30, 31, 32 ],
                   [ 40, 41, 42 ]
                ];

De tels tableaux sont appelés tableaux bidimensionnels parce qu'ils peuvent être vus comme ayant des lignes et des colonnes.

Les tableaux bidimensionnels sont utilisés comme n'importe quel autre tableau du moment que l'on se souvient que chaque élément est lui-même un tableau et qu'il est utilisé avec les opérations sur les tableaux :

 
Sélectionnez
array ~= [ 50, 51 ]; // ajoute un nouvel élément (une tranche)
array[0] ~= 13;      // ajoute un élément au premier élément

Le nouvel état du tableau :

 
Sélectionnez
[[10, 11, 12, 13], [20, 21, 22], [30, 31, 32], [40, 41, 42], [50, 51]]

Les éléments du tableau et le tableau lui-même peuvent être de taille fixe. Ce qui suit est un tableau tridimensionnel où toutes les dimensions sont de taille fixe :

 
Sélectionnez
int[2][3][4] tableau;   // 2 colonnes, 3 lignes, 4 pages

La définition ci-dessus peut être vue comme 4 pages de trois lignes de deux colonnes. Par exemple, un tel tableau peut être utilisé pour représenter un bâtiment à 4 étages dans un jeu d'aventure, chaque étage consistant en 2×3=6 pièces.

Par exemple, le nombre d'objets dans la première pièce du deuxième étage peut être incrémenté de cette manière :

 
Sélectionnez
// L'indice du deuxième étage est 1 et on accède à
// la première pièce de cet étage par [0][0]
++nombresObjets[1][0][0];

En plus de la syntaxe ci-dessus, l'expression new peut aussi être utilisée pour créer une tranche de tranches. L'exemple suivant utilise seulement deux dimensions :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int[][] s = new int[][](2, 3);
   writeln(s);
}

L'expression new ci-dessus crée deux tranches contenant 3 éléments chacune et retourne une tranche qui donne accès à ces tranches et éléments.

Sortie :

 
Sélectionnez
[[0, 0, 0], [0, 0, 0]]

20-9. Résumé

  • Les tableaux de taille fixe stockent leurs éléments ; les tranches donnent accès à des éléments qui ne leur appartiennent pas.
  • À l'intérieur des crochets, $ est l'équivalent de nom _tableau.length.
  • .dup crée un nouveau tableau qui est composé des copies des éléments d'un tableau existant.
  • Avec les tableaux à taille fixe, l'affectation change les valeurs des éléments ; avec les tranches, elle fait pointer la tranche vers d'autres éléments.
  • Les tranches qui grandissent peuvent arrêter de partager les éléments et commencer à donner accès à des éléments nouvellement copiés. .capacity détermine si ce sera le cas.
  • array[] fait référence à tous les éléments ; l'opération qui est appliquée à array[] est appliquée à chaque élément individuellement.
  • Les tableaux de tableaux sont appelés tableaux multidimensionnels.

20-10. Exercice

Itérez sur les éléments d'un tableau de doubles et divisez par deux ceux qui sont plus grands que 10. Par exemple, étant donné le tableau suivant :

 
Sélectionnez
double[] array = [ 1, 20, 2, 30, 7, 11 ];

On doit obtenir :

 
Sélectionnez
[1, 10, 2, 15, 7, 5.5]

Même s'il y a beaucoup de solutions à ce problème, essayez de n'utiliser que les fonctionnalités des tranches. Vous pouvez commencer avec une tranche qui donne accès à tous les éléments. Ensuite, vous pouvez réduire la tranche depuis le début et ne travailler que sur le premier élément.

L'expression suivante réduit la tranche depuis le début :

 
Sélectionnez
tranche = tranche[1 .. $];

La solutionTranches et autres fonctionnalités des tableaux - Correction.


précédentsommairesuivant