Outils pour utilisateurs

Outils du site


cours:informatique:dev:programmation_objet_pharo:325_principales_collections

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:

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:

  1. La création: with:, with:with:, withAll: qu'on va envoyer plutôt aux classes des collections;
  2. 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.
  3. Tests: isEmpty, includes:, contains: savoir est-ce que la collection est vide ou pas.
  4. Ajout: add:, addAll: ajout et de retrait d'éléments;
  5. Suppression: remove:, remove:ifAbsent:, removeAll:
  6. 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.
  7. 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.

  1. |ordCol|
  2.  
  3. ordCol := OrderedCollection new.
  4. ordCol
  5. add: 'Reef';
  6. add: 'Pharo';
  7. addFirst: 'Pharo'.
  8.  
  9. ordCol
  10.  
  11. "> an OrderedCollection('Pharo' 'Reef2' 'Pharo')"
  12.  
  13. ordCol
  14. add: 'Seaside'.
  15.  
  16. ordCol
  17.  
  18. "> 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.

  1. " Création d'un Set via une collection littérale"
  2. #('Pharo' 'Reef' 'Pharo' 'Pharo') asSet
  3.  
  4. "> a Set('Pharo' 'Reef')"
  5.  
  6. "Création dynamique"
  7. 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.

cours/informatique/dev/programmation_objet_pharo/325_principales_collections.txt · Dernière modification : 2022/08/21 14:35 de yoann