programmez-en-d/tranches.whata

538 lines
23 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = "Tranches (''slices'') et autres fonctionnalités des tableaux"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
]
Nous avons vu dans le [[part:tableaux | chapitre sur les tableaux]] 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 élements.
- 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.
[ = 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~ :
[code=d <<<
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~ :
[code=d <<<
/* ... */ = JoursDesMois[0 .. 3]; // 0, 1, et 2 sont inclus ; mais pas 3
>>>]
[p Note~ : | 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 [c JoursDesMois] pour utiliser ses parties à travers quatre tableaux plus petits.
[code=d <<<
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~ ; ils donnent accès aux 4 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 élements de chaque tranche et affichons le tableau :
[code=d <<<
premierTrimestre[0] = 1;
deuxiemeTrimestre[0] = 2;
troisiemeTrimestre[0] = 3;
quatriemeTrimestre[0] = 4;
writeln(JoursDesMois);
>>>]
[output ~[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 élements d'un tableau à trois éléments serait [c <<<tableau[0..3]>>>].
Évidemment, l'indice de début ne peut pas être plus grand que l'indice de fin~ :
[code=d <<<
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'[c indice] est valide~ :
[code=d <<<
int[] tranche = unTableau[indice .. indice];
writeln("La taille de la tranche : ", tranche.length);
>>>]
[output La taille de la tranche : 0]
]
[ = [c $], à la place de [m tableau.length]
Quand on accède à un élément, [c $] est un raccourci pour désigner la taille du tableau~ :
[code=d <<<
writeln(array[array.length - 1]); // le dernier élément
writeln(array[$ - 1]); // la même chose
>>>]
]
[ = [c .dup] pour copier
La propriété [c .dup] (comme «~ dupliquer~ ») crée un nouveau tableau depuis les copies des éléments d'un tableau existant~ :
[code=d <<<
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.
[code=d <<<
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);
}
>>>]
[output <<<
Année none 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]
>>>]
]
[ = 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~ :
[code=d <<<
int[3] a = [ 1, 1, 1 ];
int[3] b = [ 2, 2, 2 ];
a = b; // les éléments de 'a' deviennent 2
writeln(a);
>>>]
[output <<< [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.
[code=d <<<
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 [c impairs] et ensuite à certains éléments de [c pairs]~ :
[code=d <<<
[5, 7]
[4, 6, 8]
>>>]
]
[ = 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 huits éléments ci-dessous à travers trois tranches~ :
[code=d <<<
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 [c quart] se remarque sur toutes les tranches~ :
[code=d <<<
[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 à [c quart] avant de modifier son second élément~ :
[code=d <<<
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 [c quart] n'affecte pas les autres~ :
[code=d <<<
[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~ :
[code=d <<< ++quart.length; // Abandonne le partage >>>]
ou
[code=d <<< 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~ :
[code=d <<<
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 [c d] est vue depuis [c a] ; le partage est toujours là.
[code=d <<< [1, 42, 111] >>>]
Réduire la longueur de différentes manières ne termine pas le partage non plus~ :
[code=d <<<
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à.
]
[ = ``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 ait é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~ :
[code=d <<<
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.
[code=d <<<
[1, 0]
[1, 0, 5, 7]
[1, 0, 5, 7, 9, 11, 13, 15, 42]
>>>]
La propriété [c 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~ :
[code=d <<<
if (tranche.capacity == 0) {
/* Ses éléments serait 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;
// ...
}
>>>]
]
[ = Opérations sur tous les éléments
Cette fonctionnalité concerne et les tranches, et les tableaux à taille fixe.
Les caractères [c ~[~]] é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.
Note : 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.
[code=d <<<
import std.stdio;
void main()
{
double[3] a = [ 10, 20, 30 ];
double[3] b = [ 2, 3, 4 ];
double[3] resultat = a[] + b[];
writeln(resultat);
}
>>>]
[output <<<[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 certain 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 peuvent aussi être utilisée. Par exemple, l'opération suivante divise tous les éléments d'un tableau par 4~ :
[code=d <<<
double[3] a = [ 10, 20, 30 ];
a[] /= 4;
writeln(a);
>>>]
[output <<<[2.5, 5, 7.5]>>>]
Pour affecter tous les éléments à une valeur spécifique~ :
[code=d <<<
a[] = 42;
writeln(a);
>>>]
[output <<<[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 expression suivantes ont un sens très différent :
[code=d <<<
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ême é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ée.
On peut voir un exemple de cette différence dans le programme suivant~ :
[code=d <<<
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~ :
[output <<<
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.
]
[ = 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~ :
[code=d <<<
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~ :
[code=d <<<
/* ... */ 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``~ :
[code=d <<<
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 :
[code=d <<<
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~ :
[code=d <<<[[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~ :
[code=d <<<
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 :
[code=d <<<
// 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~ :
[code=d <<<
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~ :
[output <<<[[0, 0, 0], [0, 0, 0]]>>>]
]
[ = 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``.
- [c .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. [c .capacity] détermine si ça sera le cas.
- [c array~[~]] fait référence à tous les éléments~ ; l'opération qui est appliquée à [c array~[~]] est appliquée à chaque élément individuellement.
- Les tableaux de tableaux sont appelés tableaux multidimensionnels.
]
[ = Exercice
Itérez sur les éléments d'un tableau de ``double``s et divisez par deux ceux qui sont plus grands que 10. Par exemple, étant donné le tableau suivant~ :
[code=d <<<
double[] array = [ 1, 20, 2, 30, 7, 11 ];
>>>]
On doit obtenir :
[code=d <<<
[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~ :
[code=d <<<
tranche = tranche[1 .. $];
>>>]
[[part:corrections/tranches | … La solution]]
]