{{tag>dev cours informatique pharo}} ====== Vue générale des principales collections ====== Dans cette séquence nous allons voir les éléments essentiels de la hiérarchie des collections en Pharo. Vous allez voir que Pharo est riche du point de vue des différents types de collections, mais il facilite la vie du programmeur puisqu'ils présentent tous une **API commune**. On verra également la différence entre les collections littérales et les collections dynamiques. L'API des collections, comme je disais, est riche, on verra qu'il y a beaucoup de types de collections différents. Toutes présentent une API commune, on le verra, qui est assez bien organisée, qui facilite énormément la vie du programmeur. Petit point particulier en Pharo les index des collections, commencent à 1 alors que ça commence à 0 dans d'autres langages. Et puis les collections peuvent contenir n'importe quel type d'objet en Pharo, ce qui n'est pas forcément le cas dans dans les autres langages. Donc quelques-unes des collections les plus remarquables et les plus utilisées: * **OrderedCollection** (extensible dynamiquement) * **Array** (taille fixe, accès direct) * **Set** (pas de doublons) * **Dictionnary** (table de hachage, accès à clé/valeur) **OrderedCollection** qui est une collection dynamique dont la taille va grossir à chaque fois qu'on ajoute des éléments dedans. **Array** qui est une collection de taille fixe. Et puis on va accéder aux éléments en fonction d'un indice. **Set**, qui va contenir des éléments mais sans doublon. On ne pourra pas insérer 2 fois le même élément dans un Set. Et puis les **dictionnaires**, donc les dictionnaires ce sont des tables de hachage, à une clé donnée j'associe une valeur. Voici un extrait de la hiérarchie des collections: {{pharo_hierarchie_des_collections.png}} C'est seulement un extrait, il est plus riche que ça en Pharo. Vous pouvez voir qu'il y a plein de classes. Elles héritent toutes de ''Collection'', ce qui nous fournit une API commune pour l'ensemble des collections. Et puis on va voir celles qui sont en gras, on voit les expliciter un peu au fur et à mesure de cette séquence. Il existe une API commune, je vous disais, répartie en 7 points, avec des méthodes spécifiques pour: - La création: **''with:''**, **''with:with:''**, **''withAll:''** qu'on va envoyer plutôt aux classes des collections; - Accès: **''size:''**, **''at:''**, **''at:put:''** pour accéder aux propriétés des collections, que ce soit accéder par exemple à la taille d'une collection ou accéder même aux éléments que la collection contient. - Tests: **''isEmpty''**, ''**includes:**'', **''contains:''** savoir est-ce que la collection est vide ou pas. - Ajout: ''**add:**'', **''addAll:''** ajout et de retrait d'éléments; - Suppression: **''remove:''**, ''**remove:ifAbsent:**'', ''**removeAll:**'' - Enumération: ''**do:**'', ''**collect:**'', ''**select:**'', ''**reject:**'', ''**detect:**'' permettant de parcourir l'ensemble des éléments et de savoir si un élément existe, est présent dans une collection ou pas. - Convertir: ''**asBag**'', ''**asSet**'', ''**asOrderedCollection**'', ''**asSortedCollection**'', ''**asArray**'' pour passer d'un type de collection à un autre. Commençons par un exemple. Donc je veux créer une collection en Pharo tout bêtement. Je vais sélectionner la bonne classe qui m'intéresse et lui envoyer un message new pour instancier, pour créer un nouvel instance sur cette classe. "Instanciation d'une collection via new" Array new "On peut spécifier la taille de la collection avec new:" Array new: 4 Array new: 2 OrderedCollection new: 1000 Donc premier cas de figure, j'utilise new. Deuxième cas de figure, en spécifiant la taille de la collection. Typiquement ''Array'' je peux lui envoyer ''new: 4'', donc je fais un tableau de taille 4 ou un tableau de taille 2, ça marche également sur les ''OrderedCollection'' je pourrais faire un ''OrderedCollection'' de taille 1000. On a d'autres types de méthodes pour créer des collections pré-initialisées, donc avec ''withAll'' par exemple où je vais passer une collection littérale. OrderedCollection withAll: #(7 7 3 33) Une collection littérale commence par un **''#(''** . Une nouvelle instance de la classe ''OrderedCollection'' est créée qui contiendra bien tous les éléments qui ont été placés au moment de sa création. Je peux faire la même chose avec un Set, par contre je vous rappelle dans un ''**Set**'' on ne peut pas avoir d'objets doublons. Set withAll: #(7 7 3 33) "> a Set(7 3 33)" Donc le chiffre 7 qu'on avait mis 2 fois dans la collection littérale il ne peut pas se retrouver 2 fois dans le Set. Il y a d'autres sortes de messages qu'on peut envoyer aux classes collection pour les initialiser. Ici j'en ai un autre exemple. OrderedCollection new: 5 withAll: 'a' "> an OrderedCollection('a' 'a' 'a' 'a' 'a')" Avec le message **''new:withAll:''**, je veux faire une collection de taille 5 mais je veux que toutes les classes soient initialisées avec un certain objet. Donc en l'occurrence ici une String d'un seul élément 'a'. Petite subtilité en Pharo **toutes les collections commencent à l'indice 1**. #('Calvin' 'hates' 'Suzie') at: 2 "> 'hates' " Si je demande à cette collection de 3 éléments de me rendre l'élément à l'indice 2, c'est bien ''**'hates'**''. Même chose pour les ''**OrderedCollection**'', si je convertis cette collection en ''**OrderedCollection**'' et je lui demande son élément indice 2: #('Calvin' 'hates' 'Suzie') asOrderedCOllection at: 2 "> 'hates' " Le code retourne ''**'hates'**'' la même chose. Les collections peuvent contenir toutes sortes d'objets, comme je l'ai dit, et ici je vous en montre un exemple cette collection littérale va contenir, dans son premier élément, la chaîne des caractères 'calvin', et dans son second élément une collection qui contiendra les nombres 1, 2, 3. "Collection de type Array contenant des objets différents" #('calvin' (1 2 3)) "> #('calvin' #(1 2 3)) Cet extrait permet d'aboutir a la création d'un tableau contenant un ensemble vide et des entiers: |s| s:= Set new. s add: Set new; add: 1; add: 2. s asArray "> an Array(1 2 a Set())" On peut parcourir les éléments d'une collection, en utilisant le message ''**do:**'' par exemple. #('Calvin' 'hates' 'Suzie') do: [ :each | Transcript show: each; cr ] Ici j'ai une collection et je vais lui envoyer le message ''**do:**''. Je vais lui passer un block, donc je vous rappelle le block commence avec le crochet ouvrant, il se ferme avec le crochet fermant. Le paramètre du block il s'appelle ''each'', donc qui commence par un ''**:**''. Il est séparé du corps de block par la barre verticale ''**|**'' et à chaque tour de boucle, "each" vaudra le premier élément de la collection, le deuxième et caetera jusqu'à la fin. Au final on affichera sur le Transcript: Calvin hates Suzie ===== Les tableaux ===== Les tableaux sont des collections de taille fixe. Donc on peut demander à un tableau sa taille, j'y envoie le message "size". #('Calvin' 'hates' 'Suzie') size "> 3" On peut accéder directement à un élément d'un tableau en lui envoyant le message ''**at:**''. On peut modifier/spécifier l'élément à l'indice 1 dans la collection en envoyant %%at: 1 put: 'Calvin'%%. #('Calvin' 'hates' 'Suzie') size " > 3" " Equivalent dynamique" ((Array new: 3) at: 1 put: 'Calvin'; at: 2 put: 'hates'; at: 3 put: 'Suzie'; size) " > 3" Donc je vais insérer la chaîne Calvin dans la case 1. Et puis je peux demander la taille. L'élément intéressant ici dans cet exemple c'est qu'on voit qu'on a construit le même tableau de 2 manières différentes: une première version littérale et une deuxième version dynamique, où j'ai vraiment instancié la classe Array à la main et rempli chacune des cases. Il faut faire attention il y a une subtilité. Quand j'accède à un élément d'une collection en fournissant un indice, il faut faire attention que cet indice soit bien dans les bornes acceptées par la collection, qu'il soit inférieur à la taille de la collection. #('Calvin' 'hates' 'Suzie') at: 55 " > SubscriptOutOfBounds Error" Si je demande à cette collection l'élément à l'indice 55 forcément il n'existe pas puisque cette collection contient 3 éléments, donc ça va bien me rendre une erreur. ===== Modifier des éléments ===== Pour modifier les éléments, j'en ai parlé, à l'indice 2 je veux modifier, insérer un nouvel élément dans la collection, ici la chaîne de caractères 'loves': #('Calvin' 'hates' 'Suzie') at: 2 put: 'loves' ===== Tableau littéraux ===== Un exemple de tableau littéral: #( 45 'milou' 1300 true #tintin ) class "> Array" Commence par ''**#(**'', on peut mettre n'importe quoi dedans: un nom ou une chaîne de caractères et cetera. Et puis tous les tableaux littéraux en Pharo sont instance de la classe Array par défaut. Donc je peux envoyer le message "class" à un tableau littéral et ça me rend bien Array. C'est bien une instance de la classe Array. Les versions dynamiques et les versions littérales sont exactement équivalentes en Pharo. C'est juste une version plus concise permettant d'écrire plus vite. "Forme littérale" #(45 38 'milou' 8) "Forme dynamique" Array with: 45 with: 38 with: 'milou' with: 8 "> #(45 38 'milou' 8)" Donc ici vous avez la version littérale d'une collection et vous avez sa version dynamique où j'instancie vraiment la classe ''**Array**'' à la main. Mais c'est complètement équivalent. ===== Classe OrderedCollection ===== La classe ''**OrderedCollection**'' définit une collection particulière, qui est extensible, donc à chaque fois qu'on va ajouter des éléments elle va s'agrandir. |ordCol| ordCol := OrderedCollection new. ordCol add: 'Reef'; add: 'Pharo'; addFirst: 'Pharo'. ordCol "> an OrderedCollection('Pharo' 'Reef2' 'Pharo')" ordCol add: 'Seaside'. ordCol "> an OrderedCollection('Pharo' 'Reef2' 'Pharo' 'Seaside')" On instancie la classe ''OrderedCollection'' en envoyant un message new (ligne 3). J'utilise la méthode ''**add:**'' pour ajouter différents éléments dans cette collection (lignes 5 à 7). J'ai même des variations je peux utiliser ''**addFirst:**'' pour ajouter un élément au début de la collection, par défaut il s'ajoute à la fin. Vous voyez à chaque fois ce que nous rend la collection. Donc là on a bien 3 éléments dans la collection: Pharo, Reef et Pharo. Si je fais ''**add: 'Seaside'**'', "Seaside" est bien ajouté à la fin de la collection (valeur retournée ligne 18). J'ai des méthodes de conversion entre un type de collection et un autre. Donc par exemple "Conversion d' un Array en OrderedCollection" #('Pharo' 'Reef' 'Pharo' 'Pharo') asOrderedCollection "> an OrderedCollection('Pharo' 'Reef' 'Pharo' 'Pharo')" j'utilise une collection littérale, donc qui va être un Array et le message asOrderedCollection qui va transformer ce tableau en collection ordonnée. ===== Les ensembles (classe Set) ===== Les Set sont un type de collection sans doublon. Donc pareil de taille extensible, donc à chaque fois que va ajouter un élément ça va grandir. " Création d'un Set via une collection littérale" #('Pharo' 'Reef' 'Pharo' 'Pharo') asSet "> a Set('Pharo' 'Reef')" "Création dynamique" Set with:(Set with: 1) with:(Set with: 2) Je peux utiliser une collection littérale que je transforme en Set et ça me donne bien un Set dans lequel les doublons ont été retirés (retour ligne 4). Ou alors je peux accéder à la version dynamique plutôt qu'une collection littérale. Les messages ''**with:**'' peuvent se succéder et permettent de créer un Set et de le remplir avec n éléments, deux Set à chaque fois. Donc les méthodes de conversion sont extrêmement pratiques pour jongler, pour transformer une collection en un autre type. Elles ont toujours la même nomenclature c'est "as" + le nom de la collection qu'on voudra avoir. * asOrderedCollection * asSet * asArray * etc ===== Les dictionnaires ====== C'est un type de collection clés, valeurs. À une clé donnée j'associe une valeur. Elles sont aussi extensibles à chaque fois qu'on va ajouter des éléments elles vont grandir, et puis on a une API un peu particulière sur cette collection: * Le message ''**at:**'' qui lui est classique; * ''**at:ifAbsent:**'' c'est-à-dire si je veux accéder à un élément, à une clé particulière mais qu'elle n'existe pas, qu'est-ce que je dois rendre? * ''**at: put:**'' à une clé particulière je veux insérer une nouvelle valeur. * Et puis je vais pouvoir itérer avec des messages tout à fait classiques ''**do:**''", on a déjà vu, mais on va avoir des nouveaux messages ''**KeysDo:**'' donc je parcours toutes les clés du dictionnaire ou ''**KeysAndValuesDo:**'' pour les clés et les valeurs. |days| days := Dictionary new. days at: #January put: 31; at: #Febuary put: 28; at: #March put: 31. On créé l'instance de la classe dictionnaire, et puis dans ce dictionnaire, donc il faut voir un dictionnaire un peu comme un tableau, à la clé janvier j'associe le nombre 31, à la clé février le nombre 28 et à la clé mars le nombre 31. Donc c'est complètement équivalent à une collection dynamique. Donc une **collection dynamique** cette fois c'est créé **avec une accolade**. |days| days := { #January -> 31. #Febuary -> 28. #March -> 31. } asDictionary Accolade ouvrante, accolade fermante. Ici je vous rappelle la flèche c'est pour créer des associations. Donc ici j'ai un symbole. Donc au symbole janvier j'associe le nombre 31, donc ici j'ai bien une collection d'associations que je transforme en dictionnaire avec à le message ''**AsDictionary**''. Donc ces deux formes pour créer le dictionnaire sont complètement équivalentes. Si je demande à une association sa clé ça va bien nous renvoyer la clé, donc le début. (#January -> 31) class "> Association" (#January -> 31) key "> #January" (#January -> 31) value "> 31" Et puis si je demande à une association de me retourner sa valeur ça ne me retourne que la valeur. C'est une paire ou une association. ===== Accès dans un dictionnaire ===== Si je veux dans un dictionnaire accéder à une valeur particulière, il suffit que j'utilise 'le message à mot clé ''**at:**'' et la clé, je spécifie la clé pour laquelle je veux la valeur. Si c'est une clé qui est inexistante forcément je vais récupérer une erreur en retour. |days| days := Dictionary new. days at: #January put: 31; at: #Febuary put: 28; at: #March put: 31. days at: #January days at: #NoMonth "> KeyNotFound Error" " Définir une valeur si la clé est inexistante" days at: #NoMonth ifAbsent: [ nil ] "> nil" Et si je veux éviter ça ce que je peux faire c'est utiliser ''**at: ifAbsent:**'', donc je fais ''**at:**'' une clé qui n'existe pas dans le dictionnaire, et si c'est absent donc je vais récupérer la valeur qui est ici le ''**nil**''. On voit bien cette clé n'existe pas dans le dictionnaire donc je récupère la valeur ''nil''. ===== Itération sur un dictionnaire ===== On peut itérer sur un dictionnaire, donc si je fais un ''**do:**'' sur les éléments d'un dictionnaire je récupère en fait ici que les valeurs du dictionnaire. |days| days := { #January -> 31. #Febuary -> 28. #March -> 31. } asDictionary. days do: [ :each | Transcript show: each; cr ] On ne voit pas les clés. Alors pourquoi on peut se demander ça? C'est un peu bizarre de ne récupérer que les valeurs. En fait c'est complètement logique puisque si on regarde dans la classe "dictionnaire" l'implémentation de la méthode "do", qui prend un block, en fait elle fait à l'intérieur ''%%self valuesDo:%%''. " Implémentation de la méthode Dictionary >> do: aBlock ^ self valuesDo: aBlock Par défaut quand en fait un "do:" sur un dictionnaire, on ne parcourt que ses valeurs et pas les clés. Si je veux parcourir les 2 en fait j'ai une méthode particulière qui s'appelle "KeysAndValuesDo:", qui prend en paramètre un block avec 2 arguments: ici k et v. |days| days := { #January -> 31. #Febuary -> 28. #March -> 31. } asDictionary. days keysAndValuesDo: [ :k :v | Transcript show: k asString, ' has ', v printString, ' days.'; cr ] Et donc k ça correspondra bien à une clé et v à la valeur. Donc cette fois j'ai bien mon dictionnaire complet. ===== Récapitulatif ===== En résumé dans cette séquence on a vu que: * Pharo propose énormément de types de collections différents. * Toutes les collections proposent un vocabulaire commun, que ce soit pour créer des collections, pour accéder aux éléments, pour accéder à la taille d'une collection et caetera. Donc ça, ça facilite la vie du programmeur. * C'est simple aussi de convertir un type de collection en un autre type. En complément, on a vu que quand on se pose des questions il est facile d'aller découvrir dans le système, dans Pharo, en allant lire le code des classes, en allant découvrir de nouvelles classes de collections.