- l'objet a une taille fixe qui est déterminée à sa création par le message `new:n`. Smalltalk réserve alors un espace mémoire de *n* cases pour y stocker les *n* éléments de l'objet (octets dans le cas de la classe `byteArray`, caractères dans le cas de `String`). Il est impossible de modifier la taille de l'objet après sa création. Des méthodes d'instance permettent ensuite d'accéder à telle ou telle case dans l'espace réservé à l'instance. On dit des classes qui possèdent ce type d'objet qu'elles autorisent l'emploi de « variables d'instance indexées » : ces variables sont numérotées de 1 à *n* et désignent le contenu de chacune des cases mémoire de l'instance. La méthode `at:put`: de `String` est une méthode typique de ce genre de classe, puisqu'elle accède directement au contenu d'une case de l'instance;

- l'objet est composé de plusieurs éléments (*n* au maximum<fnref n="17" />) de taille variable. Smalltalk réserve un espace mémoire de *n* cases pour y stocker les *n* pointeurs (éventuels) sur les éléments constitutifs de l'objet. La valeur des pointeurs est initialisée au fur et à mesure du chargement des éléments<fnref n="18" /> (ex.: classe `Bag`);

Nous voyons que Smalltalk permet de gérer des classes contenant :

- des objets de taille fixe (byte ou word<fnref n="19" />);

- des objets composés d'une suite d'objets de taille fixe (byte ou word);

- des objets composés d'une suite d'objets de taille variable.

Dans le dernier cas, le processeur range les objets composant l'instance dans des endroits à part, et stocke uniquement les pointeurs sur ces objets dans l'instance.

Il est nécessaire de signaler à Smalltalk le type d'objet qu'une classe contiendra, pour préparer le processeur à traiter correctement ses instances. Dans le cas de Smalltalk V<fnref n="20" />, il existe quatre types de sous-classes :

**subclass** ses instances ont une taille fixe. Elles n'ont pas de variables indexées (car elles ne sont pas composées d'une suite d'éléments) mais peuvent avoir des variables nommées<fnref n="21" />. Ce n'est pas la valeur elle-même de l'instance qui est conservée dans l'espace mémoire alloué par le processeur, mais son adresse.

**variableSubclass** ses instances ont des variables indexées (elles sont composées d'une suite de cases correspondant chacune aux éléments qui les composent) et peuvent avoir des variables nommées. Chaque case (accessible par une variable d'instance indexée) contient un pointeur sur un élément de l'instance (ou sur

<footnote n="17">17 Nous verrons au chapitre 4 qu'il est possible de mettre plus de *n* éléments dans une instance initialement prévue pour en recevoir au maximum *n* (message `new:n`): Smalltalk crée une copie de l'instance en voie de saturation en calculant plus largement sa taille.</footnote>

<footnote n="18">18 Les pointeurs non affectés désignent l'objet nil.</footnote>

<footnote n="19">19 Octet ou mot machine.</footnote>

<footnote n="20">20 C'est aussi le cas pour Smalltalk 80.</footnote>

<footnote n="21">21 Nous avons vu au chapitre 1 le rôle de ces variables d'instance nommées.</footnote>
l'objet `nil` si cette case n'est pas encore attribuée à un élément).

*variableByteSubclass*

ses instances n'ont que des variables indexées (aucune variable nommée), et la valeur de chaque élément (un octet, soit huit bits) est directement stockée dans la case mémoire correspondante.

*variableWordSubclass*

ses instances n'ont que des variables indexées (aucune variable nommée), et la valeur de chaque élément (un mot, soit seize bits) est directement stockée dans la case mémoire correspondante.

La différence entre les deux dernières catégories correspond aux particularités du processeur des machines pour lesquelles ce logiciel a été conçu<fnref n="22" />. Les octets permettent de représenter tous les caractères du code ASCII (256 valeurs différents). Les mots de deux octets sont utilisés pour coder les entiers longs (*LargePositiveInteger*, *LargeNegativeInteger*)<fnref n="23" />.

Pour bien faire la distinction entre ces différents types de classe, nous allons essayer de créer une sous classe de type *variableSubclass* dans *String* (qui est définie comme *variableByteSubclass*). Un message d'erreur s'affiche, signalant *Superclass is non-pointers*. En effet, les instances de *String* sont composées d'une suite d'octets. Des instances composées d'une suite d'adresses (*variable Subclass*) ne peuvent hériter de la classe *String*<fnref n="24" />.

Si nous essayons de nouveau en choisissant *subclass*, un message nous signale que le processeur nous imposera un type de classe autorisant les variables indexées (puisque *String*, la classe mère, a déjà ce type de variables). Après accord, le message d'erreur *Superclass is non-pointers* s'affiche de nouveau : *subclass* associe des adresses à ses instances, et non des valeurs.

Enfin, si nous essayons avec *variableWordSubclass*, un autre message d'erreur apparaît : *Superclass is Bytes*.

Un examen attentif d'une fenêtre d'éditions des classes (*Class Hierarchy Browser*) montre que les classes de type *subclass* sont les seules qui permettent de définir n'importe quel type de sous-classe.

<footnote n="22">22 Cette distinction disparaît dans la version Smalltalk V 286 pour processeur Intel 80286.</footnote>

<footnote n="23">23 Ils forment les briques élémentaires du codage de ces entiers longs. Smalltalk V permet ainsi de coder un entier dont la représentation binaire atteint une taille de 64 kilo-octets (32768 mots machine) !</footnote>

<footnote n="24">24 De même, *variableByteSubclass* ne peut descendre de *variableSubclass*. Les variables indexées ne sont pas en cause, les deux classes les autorisant. Mais la classe mère traite des adresses, la sous-classe des valeurs.</footnote>
### 3.4.2 - Comment sauvegarder une classe et ses méthodes ?

Le menu de gestion des classes du *Class Hierarchy Browser* offre une option qui permet de sauvegarder la description d'une classe (définition et méthodes seulement : les instances existantes ne sont pas sauvegardées). Il s'agit de "file out".

Cliquer sur cette option amène l'ouverture d'une fenêtre réclamant le nom du fichier de sauvegarde. La figure 3.26 reprend un extrait du fichier de sauvegarde de la classe *Array*.

```perl
FixedSizeCollection variableSubclass: #Array
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: '' !
!Array class methods ! !
!Array methods !
printOn: aStream
"Append the ASCII representation
of the receiver to aStream."
(RecursiveSet includes: self)
ifTrue: [^self printRecursionOn: aStream].
RecursiveSet add: self.
aStream nextPut: $.
1 to: self size - 1 do: [ :element |
(self at: element) printOn: aStream.
aStream space].
self isEmpty
ifFalse: [
self last printOn: aStream].
aStream nextPut: $).
RecursiveSet remove: self ifAbsent: []!
...
```

<caption>Figure 3.26 : début du fichier de sauvegarde de la classe *Array*</caption>

Dans ce fichier sont énumérées les définitions de la classe et de ses méthodes telles qu'elles apparaissent dans les fenêtres du *Class Hierarchy Browser*, à une exception près : les paragraphes sont délimités par le signe !.

Ce signe ! est la marque qui permet à Smalltalk de sectionner le fichier en modules homogènes, correspondant chacun à une méthode ou une définition.

### 3.4.3 - Restaurer une classe

Il existe dans Smalltalk une classe spécialisée dans la manipulation de fichiers : *FileStream*. Cette classe présente un certain nombre de méthodes, dont
une bonne proportion est réservée à l'interprétation de fichiers-sources de Smalltalk.

Pour récupérer une classe, ses méthodes et ses descendants il suffit d'utiliser la méthode `fileIn`, comme dans l'exemple proposé ci-dessous :

```perl
(File pathname: 'd:\array.cls') fileIn; close.
```

La méthode `patname`: appliquée à la classe `File` renvoie une instance de `FileStream` ouverte sur le fichier dont le nom a été donné en paramètre. La méthode `fileIn` s'occupe de charger et mettre à jour les classes décrites dans le fichier qui sera ensuite refermé par le message `close`.

## 3.5 - Les dictionnaires de Smalltalk

Nous allons maintenant aborder une classe qui permet de gérer une collection d'informations avec un accès associatif. Cette classe permet d'accéder à chaque information dès lors qu'on en connaît la partie qui sert de critère d'accès et que l'on appelle la clé. Il s'agit de la classe *Dictionary*, dont chaque instance rassemble des informations qui sont elles-mêmes des instances de la classe *Association*. La classe *Dictionary* est souvent utilisée, dans de nombreuses applications, y compris par Smalltalk lui-même, qui gère l'ensemble des classes et des variables globales à l'aide d'un dictionnaire (Cf 3.5.7).

### 3.5.1 - La classe *Association*

Cette classe permet de représenter des couples d'informations clé/valeur, en vue d'un accès associatif : si on connaît clé, on peut obtenir valeur. Une instance de la classe *Association* peut être créée avec la méthode de classe `key:value:` comme le montre l'évaluation de la première expression de la figure 3.27. Chaque objet de la classe *Association* possède deux variables d'instances key et value qui désigneront respectivement la clé et la valeur de l'association. Dans l'exemple de la figure 3.27, l'objet `a1` est l'association *France* ↔ *Paris* (le symbole ↔ est habituellement utilisé par Smalltalk pour exprimer la relation clé/valeur ; dans les écrans de Smalltalk V, il est représenté par la combinaison de caractères ==>).

```perl
|a1 a2 a3|
a1 := Association key: 'France' value: 'Paris'.
    "a1 est l'association 'France' ↔ 'Paris'"
a2 := Association key: 'Italie'.
    "a2 est l'association 'Italie' ↔ nil"
a3 := Association new.
    "a3 est l'association nil ↔ nil"
```

<caption>Figure 3.27 : création d'instances de la classe *Association*</caption>

Trois méthodes de classe permettent de créer des instances de la classe *Association*. Nous venons d'étudier la première d'entre elles (sélecteur `key:value:`).
La seconde est la méthode *key*: qui permet de créer une instance dont la valeur reste indéterminée, comme le montre l'instanciation de *a2* à la figure 3.27. La dernière est la méthode *new*, héritée de la classe *Object*, qui permet de créer une association dont la clé et la valeur sont indéterminées (Cf instanciation de *a3*, sur la figure).

La classe *Association* est une sous-classe de la classe *Magnitude* (Cf 1.6) et, à ce titre, elle hérite des méthodes `<`, `<=`, `=`, `>`, `>=` qui permettent de comparer entre elles deux associations. Dans cette classe, ces méthodes sont toutes redéfinies pour exprimer la comparaison sur les clés (et non sur les valeurs), ce qui est conforme au principe de l'utilisation de la clé pour l'accès associatif. Ainsi, la méthode `<` est-elle définie de la manière suivante :

`< anAssociation`

"Answer true if the receiver key is less than anAssociation key, else answer false."

^ key < anAssociation key

Les autres méthodes d'instance permettent notamment d'obtenir ou de modifier la clé d'une association (*key* et *key:*), d'obtenir ou de modifier la valeur d'une association (*value* et *value*). Par exemple, si l'on souhaitait compléter la séquence de la figure 3.27 pour affecter à *a2* la valeur manquante, il suffirait d'ajouter *a2 value: 'Rome'*.

### 3.5.2 - Un tableau pour enregistrer des associations

Examinons la séquence de la figure 3.28. L'évaluation de la séquence qu'elle présente peut être décomposée en quatre étapes. La première crée un tableau de cinq éléments, appelé *repertoire*, et y enregistre des associations pays ⇔ capitale pour cinq pays méditerranéens.

La seconde étape demande à l'utilisateur de donner le nom d'un pays. Le nom frappé par l'utilisateur deviendra la valeur de la variable *paysChoisi*. Ce bref dialogue avec l'utilisateur est effectué à l'aide d'un objet de la classe *Prompter* qui est créé par la méthode de classe *prompt:default*: de cette classe. Si *v* est une variable, pour évaluer le message

`v := Prompter prompt: question default: reponseParDefaut`

Smalltalk effectue les opérations suivantes :

- Une fenêtre (objet de la classe *Prompter*) est ouverte à l'écran. Sa barre de titre indique le texte donné avec l'argument *question*. Son panneau de texte propose *reponseParDefaut*.

- L'utilisateur peut alors modifier à sa guise le texte proposé puis valider la réponse qu'il choisit, en appuyant sur la touche « Entrée ».

- La fenêtre « Prompter » disparaît de l'écran (l'instance correspondante est détruite) et la réponse fournie par l'utilisateur est renvoyée par la fin du traitement du message. Cette réponse devient la valeur de la variable *v*.
```
|repertoire paysChoisi index|
"on enregistre, dans repertoire, les capitales de cinq pays méditerranéens"
repertoire := Array new: 5.
repertoire
    at:1 put: (Association key: 'Grèce' value: 'Athènes').
repertoire
    at:2 put: (Association key: 'Egypte' value: 'Le Caire').
repertoire
    at:3 put: (Association key: 'Maroc' value: 'Rabat').
repertoire
    at:4 put: (Association key: 'Espagne' value: 'Madrid').
repertoire
    at:5 put: (Association key: 'Albanie' value: 'Tirana').
"maintenant, on demande à l'utilisateur d'indiquer un pays"
paysChoisi := Prompter prompt: 'indique un pays'
                        default: 'Grèce'.
"ici, on recherche le pays indiqué par paysChoisi dans repertoire"
index := 1.
[(index < 5) and: [paysChoisi =~ (repertoire at: index) key]]
whileTrue: [index := index+1].
"ici, on répond, en fonction du résultat de la recherche"
paysChoisi = (repertoire at: index) key
ifTrue: [Transcript
        nextPutAll: paysChoisi, ' capitale : ',
                 (repertoire at: index) value;
        cr]
ifFalse: [Transcript
        nextPutAll: paysChoisi,
                 ': pays absent du répertoire';
        cr]
```

<caption>Figure 3.28 : utilisation d'un tableau d'associations</caption>

Après l'enregistrement, dans *paysChoisi*, du pays indiqué par l'utilisateur, la troisième étape explore le tableau *repertoire* pour rechercher la capitale correspondante. A l'issue de la recherche, la quatrième étape fournit la réponse à l'utilisateur, en fonction du résultat trouvé (pays présent ou non dans *repertoire*) au moyen d'un affichage dans la fenêtre *Transcript*.

On peut trouver plusieurs inconvénients au traitement proposé par l'exemple de la figure 3.28 :

- l'objet utilisé pour l'enregistrement des informations est un tableau et ce choix ne facilite pas la mise à jour, ni la croissance du volume d'informations. Comment supprimer une association, comment en ajouter une sixième ?

- pour traiter un large volume d'informations (chargées par exemple dans le tableau, à partir d'un fichier), il sera nécessaire de prévoir dès le départ un tableau de grandes dimensions et, plus le nombre
d'informations sera grand, plus la recherche d'une capitale sera longue. Cette recherche s'effectue en effet par une itération d'exploration du tableau.

En fait, pour un tel traitement, un objet de la classe *Dictionary* est bien mieux adapté, comme nous allons le constater au paragraphe suivant.

### 3.5.3 - Un dictionnaire pour enregistrer des associations

La figure 3.29 présente le même traitement que celui que nous avons étudié au paragraphe précédent, mais réalisé en utilisant un *dictionnaire*, objet de la classe *Dictionary*, pour la variable *repertoire*. Un objet de cette classe, sous-classe de la classe *Set*<fnref n="25" />, est une collection d'associations clé↔valeur, dont toutes les clés sont distinctes les unes des autres. Les associations sont rangées et retrouvées rapidement en utilisant une technique de hachage<fnref n="26" /> dont l'utilisateur n'a pas à se préoccuper : elle fait partie des techniques de base utilisées par Smalltalk.

```Smalltalk
|repertoire paysChoisi|
"on enregistre, dans repertoire, les capitales de cinq pays méditerranéens"
repertoire := Dictionary new.
repertoire add: (Association key: 'Grèce' value: 'Athènes').
repertoire add: (Association key: 'Égypte' value: 'Le Caire').
repertoire add: (Association key: 'Maroc' value: 'Rabat').
repertoire add: (Association key: 'Espagne' value: 'Madrid').
repertoire add: (Association key: 'Albanie' value: 'Tirana').
"maintenant, on demande à l'utilisateur d'indiquer un pays"
paysChoisi := Prompter prompt: 'indique un pays'
                      default: 'Grèce'.
"ici, on recherche le pays indiqué par paysChoisi dans repertoire"
(repertoire includesKey: paysChoisi)
    ifTrue: [Transcript
              nextPutAll: paysChoisi, ', capitale : ',
                          (repertoire at: paysChoisi);
              cr]
    ifFalse: [Transcript
              nextPutAll: paysChoisi,
                          ': pays absent du répertoire';
              cr]
```

<caption>Figure 3.29 : exemple d'utilisation d'un dictionnaire</caption>

Par ailleurs, la taille d'un dictionnaire est variable : la méthode new permet de créer un dictionnaire comportant un nombre d'emplacements prédéfini (quatre par

<footnote n="25">25 La classe Set est étudiée au chapitre 4, dans le cadre duquel on revient également sur les particularités de la classe Dictionary.</footnote>

<footnote n="26">26 Le hachage s'appuie sur une règle de calcul qui permet ici, par une simple transformation, d'obtenir, à partir d'une association, l'adresse de rangement de cette association (Cf chapitre 4, 4.9.2).</footnote>
défaut, avec Smalltalk V) et chaque enregistrement d'une association dans le dictionnaire va comparer le nombre d'emplacements effectivement utilisés par rapport au nombre total d'emplacements. Pour maintenir les performances du hachage, un agrandissement sera automatiquement déclenché<sup>27</sup> dès que la proportion d'emplacements disponibles sera inférieure à un seuil prédéfini. L'utilisateur peut donc ajouter des informations à un dictionnaire, sans avoir à se soucier de la capacité du dictionnaire<sup>28</sup>.

Si nous reprenons l'exemple de la figure 3.29, nous pouvons donc dire que le nombre d'associations pays ↔ capitale qui peuvent être ajoutés au dictionnaire n'est plus limité à 5, comme il l'était avec un tableau dans l'exemple de la figure 3.28.

On remarquera que chaque nouvelle association est ajoutée au dictionnaire par la méthode *add*: dont l'argument doit être une association. On aurait pu utiliser la méthode *at:put*: pour écrire, par exemple, directement

```
repertoire at:'Grèce' put:'Athènes'
```

car la méthode *at:put*: telle qu'elle est définie pour la classe *Dictionary*, fait appel à la méthode *add*. En effet, son protocole est le suivant :

```
at: aKey put: anObject
```

> "If the receiver dictionary contains the key/value pair whose key equals aKey, then replace the value of the pair with anObject, else add the aKey/anObject pair."

```
self add: (Association key: aKey value: anObject).
```

```
^ anObject
```

Enfin, pour terminer la comparaison entre les deux techniques présentées dans les figures 3.28 et 3.29, remarquons que, si l'accès à l'information exige, avec un tableau, un parcours séquentiel d'autant plus long que le tableau est grand, ce même accès est immédiat avec un dictionnaire. En effet, cette opération est à la figure 3.29 représentée par l'évaluation de :

```
repertoire includesKey: paysChoisi
```

qui recherchera, par un hachage, la clé indiquée par *paysChoisi* et renverra *true* ou *false* selon le résultat de la recherche.

### 3.5.4 - Un exemple de dictionnaire analogique

Pour nous familiariser davantage avec les objets de la classe *Dictionary*, étudions maintenant un exemple, qui restera très élémentaire dans ce paragraphe, mais qui sera repris et enrichi au chapitre 6. Notre but est de construire une application permettant de gérer un dictionnaire analogique. Un tel dictionnaire peut être utilisé par le rédacteur d'un texte qui souhaite disposer d'un outil pour faciliter l'expression de ses idées. Ce dictionnaire permet d'obtenir, à partir d'un mot représentant un objet ou une idée, une liste de mots voisins ou apparentés. Par exemple, un tel dictionnaire fournira pour l'adjectif *fragile* les analogies suivantes

```
fragile : frêle, faible, grêle, mince, menu, branlant, précaire, inconsistant.
```

<footnote n="27">27 Cf chapitre 4, méthode *grow*, paragraphe 4.9.3.</footnote>

<footnote n="28">28 Pour autant, bien sûr, que l'espace-mémoire géré dynamiquement par Smalltalk soit suffisant.</footnote>
Nous souhaitons réaliser une application permettant à la fois de consulter et d'enrichir un dictionnaire analogique conservé de manière permanente dans un fichier et accessible à travers une interface graphique. L'application complète sera construite au chapitre 6. Nous allons ici nous contenter de définir les objets et les méthodes de base sur lesquels s'appuiera cette application.

La nature même du problème suggère de représenter la relation entre un mot *m* et la liste *l* des mots apparentés par une association dont la clé est *m* et la valeur est *l*. Dans une telle association, nous choisisons de représenter la liste *l* par une chaîne de caractères dans laquelle les mots associés à *m* apparaîtront séparés les uns des autres par un espace :

`amour ↔ 'passion amitié tendresse'`

Il paraît également bien adapté au problème posé, de rassembler ces associations dans un objet de la classe *Dictionary*, qui sera créé en début d'application et immédiatement initialisé à partir du fichier permanent de stockage du dictionnaire d'analogies. En fin d'application, cet objet, s'il a été modifié, enverra les informations qu'il contient dans ce fichier permanent.

Nous laisserons de côté, ici, la liaison avec le fichier (qui sera étudiée au chapitre 6) pour nous intéresser uniquement au nécessaire enrichissement de la classe *Dictionary* pour disposer de méthodes adaptées aux besoins de notre application, comme par exemple l'enregistrement d'une relation d'analogie entre deux mots, qui ne correspondra pas forcément à l'utilisation directe de la méthode *add*: de la classe *Dictionary*.

En fait plutôt que de modifier l'environnement de la classe *Dictionary*, nous allons créer une classe, que nous appellerons *DicoAnalogies* et qui sera reprise et enrichie au chapitre 6. Nous avons étudié, en 3.4, les techniques de création d'une classe. Indiquons ici simplement que nous définissons cette classe comme sous-classe de *Dictionary*, sans variables d'instances nommées :

```
Dictionary variableSubclass: #DicoAnalogies
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
```

Bien entendu, dès à présent, la classe *DicoAnalogies* dispose de l'ensemble des méthodes héritées de *Dictionary* et nous pouvons les utiliser, pour réaliser à travers une fenêtre d'inspection (Cf 3.1.3), une interface-utilisateur rudimentaire. Evaluons, par exemple, la séquence suivante avec *do it* :

```
|a1 a2 a3 dictionnaire|
a1 := Association key: 'amour'
    value: 'passion amitié tendresse'.
a2 := Association key: 'colère'
    value: 'rage fureur'.
a3 := Association key: 'oubli'
    value: 'étourderie distraction omission'.
dictionnaire := DicoAnalogies with: a1 with: a2 with: a3.
dictionnaire inspect
```
La séquence précédente enregistre trois relations d'analogie, pour les mots *amour*, *colère* et *oubli*, respectivement dans les trois variables temporaires *a1*, *a2* et *a3*. Puis elle utilise la méthode *with:with:with*:29 pour créer, dans la variable *dictionnaire*, un dictionnaire analogique qui répertorie les trois relations. Enfin, la dernière expression de la séquence envoie le message *inspect* au dictionnaire. Comme nous l'avons vu en 3.1.3, cette méthode, définie dans la classe *Object*, est disponible pour toutes les classes et permet d'examiner l'objet qui reçoit le message correspondant. Signalons que la classe *Dictionary* (et donc également *DicoAnalogies*) redéfinit cette méthode pour l'adapter à l'examen d'un dictionnaire. La figure 3.30 montre le résultat de l'évaluation de l'expression *dictionnaire inspect* et va nous permettre de préciser les caractéristiques de cette redéfinition.

<figure><img src="image_22.png" /><figcaption>Figure 3.30 : inspection d'un dictionnaire</figcaption></figure>

Comme les fenêtres d'inspection habituelles, cette fenêtre est divisée en deux panneaux. Cependant, le panneau de gauche n'indique pas la liste des variables d'instances mais la liste des clés des associations enregistrées dans le dictionnaire. Dans ce panneau, la clé 'colère' est sélectionnée : la valeur correspondante (c'est-à-dire la chaîne formée par les mots apparentés) est affichée dans le panneau de droite. D'une manière générale, cette fenêtre d'inspection est bien adaptée à la consultation et même à la mise à jour d'un dictionnaire. En sélectionnant, par un simple clic de la souris, une clé du panneau de gauche, on fait immédiatement apparaître, en regard, dans la fenêtre de droite, la valeur correspondante. Cette valeur peut être modifiée. Si, dans la fenêtre représentée à la figure 3.30, on clique dans le panneau de droite, entre *rage* et *fureur*, on fait apparaître le curseur-texte et l'on peut, par exemple frapper le mot *exaspération* qui s'insèrera entre les deux mots précédents. Pour enregistrer la modification de l'analogie dans le dictionnaire sur lequel est ouverte la fenêtre d'inspection, il suffit de cliquer ensuite avec le bouton droit de la souris pour faire apparaître le menu d'édition de texte, dans lequel on sélectionnera l'option *save* de ce menu.

<footnote n="29">29 Cette méthode de classe est héritée de la classe *Collection* et permet de créer une instance comprenant trois éléments.</footnote>
Bien que la méthode *inspect* redéfinie pour la classe *Dictionary*, soit particulièrement adaptée aux caractéristiques des instances de cette classe, on peut cependant se demander comment il serait possible d'effectuer, sur une instance de cette classe, une inspection « standard » telle qu'elle est définie dans la classe *Object* et qui permettrait d'examiner les détails internes d'une telle instance et non plus seulement les informations qu'elle répertorie. Nous allons maintenant répondre à cette question.

### 3.5.5 - La variable super

Examinons la fenêtre de gauche, intitulée « Inspection propre à *Dictionary* », de la figure 3.31. Elle résulte, à peu de choses près<fnref n="30" />, de l'évaluation de la séquence suivante :

|d|

d := *Dictionary* with: (Association key: 'Lille' value: 59).

d inspect.

Cette séquence crée, dans la variable *d*, un dictionnaire avec une seule association ('Lille' ↔ 59) puis fait appel à la méthode *inspect* de la classe *Dictionary* pour ouvrir une fenêtre d'examen de ce dictionnaire. Cette fenêtre montre bien l'unique association enregistrée.

<figure><img src="image_23.png" /><figcaption>Figure 3.31 : deux fenêtres d’inspection sur un même objet</figcaption></figure>

La fenêtre de droite de la même figure, intitulée « Inspection standard », est le résultat de l'évaluation de :

|d|

d := *Dictionary* with: (Association key: 'Lille' value: 59).

d inspectionStandard

<footnote n="30">30 A peu de choses près, car la barre de titre qui devrait indiquer « Inspecting: *Dictionary* », a été modifiée, après création de la fenêtre, pour la clarté de la présentation dans la figure. Cette modification est obtenue avec l'option « label » du menu de la barre de titre.</footnote>
Cette séquence crée le même dictionnaire que la séquence précédente mais fait appel, pour son examen, à la méthode *inspectionStandard*. Comme nous allons le voir, cette méthode déclenche la méthode *inspect* de *Object* et non la méthode de même nom de *Dictionary*. La fenêtre montre alors le détail du dictionnaire<fnref n="31" /> et nous pouvons constater que celui-ci possède une variable d'instance nommée, dont le nom *elementCount* n'est, compte tenu de la dimension de la fenêtre, que partiellement apparent. Cette variable indique le nombre d'associations enregistrées : si l'on cliquait sur cette variable dans le panneau de gauche, on verrait apparaître la valeur 1 dans le panneau de droite : il n'y a, dans le dictionnaire, que l'association 'Lille' ↔ 59.

La fenêtre « Inspection standard » montre également que le dictionnaire possède des variables d'instance indexées numérotées de 1 à 4 (car la méthode *with*: crée un dictionnaire pouvant, au départ, contenir quatre associations<fnref n="32" />). Ces variables servent à répertorier les associations du dictionnaire et la fenêtre montre que l'unique association enregistrée est dans la troisième variable d'instance. On pourrait vérifier, en cliquant sur chacune des autres variables d'instances indexées, qu'elles désignent toutes l'objet *nil*, indiquant ainsi qu'elles restent inutilisées.

Expliquons maintenant le principe de fonctionnement de la méthode *inspectionStandard*. Précisons tout de suite qu'il ne s'agit pas d'une méthode pré définie en Smalltalk mais que c'est une méthode que nous avons ajoutée à la classe *Dictionary* avec le protocole suivant :

**inspectionStandard**

“fait appel à la méthode *inspect* de *Object*”

super inspect

On constate que cette méthode ne fait rien d'autre qu'envoyer à l'« objet » *super* le message *inspect*. En fait, *super* est une variable accessible dans toute méthode et qui représente toujours l'objet récepteur du message qui correspond à la méthode. La variable *super* joue donc un rôle tout à fait comparable à celui de *self*, mais avec une exception notable :

Lorsque, dans une méthode de la classe *C* un message correspondant à une méthode d'instance *m* de la classe *C* est envoyé à la variable *super*, la méthode *m* de la classe *C* n'est pas utilisée. Smalltalk recherche, en remontant dans la hiérarchie des classes, dont *C* est l'héritière, la méthode *m* à exécuter.

Ainsi, dans la méthode *inspectionStandard*, la méthode *inspect*, qui est exécutée par l'évaluation de *super inspect*, n'est pas celle de *Dictionary* mais celle que Smalltalk trouvera, dans la classe *Object*, en remontant la hiérarchie : *Dictionary* → *Set* → *Collection* → *Object*. En effet, la méthode *inspect* n'est définie que dans les classes *Dictionary* et *Object*.

Remarquons que, avec l'utilisation de *super*, dans l'exploration ascendante de la hiérarchie, Smalltalk s'arrête dès qu'il trouve une méthode qui convient : si

<footnote n="31">31 On reviendra au chapitre 4, en 4.10, sur la structure interne d'une instance de la classe *Dictionary*.</footnote>

<footnote n="32">32 Ce dictionnaire pourra s'agrandir (Cf 4.10.1) au fur à mesure qu'on lui ajoutera de nouvelles associations.</footnote>
plusieurs méthodes peuvent convenir, il choisira donc celle de la classe la plus proche de la classe de départ. Par exemple, supposons que nous n'ayons pas défini la méthode *inspectionStandard* dans la classe *Dictionary*, mais que nous l'ayons ajoutée à la classe *DicoAnalogies* avec le même protocole :

```
inspectionStandard
  "méthode définie dans la classe DicoAnalogies"
super inspect
```

Alors l'évaluation de *DicoAnalogies new inspectionStandard*, entraînera l'appari-
tion du même type de fenêtre que « Inspection propre à Dictionary » (Cf figure
3.31) car Smalltalk, dans la recherche d'une méthode *inspect* au dessus de la clas-
se DicoAnalogies, s'arrêtera à la classe *Dictionary*, qui contient cette méthode.

Insistons enfin sur un dernier détail du mécanisme associé à l'utilisation de *super* et supposons :

- que la méthode *inspectionStandard* est définie par *super inspect*, dans la classe *Dictionary*,

- que cette même méthode *inspectionStandard* n'est pas redéfinie dans la sous-classe *DicoAnalogies*.

Alors, l'évaluation de *DicoAnalogies new inspectionStandard* fera bien appel à la
méthode *inspect* de *Object* car Smalltalk qui, dans cette évaluation, travaille sur
une instance de *DicoAnalogies*, commencera la recherche de la méthode *inspect*
non pas dans la classe-mère de *DicoAnalogies* mais dans la classe *Set*, mère de
*Dictionary*. En effet, le message *inspectionStandard*, adressé à une instance de
*DicoAnalogies*, fait appel à la méthode *inspectionStandard* que Smalltalk trouvera
dans *Dictionary*. Quand Smalltalk évaluera, dans cette méthode, l'expression
*super inspect*, il remontera à la classe-mère de *Dictionary* pour rechercher la
méthode *inspect* qui convient. L'utilisation de *super* déclenche la recherche
ascendante en commençant par la classe qui se trouve au-dessus de celle qui défi-
nit la méthode dans laquelle *super* est utilisée.

### 3.5.6 - Enrichir le dictionnaire des analogies

Revenons à la classe *DicoAnalogies* et ajoutons-lui les moyens d'enrichir une
de ses instances en y enregistrant une nouvelle relation de parenté entre deux mots
de sens voisins. Auparavant, pour nous permettre de vérifier, sur la fenêtre
*System Transcript*, les opérations effectuées sur un tel dictionnaire, ajoutons, à la
classe *DicoAnalogies*, les méthodes d'instances *affichePour: unMot* et *afficheTout*
qui permettent de faire apparaître sur la fenêtre *System Transcript*, les analogies
correspondant à *unMot* ou l'ensemble des analogies enregistrées. La réalisation de
ces méthodes ne présentant pas de difficulté particulière, nous les présentons, sans
autre commentaire, à la figure 3.32.
### affichePour: unMot

"affiche sur Transcript, s'il en existe, les analogies enregistrées pour unMot dans le dictionnaire des analogies récepteur"

(self includesKey: unMot)

```ruby
ifTrue: [Transcript nextPutAll: 'analogies pour ', unMot;
         cr;
```

```ruby
nextPutAll: ' ', (self at: unMot);
cr]
```

```ruby
ifFalse: [Transcript nextPutAll: 'pas d''analogies pour ',
          unMot;
cr]
```

### afficheTout

"envoie sur Transcript toutes les analogies"

```ruby
Transcript nextPutAll: 'contenu complet du dictionnaire';
cr.
```

```ruby
self associationsDo:
```

```ruby
[:couple "désignera chaque analogie enregistrée"]
```

```ruby
Transcript nextPutAll: 'analogies pour ', couple key;
cr;
```

```ruby
nextPutAll: ' ', couple value; cr]
```

<caption>Figure 3.32 : deux méthodes pour l'affichage d'analogies</caption>

Nous pouvons maintenant nous intéresser à l'addition d'une analogie au dictionnaire. Il s'agit d'enregistrer le fait que deux mots sont de sens proches, par exemple *colère* et *exaspération*, *calme* et *tranquillité*. Bien entendu, la proximité de sens est une relation symétrique et il faudra que le dictionnaire enregistre aussi bien *calme* ↔ *tranquillité* que *tranquillité* ↔ *calme*. Cependant, dans un premier temps, nous allons considérer que la relation est dissymétrique et privilégier l'un des deux mots qui servira de clé d'accès au dictionnaire. Construisons donc la méthode

```ruby
ajoute: motProche pour: motCle
```

qui ajoutera aux analogies déjà enregistrées pour *motCle*, le mot *motProche*.

Deux cas sont à considérer :

1) Aucune analogie n'est encore enregistrée pour *motCle* dans le dictionnaire. Dans ce cas, il faut créer l'association correspondante.

2) Le dictionnaire contient déjà une association dont *motCle* est la clé. Dans ce cas, il faut compléter la valeur de cette association avec le nouveau mot.

La figure 3.33 décrit la méthode qui correspond à ces opérations. Les commentaires qui accompagnent le code sont suffisants pour que le lecteur puisse comprendre le protocole de cette méthode. Remarquons seulement que, lorsque *motCle* est déjà une clé du dictionnaire, l'opération qui détermine si *motProche* est déjà une analogie enregistrée pour *motCle* est, en Smalltalk, exprimée de manière parlante et concise par :

```ruby
listeMotsProches asAoFSubstrings includes: motProche
```
Dans l'expression précédente, la variable *listeMotsProches* contient la chaîne de caractères formée des mots qui constituent des analogies déjà connues de *motCle*. Le lecteur qui a déjà une expérience de programmation pourra établir un parallèle avec des langages classiques dans lesquels la même opération se décrirait de manière bien moins aisée. Dans ces langages, le programmeur sera en général obligé de construire lui-même la primitive de transformation d'une chaîne en tableau de sous-chaînes ainsi que la primitive de recherche d'un mot dans un tableau de chaînes. Cet exemple met en évidence aussi bien le confort apporté par la programmation orientée objets, qui facilite la réutilisation de code, que la richesse de Smalltalk, dont la très large bibliothèque de classes fournit de puis- santes primitives de traitement.

```ruby
ajoute: motProche pour: motCle
"ajoute l'analogie motCle motProche dans le dictionnaire des analogies récepteur :
crée l'association si motCle n'était pas répertorié,
renvoie true en cas de succès et false si l'analogie était déjà enregistrée."
|listeMotsProches|
(self includesKey: motCle)
ifTrue: "motCle est déjà une clé enregistrée dans le dictionnaire"
    [listeMotsProches := self at: motCle.
     "listeMotsProches contient maintenant les analogies de motCle"
     "on va regarder si motProche est déjà une analogie de motCle"
     (listeMotsProches asAoFSubstrings
        includes: motProche)
     ifTrue: [^false "car on a trouvé motProche"]
     ifFalse: [self at: motCle
                 put: listeMotsProches,
                      '', motProche.
                 "on vient d'ajouter motProche à la liste des
                  analogies déjà existantes de motCle"
                 ^ true]
    ]
ifFalse: "motCle n'est pas une clé déjà enregistrée dans le dictionnaire"
    [self add: (Association key: motCle
                          value: motProche).
       ^ true]
```

<caption>Figure 3.33 : enregistrement dissymétrique d'une analogie</caption>

Pour contrôler le fonctionnement de la méthode *ajoute:Pour:*, créons un dictionnaire d'analogies vide au départ et ajoutons-lui quelques analogies avant d'utiliser la méthode *afficheTout* pour présenter le contenu du dictionnaire sur la fenêtre *System Transcript*. La figure 3.34 montre le résultat obtenu. La séquence évaluée apparaît dans la fenêtre en arrière-plan. Le résultat de l'évaluation est affiché dans *System Transcript*.
<figure><img src="image_24.png" /><figcaption>Figure 3.34 : utilisation de la méthode ajoute:Pour :</figcaption></figure>

Nous pouvons maintenant définir la méthode qui permettra d'enregistrer une relation d'analogie de manière symétrique. Cette méthode s'écrira simplement en faisant deux appels à la méthode ajoute:Pour : et nous pouvons en donner le description suivante :

```
ajouteAnalogieEntre: mot1 et: mot2
"ajoute, si elles n'y sont pas déjà, les analogies mot1/mot2 et
mot2/mot1 au dictionnaire récepteur,
renvoie false si l'une des deux analogies était déjà présente, sinon renvoie true"
|reponse "indiquera si tout s'est bien passé" |
reponse := self ajoute: mot1 pour: mot2.
^ (self ajoute: mot2 pour: mot1) & reponse
```

Cette méthode étant ajoutée à la classe DicoAnalogies, on peut désormais considérer la méthode ajoute:pour : comme une méthode privée, réservée à la seule utilisation de la méthode ajouteAnalogieEntre:et :. de manière à garantir, avec l'emploi exclusif de cette dernière méthode pour l'ajout de relation d'analogie, la symétrie d'un dictionnaire d'analogies.

### 3.5.7 - Le dictionnaire de Smalltalk

Smalltalk répertorie les variables globales (Cf 2.9.1.), les dictionnaires partagés (Cf 2.9.2.) ainsi que les classes, dans un dictionnaire, de nom Smalltalk. Ce dictionnaire est la seule instance de la classe SystemDictionary et c'est une instance permanente. Le dictionnaire Smalltalk contient, comme tout dictionnaire, des associations, c'est-à-dire des couples clé/valeur.

La clé est toujours le symbole qui correspond au nom de l'objet répertorié (nom de la variable globale, nom du dictionnaire partagé, nom de la classe). On notera, à ce propos, que la classe SystemDictionary redéfinit les méthodes add: et at:put : pour vérifier que la clé de toute nouvelle association est bien un symbole.
Quand une association répertoriée dans le dictionnaire *Smalltalk* désigne une variable globale, la valeur de cette association est la valeur de la variable. Par exemple, si l'on a affecté à la variable globale *Pays* la chaîne 'France', l'évaluation, avec *show it*, de *Pays* renverra 'France' et l'évaluation de

Smalltalk at: #Pays

renverra la même valeur. On notera que, dans l'expression précédente, l'argument du message *at*: est le symbole `#Pays`. En effet, l'évaluation de *Smalltalk at: Pays* renverra une fenêtre de diagnostic indiquant l'erreur *Key is missing*, car *Smalltalk rechercherait*, dans le dictionnaire *Smalltalk*, la valeur de la variable *Pays* et non le symbole `#Pays`. En revanche, si *France* est une autre variable globale, l'évaluation, avec *show it*, de :

Pays := #France.
France := 'pays de la CEE'.
Smalltalk at: Pays

renverra la valeur 'pays de la CEE'.

Quand une association répertoriée dans le dictionnaire *Smalltalk* désigne un dictionnaire partagé, la valeur de cette association est le dictionnaire lui-même. Ainsi, l'évaluation de

(Smalltalk at: #CharacterConstants) inspect

ouvrira une fenêtre d'examen sur le dictionnaire partagé *CharacterConstants* (Cf 2.9.2. pour des informations sur ce dictionnaire).

Quand une association répertoriée dans le dictionnaire *Smalltalk* désigne une classe, la valeur de cette association est la classe elle-même<fnref n="33" />. On peut vérifier que c'est bien la classe qui est accessible par l'intermédiaire de la valeur de l'association en évaluant par exemple, avec *show it*, l'expression :

(Smalltalk at: #Array) with: 'Paris' with: 'France'

on obtient le même tableau ('Paris' 'France'), que celui que l'on aurait obtenu, en évaluant *Array with: 'Paris' with: 'France'*.

On ne peut supprimer une classe en adressant un message au dictionnaire *Smalltalk*. Nous examinerons, au chapitre 7, les opérations nécessaires à la suppression d'une classe définie par l'utilisateur<fnref n="34" />. On peut en revanche supprimer une variable globale définie par l'utilisateur, en utilisant la méthode `removeKey`, héritée de *Dictionary*. Ainsi, pour supprimer la variable globale *Pays*, il suffira d'évaluer l'expression *Smalltalk removeKey: #Pays*.

Le dictionnaire *Smalltalk* est bien entendu utilisé par *Smalltalk* lui-même pour de très nombreuses opérations internes. L'utilisateur peut lui aussi le consulter et en extraire des informations. A titre d'exemple, la séquence suivante permet, quand elle est évaluée, d'obtenir, sur la fenêtre *System Transcript*, la liste des noms de variables globales et des dictionnaires partagés.

<footnote n="33">33 En fait, c'est un pointeur sur la description de la classe qui est stocké dans le dictionnaire *Smalltalk*. On se reportera au chapitre 7 pour davantage d'explications sur la représentation des classes.</footnote>

<footnote n="34">34 Les classes de bases, prédéfinies, ne peuvent être supprimées car elles font partie de l'environnement de *Smalltalk*.</footnote>
```
Smalltalk associationsDo:
    [:element| "désignera chaque association enregistrée dans le dictionnaire"
    (element value isKindOf: Class)
    ifFalse: [element key printOn: Transcript.
              Transcript cr.]
]
```

Cette séquence évalue répétitivement (méthode `associationsDo:`) un bloc, pour chaque association répertoriée dans le dictionnaire *Smalltalk*. Dans ce bloc, chaque association sera désignée par l'argument *element*. La valeur de chaque association (*element value*) reçoit le message `isKindOf: Class` et renverra la valeur *true* ou *false* selon que cette valeur représente ou non une classe. Chaque fois que la valeur renvoyée est *false*, l'objet répertorié n'est pas une classe et ne peut être qu'une variable globale ou un dictionnaire partagé : on affiche alors la clé de l'association (c'est-à-dire le nom de l'objet représenté) dans la fenêtre *System Transcript*.

<figure><img src="image_25.png" /><figcaption>Figure 3.35 : trois inspections successives</figcaption></figure>

Pour terminer, examinons la figure 3.35 qui montre comment on peut également utiliser le dictionnaire *Smalltalk* pour examiner la structure interne d'un objet. Cette figure a été obtenue par la succession d'opérations suivantes :

- On a déclenché l'évaluation de *Smalltalk inspect* qui a fait apparaître la fenêtre d'inspection figurant en arrière-plan et dont la barre de titre est : « *Inspecting SystemDictionary* ».

- Dans le panneau de gauche, on a fait défiler la liste des symboles jusqu'à sélectionner la classe *Magnitude*.

- On a alors cliqué dans le même panneau avec le bouton droit de la souris pour faire apparaître le menu correspondant ; dans ce menu, on a sélectionné l'option *inspect* qui a déclenché l'ouverture d'une nouvelle fenêtre d'inspection sur la sélection (*Magnitude*).
- Cette deuxième fenêtre est la fenêtre « Inspecting Magnitude Class ». Elle donne, dans son panneau de gauche, la liste des variables d'instances de l'objet qui représente la classe. Dans cette liste, la variable *subclasses* est sélectionnée, et l'on voit apparaître, en regard, dans le panneau de droite, le début d'un tableau qui répertorie les sous-classes de *Magnitude*.

- On a enfin, dans le panneau de gauche de cette deuxième fenêtre, activé l'option *inspect* du menu, qui a déclenché l'ouverture de la troisième fenêtre « Inspecting: Array ». Cette dernière fenêtre inspecte la variable *subclasses*. Dans le panneau gauche de cette dernière fenêtre, la troisième variable d'instance indexée est sélectionnée : elle désigne la sous-classe *Number* de *Magnitude*.
# 4 - La classe Collection et sa descendance

## 4.1 - Collection et ses sous-classes

'Chaîne',
'Ligne contenant des mots',
(0 F F F C 4 3 D 5 A),
('poulet' 'vermicelles' 'raton laveur' 'ordinateur'),
('fichier-ext' 12345 (Nov 14, 1990) 14:31:09),
#(4 5 6 7)...

A l'instar de Prévert, Smalltalk V regroupe toutes ces énumérations sous la même classe mère. En effet, qu'il s'agisse de nombres, d'octets, de mots, de lettres ou de date, les mêmes traitements sont nécessaires : ajouter un élément à la liste, en enlever un, tester la présence de tel ou tel, réunir deux listes, les vider etc.

C'est le rôle de la classe *Collection* de regrouper tous les types d'énumérations<fnref n="1" /> ou de listes imaginables et de leur fournir quelques méthodes de base.

Il est bien évident que certaines énumérations appellent des traitements spécifiques. Une suite de mots nécessitera des opérations différentes de celles d'un tableau de chiffres ou d'octets. La classe *Collection* est donc divisée en plusieurs sous-classes, selon une hiérarchie basée sur :

- l'accès direct à un élément de la collection (par une clé ou un indice) ;

- l'ordre imposé (ou non) aux éléments de la collection ;

- la possibilité de redondances dans la collection ;

- la nature des éléments stockés.

La figure 4.1 donne le détail de cette hiérarchie. La première sous-classe, *Bag*, est une sorte de fourre-tout dans laquelle les éléments n'ont aucun type imposé (nombre, caractère, symbole, tableau, etc.) et peuvent être dupliqués autant de

<footnote n="1">1 Par la suite, nous parlerons indifféremment de « collection » ou d'énumération pour désigner les instances de la classe *Collection*, et d'éléments pour désigner les objets qui composent ces énumérations.</footnote>
fois que nécessaire. Nous verrons un peu plus loin quelques méthodes utilisables sur les instances de cette classe.

La seconde, *IndexedCollection*, regroupe toutes les collections dont chaque élément peut être repéré par sa position. Contrairement à *Bag*, la classe *IndexedCollection* est une classe abstraite : il ne peut en exister aucune instance. C'est au niveau de ses sous-classes que les instances peuvent être créées. Une classe abstraite regroupe un certain nombre de méthodes utilisables par plusieurs classes apparentées. Mais ces méthodes sont insuffisantes pour qu'une instance puisse être directement créée, ou manipulée.

<figure><img src="image_26.png" /><figcaption>Figure 4.1 : la classe *Collection* et sa descendance</figcaption></figure>

La troisième, *Set*, contient des collections dont les éléments sont uniques (alors qu'ils peuvent être présents en plusieurs exemplaires dans les instances de *Bag*). Contrairement à *IndexedCollection*, on ne peut accéder aux éléments d'une instance par leur position (les éléments d'une instance de *Set* ne sont pas ordonnés). Certaines des sous-collections de *Set* (*Dictionary*) permettent toutefois d'accéder à leurs éléments par une clé (un index plus exactement). *Set* n'est pas une classe abstraite.

Nous expliquerons dans la première partie (4.2 à 4.6) comment utiliser les différentes sous-classes de *Collection*, à l'aide d'exemples. Nous aborderons en deuxième partie (4.7 à 4.10) le contenu des méthodes, et la manière dont les concepteurs de Smalltalk V ont implémenté cette classe et sa descendance.
## 4.2 - La classe Bag : aperçu de quelques méthodes

### 4.2.1 - Créer une instance de la classe Bag

Prenons l'exemple d'une ménagère s'approvisionnant au marché. Nous simulerons son panier à provisions avec une instance de la classe *Bag*, ce qui lui permettra d'ajouter autant d'exemplaires que nécessaire d'un même objet.

Définissons donc *panierDeCourse* comme une instance de la classe *Bag*, en envoyant le message *new*.

```
|panierDeCourse|
panierDeCourse := Bag new
```

et vérifions qu'il est vide avec le message *size* :

```
panierDeCourse size
```

L'évaluation par l'option *show it* donne 0.

### 4.2.2 - Remplir une instance de la classe Bag

Il reste à remplir notre panier, avec le message *add: unObjet*, en ajoutant à notre évaluation précédente les lignes :

```
panierDeCourse add: 'chou'.
panierDeCourse add: 'salade'.
panierDeCourse add: 'gros sel'.
panierDeCourse size
```

Le dernier message nous permet de vérifier que notre panier contient maintenant trois éléments.

Si notre ménagère décide soudain de racheter d'autres salades, rien de plus simple :

```
panierDeCourse add: 'salade'.
```

Nous pouvons vérifier avec le message *size* que *panierDeCourse* contient un élément de plus. Les deux occurrences de *'salade'* apparaissent bien comme deux objets distincts dans l'instance de *Bag*.

Si les tomates particulièrement bon marché tentent notre ménagère, elle pourra en acheter une douzaine avec le message :

```
panierDeCourse add: 'tomate' withOccurrences:12
```
### 4.2.3 - Explorer le contenu d'une instance de la classe Bag

Et pour afficher le contenu de notre panier ?

Il se trouve que la classe *Bag* n'a pas de méthode naturelle pour un tel affichage, contrairement à la plupart des autres sous-classes de *Collection*. Nous allons créer *panierDeCourse* comme illustré figure 4.2.

```perl
|panierDeCourse|
panierDeCourse := Bag new.
panierDeCourse add:'salade'.
panierDeCourse add:'poireau'.
panierDeCourse add:'salade'
panierDeCourse add:'radis' withOccurrences:4.
```

<caption>Figure 4.2 : création de *panierDeCourse*</caption>

Explorons les méthodes d'instance de *Bag* qui pourraient nous aider à détailler le contenu de notre panier.

Avec `occurrencesOf:` *unObjet*, nous pouvons interroger le récepteur pour qu'il nous renvoie le nombre de *unObjet* qu'il contient. Ajoutons par exemple l'instruction suivante à la séquence de la figure 4.2 avant d'évaluer le tout avec *show it* :

```
panierDeCourse occurrencesOf: 'salade'
```

Nous obtenons 2.

Complétons ensuite la figure 4.2 avec la ligne suivante, avant d'évaluer le tout avec *show it* :

```
panierDeCourse includes:'radis'
```

La réponse est *true*. L'objet *'radis'* fait bien partie de *panierDeCourse*.

La méthode `includes:` *unObjet* (héritée de la classe *Collection*) nous permet donc de tester la présence de *unObjet* dans notre sac. Mais elle ne nous donne pas le nombre d'occurrences de cet objet.

On le voit, il n'est pas très pratique d'examiner le contenu de notre *panierDeCourse*. Une dernière méthode, *do:* *unBloc*, pourrait nous aider. Cette méthode exécute le bloc *unBloc* pour chacun des éléments de l'objet recevant le message. Dans notre cas, nous pourrions demander l'affichage à l'écran des éléments de *panierDeCourse*.

```
panierDeCourse do:[:legume| legume printOn: Transcript.
Transcript cr]
```

Le bloc d'instructions est évalué pour chacun des éléments de l'objet *panierDeCourse*. Il a pour effet d'afficher sur la fenêtre *System Transcript* tous les légumes de *panierDeCourse*, tout en revenant à la ligne après chaque affichage d'un légume.
## Supprimer des éléments d'une instance de la classe Bag

Une fois rentrée à la maison, la cuisinière se précipite sur notre *panierDeCourse* et en retire les ingrédients. Pour le mettre à jour, la méthode *remove:ifAbsent* est toute indiquée. La figure 4.3 montre la suppression du premier radis. On remarquera que nous avons légèrement amélioré le rendu de notre exemple en obtenant l'affichage de la taille avant le message *remove: ifAbsent*:

```
|panierDeCourse|
  panierDeCourse  := Bag new.
  panierDeCourse  add: 'salade'.
  panierDeCourse  add: 'poireau'.
  panierDeCourse  add: 'salade'
  panierDeCourse  add: 'radis' withOccurrences: 4.
  'Mon panier contient' printOn: Transcript.
  panierDeCourse  size  printOn: Transcript.
  ' légumes '      printOn: Transcript.
  panierDeCourse  remove: 'radis'
                  ifAbsent: [self error:
                             'pas de radis dans le panier'].
  panierDeCourse  size printOn: Transcript.
```

<caption>Figure 4.3 : enlever un élément de *panierDeCourse*</caption>

Si nous tentons maintenant d'enlever un légume qui n'existe pas (ou n'existe plus) dans *panierDeCourse*, le bloc paramètre de *ifAbsent*: est évalué.

<figure><img src="image_27.png" /><figcaption>Figure 4.4 : erreur lors d'une suppression d'objet dans *panierDeCourse*</figcaption></figure>
Une fenêtre d'erreur s'affiche, dont le titre est la chaîne que nous avons donnée en paramètre du message error: *uneChaine*. C'est le cas illustré figure 4.4, où nous avons tenté d'enlever 'asperge' au lieu de 'radis'.

## 4.3 - Les classes Set et Dictionary

### 4.3.1 - Des méthodes de conversion présentes dans Collection

Pour extraire de *panierDeCourse* les différents types de légume que nous y avons mis, sans nous préoccuper de leur nombre, il suffit de transposer les éléments constituant *panierDeCourse* dans une instance de la classe *Set*, puisque une instance de la classe *Set* ne peut contenir plusieurs exemplaires d'un même objet. Il existe une méthode, définie au niveau de *Collection*, qui permet un tel transfert : c'est la méthode *asSet*.

La figure 4.5 donne un exemple de conversion d'une instance de *Bag* en instance de *Set*.

```perl
|panierDeCourse typeDeLegume|
panierDeCourse := Bag new.
panierDeCourse add:'patate'.
panierDeCourse add:'poireau'.
panierDeCourse add:'patate'.
panierDeCourse add: 'radis' withOccurrences:4.
panierDeCourse size printOn:Transcript.
typeDeLegume := panierDeCourse asSet.
typeDeLegume size printOn:Transcript
```

<caption>Figure 4.5 : passage d'une instance de *Bag* à une instance de *Set*</caption>

L'évaluation de la séquence de la figure 4.5 provoque l'affichage dans la fenêtre *Transcript* de la taille de *panierDeCourse* (7) puis de celle de l'instance de *Set* qui en est tirée (3) : tous les doublons ont été éliminés lors du transfert des éléments constituant *panierDeCourse* dans *typeDeLegume*.

D'autres méthodes permettent de transférer des instances d'une sous-classe de *Collection* en instance de *Bag* (*asBag*), de *Array* (*asArray*), de *SortedCollection* (*asSortedCollection*), de *IndexedCollection* (*asIndexedCollection*) etc.

Nous aurions aussi pu créer directement *typeDeLegume* avec la méthode *with: with: with:*, héritée de *Collection* qui crée et charge immédiatement une instance de la classe réceptrice avec les objets donnés en paramètres. Des méthodes comparables existent pour mettre un, deux ou quatre objets dans l'instance.

```perl
|typeDeLegume|
typeDeLegume := Set with:'patate'
                  with:'poireau'
                  with:'radis'.
```
### 4.3.2 - Une sous-classe importante de Set : Dictionary

Nous avons utilisé cette classe au chapitre 3 (Cf 3.5). Nous allons décrire ses méthodes de manière plus exhaustive dans ce paragraphe.

Construite sur le principe des dictionnaires, cette classe permet de stocker des associations. Elle est donc particulièrement utile dès que l'on veut retrouver un objet par la clé qui lui est associée. Les problèmes suivants peuvent tous être résolus en utilisant les possibilités de cette classe :

- rechercher la traduction anglaise d'un mot français,

- trouver le prix d'un article de supermarché,

- afficher le code constructeur d'une pièce de rechange,

- retrouver un individu par son numéro de Sécurité Sociale,

etc.

Comme n'importe quelle instance de Set, un dictionnaire ne doit contenir qu'un exemplaire de chaque association. La règle est en fait plus rigoureuse, pour une meilleure utilisation de cette classe, car un dictionnaire ne doit pas contenir plus d'un exemplaire de chaque clé. Ainsi, (Charles ↔ De Gaulle) et (Charles ↔ Péguy), s'ils cohabitent dans l'histoire, ne peuvent être contenus dans le même dictionnaire, car ils ont la même clé (Charles).

### 4.3.3 - Les méthodes de Dictionary classées par fonctions

Le stockage des éléments d'un dictionnaire étant basé sur la clé de l'association et non sur l'association elle-même, nombreuses sont les méthodes d'accès qui sont redéfinies dans la classe *Dictionary*.

Nous allons les parcourir rapidement, avant d'en utiliser quelques unes pour résoudre un problème classique de gestion de personnel.

#### Ajouter un objet au dictionnaire

*add: anAssociation* ajoute l'association *anAssociation* au dictionnaire.

*at: key put: value* ajoute l'association (*key* ↔ *value*) au dictionnaire.

#### Tester la présence d'un objet

*includes: value* répond vrai si *value* correspond à une clé (quelle qu'elle soit) existant dans le dictionnaire, faux sinon.

*includesKey: aKey* répond vrai si *aKey* fait partie des clés du dictionnaire, faux sinon.

*includesAssociation: anAssociation* répond vrai si l'association *anAssociation* fait partie du dictionnaire, faux sinon.

Ces trois méthodes permettent donc de tester la présence d'une association, de sa clé ou de sa valeur dans un dictionnaire.
## Récupérer un objet

à partir de sa clé :

*   **at: aKey** renvoie la valeur associée à la clé *aKey*

*   **at: aKey ifAbsent: aBlock** renvoie la valeur associée à *aKey*, exécute le bloc *aBlock* si la clé n'existe pas dans le dictionnaire.

*   **associationAt: aKey** comme *at:*, mais renvoie l'association.

*   **associationAt: aKey ifAbsent: aBlock** comme *at: ifAbsent:*, mais renvoie l'association complète.

à partir de la valeur :

*   **keyAtValue: value** renvoie la première clé associée à la valeur *value*

*   **keyAtValue: value ifAbsent: aBlock** comme le précédent, mais exécute *aBlock* si *value* n'existe pas dans le dictionnaire.

Ces deux dernières méthodes sont délicates à manipuler, car elles ne renvoient que la première clé correspondant à l'argument. Or nous avons vu que si les clés sont uniques pour un dictionnaire, ce n'est pas le cas des valeurs qui leur sont associées. Ces deux méthodes peuvent donc donner des résultats inattendus si l'on oublie cette propriété.

Il existe des méthodes plus exhaustives :

*   **keys** renvoie une instance de *set* contenant toutes les clés

*   **values** renvoie une instance de *Bag* contenant les valeurs correspondant aux clés du dictionnaire

Nous revoyons la particularité des dictionnaires dans la différence d'implémentation des dernières méthodes : *keys* peut renvoyer une instance de *Set* car les clés sont uniques, alors qu'il faut une instance de *Bag* pour les valeurs qui peuvent être dupliquées.

Pour savoir combien de clés différentes mènent à la même valeur, on pourra utiliser la méthode `occurrencesOf: value`.

## Traiter tous les objets d'un dictionnaire

La méthode `do:` de la classe *Set* présente trois variations dans la classe *Dictionary*, pour exploiter totalement la richesse de cette classe.

*   **associationDo: aBlock** évalue *aBlock* en lui fournissant chaque association en paramètre (c'est donc l'équivalent de la méthode `do:` de la classe *Set*).
keysDo: aBlock

évalue *aBlock* en lui fournissant la clé de chaque association en
paramètre.

do: aBlock évalue aBlock en lui fournissant la valeur de chaque association en paramètre alors que la méthode associationDo: donne l'association (clé et valeur) en paramètre du bloc.

Enlever un objet

removeKey: aKey enlève l'association de clé aKey du dictionnaire.

removeKey: aKey ifAbsent: aBlock

comme la précédente, mais exécute aBlock si la clé aKey est
absente du dictionnaire.

removeAssociation: anAssociation

enlève l'association *anAssociation* du dictionnaire<fnref n="2" />. Cette
méthode fait appel à *removeKey*.

4.3.4 - Un exemple d'utilisation de dictionnaire : la paye du personnel

Monsieur Leriche, nouveau chef du personnel de la huitième chaîne de télévision, veut travailler sur une liste du personnel classée par catégories socio-professionnelles. Le montant du salaire risquant de suivre les résultats de sa chaîne, il veut aussi pouvoir modifier facilement le salaire correspondant à chaque catégorie socio-professionnelle.

Soit *personnel* le dictionnaire stockant pour chaque individu sa catégorie, et *indice* celui dans lequel Monsieur Leriche indiquera la correspondance entre la catégorie et le salaire.

Monsieur Leriche évalue le contenu de la figure 4.6. La chaîne est récente, et Monsieur Leriche n'a pas encore de secrétaire ni de balayeur. Outre le fait qu'il espère en recruter bientôt, rajouter ces deux catégories nous permettra de présenter un éventail plus large de méthodes utilisables dans *Dictionary*.

Pour obtenir le nombre de catégories réellement présentes, il ajoute au chargement des dictionnaires la ligne suivante avant d'évaluer tout le paragraphe avec *show it*.

personnel values asSet size.

La méthode *values* appliquée à *personnel* rend une instance de *Bag* contenant les catégories professionnelles, *asSet* en élimine les duplicata et *size* donne le résultat attendu.

<footnote n="2">2 Il n'existe pas de méthode permettant de supprimer toutes les associations correspondant à une valeur donnée. La méthode remove:ifAbsent;, de libellé trop imprécis, est d'ailleurs redéfinie dans la classe *Dictionary* de manière à signaler qu'elle est invalide dans cette classe.</footnote>
```
|personnel indice|
personnel := Dictionary new.
personnel add: (Association key:'François'
                  value:'Directeur');
at:'Michel' put:'Adjoint';
at:'Philippe' put:'Présentateur';
at:'Jack' put:'animateur';
at:'André' put:'Présentateur';
at:'René' put:'Cameraman';
at:'Paul' put:'Cameraman'.
indice := Dictionary new.
indice at:'Directeur' put:20000;
at:'Adjoint' put:10000;
at:'Présentateur' put:180000;
at:'animateur' put:360000;
at:'Cameraman' put:5000;
at:'Secrétaire' put:7000.
at:'Balayeur' put:4000.
```

<caption>Figure 4.6 : chargement des dictionnaires *indice* et *personnel*</caption>

Les bas salaires disposant d'aides de l'état, Monsieur Leriche désire obtenir la
liste des catégories concernées :

```
(indice select:[:salaireMensuel|salaireMensuel < 8000])
keysDo:[:categorie|categorie printOn:Transcript.
Transcript cr].
```

La méthode *select*: construit un nouveau dictionnaire en retenant toutes les
associations du dictionnaire récepteur pour lesquelles le bloc argument renvoie
*true*. L'évaluation du bloc argument se fait en utilisant l'association entière comme
paramètre. Définie au niveau de la classe *Dictionary*, cette méthode est tout à fait
adaptée à ce genre de problème. Comme elle renvoie un dictionnaire, nous
appliquons la méthode *keysDo*: pour obtenir l'affichage sur la fenêtre *System
Transcript* de chaque catégorie de personnel :

Cameraman
Secrétaire
Balayeur

Les feuilles de paye vont être stockées dans une nouvelle variable, *salaire*.
Pour la créer, nous compléterons le contenu de la figure 4.6 avec les expressions
suivantes :

```
|personnel indice salaire|
......"definition et chargement du dictionnaire personnel"
......"definition et chargement du dictionnaire indice"
salaire := Dictionary new.
personnel associationsDo:[:agent|
salaire at:(agent key)
    put:(indice at:(agent value))].
```
Nous avons utilisé la méthode `associationsDo:` (qui évalue le bloc avec l'association complète en paramètre) pour recréer un nouveau dictionnaire associant les noms (`agent key`) récupérés dans le dictionnaire `indice` à partir des catégories professionnelles (`indice at:(agent value)`) au salaire.

La huitième chaîne ne cesse de descendre dans les sondages. Monsieur Leriche doit se débarrasser des salaires les plus lourds. Pour cela, il évalue :

```
(salaire reject:[paye|paye>20000])
keysDo:[:nom|nom printOn:Transcript. Transcript cr]
```

et obtient la liste du personnel restant après licenciement :

* Philippe
* Jack
* André

La méthode `reject: aBlock` est basée sur le même principe que `select: aBlock`, mais ne retient que les éléments pour lesquels l'évaluation de `aBlock` donne le résultat `false`. Il reste une dernière et douloureuse opération à réaliser : calculer la somme totale versée en salaires. Pour cela, il évalue avec `show it` :

```
salaire inject:0 into:[:somme :paye | somme:=somme+paye].
```

La méthode `inject: initialValue into: aBinaryBlock` évalue successivement le bloc binaire `aBinaryBlock` en lui donnant comme premier argument `initialValue` et comme deuxième argument la valeur d'une association du dictionnaire. Le bloc est évalué pour chaque association du dictionnaire : le premier argument prend `initialValue` comme valeur initiale, puis sa valeur recalculée le cas échéant pour les évaluations suivantes ; le deuxième argument prend la valeur de chacune des associations du dictionnaire.

Dans notre exemple, la variable `somme` a pris la valeur `0` à la première évaluation, puis `0+paye` à la seconde, etc.

## 4.4 - Classe IndexedCollection

Classe mère d'une descendance touffue, `IndexedCollection` est aussi la seule dont les éléments sont naturellement accessibles par un index.

Cet index correspond à la position de l'objet dans l'énumération. Il est donc entier, et compris entre 1 et le nombre d'objets de la collection. La plupart des méthodes qui s'ajoutent à celles héritées de `Collection` concerne évidemment la manipulation de cet index.

Dans la classe `FixedSizeCollection` ces nouvelles méthodes sont plutôt orientées vers un accès à une position absolue (je veux accéder à l'objet n° 3, au n° 13, au n° 1) alors qu'elles permettent des accès à des positions relatives dans la sous-classe `OrderedCollection` (je veux accéder au premier objet, au dernier, au suivant, au précédent, au troisième à partir d'ici).

`IndexedCollection` est une classe abstraite : il n'en existe et ne peut en exister aucune instance. `Collection`, classe abstraite elle aussi, définit plusieurs méthodes (`add`: par exemple) tout en précisant qu'il est nécessaire de les implémenter dans chaque sous-classe : pratiquement aucune de ces méthodes n'est redéfinie dans
*IndexedCollection*. Il faudra descendre dans ses sous-classes avant de voir une implémentation de *add*.

Le but de *IndexedCollection* est donc de rassembler quelques méthodes communes à toutes les classes ayant un index numérique.

### 4.4.1 - Quelques méthodes pour manipuler les index

<table><tr><td>first</td><td>renvoie le premier objet de la collection réceptrice.</td></tr><tr><td>last</td><td>renvoie le dernier objet de la collection.</td></tr><tr><td>findFirst: aBlock</td><td>renvoie l'index du premier objet répondant vrai à l'évaluation de aBlock.</td></tr><tr><td>findLast: aBlock</td><td>renvoie l'index du dernier objet répondant vrai à l'évaluation de aBlock.</td></tr><tr><td>detect: aBlock</td><td>renvoie le premier objet du récepteur répondant vrai à l'évaluation de aBlock (cette méthode est héritée de la classe Collection).</td></tr><tr><td>indexOf: anObject</td><td>renvoie l'index de anObject (de sa première occurrence dans l'énumération plus exactement) ou 0 si anObject est absent du récepteur.</td></tr><tr><td>indexOf: object ifAbsent:aBlock</td><td>idem. avec exécution de aBlock si object est absent de l'énumération.</td></tr></table>

### 4.4.2 - Traiter la collection (méthodes définies dans indexedCollection)

<table><tr><td>reversed</td><td>renvoie une énumération partant du dernier objet du receveur et terminant par le premier.</td></tr><tr><td>, aCollection</td><td>sélecteur, (virgule) : renvoie une nouvelle collection composée du receveur suivi de aCollection.</td></tr><tr><td>= aCollection</td><td>renvoie vrai si le receveur et aCollection appartiennent à la même classe et correspondent à la même collection d'objets.</td></tr></table>

Cette dernière définition est très importante : la méthode `=` (égale) renvoie faux si les deux collections ne font pas partie de la même classe (même si leurs contenus sont identiques). Nous avons abordé au chapitre 3.3 les difficultés que cette implémentation introduisait dans la manipulation des chaînes de caractères.
### 4.4.3 - Modifier des objets de la collection

**replaceFrom: start to: stop with: aCollection**
remplace les éléments du receveur compris entre les index *start* et *stop* par les éléments de la collection *aCollection*. Il faut, bien sûr, que *aCollection* contienne exactement le nombre d'éléments nécessaires.

**replaceFrom: start to: stop with: aCollection startingAt: beg**
comme la précédente, mais prend les objets de remplacement à partir de l'index *beg* dans *aCollection*.

**replaceFrom: start to: stop withObject: object**
remplace tous les éléments du receveur entre les index *start* et *stop* par *object*.

**atAll: aCollection put: object**
*aCollection* est un ensemble d'index (tous compris entre 1 et la taille du récepteur). Ce message a pour effet de remplacer tous les objets du receveur situés aux positions énumérées dans *aCollection* par l'objet *object*.

**atAllPut: object**
remplace tous les éléments du receveur par *object*.

### 4.4.4 - Copier la collection

**copyWith: object**
renvoie une copie du receveur à laquelle est ajouté *object* (en dernière position).

**copyWithout: object**
renvoie une copie du receveur dans laquelle toutes les occurrences de *object* ont été supprimées.

**copy**

**copyFrom: start to: stop**

**copyReplaceFrom: start to: stop with: aCollection**
ces méthodes renvoient une copie du receveur (entier - tronqué entre *start* et *stop* - avec les éléments de *aCollection* remplacant les objets du receveur entre *start* et *stop*).

### 4.4.5 - Effectuer des traitements sur tous les objets d'une collection

De même que dans toutes les autres classes issues de *Collection*, il est possible d'exécuter un bloc pour chaque élément de la collection (méthode *do:aBlock*).

**reverseDo: aBlock**
id. que *do:*, mais commence par le dernier objet du receveur pour aller vers le premier.

**with: aCollection do: aBinaryBlock**
exécute le bloc à deux variables *aBinaryBlock* en prenant comme premier paramètre un élément du receveur et comme
deuxième paramètre un élément de *aCollection*. Le receveur et *aCollection* doivent avoir la même taille.

*collect: aBlock* renvoie une collection composée des résultats de l'évaluation de *aBlock* en prenant successivement chacun des éléments du receveur comme paramètre.

L'absence de la méthode *size* confirme le caractère abstrait de la classe *IndexedCollection*: en effet, le calcul de la taille de la collection dépend du type de sous-classe auquel nous avons affaire :

- les instances ont une taille fixe dès leur création (*FixedSizeCollection*),

- les instances peuvent s'accroître, mais dans un sens précis (*orderedCollection*).

Nous allons étudier chacune de ces deux sous-classes en réalisant quelques exercices, qui nous permettront ainsi de mettre en pratique les méthodes que nous venons d'énumérer.

## 4.5 - La classe FixedSizeCollection

La classe *FixedSizeCollection* étant une classe abstraite, seules ses sous-classes permettent de créer des objets. Celles-ci sont classées selon le type d'objet énuméré par leur instance :

- *String* pour les caractères,

- *Array* pour les éléments d'un tableau (quels qu'ils soient),

- *byteArray* pour des octets,

- *Interval* pour des nombres.

De ces quatre sous-classes, *Array* est celle qui se rapproche le plus de la conception générale de *FixedSizeCollection*, puisqu'elle permet d'énumérer n'importe quel objet. Elle n'a d'ailleurs que deux méthodes supplémentaires (*printOn*: et *storeOn*: ) permettant de traduire ses instances en suite de caractères imprimables.

### Un exemple de manipulation de tableau

Newton observait des pommes et découvrit la théorie de la gravité. Einstein griffonnait sur des cabs et découvrit la théorie de la relativité. L'exercice que nous vous proposons vous permettra d'étudier les planètes tout en croquant des pommes et sans avoir besoin de rayer votre carrosserie de voiture pour prendre des notes. Qui sait la théorie que vous découvrirez alors !

Observant le ciel étoilé par une belle nuit d'été, l'envie vous prend de noter les horaires de lever et de coucher des astres les plus connus. Avec la classe *Array* de Smalltalk, rien n'est plus simple.

Soit *saisie* un tableau dans lequel nous allons mémoriser le nom de l'étoile, son heure et son sens d'apparition ou de disparition et *newton* le tableau dans lequel nous allons conserver toutes ces informations.

Il reste à évaluer, à chaque mouvement planétaire, les lignes suivantes :
```
|newton saisie|
saisie := #('lune' $L (Time dateAndTimeNow)).
newton := Array with:saisie.
```

Smalltalk V offrant une classe *Time*, autant s'épargner la lecture fastidieuse du chronomètre (*Time dateAndTimeNow* donne l'heure et le jour au moment de l'évaluation). Le sens de déplacement est indiqué par le caractère ($L$ pour lever et $\$C$ pour coucher<fnref n="3" />).

Et ainsi de suite. La procédure est un peu lourde si l'on veut saisir plusieurs observations pour les stocker dans le tableau *newton* (nous avons vu en 3.2 qu'il est laborieux de créer une instance de *Array* contenant plus de quatre éléments). Nous allons simplifier notre travail en créant la classe *Astre*, sous-classe de *Array*, dont les instances seront les tableaux concernant une mesure (variable *saisie* de notre exemple).

Dans *Astre*, nous allons ajouter la méthode de classe *entre*, décrite figure 4.7, pour saisir une observation de manière interactive.

```
entre
    "crée et initialise un tableau avec planete, heure et sens"
    | planete heure sens |
    planete := Prompter prompt:'Quelle planete ? ' default:''.
    "planete mémorise le nom de l'astre observé"
    heure := Time dateAndTimeNow.
    "heure récupère la date et l'heure courante de la machine
    [#('L' 'C') includes: sens] whileFalse:
        [sens := Prompter prompt:'Lever (L) ou Coucher (C) ? ' default:'L'].
    "tant que l'opérateur n'a pas répondu un caractère correct, la question reste posée.
    sens mémorise ensuite le déplacement observé."
    ^ (self with:planete with:(sens at:1) with:heure)
    "(sens at:1) renvoie un caractère"
    "la méthode renvoie un tableau chargé des trois informations nécessaires"
```

<caption>Figure 4.7 : méthode *entre* pour la classe *Astre*</caption>

Cette méthode permet de créer une nouvelle instance de *Astre* en introduisant
au clavier les informations nécessaires (appel au *Prompter*).

Il suffit d'évaluer

```
|newton|
newton := Array with:(Astre entre) with:(Astre entre)
```

pour initialiser *newton*. Pour tester la méthode, nous avons saisi deux obser-
vations concernant le soleil et la lune. L'évaluation par l'option *show it* a donné le
tableau suivant :

```
(( 'soleil' $C (Nov 13, 1990 16:58:00))
('lune' $L (Nov 13, 1990 16:58:03)))
```

<footnote n="3">3 Nous aurions aussi pu coder le mouvement par une chaîne ('lever'/'coucher') ou un entier (+1/-1) : la classe *Array* permet d'énumérer les objets de n'importe quelle classe (y compris *Array*).</footnote>
Pour ajouter une nouvelle ligne à notre tableau, nous pourrions utiliser la méthode, (virgule) qui renvoie une concatenation du récepteur et de l'argument :

newton := newton, (Array with: (Astre entre))

Pour mieux simuler notre expérience, nous allons construire une méthode qui permette de noter les mouvements planétaires en temps réel, et laisse la possibilité à l'opérateur de cesser l'observation quand il le désire. Cette méthode, observe, sera définie comme une méthode de classe dans Astre (figure 4.8).

### observe

```
"permet de noter au coup par coup les mouvements planétaires "
|galilee|
galilee := Array with: (Astre entre).
"on suppose qu'il y a au moins un mouvement planétaire à noter"
[(Prompter prompt: 'D' 'autres observations (oui/non) ?'
    default: 'oui') = 'oui']
"tant qu'il y a des planètes à observer..."
whileTrue: [galilee:=galilee,
    (Array with: (Astre entre))]
"on ajoute une entrée à la variable tableau galilee".
^galilee
```

<caption>Figure 4.8 : méthode de classe observe</caption>

Cette méthode simplifie l'écriture de la procédure à lancer pour mémoriser les observations.

```
|newton|
newton := Astre observe
```

Cette séquence très simple, évaluée par l'option *show it*, suffit pour lancer la procédure de mesure. Pour en sortir, il suffira de répondre 'non' à la question 'D'autres observations (oui/non) ?'. Pour pouvoir effectuer différents traitements sur les observations stockées dans le tableau *newton*, nous allons plutôt utiliser une variable globale (sa valeur sera maintenue même en fin d'évaluation) pour entrer nos observations. La séquence à évaluer au début de la nuit devient alors :

Newton := Astre observe

Après une nuit d'observation attentive, nous voulons obtenir la liste des planètes qui ont été aperçues. Nous utiliserons la méthode *collect*, qui permet de rassembler dans une nouvelle collection le résultat du traitement de chaque objet de l'énumération, soit :

Newton collect: [:planete| (planete at:1) asUppercase]
qui donnera par exemple (avec *show it*) :

```
('SOLEIL' 'LUNE' 'VENUS' 'JUPITER' 'SOLEIL')
```

La méthode *asUppercase*, qui s'adresse à des chaînes de caractères transforme celles-ci en majuscules (Cf 3.3). Elle nous permet d'obtenir des noms de planètes
homogènes, quelle que soit la manière dont ces noms ont été entrés par l'observateur.

Pour vérifier la cohérence de nos données, nous allons extraire du tableau les deux seules lignes concernant le soleil. Pour cela, nous utiliserons la variable *liste* pour récupérer la place du mot 'SOLEIL' dans le tableau (méthodes *findFirst* et *findLast* qui nous donnent l'index de la première et la dernière position de 'SOLEIL'). Evaluons avec l'option *show it* la séquence de la figure 4.9.

```perl
|liste soleil|
Newton := Astre observe.
"on suppose que nous avons fait cinq mesures, la première et la troisième concernant le
soleil"
liste := Newton collect:[:planete|
    (planete at:1) asUppercase].
soleil := Array with: (Newton at:
    (liste findFirst:[:nom|nom='SOLEIL'])
    with: (Newton at:
        (liste findLast:[:nom|nom='SOLEIL'])).
```

<caption>Figure 4.9 : extraction des mesures concernant le soleil</caption>

Dans notre cas, nous avons obtenu :

```
( 'soleil' $C (Nov 14, 1990 09:51:15))
( 'soleil' $L (Nov 14, 1990 09:51:25)))
```

Nous en déduisons que nous sommes près du pôle ou que notre chronomètre est très mal en point.

Compliquons un peu l'exercice. Après une seconde nuit à scruter les étoiles, nous avons noté huit mouvements planétaires (figure 4.10).

```perl
|liste|
Newton := Astre observe. "nous ferons huit observations"
liste := Newton collect:[:planete|
    (planete at:1) asUppercase].
"liste contient le nom des planètes observées, en majuscule".
```

<caption>Figure 4.10 : initialisation des variables *Newton* et *liste*</caption>

Complétons la séquence de la figure 4.10 par le message *liste printOn:Transcript* avant d'évaluer le tout par l'option *show it*. Nous voyons alors s'afficher sur la fenêtre *System Transcript* une liste semblable à :

`('SOLEIL' 'PLUTON' 'LUNE' 'PLUTON' 'VENUS' 'MARS' 'JUPITER' 'SOLEIL')`
Au vu de cette liste, l'astronome se désespère : Pluton est invisible à l'oeil nu. L'observateur devra corriger la liste en complétant la séquence précédente par :

```perl
liste copyWithout: 'PLUTON'.
```

qui renvoie après évaluation du tout :

```perl
('SOLEIL' 'LUNE' 'VENUS' 'MARS' 'JUPITER' 'SOLEIL')
```

Ce n'est pas suffisant : le tableau Newton n'a pas été corrigé. Connaissant la position des deux observations litigieuses, et mettant en doute l'éveil de son observateur pendant cette période, l'astronome préfère supprimer toutes les mesures comprises entre les deux mouvements de Pluton pour ne garder qu'un tableau plus fiable, Einstein<fnref n="4" />. Il va donc évaluer :

```perl
Einstein := Newton copyReplaceFrom:2 to:4
           with:#{ 'Douteux' }.
```

Il obtiendra ainsi un tableau dans lequel les trois entrées litigieuses ont été supprimées au profit d'un seul objet : 'Douteux' :

```perl
(( 'soleil' $C (Nov 14, 1990 10:10:38))
 'Douteux'
 ('venus' $L (Nov 14, 1990 10:10:59))
 ('mars' $L (Nov 14, 1990 10:11:04))
 ('jupiter' $C (Nov 14, 1990 10:11:12))
 ('soleil' $L (Nov 14, 1990 10:11:15)))
```

L'observateur ayant expliqué qu'il a confondu Pluton et Saturne, l'astronome préfère alors corriger le tableau Newton avec le message `atAll: aCollection put: Object` qui va remplacer les éléments du récepteur aux indices énumérés dans `aCollection` par le nouvel élément `object`.

La position des entrées concernant Pluton est obtenue en examinant la première sortie de la variable `liste` (Pluton est aux indices 2 et 4). L'astronome évalue alors avec `show it`:

```perl
Einstein := Newton copy. "Einstein est maintenant une copie de l'ancienne version de Newton"
Newton atAll:#{(2 4)} put: (Astre with: 'saturne'
                         with:nil
                         with:nil).
```
"il faudra récupérer le sens et l'heure dans Einstein"

Il obtiendra un tableau comparable à : :

```perl
(( 'soleil' $C (Nov 14, 1990 10:10:38))
 ('saturne' nil nil)
 ('lune' $L (Nov 14, 1990 10:10:47))
 ('saturne' nil nil)
 ('venus' $L (Nov 14, 1990 10:10:59))
 ('mars' $L (Nov 14, 1990 10:11:04))
 ('jupiter' $C (Nov 14, 1990 10:11:12))
 ('soleil' $L (Nov 14, 1990 10:11:15)))
```

<footnote n="4">4 Défini comme variable globale.</footnote>
Il ne restera plus qu'à récupérer l'heure et le sens dans la copie de *newton* avant
correction (Einstein) pour corriger les lignes 'saturne'.

Il est temps d'interpréter les données. Pour cela, nous allons calculer le temps
écoulé entre le coucher et le lever du soleil (figure 4.11) en utilisant les méthodes
detect: (pour récupérer la première observation correspondant au soleil) et
reversed detect: (pour récupérer la dernière).

```
|lever coucher|
coucher := Newton detect:[:planete|
               ((planete at:1) asUppercase = 'SOLEIL')
               and:[(planete at:2) = $C]].
lever := (Newton reversed) detect:[:planete|
               ((planete at:1) asUppercase = 'SOLEIL')
               and:[(planete at:2) = $L]].
```

<caption>Figure 4.11 : détermination des heures de lever et de coucher du soleil</caption>

Nous avons renforcé le test évalué par *detect* en vérifiant qu'il s'agit bien du
coucher ($C$) et du lever ($L$) au cas où un observateur affaibli par le mal de
l'altitude aurait multiplié les apparitions noctumes du soleil. Pour calculer le
temps écoulé, complétons la séquence de la figure 4.11 par :

(((lever at:3) at:2) asSeconds) - (((coucher at:3) at:2) asSeconds)

qui donne 27, une fois évaluée par *show it*.

Le temps que nous avons obtenu de l'horloge de la machine se présente sous la
forme d'un tableau contenant la date et l'heure. Il faut d'abord extraire ce temps
de la ligne concernée (lever at:3) puis isoler l'heure (at:2). Le message asSeconds
permet de traduire l'heure au format normal en un nombre de secondes absolu
depuis le début de la journée.

Le plus dur est fait : il ne reste plus qu'à interpréter ce chiffre de vingt-sept
secondes. Le lecteur n'oubliera pas ensuite de détruire les variables globales
Newton et Einstein en évaluant :

Smalltalk removeKey:#Newton; removeKey:#Einstein.

## 4.6 - La classe OrderedCollection

La seconde sous-classe de *IndexedCollection* n'impose pas de connaître la
taille de ses instances dès leur création. Au contraire, elles doivent pouvoir
s'agrandir et diminuer à loisir, puisque leur conception les rapproche des listes
chaînées.

L'utilisation la plus fréquente de cette classe concerne la simulation de piles,
de files d'attente ou de tableaux dynamiques.

Une instance de *OrderedCollection* mémorise non seulement les éléments,
mais aussi les liaisons qui les ordonnent. De nombreuses méthodes ont donc pour
but de gérer ces liaisons.
Nous verrons d'abord une liste assez complète des nouvelles méthodes de cette classe avant d'en utiliser quelques unes pour simuler une file d'attente chez un médecin.

### 4.6.1 - Principales méthodes de OrderedCollection

#### Ajouter des éléments dans la liste

**add: object** ajoute *object* en fin de liste.

**add: newObject after: oldObject**
insère *newObject* immédiatement après *oldObject*.

**add: object afterIndex: i**
insère *object* juste après l'objet en position *i*.

**add: newObject before: oldObject**
insère *newObject* juste avant *oldObject*.

**add: object beforeIndex: i**
insère *object* juste avant l'objet en position *i*.

** addAllFirst: aCollection**
insère tous les éléments de *aCollection* avant le premier objet de la liste.

** addAllLast: aCollection**
ajoute tous les objets de *aCollection* en fin de liste.

** addFirst: object**
insère *object* en première position.

** addLast: object**
ajoute *object* en dernière position.

**at: index put: object**
héritée, toujours utilisable.

**replaceFrom: i to: j with: aCollection**
remplace les éléments de la liste compris entre les positions *i* et *j* par ceux de *aCollection*.

#### Consulter la liste

**at: index** toujours utilisable.

**after: object** rend l'élément suivant *object* (ou une erreur en cas d'absence).

**after: object ifNone: aBlock**
permet de traiter l'erreur due à l'absence de *object* dans la liste.

**before: object** rend l'élément précédant *object*.

**before: object ifNone: aBlock**
id. avec traitement possible de l'absence de *object*.
## Enlever un élément

```
remove: object ifAbsent: aBlock
```

enlève *object* de la liste ou évalue *aBlock* si *object* n'existe pas dans la liste.

```
removeFirst      enlève le premier élément.
```

```
removeIndex: i  enlève l'élément d'index i de la collection.
```

```
removeLast       enlève le dernier élément de la liste.
```

## 4.6.2 - Un exemple d'utilisation de OrderedCollection : une salle d'attente.

Le docteur Knock a perdu son carnet de rendez-vous. Un pugilat ayant opposé ses patients qui prétendaient tous être arrivés le premier, le médecin décide de les enregistrer désormais dans leur ordre d'arrivée.

La variable *salle* représentera la file d'attente de ses clients. La variable *position* sera utilisée dans la suite pour repérer la place d'un client particulier<fnref n="5" />.

```
|salle position|
```

```
salle := OrderedCollection new.
```

Au premier coup de sonnette, le docteur Knock évalue :

```
salle add: 'Argan'.
```

Lorsque la famille Fenouillard entre, avant qu'il ait eu le temps de terminer d'examiner Argan, il évalue :

```
salle addAllLast:#{ 'Lise' 'Michel' 'Guy' 'Hubert' 'Aline'
'Florence' }.
```

Au moment où Argan sort, Noah entre. Il est blessé et souffre visiblement beaucoup. Le docteur Knock décide de le voir en priorité, soit :

```
salle removeFirst. "Argan part"
```

```
salle addFirst: 'Noah'. "Noah est inséré en première position"
```

Pendant ce temps, Hubert Fenouillard implore sa maman pour qu'elle le laisse sortir quelques minutes.

```
salle remove: 'Hubert' ifAbsent: [nil].
```

```
"le paramètre de ifAbsent ([nil]) signifie qu'aucun traitement spécifique n'est réclamé si
Hubert est absent de la liste"
```

<footnote n="5">5 Dans tout ce paragraphe, on supposera que les séquences données en exemple sont évaluées en partant de la déclaration des variables *salle* et *position*.</footnote>
Mais quand il revient, une bonne demi-heure plus tard, plusieurs clients sont
passés :

salle add:'Becker';
add:'Papin';
add:'Devos';
add:'Fignon'.

Il est hors de question de le rajouter en fin de liste, d'où :

salle add:'Hubert' after:'Aline'.

Et pour être sûr que l'insertion a fonctionné, on peut évaluer l'expression :

salle before:'Hubert'

qui renverra 'Aline'.

Le docteur Knock a remarqué la présence de Monsieur Devos en introduisant
un nouveau patient. Il consulte sa liste pour connaître sa position :

position := salle indexOf:'Devos'

(méthode héritée de IndexedCollection).

Et comme il meurt d'envie de s'amuser, il va subrepticement modifier l'ordre
de passage de son chansonnier favori :

salle add:'Devos' beforeIndex:(position - 1);
"ajoute Devos avant la personne qui le précédait"
removeIndex:(position + 1).
"supprime l'ancien Devos qui se retrouve maintenant décalé
d'une position en arrière"

Dans cet exemple, nous avons utilisé la notion de file d'attente, très employée
en programmation. Outre la simulation d'événements (caisses de super-marché,
guichets de banque, péages d'autoroute), l'informatique utilise les files d'attente
pour gérer ses propres ressources : l'impression des fichiers sur une imprimante, le
lancement de plusieurs processus sur l'ordinateur, la gestion des accès simultanés
à un disque font appel à des files d'attente.

Le principe est simple : lorsqu'une ressource est occupée, les nouvelles
requêtes pour cette ressource s'accumulent dans une file d'attente. Elles seront
prises en charge dans leur ordre d'arrivée (« premier arrivé - premier sorti ») ou
dans l'ordre inverse (« dernier entré - premier sorti »). Ce dernier cas s'apparente
d'ailleurs au mode de fonctionnement d'une pile, notion également très utilisée en
informatique. Une pile s'apparente à la manipulation d'une pile d'assiettes. Celles-
ci sont entassées les unes sur les autres, mais seule la dernière empilée reste acces-
sible. La mémoire de l'ordinateur peut aussi être gérée comme une pile.

Les instructions indispensables pour utiliser une pile sont « empiler »,
« dépiler », « consulter le sommet de la pile ». Les messages Smalltalk corres-
pondants sont add (ou addLast), removeLast et last.

Pour simuler une file d'attente de discipline « premier entré - premier sorti », il
est nécessaire de disposer au moins des instructions « s'ajoute à la file », « sort de
la file », « prochain à sortir ? » qui pourront être respectivement réalisées en
Smalltalk avec les méthodes add, removeFirst et first.
### 4.6.3 - La classe SortedCollection

Sous-classe de OrderedCollection, son examen dans le Class Hierarchy Browser peut paraître un peu inquiétant : encore une énumération fastidieuse de nouvelles méthodes ! Il n'en est rien. Une grande partie des méthodes créées dans la classe mère OrderedCollection n'est pas applicable dans cette classe : toutes ces méthodes sont redéfinies dans SortedCollection pour signaler une erreur.

En effet, chaque instance de SortedCollection ordonne elle-même ses éléments selon une règle de classement (contenue dans un bloc à évaluer) qui lui a été donnée à sa création (méthode de classe sortBlock: aBlock qui crée l'instance et détermine sa règle de classement) ou par la suite (méthode d'instance sortBlock: aBlock). La méthode sortBlock renvoie le bloc contenant la règle de classement de l'instance réceptrice.

Comme on peut s'en douter, une variable d'instance permet de stocker le bloc de classement. Elle s'appelle évidemment sortBlock.

Pour illustrer le fonctionnement de cette classe, prenons l'exemple d'une école. L'instituteur désire obtenir une liste alphabétique de ses élèves. Mais le directeur lui impose de noter aussi le prénom et l'âge de l'enfant concerné. L'instituteur choisit d'écrire les informations concernant un élève dans un tableau. Il évalue avec do it la séquence de la figure 4.12.

```pascal
|class|
classe := SortedCollection
    sortBlock:
        [:elevel :eleve2 |
         (elevel at:1) < (eleve2 at:1)].
        "le bloc de tri compare le premier champ de l'élément qui
        sera dans notre cas le nom de famille"
    classe add:#{Spangheroo Walter 12};
        add:#{Bidochon Paul 11};
        add:#{Vautry Raoul 14};
        add:#{Vergniaud Leon 13};
        add:#{Johannes Jules 10}
    classe
```

<caption>Figure 4.12 : création d'une liste d'élèves ordonnée alphabétiquement</caption>

Le bloc contenant la règle de classement est placé en argument du message sortBlock. Ce doit être un bloc binaire, dont les variables locales représentent deux éléments de l'instance de SortedCollection. Pour être placée avant la deuxième variable (eleve2) dans l'instance de SortedCollection, la première variable (elevel) doit respecter la condition donnée dans la séquence d'instructions ((elevel at:1) < (eleve2 at:1)).

Le résultat de l'évaluation précédente est SortedCollection ((Bidochon Paul 11) (Johannes Jules 10) (Spangheroo Walter 12) (Vautry Raoul 14) (Vergniaud Leon 13)).
Un nouveau ministre impose de conserver des listes par âge décroissant : rien de plus facile. Il suffit d'ajouter le message :

```
classe sortBlock:[:eleve1 :eleve2 |
                  (eleve1 at:3)>(eleve2 at:3)]
```

La variable *classe* désigne alors ((Vautry Raoul 14) (Vergniaud Leon 13) (Spangheroo Walter 12) (Bidochon Paul 11) (Johannes Jules 10)).

Nous venons d'avoir un aperçu assez complet sur la classe *Collection* et sa descendance. Nous allons maintenant étudier en détail la manière dont Smalltalk V travaille pour manipuler leurs instances.

## 4.7 - La classe Bag et ses méthodes

### 4.7.1 Redéfinition des méthodes inadéquates pour la classe Bag

Nous avons vu, dans la classe *IndexedCollection*, une méthode d'accès aux objets par leur position dans la collection. Cette méthode, *at*., est aussi présente dans la classe *Bag*. Or la classe *Bag*, par définition, ne permet pas d'accéder aux objets contenus dans ses instances par leur position.

Le contenu de la méthode *at*: examiné dans le *Class Hierarchy Browser* est repris figure 4.13.

```
at: anInteger
```

```
"Answer the element of the receiver at index position anInteger.
Report an error since bags are not indexable."
```

```
self errorNotIndexable
```

<caption>Figure 4.13 : la méthode *at*: définie dans *Bag*</caption>

Le commentaire, placé entre guillemets, nous explique que les objets de la classe *Bag* ne sont pas indexables, et qu'en conséquence, un message d'erreur est renvoyé. Nous pouvons tester ce message en reprenant notre exemple du chapitre 4.1 pour évaluer :

```
panierDeCourse at: 4.
```

Le message d'erreur qui est alors affiché est envoyé par la méthode *errorNotIndexable* qui est appliquée à notre instance de la classe *Bag*. Mais cette méthode n'existe pas au niveau de la classe *Bag*. Il faut donc la chercher en remontant la hiérarchie des classes. Nous la trouverons très rapidement, au niveau de la classe *Collection*.

Quel intérêt peut-on trouver à définir dans la classe *Bag* deux méthodes (*at*: et *at:put:*) pour expliquer aussitôt qu'elles ne sont pas applicables dans notre cas ?

Les objets de la classe *Bag* n'étant pas indexables, si les méthodes *at*: et *at:put*: existent déjà dans une classe mère de *Bag*, il est nécessaire de les redéfinir dans *Bag* pour éviter des erreurs. Effectivement, lorsque nous remontons de nouveau la hiérarchie des classes pour les rechercher, nous en trouvons une
implémentation dans la classe *Object*, classe mère de toutes les classes du système.

Grâce à la redéfinition de ces deux méthodes, si un utilisateur envoie malgré tout le message *at*: à une instance de la classe *Bag*, un message d'erreur explique que les instances de *Bag* ne sont pas indexables.

Cette construction est nécessaire lorsque des sous-classes voisines ne peuvent utiliser toutes les méthodes de leur classe mère. Par analogie avec une sous-classe voisine, il est fréquent d'envoyer un message inadéquat. L'instance réceptrice tentera quand même de traiter ce message, puisque la méthode correspondante est définie dans une des classes mères (c'est le cas de notre méthode *at*: pour *Bag*). Il est donc conseillé de redéfinir toutes les méthodes inappropriées pour une classe donnée, en affichant un message d'erreur. Si plusieurs sous-classes font appel au même message d'erreur (c'est le cas de *Bag* et de *Set* pour les erreurs d'accès par position), il est plus pratique de placer la méthode affichant l'erreur dans la classe mère (ici, *errorNotIndexable* est dans *Collection*).

### 4.7.2 - Initialisation de la classe *Bag*

Lorsqu'une instance est créée dans une classe, il est fréquent que la méthode *new* (méthode définie dans la classe *Behavior* et généralement utilisée pour créer une instance) soit redéfinie localement et fasse appel à une méthode d'instance supplémentaire.

Dans le cas de la classe *Bag*, telle qu'elle est implémentée sous Smalltalk V, cette méthode supplémentaire est *initialize*. Son contenu (figure 4.14) n'est pas très explicite. Le commentaire *"Private"* en tête de méthode signifie qu'elle n'a pas de raison d'être employée telle quelle par l'utilisateur, mais est appelée en complément d'une autre méthode.

Cette méthode est privée, en ce sens qu'elle n'a aucune raison d'être exécutée directement par le programmeur. Seule la méthode de classe *new* y fait appel.

```
initialize
    "Private - initialize the receiver to be empty."
elements := Dictionary new
```

<caption>Figure 4.14 : méthode d'initialisation des instances de *Bag*</caption>

La seule activité du message *initialize* est de créer un dictionnaire (classe *Dictionary*) appelé *elements*. L'objet *elements* n'a pas été défini comme variable locale, il n'est pas non plus une variable globale (pas de majuscule en première lettre). Que peut-il être ?

En cliquant sur *Bag* dans la fenêtre des classes, nous voyons s'afficher dans la sous fenêtre inférieure du *Class Hierarchy Browser* la définition de la classe *Bag* (figure 4.15).
```
Collection subclass: #Bag
instanceVariableNames:
    'elements '
classVariableNames: ''
poolDictionaries: ''
```

<caption>Figure 4.15 : définition de la classe *Bag*</caption>

L'objet *elements* est défini comme une variable d'instance. Autrement dit, chaque instance de la classe *Bag* a une variable personnelle, *elements*, dont le contenu lui est propre.

### 4.7.3 - La variable d'instance elements

Toutes les méthodes définies dans la classe *Bag* font appel aux méthodes de *Dictionary*, par l'intermédiaire de la variable d'instance *elements*, définie comme une instance de *Dictionary*. La figure 4.16 montre précisément l'usage qui est fait de ce dictionnaire variable d'instance, en prenant comme exemple la méthode *add*: telle qu'elle est définie dans la classe *Bag*.

```
add: anObject
    "Answer anObject. Add anObject to the elements of the receiver."
    elements
        at: anObject
        put: (self occurrencesOf: anObject) + 1.
    ^anObject
```

<caption>Figure 4.16 : méthode *add*: de la classe *Bag*.</caption>

A la clé *anObject*, Smalltalk V associe son nombre d'occurrences dans l'instance de *Bag*. Cette valeur augmente d'une unité lorsqu'on ajoute *anObject* dans l'instance de *Bag* propriétaire du dictionnaire *elements*.

Plutôt que d'entasser les objets en vrac dans les instances de *Bag*, Smalltalk V utilise un dictionnaire dont les clés sont les objets eux-mêmes et les valeurs associées le nombre de fois où ces objets apparaissent dans l'instance de *Bag*. Cette manière de construire la classe *Bag* simplifie considérablement les méthodes, comme nous pouvons le vérifier avec `occurrencesOf`, qui rend le nombre d'occurrences d'un objet dans le receveur. Smalltalk V renvoie simplement la valeur associée dans le dictionnaire *elements* à la clé de nom *anObject*.

La méthode `size` (figure 4.17) fait appel à la méthode `associationsDo:` de *Dictionary* pour totaliser le nombre d'objets de l'instance réceptrice. Il lui suffit de totaliser la valeur (*element value*) de chacune des associations composant le dictionnaire *elements* pour cumuler le nombre d'occurrences de chaque objet composant l'instance de *Bag* et obtenir ainsi la taille de cette instance.
```java
size
    "Answer the number of elements in the receiver collection."
  | answer |
answer := 0.
elements associationsDo: [ :element |
    answer := answer + element value].
^answer
```

<caption>Figure 4.17 : méthode *size* de la classe *Bag*.</caption>

Dans une classe donnée, Smalltalk V emploie souvent comme variables d'instances des objets appartenant à une classe voisine, parfois même à une classe fille. Il n'est pas rare de trouver de telles imbrications de classes dans Smalltalk V. Nous en verrons de nouveau en étudiant la classe *Stream*.

## 4.8 - Méthodes directement définies dans la classe Collection

### 4.8.1 - Ajouter un objet dans une collection

La méthode `all:` est définie au niveau de la classe *Collection*. Comme son nom le laisse supposer, elle ajoute en bloc le contenu d'une collection dans une autre (figure 4.18).

```java
all: aCollection
    "Answer aCollection. Add each element of aCollection to the elements of the receiver."
aCollection do: [ :element | self add: element].
^aCollection "renvoie l'argument aCollection et non le récepteur modifié"
```

<caption>Figure 4.18 : méthode `all:` définie dans *Collection*</caption>

En réalité, elle ne fait qu'appliquer la méthode `add:` à tous les éléments contenus dans la collection à transférer. Nous avons déjà utilisé cette méthode `add:` pour charger *panierDeCourse*. L'examen du *Class Hierarchy Browser* nous montre que `add:` est définie pour la classe *Collection*. Mais cette définition reste assez vague (figure 4.19).

```java
add: anObject
    "Answer anObject. Add anObject to the receiver collection."
^self implementedBySubclass
```

<caption>Figure 4.19 : méthode `Add:` définie dans *Collection*</caption>

L'exécution de la méthode `add:` telle qu'elle est définie dans la classe *Collection* envoie un message d'erreur (méthode `implementedBySubclass`, que l'on trouvera dans *Object*). Si le programmeur crée une sous-classe de *Collection* en
oubliant de reconstruire la méthode *add*: un message d'erreur s'affiche à l'écran, expliquant que *add*: aurait du être redéfini dans cette nouvelle sous-classe.

La variable *panierDeCourse* supportait sans broncher le message *add*. On a déjà constaté (Cf figure 4.16) qu'il est redéfini dans la sous-classe *Bag*. Plus largement, toute sous-classe de *Collection* doit redéfinir le message *add*. Sans cela, un message d'erreur s'affichera.

Cette manière d'implémenter les méthodes permet de standardiser la forme des messages qui ont un effet comparable (cas d'un ajout d'objet dans une collection, avec la méthode *add*). Les sous-classes étant malgré tout assez différentes, la construction pratique de la méthode est laissée à la charge de chacune des sous-classes<fnref n="6" />.

Ce point est à garder en mémoire, lors de la création de nouvelles sous-classes : il faut redéfinir toutes les méthodes incomplètes de la classe mère.

### 4.8.2 - Passer d'une sous-classe de *Collection* à une autre

La figure 4.20 détaille le contenu de la méthode *asSet*, dont le rôle est de renvoyer la traduction de l'objet récepteur en instance de *Set*.

**asSet**

"Answer a new Set containing all the elements of the receiver."

^ (Set new: self size) "crée une instance de Set de la taille du récepteur"

addAll: self; "ajoute tous les objets du récepteur à cette nouvelle instance de Set"

yourself "renvoie cette nouvelle instance de Set comprenant tous les éléments du récepteur"

<caption>Figure 4.20 : méthode de traduction d'une collection en instance de *Set*</caption>

La méthode *yourself* est définie dans la classe *Object* et renvoie l'objet récepteur (du message *yourself*)<fnref n="7" />. Ici, nous récupérons donc après évaluation de *asSet* l'équivalent de (*Set new:self size*) *ALL*: *self*. Une nouvelle instance de *Set* est créée, de taille équivalente à celle du récepteur. Tous les objets du récepteur y sont ensuite chargés par la méthode *ALL*: . C'est cette nouvelle instance contenant les éléments du récepteur qui est renvoyée en fin d'évaluation par la méthode *yourself*.

L'utilisation de *yourself* permet d'éviter l'emploi d'une variable locale. En effet, l'instruction `^ (Set new:self size) addAll:self` renverra le contenu de *self*, puisque le dernier message évalué dans la séquence (*ALL*: *self) renvoie *self* (Cf figure 4.18). En complétant cette instruction par `:yourself`, le dernier objet renvoyé devient le récepteur de *yourself*, soit la nouvelle instance de *Set* (qui reçoit une cascade de deux messages séparés par le signe ; (point virgule)). Une variable locale aurait pu être employée pour un résultat identique, comme dans la séquence suivante :

<footnote n="6">6 Principe du polymorphisme, abordé en 1.6.</footnote>

<footnote n="7">7 Le corps de sa définition est : `^self`.</footnote>
```
aSet := (Set new size:self) addAll:self.
^aSet
```

Comme `asSet`, les autres méthodes de transfert sont basées sur le principe suivant : créer une instance de la classe cible, et y placer tous les éléments du récepteur.

## 4.9 - Quelques méthodes de Set

### 4.9.1 - Ajouter un élément dans une instance de Set

Nous pouvons examiner le contenu de la méthode `add`: telle qu'elle est redéfinie au niveau de `Set` (figure 4.21). Nous allons détailler cette méthode pour illustrer la manière dont Smalltalk gère les éléments d'une collection en mémoire.

L'en-tête nous signale que la méthode ajoute un élément à une instance de `Set` après avoir vérifié qu'il n'existe pas déjà. En effet, une instance de `Set` ne peut contenir plus d'un exemplaire du même élément, contrairement aux instances de `Bag`.

En consultant la définition de la classe `Set`, nous pouvons vérifier que l'objet `elementCount` (utilisé à la treizième ligne) est une variable d'instance. Son nom, et la façon dont elle est utilisée montre qu'il s'agit d'un compteur, qui mémorise en permanence le nombre d'éléments contenus dans l'instance de `Set` à laquelle elle appartient.

```ruby
add: anObject
"Répond anObject. Ajoute anObject au récepteur si le récepteur ne le contient pas déjà."
| index |
"index repère la position de anObject ou la première case libre si anObject n'existe pas"
anObject isNil ifTrue: [^anObject].
"si l'argument est nil, la méthode s'arrête en renvoyant nil"
self adjustSize.
"On vérifie qu'il est possible d'accueillir le nouvel objet dans le récepteur"
(self basicAt:
  (index := self findElementIndex: anObject)) isNil
"si l'objet désigné par index est nil, alors anObject n'est pas encore dans le récepteur et il
faut l'ajouter"
ifTrue: [elementCount := elementCount + 1.
         "il y a un élément de plus dans le récepteur"
         ^self basicAt: index put: anObject].
         "on place le nouvel objet à la place vide désignée par index"
         ^anObject
```

<caption>Figure 4.21 : méthode `add` de la classe `Set`</caption>
L'évaluation de

```
|unEnsemble| unEnsemble := Set new. "crée unEnsemble, nouvelle instance de Set"
```

```
unEnsemble add:1. "met le chiffre 1 dans unEnsemble"
```

```
unEnsemble add:#lapin. "ajoute le symbole lapin dans unEnsemble"
```

```
unEnsemble add:'chasseur'. "ajoute la chaîne de caractères 'chasseur' dans unEnsemble"
```

```
unEnsemble size "renvoie le cardinal de unEnsemble"
```

donne 3. Chacun des messages reçus par *unEnsemble* a utilisé sa variable d'instance *elementCount* :

- la méthode *new* fait appel à la méthode *new*:, qui elle-même déclenche l'exécution de *initialize* (mise à 0 de *elementCount*). Comme pour la classe *Bag*, la méthode *initialize* est une méthode privée, appelée par *new*:, et dont le seul but est d'initialiser la variable d'instance.

- La méthode *add*: recalcule *elementCount* (voir fig. 4.21).

- La méthode *size* renvoie tout simplement le contenu de *elementCount*.

### 4.9.2 - Stockage des objets dans une instance de Set : notion de hachage

Remarquons qu'une instance de *Set* peut enregistrer comme élément n'importe quel objet (nombre, symbole, chaîne, tableau, collection etc.). Comme dans la classe *Bag*, les instances de *Set* répondent aux messages *includes*:, *occurrencesOf*:, *remove*: *ifAbsent*:, *do*:, et signalent une erreur (instance non indexable) lors de l'exécution de *at*: et *at:put*:.

Le fait qu'un objet ne soit présent qu'en un seul exemplaire autorise des méthodes supplémentaires :

*findElementIndex: anObject* (à laquelle fait appel *add*:)
rend la position de *anObject*

*find: anObject ifAbsent: aBlock*
rend la position de *anObject* s'il existe, exécute *aBlock* sinon.

Nous venons pourtant de noter que les méthodes *at*: et *at:put*: (qui font référence à un index ou position de l'objet dans la collection) bloquent sur le message d'erreur « Instance non indexable ». Regardons plus précisément l'effet de ces méthodes.

Dans notre cas, l'évaluation à la suite de la séquence précédente de :

```
unEnsemble findElementIndex: unObjet
```

donnera 1 pour \#lapin, 5 pour 'chasseur' et 2 pour 1.

Or, nous n'avons entré que trois éléments dans notre instance de *Set*. Pour vérifier la taille réelle de *unEnsemble*, complétons les messages précédents par

```
unEnsemble inspect.
```

Le message *inspect* (Cf 3.1.3) a pour effet d'ouvrir une fenêtre d'inspection sur l'instance qui le reçoit. Dans notre cas, nous observons sur la moitié gauche de cette fenêtre la liste des variables nommées liées à notre instance (*self*,
*elementCount*, et une suite de numéros<fnref n="8" /> allant de 1 à 15). En cliquant successivement sur chacune de ces variables, nous observons leur contenu dans la moitié droite de la fenêtre.

La variable *elementCount* affiche bien entendu 3, puisque nous n'avons chargé que trois éléments. Quant aux numéros suivants, il s'agit des quinze cases disponibles que comporte *unEnsemble*. La case 1 contient bien le symbole #lapin, la case 5 la chaîne 'chasseur' et la case 2 le chiffre 1. Toutes les autres désignent l'objet nil. La figure 4.22 reprend cette fenêtre dans laquelle la variable indexée de rang 5 a été sélectionnée.

<figure><img src="image_28.png" /><figcaption>Figure 4.22 : Fenêtre d'inspection d'une instance de *Set*</figcaption></figure>

Smalltalk V utilise le principe du hachage pour placer les objets d'une collection en mémoire. Lorsqu'un utilisateur crée une nouvelle collection, il a l'intention d'y placer une suite d'objets. Nous pourrions mettre ces objets en mémoire dans l'ordre de leur apparition. Ce n'est pourtant pas la meilleure façon de procéder, pour deux raisons.

La première est qu'il est impossible pour Smalltalk de « deviner » à l'avance quelle sera la taille des objets à placer dans la collection, et donc encore moins la taille à réserver en mémoire pour stocker la longue suite d'objets qui pourraient bien faire partie de l'énumération.

La deuxième est que les accès à des objets précis deviennent très longs : le seul moyen de retrouver un objet est de parcourir du début jusqu'à la fin l'espace mémoire alloué à la collection. Pire : à chaque chargement d'un nouvel objet, Smalltalk doit vérifier si cet objet n'existe pas déjà dans l'instance de *Set*, puisque celle-ci n'est pas censée accueillir plus d'une fois le même objet. L'opération conduite par *add*: tiendrait alors du marathon.

<footnote n="8">8 Ce sont les variables d'instance indexées de *unEnsemble*.</footnote>
Pour résoudre la première difficulté, Smalltalk ne charge pas les objets eux-mêmes dans l'espace-mémoire alloué à la collection, mais leurs adresses. Les objets sont chargés à un autre endroit. Les langages structurés (C, Pascal) ap-pellent ces adresses des « pointeurs »<fnref n="9" />. La particularité d'une adresse est d'être codée sur un nombre constant de cases mémoires. Si la collection doit accueillir dix objets (qu'il s'agisse d'une chaîne de vingt caractères, d'un tableau de mille éléments ou d'un chiffre), Smalltalk réserve une place égale à la taille de dix pointeurs consécutifs.

Toute création d'instance de la classe *Set* réserve donc une certaine place en mémoire, correspondant au nombre maximum de pointeurs que l'on pourra y stocker. Par défaut, Smalltalk V crée un emplacement mémoire pouvant recevoir quatre pointeurs (message *new: 4* appelé dans la méthode *new*). Le programmeur peut, s'il le désire, réserver plus d'espace (*new: n*).

Nous verrons plus loin comment l'espace réservé en mémoire peut s'accroître, si le programmeur charge plus d'objets qu'il n'en a spécifié avec *new: n*.

Si nous avons réglé la première difficulté, la seconde (perte de temps induite par un chargement linéaire des objets) n'est absolument pas réduite par le stockage de pointeurs sur ces objets. Au contraire, le parcours de la collection est deux fois plus long puisque le système lit pour chaque objet son adresse dans le pointeur, se déplace à cette adresse et y lit l'objet.

C'est à ce niveau qu'intervient la notion de hachage. Son principe est assez simple. Chaque objet étant unique, nous pourrions le « traduire » en un nombre, unique lui aussi, grâce à une fonction adaptée (« fonction de hachage »). Il suffirait ensuite d'allouer un espace mémoire suffisamment grand à la collection, et de placer le pointeur sur l'objet à la position indiquée par le nombre obtenu par hachage.

Un exemple de « traduction » d'un objet de la classe *Character* en nombre serait de le remplacer par son rang dans l'alphabet (entre 1 et 26) si c'est une lettre, ou bien par sa position dans la table des caractères au standard ASCII. Pour une chaîne de caractères (objet de la classe *String*), une fonction de hachage pourrait consister en la concatenation des rangs de chacun des caractères qui la composent (par exemple, 'bac' se « traduirait » 213). Nous remarquons que cette fonction n'est pas une bijection, puisque la chaîne 'uc' donnerait elle aussi le nombre 213.

Tel quel, ce hachage présente un avantage et deux inconvénients :

- nous avons éliminé les parcours inutiles de l'espace mémoire pour retrouver un objet. La fonction de hachage appliquée à l'objet donne la position dans l'espace mémoire du pointeur qui le désigne. Un seul accès suffit pour aller lire le pointeur, un deuxième pour l'objet lui-même. Quant au chargement d'un nouvel objet, il se fait de la même manière : si l'emplacement indiqué par la fonction de hachage contient un pointeur, c'est que l'objet existe déjà dans la collection ;

<footnote n="9">9 Les pointeurs sont des types de données au même titre que des entiers, des caractères etc. L'espace mémoire qui leur est alloué dépend de la taille d'une adresse-mémoire et donc du type de machine.</footnote>
- mais il faut certainement un espace mémoire énorme pour assurer la correspondance entre tous les numéros qui pourraient être obtenus par la fonction de hachage et la position en mémoire ;

- de plus, la fonction de hachage devrait nous donner un nombre unique pour chaque objet. Il est impossible de trouver une telle fonction, sans que le temps de calcul devienne à son tour prohibitif.

En réalité, la mise en oeuvre du hachage est un compromis :

- l'espace mémoire alloué dépasse assez largement le nombre d'objets que l'on prévoit de stocker dans la collection, sans prendre pour autant des proportions anormales (dans notre exemple, nous avions quinze emplacements pour trois objets effectivement stockés). Du coup, le nombre obtenu par la fonction de hachage a toutes les chances de dépasser la dernière position en mémoire de la collection. La fonction de hachage divise donc ce nombre par la taille de la collection et rend le reste de cette division. C'est ce reste (compris entre 0 et la taille de la collection) qui sera utilisé pour placer l'adresse de l'objet considéré.

- D'autre part, il est admis que les fonctions de hachage puissent donner le même nombre pour deux objets différents. Il faut alors gérer ce qu'on appelle une « collision ». Dans ce cas, le programme parcourt les cases mémoires suivantes jusqu'à trouver une case disponible. Pour vérifier la présence d'un objet précis, la méthode est la même : la fonction de hachage donne une position en mémoire, à partir de laquelle le programme recherche systématiquement si l'objet est présent en explorant les adresses successives. Soit l'objet existe, et dans ce cas il a été stocké à la première case disponible, soit il n'existe pas, et dans ce cas, le système finira par tomber sur une case vide.

Ce compromis entre un accès direct à l'objet et l'économie de mémoire optimise les temps d'accès en mémoire, en calcul, et l'espace total réservé à une collection.

Les méthodes chargées d'explorer les objets désignés par les pointeurs sont *basicAt*, *basicAt:put*, *basicSize*. Elles sont définies dans la classe *Object*, mère de toutes les classes et font appel à des primitives écrites en langage machine. Leur effet est comparable à celui des méthodes *at*, *at:put* et *size* que nous avons vues pour la classe *IndexedCollection*. L'index qui est donné en argument correspond à la position du pointeur dans l'espace alloué à la collection, et non à la position de l'objet dans la collection. Quant à la taille renvoyée par *basicSize*, il s'agit du nombre maximum de pointeurs que peut stocker la collection.

La méthode *findElementIndex:anObject* renvoie la position du pointeur sur *anObject* dans l'espace-mémoire alloué à la collection réceptrice. Si *anObject* n'existe pas dans la collection, cette méthode renvoie la première position inoccupée (nous allons voir dans le paragraphe suivant qu'il y a toujours des positions inoccupées). Cette position vide est en fait occupée par un pointeur sur *nil*<fnref n="10" />. C'est pour cela que la méthode *add*: (figure 4.21) vérifie, avant de charger un nouvel objet, si le pointeur renvoyé par *findElementIndex*: désigne *nil* (dans le cas

<footnote n="10">10 A l'initialisation, tous les pointeurs désignent l'objet nil.</footnote>
contraire, il est inutile d'ajouter l'objet puis qu'il est déjà présent dans la collection).

Pour trouver la place mémoire correspondant à la méthode `anObject`, `findElementIndex: anObject` fait appel à la fonction de hachage de `anObject` (dans Smalltalk, cette méthode s'appelle `hash`, quelle que soit la classe considérée).

La méthode `hash` fait appel, selon les classes, à des primitives en langage machine (classe `String`), à une addition bit à bit d'une constante et de la représentation binaire de l'objet (classe `Symbol`) ou récupère le nombre obtenu par le hachage d'un des éléments qui compose l'instance réceptrice (classe `IndexedCollection`, qui renvoie dans certains cas le nombre obtenu par hachage de son premier élément).

Dans tous les cas, un objet ayant toujours une représentation binaire plus ou moins longue dans la mémoire de la machine, la fonction de hachage récupère les derniers bits de cette représentation (les trente-deux derniers dans le cas de la classe `Symbol` sous Smalltalk V) et les interprète comme un entier.

### 4.9.3 - Croissance d'une instance de Set

A chaque ajout d'objet (méthode `add:`), le système vérifie qu'il reste encore suffisamment de cases libres pour accueillir un nouveau pointeur. C'est le rôle de la méthode `adjustSize` appelée dans `add:`. De manière à ne pas dégrader les performances du hachage, un algorithme vérifie si la proportion de cases vides est toujours acceptable. Si ce n'est pas le cas, un appel à la méthode `grow` génère une nouvelle instance de `Set`, plus grande, dans laquelle vont être recopiés tous les éléments de l'instance initiale, augmentée du nouvel objet à placer.

La figure 4.23 nous permet d'étudier en détail le fonctionnement des méthodes `adjustSize` et `grow`.

adjustSize
"Answer the receiver. If the receiver set is getting full, expand it to accomodate more objects."
 elementCount * 10) >= (self basicSize - 2 * 9)
ifTrue: [^self grow]

grow
"Answer the receiver expanded to accomodate more elements."
| aSet |
aSet := self species new: self basicSize * 4 // 3 + 10.
self do: [ :element | aSet add: element].
^self become: aSet

<caption>Figure 4.23 : méthodes `adjustSize` et `grow` de la classe `Set`</caption>

Pour savoir si l'instance atteint un taux de remplissage excessif, la méthode `adjustSize` compare le nombre d'objets du receveur (`elementCount`) à sa capacité maximale (nombre fourni par la méthode `basicSize`). La relation numérique
proposée ici correspond à la recherche d'une efficacité optimale de Smalltalk V. Nous pouvons vérifier que l'instance de *Set* que nous avons créée avec quatre cases (*new*:4 appelé par *new*) a changé de taille dès le deuxième objet que nous y avons mis. Sa nouvelle taille est donnée par l'algorithme de la méthode *grow* (figure 4.23).

Dans notre cas, la taille est passée à quinze cases. Nous pourrions calculer qu'elle ne changera plus avant le chargement du deuxième objet dans la collection.

Nous remarquons trois choses :

- la méthode *grow*, définie dans la classe *Set* pourrait être utilisée dans toutes ses sous-classes. En effet, cette méthode crée une nouvelle instance de la classe à laquelle appartient le receveur, mais de taille un peu plus grande, puis transfère les objets du receveur dans la nouvelle instance avant de renvoyer cette nouvelle instance. C'est le message *species* qui rend cette méthode utilisable par toutes les sous-classes. Son rôle consiste à renvoyer la classe à laquelle appartient le receveur<fnref n="11" />. C'est ensuite à cette classe (et non à la classe mère *Set*) qu'est adressé le message *new*: pour créer une instance.

- Le transfert d'objet est exécuté élément par élément (méthode *do*:) avec *add*: Or nous avons vu que *add*: teste systématiquement la taille du récepteur avant de lui rajouter l'élément. Récapitulons : pour ajouter un élément dans *unEnsemble*, le système vérifie si *unEnsemble* n'est pas trop rempli. S'il l'est, il va ajouter le contenu d'origine dans une nouvelle instance, plus grande, mais va vérifier pour chaque objet si cette instance est suffisamment vaste. Cette vérification est inutile, puisque la taille de la nouvelle instance est plus grande que celle de *unEnsemble*. Une amélioration possible serait de faire appel à une autre méthode que *add*:, qui éviterait cette vérification de taille.

- *grow* fait appel à la méthode *become*: pour renvoyer la nouvelle collection. En effet, une instruction comme `^aSet` est incorrecte, puisqu'elle ne fait que renvoyer une nouvelle collection sans modifier celle de départ (un peu comme la méthode *asSet* qui renvoie une instance de *Set* à partir du récepteur mais ne transforme en aucun cas le récepteur en instance de *Set*). Le récepteur doit changer de taille, il va certainement changer de place en mémoire. Mais pour le moment, c'est la variable locale *aSet* qui a la bonne taille, contient les bons éléments et se situe à un nouvel emplacement mémoire. La méthode *become*:, définie dans *Object*, inverse correctement le récepteur et l'argument dans la mémoire, y compris toutes les références que des pointeurs extérieurs pourraient faire à l'un ou l'autre.

<footnote n="11">11 Le lecteur pourrait s'étonner de voir *species* faire le même travail que la méthode *class*. En fait, class renvoie systématiquement la classe propriétaire de l'instance, alors que *species* est parfois redéfinie, comme dans * fileStream (Cf chapitre 5) pour renvoyer une autre classe. Cette redéfinition s'impose lorsque la méthode *new* (qui, ici, est appliquée juste après *species*) est insuffisante pour définir une instance (variables d'instances non définies etc.).</footnote>
## 4.10 - La classe *Dictionary* et ses sous-classes

### 4.10.1 - Ajouter des éléments à un dictionnaire

Comme pour toute autre sous-classe issue de *Collection*, la méthode *add*: doit être redéfinie pour la classe *Dictionary*.

L'examen de la méthode *add*: redéfinie dans la classe *Dictionary* montre que chaque objet est placé en fonction de sa clé (*findKeyIndex:anAssociation key*) et non de l'association clé/valeur (figure 4.24). Ce n'est pas gênant dans la mesure où une instance de *Dictionary* rassemble des associations dont la clé est unique.

```perl
add: anAssociation
"Answer anAssociation. Add anAssociation to the receiver dictionary (hash is by the
key of anAssociation)."
| index element |
index := self findKeyIndex: anAssociation key.
 element := self basicAt: index) == nil
    ifTrue: [elementCount := elementCount + 1.
             self basicAt: index put: anAssociation]
    ifFalse: [element value: anAssociation value].
self adjustSize.
^anAssociation
```

<caption>Figure 4.24 : méthode *add*: de *Dictionary*</caption>

Si la clé n'existe pas déjà, la variable héritée de la classe *Set* (*elementCount*) est incrémentée et l'association est chargée dans le dictionnaire. Dans le cas contraire, la nouvelle association donnée en argument de la méthode *add*: remplace l'ancienne dans le dictionnaire récepteur.

Contrairement à la classe *Set*, la vérification de la taille du dictionnaire n'est faite qu'à la fin de la méthode *add*. De plus, la méthode *grow* est redéfinie (figure 4.25).

```perl
grow
"Answer the receiver doubled in size to accomodate more key/value pairs."
| aDictionary |
aDictionary := self class new: self basicSize * 2.
self associationsDo:[:anAssociation |
    aDictionary add: anAssociation].
^self become: aDictionary
```

<caption>Figure 4.25 : méthode *grow* de *Dictionary*</caption>

La taille du dictionnaire est multipliée par deux, quand elle n'était multipliée que par 4/3 puis augmentée de 10 dans la classe *Set*. Là aussi, il s'agit d'une implémentation destinée à optimiser l'exécution des programmes.
### 4.10.2 - Limiter les redéfinitions de méthodes dans une sous-classe :
 l'exemple de select et reject

La conception particulière de la classe *Dictionary* impose de redéfinir un certain nombre de méthodes. Nous allons voir comment limiter le nombre de méthodes à redéfinir.

La classe *Collection* propose deux méthodes pour isoler des objets répondant à une condition (*select*:) ou à son contraire (*reject*:).

La méthode *select*: de *Dictionary* travaille sur la valeur de l'association (figure 4.26) et non sur l'association entière comme le ferait la méthode *select*: telle qu'elle est définie dans *Collection*. Elle est donc redéfinie au niveau de *Dictionary*.

select: aBlock

"For each key/value pair in the receiver, evaluate aBlock with the value part of the pair as the argument. Answer a new object containing those key/value pairs for which aBlock evaluates to true."

| answer |

answer := self species new.

self.associationsDo: [ :each | (aBlock value: each value)

ifTrue: [answer add: each]].

^answer

<caption>Figure 4.26 : méthode *select* dans *Dictionary*</caption>

La méthode *reject*: travaillant elle aussi sur la valeur de l'association aurait due être redéfinie dans *Dictionary*. Ce n'est pas le cas. La méthode *reject*: n'est définie qu'une seule fois, dans la classe *Collection* (figure 4.27).

reject: aBlock

"For each element in the receiver, evaluate aBlock with that element as the argument. Answer a new collection containing those elements of the receiver for which aBlock evaluates to false."

^self select: [ :element | (aBlock value: element) not]

<caption>Figure 4.27 : méthode *reject* de la classe *Collection*</caption>

Nous voyons qu'elle fait appel à *select*: De cette façon, il est inutile de redéfinir *reject*: dans chaque sous-classe, puisque l'expression *self.select:[...]* est toujours cohérente avec la classe du récepteur.

De nombreuses méthodes font ainsi appel à une méthode principale (*add*: en est un exemple pour la classe *Collection*). Plutôt que de redéfinir chaque méthode dans les sous-classes, il suffit de redéfinir la méthode principale.
### 4.10.3 - La classe IdentityDictionary : un dictionnaire un peu particulier

IdentityDictionary est une sous-classe de la précédente qui mérite d'être signée. Toutes les opérations de recherche de clés sont basées sur l'égalité (==) dans le cas de *Dictionary*, de l'identité (==) dans *IdentityDictionary*. Pour mieux saisir cette différence, évaluons la séquence :

```
|Dico cle1 cle2|
Dico := Dictionary new.
cle1 := 'index'.
cle2 := 'index'.
Dico at:cle1 put:'index cle1'.
Dico at:cle2
```

puis cette nouvelle séquence :

```
|Dico cle1 cle2|
Dico := IdentityDictionary new.
cle1 := 'index'.
cle2 := 'index'.
Dico at:cle1 put:'index cle1'.
Dico at:cle2.
```

Dans le premier cas, l'évaluation renvoie 'index cle1', dans le deuxième elle signale une erreur 'key is missing'. En effet, *cle1* et *cle2* ne représentent pas le même objet, même si leur contenu est identique.

L'évaluation de *cle1=cle2* renverra *true* alors que celle de *cle1==cle2* renverra *false*. La méthode `==` se contente de comparer le contenu de deux objets, alors que la méthode `==` vérifie que le récepteur et l'argument sont vraiment le même objet.

Les instances de la classe *IdentityDictionary* sont utiles pour manipuler des dictionnaires de symboles, lorsqu'on veut s'assurer que les clés sont uniques dans le système.

La conception des deux classes est légèrement différente. Seule la recherche de l'index d'une clé (méthode `findIndexKey: aKey`) nécessiterait une modification (`==` au lieu de `=`). En fait, *IdentityDictionary* dispose d'une variable d'instance supplémentaire, *valueArray*, qui stocke les valeurs correspondant aux clés du dictionnaire.

Le principe est le suivant : dans un espace alloué en mémoire selon les formules des méthodes *grow*, *adjustSize* et *new*, sont stockés les pointeurs sur les clés du dictionnaire. Chaque clé est unique en mémoire. Smalltalk y accède grâce au pointeur qui la désigne. Ce pointeur est stocké dans l'espace alloué au dictionnaire à une position qui dépend de la valeur de hachage de la clé. Le tableau *valueArray* a exactement la même taille que l'espace alloué au dictionnaire. Il peut donc contenir autant de pointeurs que l'espace mémoire du dictionnaire. Mais les pointeurs de *valueArray* désignent les valeurs (et non les clés) des associations du dictionnaire. Pour chaque association du dictionnaire, le pointeur repérant sa clé est mis dans l'espace alloué au dictionnaire, et le pointeur désignant sa valeur est mis dans le tableau *valueArray*. Les deux pointeurs sont placés à la même position dans leur espace mémoire respectif. C'est ce que nous
avons schématiquement représenté sur la figure 4.28, avec *Dico*, instance de *IdentityDictionary*, contenant les associations (*cleA* ↔ *valA*), (*cleB* ↔ *valB*), (*cleC* ↔ *valC*) et (*cleD* ↔ *valD*).

<figure><img src="image_29.png" /><figcaption>Figure 4.28 : représentation schématique d'une instance de *IdentityDictionary* en mémoire</figcaption></figure>

Pour accéder à une valeur à partir de sa clé, Smalltalk recherche la position du
pointeur sur cette clé dans l'espace alloué au dictionnaire (par les méthodes
`findKeyIndex` etc.), et regarde à la position correspondante dans le tableau
`valueArray`: le pointeur sur la valeur recherchée y est stocké.

Dans la classe *Dictionary*, l'espace alloué au dictionnaire contient les pointeurs sur les associations (et non sur leurs clés, comme dans *IdentityDictionary*).

De nombreuses méthodes d'accès et de manipulation des paires clé/valeur doivent être redéfinies dans *IdentityDictionary*. Nous remarquerons par exemple que la méthode `at:put:` qui faisait appel à *add*: dans la classe *Dictionary* est entièrement reconstruite dans *IdentityDictionary*.

La figure 4.29 illustre la différence d'implémentation pour la méthode keys, qui renvoie une instance de *Set* rassemblant les clés.

Dans le premier cas, il explore directement la suite d'associations, conservant uniquement la clé. Dans le second cas, Smalltalk parcourt l'espace alloué en mémoire au dictionnaire (contenant les pointeurs sur les clés), et sélectionne les objets pointés différents de *nil*.
keys "Méthode de Dictionary"
"Answer a Set containing all the keys of the key/value pair in the receiver dictionary."
| answer |
answer := Set new: self size * 2.
self associationsDo: [:assoc | answer add: assoc key].
^answer

keys "Méthode de IdentityDictionary"
"Answer a Set containing all the keys of the key/value pairs of the receiver."
| answer |
answer := Set new: self size * 2.
1 to: self basicSize do:[:index |
(self basicAt: index) == nil
ifFalse: [answer add: (self basicAt: index)]].
^answer

Figure 4.29 : comparaison des méthodes keys des classes *Dictionary* et *IdentityDictionary*

## 4.11 - La classe IndexedCollection

*IndexedCollection* est une classe abstraite. On ne peut donc définir aucune instance directement à son niveau. Les objets pourront être définis dans les sous-classes *FixedSizeCollection*, *orderedCollection* ou *SortedCollection*

### 4.11.1 - Méthodes héritées de la classe IndexedCollection

Si l'on désire utiliser un index pour accéder à une énumération d'objets, il est indispensable de disposer de quelques opérations de base :

*checkIndex: index* vérifie que *index* est cohérent avec l'énumération (*index* doit être numérique, et compris entre les bornes de l'énumération)

*errorInBounds* signale une erreur si l'index sort des limites.

Ces deux méthodes sont « privées » (elles n'ont donc pas l'occasion d'être directement appelées par l'utilisateur), mais nous pouvons aisément tester leur efficacité en évaluant pour une chaîne de caractères (*String* est une sous-classe de *IndexedCollection*) l'expression '9 lettres' at: 14. Le message 'index 14 is outside collection bounds' s'affiche en titre d'une fenêtre d'erreur.

Et comme pour d'autres sous-classes de *Collection*, les méthodes de croissance de l'énumération sont redéfinies (*grow* etc.) de manière à optimiser le fonctionnement du système.
### 4.11.2 - La classe FixedSizeCollection

Comme son nom l'indique, toutes les instances de ses sous-classes ont une taille prédéfinie dès leur création. L'examen des méthodes `with:` `with:` et `add:` est particulièrement démonstratif (figure 4.30).

```
with: firstObject with: secondObject
  "Answer a new object of two elements, firstObject and secondObject."
| answer |
answer := self new: 2.
answer at: 1 put: firstObject.
answer at: 2 put: secondObject.
^answer
```

<caption>Figure 4.30 : méthode `with:`</caption>

Contrairement à la méthode du même nom définie dans la classe mère *Collection* qui crée une instance contenant plus de places que d'éléments à mettre (4 pour `with:`), cette méthode crée exactement le nombre de places demandées dans l'instance (deux). On comprend aisément le commentaire renvoyé par le message `add:object: "Inappropriate message for this object"` étant donné que la taille de l'instance ne peut augmenter.

### 4.11.3 - La classe OrderedCollection

Cette classe permet de créer des listes ordonnées. Comme dans toutes les autres classes issues de *Collection*, à chaque instance de *OrderedCollection* correspond un espace mémoire contenant les pointeurs sur les objets qu'elle contient. Mais contrairement aux autres classes, il n'est plus nécessaire d'optimiser la recherche des objets en mémoire. Au contraire, les instances de *OrderedCollection* correspondent à une liste ordonnée d'objets. Les opérations les plus courantes sont le parcours de la liste dans les deux sens, objet par objet.

Les pointeurs sont donc placés dans l'espace alloué à la liste dans leur ordre de parcours (pointeur sur le premier objet à la première position, sur le deuxième objet à la deuxième position, etc.).

Pour améliorer les performances, il est utile de connaître la position du dernier pointeur stocké dans l'espace alloué à la liste. Ajouter un objet en fin de liste sera ainsi plus rapide. C'est le rôle de la variable `endPosition`.

Une autre variable, `startPosition` est utilisée pour mémoriser l'emplacement du premier pointeur.

La classe *OrderedCollection* permettant d'ajouter un objet en milieu ou début de liste, il est parfois nécessaire de décaler toutes les adresses concernées par cette insertion. Toutes les méthodes d'insertion d'objet font donc appel à des méthodes particulières, au nom explicite (*putSpaceAtEnd*, *putSpaceAtStart* etc.). Ces méthodes ont des algorithmes assez longs, dont le but est de décaler les adresses dans l'espace alloué à la liste afin d'ajouter l'adresse du nouvel objet à insérer.
### 4.11.4 - La classe SortedCollection

La classe *SortedCollection* redéfinit pratiquement toutes les méthodes qu'elle partage avec ses classes-mères, en y insérant l'évaluation du bloc de classement, afin de stocker à chaque instant les adresses des objets dans l'ordre imposé par ce bloc de tri.

La classe *Stream* est le complément naturel des instances de *Collection*. Nous avons d'ailleurs souvent éludé l'explication de certaines méthodes (comme *printOn*: ou *storeOn:*) qui faisaient explicitement référence à des instances de *Stream*. Nous étudierons cette classe dans le prochain chapitre.
# 5 - La classe Stream et sa descendance

## 5.1 - Premiers pas dans la classe Stream

La figure 5.1 illustre la hiérarchie des sous-classes de *Stream*. La classe *Stream* est elle-même issue de *Object*.

<figure><img src="image_30.png" /><figcaption>Figure 5.1 : classe *Stream* et ses descendants</figcaption></figure>

Les instances de la classe *Stream* sont utilisées pour manipuler des flux de données (d'objets) issus de collections ou de fichiers<fnref n="1" />. Elles font appel à de nombreuses méthodes de la classe *Collection* et une de leurs variables d'instance est précisément l'instance de la classe *Collection* sur laquelle elles travaillent. La lecture du chapitre précédent est donc nécessaire pour une bonne compréhension de ce qui va suivre.

Nous étudierons, à partir d'exemples très simples, les principales méthodes utilisables dans les classes *ReadStream*, *WriteStream* et *ReadWriteStream* avant de travailler sur un fichier en utilisant une instance de la classe *FileStream*.

<footnote n="1">1 Les fichiers peuvent être assimilés à une collection de caractères.</footnote>
### 5.1.1 - Qu'est-ce qu'un flux ?

Une collection d'éléments est sans conteste un objet. Elle a une réalité tangible, dans la mémoire de l'ordinateur (sous forme d'octets) et dans l'esprit de l'utilisateur (sous forme d'une représentation mentale des objets qu'elle contient). Un flux est une notion plus difficile à cerner. En fait, un flux est associé à une action, et en ce sens reste un peu abstrait.

Lorsque nous lisons un livre, notre oeil enregistre les caractères (ou les mots) les uns à la suite des autres. Quand un technicien recopie une série de mesures, il le fait nombre après nombre. Si après cinq cents écritures, il s'aperçoit qu'une mesure a été oubliée, il remonte sa colonne de chiffres jusqu'à trouver l'erreur, la corrige et continue son travail.

Le flux est en quelque sorte la réalisation matérielle de ces actions de lecture et d'écriture. Il n'a pas réellement d'existence physique comme on l'entendrait pour un tableau ou une page de fichier, mais grâce à lui, le parcours d'une collection est possible en lecture, en écriture, par accès séquentiel ou direct.

On ouvre donc un flux sur une énumération d'objets. Cette énumération doit impérativement être ordonnée (un flux parcourt une collection élément par élément : il faut donc que ces éléments aient une position permanente et définie dans la collection). Lorsque le programmeur ouvre un flux sur une collection, Smalltalk crée un objet de la classe *Stream* auquel il associe immédiatement une variable d'instance (*collection*). Cette variable d'instance désigne la collection parcourue par le flux. Comme il s'agit de parcourir la collection, une autre variable d'instance (*position*) mémorise la position courante (celle du dernier objet lu ou écrit). D'autres variables d'instance permettent de connaître la taille maximum de la collection à parcourir (*readLimit*, *writeLimit*).

### 5.1.2 - Pourquoi une classe réservée aux flux ?

Toutes les opérations offertes par la classe *Stream* et dont nous venons de donner un aperçu restreint sont réalisables dans la classe *Collection* : le message *at*: permet d'accéder directement à un objet de la collection, pourvu qu'on en connaisse sa position ; son alter ego *at:put*: permet d'écrire dans la collection.

Effectivement, toutes les opérations de la classe *Stream* travaillent sur la variable d'instance *collection*. De ce fait, elles utilisent des méthodes définies dans la classe *Collection*. Mais il est infiniment plus simple (et plus conforme à la philosophie orientée « objets ») de créer une classe dans laquelle nous pouvons construire des méthodes communes à des flux s'ouvrant sur des objets aussi divers que des tableaux, des chaînes de caractères, des fichiers etc.

Deux exemples illustreront mieux la nécessité de la classe *Stream*.

## Travailler sur un fichier

L'informatique dispose de matériels de plus en plus puissants. La taille des disques durs s'accroît sans cesse. Des fichiers dépassant largement la taille de la mémoire vive de l'ordinateur peuvent être conservés sur ces disques durs.

Supposons que nous voulions parcourir un fichier comportant plus d'un million de mesures stockées les unes à la suite des autres. Si nous assimilons
notre fichier à une instance de la classe *Collection*, il nous faudra manipuler un objet monstrueux (en supposant que nous puissions le charger en mémoire), et qui a toutes chances de saturer les capacités de l'ordinateur.

La classe *FileStream*, descendante de *Stream* permet d'accéder à des fichiers de n'importe quelle taille. Pour cela, elle utilise une petite « fenêtre » qu'elle va déplacer le long du fichier, permettant ainsi à l'utilisateur de le parcourir en toute sécurité, page après page (tampon après tampon). Des méthodes permettent la mise à jour temporaire, la lecture caractère par caractère, ligne par ligne ou mot à mot.

## Imprimer ou afficher des nombres

Lorsqu'un comptable signe un reçu, il écrit (ou tape à la machine) « reçu de Monsieur X. la somme de 23440 francs ». Dans cette phrase, tout est caractère, y compris le montant reçu. Pourtant, le caissier n'a pas dicté à son collègue « ... la somme de chiffre deux chiffre trois chiffre quatre... », mais « ... la somme de vingt trois mille quatre cent... ».

De même, la représentation d'un nombre (instance de la classe *SmallInteger* ou autre) dans l'ordinateur est totalement différente de cette écriture en caractères.

Si nous voulons nous-mêmes extraire un nombre d'une base de données pour l'enregistrer dans un fichier texte, ou simplement pour l'afficher à l'écran dans une fenêtre d'édition de texte, nous devrons réaliser une gymnastique numérique importante : repérer le signe, la position de la virgule, diviser successivement par dix, garder les restes, transformer chaque entier en caractère le représentant, avant d'imprimer tous ces caractères dans l'ordre correct.

Un nombre s'écrit comme une suite de chiffres et de signes dans un ordre précis. Partant de ce principe, les concepteurs de Smalltalk ont implémenté dans la classe *Integer* une méthode permettant de transférer dans un flux une représentation imprimable ou affichable des nombres. Cette méthode s'appelle *printOn: aStream*.

Plus largement, tous les objets de Smalltalk disposent de ce message. Dans la classe *Association*, la copie d'une instance dans un flux débute par l'envoi de la représentation en caractères de sa clé, puis des signes `==>` suivis de la représentation en caractères de sa valeur.

### 5.1.3 - Opérations de transfert

Les deux exemples que nous venons de donner font implicitement référence aux transferts : la classe *Stream* est totalement adaptée à ces actions. Les opérations suivantes sont typiquement des manipulations de flux :

- lire le contenu d'un fichier tout en le recopiant octet par octet dans un autre;

- récupérer la représentation imprimable d'un objet pour le recopier dans un fichier ou une fenêtre d'éditeur de texte;

- lire les objets d'une instance de *SortedCollection* et les recopier dans une instance de *Array*;

- lire les caractères d'une instance de *String* et les transférer dans une instance de *MotFrancais* (Cf 3.3).
En ce qui concerne ce dernier exemple, il aurait été beaucoup plus simple de construire la méthode *aPartirDe*: en faisant appel à *Stream* plutôt qu'en recopiant caractère par caractère le contenu de la chaîne paramètre.

### 5.1.4 - Ouvrir un flux

Avant de créer une instance de la classe *Stream*, il est nécessaire de savoir si nous voulons lire les objets de notre collection (sous-classe *ReadStream*), y écrire de nouveaux objets (sous-classe *WriteStream*) ou bien lire et écrire à la fois (sous-classe *ReadWriteStream*). Les méthodes de parcours ont le même nom dans toutes les sous-classes, mais elles sont parfois redéfinies pour interdire ou permettre l'écriture.

*Stream* est donc une classe abstraite, et n'autorise pas la création d'instances à son niveau. Elle rassemble pourtant un certain nombre de méthodes communes à toutes ses sous-classes. Deux de ces méthodes sont des méthodes de classe permettant de créer des instances. Elles sont bien entendues inutilisables sous *Stream* (car elles font appel à des méthodes d'instance qui ne sont définies que dans ses sous-classes).

Dans certains cas, il n'est pas utile de parcourir la totalité d'une énumération. La classe *Stream* permet de restreindre la collection parcourue au seul segment intéressant. Pour éviter au programmeur de gérer lui-même les bornes de ce segment (et de courir ainsi le risque d'écrire ou de lire par mégarde hors des limites), Smalltalk copie ce segment dans une nouvelle collection. C'est cette nouvelle collection qui sera associée au flux. Cette manière d'ouvrir un flux offre l'avantage de protéger totalement la collection originale puisque les opérations de lecture et d'écriture se font sur une *copie* limitée au segment. Par contre, les opérations de mise à jour de l'original ne peuvent avoir lieu de cette façon.

A ce stade, nous pourrions comparer les instances de *Stream* à des fenêtres placées sur des collections :

- une fenêtre peut avoir la taille de la collection toute entière ;

- une fenêtre peut être « vitrée » et n'autoriser que l'observation (lecture) ;

- une fenêtre peut être « ouverte » et autoriser le remplacement ou la pose de nouveaux objets (écriture) ;

- plusieurs fenêtres peuvent donner en même temps sur la même collection, côte à côte, ou se chevauchant totalement ou partiellement ;

- une fenêtre en écriture peut modifier une collection observée au même instant par une fenêtre en lecture<fnref n="2" />.

<footnote n="2">2 Il faut que les fenêtres soient ouvertes sur toute la collection, puisque nous avons vu qu'une fenêtre limitée à un segment de collection travaille sur une copie qui lui est personnelle et non sur l'original.</footnote>
## 5.2 - Utiliser une instance de ReadStream

### 5.2.1 - Comment créer une instance de ReadStream ?

Nous pouvons créer une instance de `ReadStream` en utilisant une des deux méthodes de classe héritées de `Stream` :

*on: anIndexedCollection*

ouvre un flux sur la collection toute entière

*on: anIndexedCollection from: firstIndex to: lastIndex*

ouvre un flux sur une copie de la collection comprise entre les positions *firstIndex* et *lastIndex*.

Nous avons vu précédemment que les éléments de la collection sur laquelle s'ouvre un flux doivent avoir une position bien identifiée. Nous remarquons dans le libellé des deux méthodes *on:* et *on:from:to:* qu'elles sont limitées aux classes dépendant de *IndexedCollection*, seules classes pour lesquelles chaque objet a une position connue. L'évaluation de la séquence :

```
|unFlux|
unFlux := ReadStream on: (Bag with: 'patate'
                         with: 'carotte'
                         with: 'tomate')
```

signale une erreur précisant que les instances de *Bag* ne sont pas des collections indicées.

Prenons donc le tableau *galerie* comme collection indicée sur laquelle nous appliquerons *visite*, instance de *ReadStream*. La variable *galerie* rassemblera toutes les salles d'une exposition consacrée à la peinture, chaque salle étant réservée à un peintre. L'ordre des salles correspond à l'ordre logique de parcours du bâtiment. Les peintres en vogue pourront avoir plusieurs salles<fnref n="3" />, qui ne seront pas toutes consécutives (figure 5.2).

```
|galerie visite|
galerie := # (Picasso Vinci Dali Picasso David David
              Vinci Monnet).
visite := ReadStream on:galerie.
```

<caption>Figure 5.2 : initialisation d'un flux sur un tableau</caption>

Nous sommes prêts pour la visite. Si le temps nous est compté, nous pourrions la réduire en remplaçant par exemple *visite* par la variable *visiteCourte* définie comme suit :

```
visiteCourte := ReadStream on: galerie from: 2 to: 4.
```

La méthode *on:* ouvre un flux sur la collection *galerie* en entier, alors que *on:from:to:* restreint la collection parcourue par *visiteCourte* aux salles 2, 3 et 4 (Vinci, Dali et Picasso).

<footnote n="3">3 Nous avons choisi de représenter les salles par un symbole ayant le nom du peintre.</footnote>
Pour vérifier le contenu de la collection que nous parcourons avec visiteCourte
nous utiliserons la méthode contents :

visiteCourte contents

qui affichera, avec l'option *show it* :

(Vinci Dali Picasso)

Pour connaître la taille de la collection parcourue, nous pouvons utiliser la méthode *size*, définie dans la classe *Stream*. Remarquons que cette méthode existe aussi dans la classe *Collection* : visite *size* et visite *contents size* donnent donc strictement le même résultat (8 dans notre exemple).

Pour connaître à tout instant notre position, c'est à dire l'indice du dernier objet que nous venons de lire, nous pourrons évaluer : visite *position* qui nous donnera 0 au départ de la visite (l'objet que nous nous préparons à lire étant en position 1, la position de départ est 0 par convention).

### 5.2.2 - Parcourir une collection avec un flux

Il reste donc à parcourir la collection. Le ministre de la culture, qui la connaît par coeur, la traverse au pas de charge (figure 5.3).

```
|galerie visiteMinistre|
    galerie := # (Picasso Vinci Dali Picasso David David
                     Vinci Monnet).
    visiteMinistre := ReadStream on: galerie.
    [visiteMinistre atEnd] whileFalse: [
        visiteMinistre next printOn: Transcript.
        Transcript cr]
```

<caption>Figure 5.3 : visite du ministre de la culture</caption>

La séquence de la figure 5.3, évaluée avec l'option *do it*, provoque l'affichage des lignes suivantes dans la fenêtre *Transcript* :

Picasso
Vinci
Dali
Picasso
David
David
Vinci
Monnet

Nous avons utilisé deux nouvelles méthodes :

*atEnd* demande au flux récepteur de renvoyer *true* si sa position courante est en fin de collection, *false* sinon.

*next* le flux récepteur augmente sa variable *position* d'une unité et renvoie l'objet situé à cette position dans la collection qu'il parcourt.
Il faut noter que la méthode *next* déplace la position courante avant de
lire l'objet. La séquence d'expression *galerie position:2. galerie next* renvoie donc le troisième objet et non le deuxième<fnref n="4" />.

Sans le savoir, nous avons aussi utilisé deux autres méthodes reliées à la classe *Stream* : ce sont *printOn: aStream* et *cr*. Nous aborderons ces deux méthodes au paragraphe 5.4.2.

Une promotion d'étudiants des Beaux-Arts entre dans la galerie. Le premier se sent mal et se précipite vers la sortie (figure 5.4).

```java
|galerie visiteEcourtee|
    galerie := #*(Picasso Vinci Dali Picasso David David
                      Vinci Monnet).
    visiteEcourtee := ReadStream on:galerie.
    visiteEcourtee setToEnd.
    visiteEcourtee atEnd.
```

<caption>Figure 5.4 : placer un flux en fin de collection</caption>

La méthode `setToEnd` déplace la position jusqu'à la fin de la collection, ce que nous pouvons vérifier en envoyant le message `atEnd` à `visiteEcourtee` (figure 5.4), qui renverra ici la valeur *true*.

### 5.2.3 - Découper une collection en sous-collections avec un flux

Les trois étudiants suivants décident de se partager le travail : l'un visitera les deux premières salles, un autre prendra la suite jusqu'au peintre David non compris et le dernier partira de David pour aller jusqu'à la fin (figure 5.5).

```java
|galerie visiteProposee visite1 visite2 visite3|
    galerie := #*(Picasso Vinci Dali Picasso David David
                      Vinci Monnet).
    visiteProposee := ReadStream on:galerie.
    visite1 := ReadStream on:(visiteProposee next:2).
    visite2 := ReadStream on:(visiteProposee upTo:#David).
    visite3 := ReadStream on:(
                      visiteProposee copyFrom:visiteProposee position
                      to:visiteProposee readLimit).
    visite1 contents printOn:Transcript. Transcript cr.
    visite2 contents printOn:Transcript. Transcript cr.
    visite3 contents printOn:Transcript. Transcript cr.
```

<caption>Figure 5.5 : ouvrir des flux sur les sous-collections obtenues à partir d'un autre flux</caption>

<footnote n="4">4 D'où le nom de la méthode, qui renvoie le prochain objet du flux.</footnote>
Les trois dernières lignes de la figure 5.5 permettent d'afficher dans la fenêtre Transcript le contenu des flux ouverts sur chacune des collections de salles visitées par les étudiants, soit :

(Picasso Vinci)
(Dali Picasso)
(David David Vinci Monnet)

La méthode next: anInteger appliquée à une instance de ReadStream renvoie une collection composée des anInteger objets extraits de la collection parcourue par le receveur à partir de la position courante (les deux premiers, dans notre exemple). La position courante du receveur est, bien entendu, augmentée de la valeur de anInteger.

La méthode upTo: anObject renvoie une collection extraite de celle que parcourt le receveur depuis la position courante de ce dernier jusqu'à anObject si celui-ci existe ou jusqu'à la fin dans le cas contraire.

La méthode copyFrom: firstIndex to: lastIndex renvoie une collection extraite de celle que parcourt le receveur entre les positions firstIndex et lastIndex comprises. Dans notre exemple, nous avons obtenu la position courante avec la méthode position et la dernière position avec readLimit<fnref n="5" />.

Les flux visite1, visite2 et visite3 sont totalement indépendants, à la fois de visiteProposee et de la collection désignée par la variable galerie : le déplacement de la position courante dans visiteProposee ne les affectera pas, de même que la modification d'une salle dans galerie, puisque ces trois flux travaillent sur des copies partielles de cette collection (et non sur l'original).

### 5.2.4 - Tester la présence d'un objet sans déplacer la position courante

Deux étudiants se sont trompés : ils sont rentrés par la sortie et cherchent en vain une plaquette de l'exposition. Croisant ceux de leurs camarades qui viennent de terminer la visite, ils leur empruntent leur guide et repartent au début de l'exposition. Celui qui lit la plaquette annonce à l'avance quelle est la première salle à visiter :

visiteProposee reset "remet la position courante de visiteProposee à zéro"
visiteProposee peek "renvoie le prochain objet à lire, sans modifier la position courante"

Evaluées par *show it* à la suite de la séquence de la figure 5.5, ces lignes renverront la salle Picasso.

Nos deux étudiants entament la visite, et celui qui n'a pas le catalogue essaie de deviner à l'avance quelle sera la salle suivante (figure 5.6).

Comme on le voit dans la figure 5.6, la seule différence entre les méthodes nextMatchFor: anObject et peekFor: anObject est que la première avance quoi

<footnote n="5">5 Dernière position en lecture. La dernière position en écriture sera donnée dans la classe WriteStream par la méthode writeLimit. Ces deux méthodes (readLimit et writeLimit) sont des méthodes « privées » mais leur intérêt dépasse celui d'une stricte gestion interne des instances.</footnote>
qu'il arrive alors que la seconde n'avance que si l'objet suivant est bien *anObject*. Les deux répondent *true* si l'objet à lire est *anObject*, *false* sinon.

```java
|galerie visiteProposee|
    galerie := #-(Picasso Vinci Dali Picasso David David
                      Vinci Monnet).
    visiteProposee := ReadStream on:galerie.
    visiteProposee nextMatchFor:#Picasso
                      "avance d'une salle et répond vrai car l'objet lu est Picasso".
    visiteProposee nextMatchFor:#Dali
                      "avance d'une salle et répond faux car l'objet lu n'est pas Dali, mais Vinci".
    visiteProposee nextMatchFor:#Dali
                      "avance d'une salle et répond vrai car l'objet lu est Dali".
    visiteProposee peekFor:#Monnet
                      "n'avance pas et répond faux car l'objet suivant n'est pas Monnet mais Picasso".
    visiteProposee peekFor:#Picasso
                      "avance d'une salle et répond vrai car l'objet suivant est Picasso"
```

<caption>Figure 5.6 : tester la présence d'objets dans une collection parcourue par un flux</caption>

### 5.2.5 - Parcourir une collection de repère en repère

Le dernier étudiant est un fanatique de Vinci. Seule la Joconde l'intéresse. Il va donc se déplacer directement dans les salles Vinci sans s'arrêter aux autres (figure 5.7).

```java
|galerie visiteJoconde nombreDeSalles|
    galerie := #-(Picasso Vinci Dali Picasso David David
                      Vinci Monnet).
    visiteJoconde := ReadStream on:galerie.
    nombreDeSalles := 0
    [visiteJoconde atEnd] whileFalse:[
        (visiteJoconde skipTo:#Vinci)
    ]
    ifTrue: [nombreDeSalles := nombreDeSalles +1]]
```

<caption>Figure 5.7 : parcourir une collection de repère en repère</caption>

La méthode *skipTo: anObject* déplace la position courante du receveur sur *anObject* et répond vrai si *anObject* existe, ou bien déplace la position courante jusqu'à la fin de la collection et répond faux si *anObject* est absent de la collection<fnref n="6" />. La variable *nombreDeSalles* totalise le nombre de salles exposant Vinci (elle est incrémentée selon le résultat de *skipTo:*).

<footnote n="6">6 Plus précisément de la position courante jusqu'à la fin de la collection.</footnote>
### 5.2.6 - Accès direct dans une collection

Un couple croise quelques étudiants dans la salle exposant Monnet, dernière de la galerie, et surprend un commentaire passionné sur la deuxième salle. Le mari comprend qu'il s'agit de la deuxième salle de la galerie et s'y précipite (visiteMari de la figure 5.8).

Sa femme comprend qu'il s'agit de la deuxième salle en revenant sur ses pas à partir de Monnet (visiteEpouse).

La méthode skip: anInteger permet de se déplacer de anInteger objets à partir de la position courante, dans un sens ou dans l'autre.

Dernières méthodes pour lesquelles nous ne proposerons pas d'exemples :

* `isEmpty` le receveur répond true si la collection qu'il parcourt est vide, false sinon ;

* `do:aBlock` permet d'évaluer aBlock pour chacun des objets composant la collection parcourue par le receveur, et ce depuis la position courante jusqu'à la fin.

```
|galerie visiteMari visiteEpouse|
    galerie := # (Picasso Vinci Dali Picasso David David
                  Vinci Monnet).
    visiteMari := ReadStream on:galerie.
    visiteEpouse := ReadStream on:galerie.
    visiteMari skipTo:#Monnet. "déplace la position courante sur Monnet"
    visiteEpouse skipTo:#Monnet.
    "Le mari et son épouse sont maintenant tous deux dans la salle Monnet"
    visiteMari position:2. "déplace la position de lecture jusqu'au deuxième
                          objet de la collection"
    visiteMari peek. "renvoie la salle suivante, soit Dali"

    visiteEpouse skip:-2. "revient en arrière de deux salles"
    visiteEpouse peek. "renvoie la salle suivante, soit Vinci"
```

<caption>Figure 5.8 : accès direct et relatif dans une collection</caption>

## 5.3 - Les sous-classes WriteStream et ReadWriteStream

### 5.3.1 - Différences entre les sous-classes de Stream

Les classes `WriteStream` et `ReadWriteStream` permettent les opérations d'écriture sur les collections parcourues par leurs instances. La seconde reprend également un certain nombre de méthodes que nous avons étudiées dans la classe `ReadStream` et qui permettent la lecture de la collection parcourue.

Les opérations de lecture et d'écriture découlent en fait d'un nombre réduit de primitives. Seules les méthodes `next` (lecture), `upTo:(lecture)`, `nextPut:` (écriture)
et `atEnd` (position) y font référence. Tous les autres messages aboutissent tôt ou tard à l'une de ces quatre méthodes.

Chaque sous-classe redéfinit ainsi un nombre limité de méthodes. L'orientation en lecture de *ReadStream* est due à la définition dans cette classe de la méthode *next*. L'orientation en écriture de *WriteStream* vient de la méthode *nextPut*. Toute tentative d'écriture sur une instance de *ReadStream* sera sanctionnée par un message d'erreur (*message not understood: nextPut*[:]). A l'opposé, tout essai de lecture sur une instance de *WriteStream* provoque le message d'erreur *message not understood: next*. Pour pouvoir lire, la classe *ReadWriteStream* redéfinit la méthode *next*.

A ce stade, l'emploi de la classe *WriteStream* peut paraître assez contraignant : en réalité, cette classe est particulièrement adaptée aux opérations de transferts. Les instances de *ReadStream* permettent de lire une collection objet par objet. Le cas échéant, un traitement peut être fait (éliminer certains objets par exemple) avant de recharger les objets sélectionnés dans une instance de *WriteStream*. Le transfert a lieu, et l'emploi de *WriteStream* pour écrire dans une nouvelle collection limite les erreurs de manipulations puisqu'un message de lecture envoyé par mégarde serait immédiatement détecté et refusé.

### 5.3.2 - Ouvrir un flux en écriture

Comme les instances de *ReadStream*, un flux en écriture s'ouvre sur une collection. La méthode `on:`, héritée de *Stream* peut ne pas convenir dans certains cas, puisqu'elle place le flux en début de collection. La moindre écriture détruirait les objets initiaux un par un. La classe *WriteStream* s'enrichit donc d'une nouvelle méthode de classe, qui permet de placer le flux en fin de collection afin d'en préserver le contenu initial. C'est la méthode *with: aCollection* détaillée à la figure 5.9.

with: aCollection

"Answer an instance of the receiver streaming on aCollection and positioned to the end."

^(self on: aCollection) setToEnd

<caption>Figure 5.9 : méthode *with*: de création d'instance dans *WriteStream*</caption>

Cette méthode fait appel à `on: aCollection` et place ensuite le flux en fin de collection (`setToEnd`). La méthode de classe *with: aCollection from: firstIndex to: lastIndex* restreint la collection parcourue par le flux à une copie comprise entre les positions *firstIndex* et *lastIndex*.

Reprenons notre exemple de galerie artistique, et supposons que le conservateur décide d'ouvrir de nouvelles salles à la suite de la galerie existante. Il parcourt ces salles en dictant à un de ses collaborateurs les noms des peintres à exposer. Nous utiliserons la méthode *with*: pour créer l'instance de *WriteStream* (*expo*) qui représentera cette action (figure 5.10).
Pour ajouter une nouvelle salle, il suffit d'évaluer :

```
expo nextPut:#VanGogh
```

ou bien, pour en ajouter plusieurs :

```
expo nextPutAll(VanGogh VanGogh FraAngelico Raphael)
```

La séquence de la figure 5.10 complète la collection initiale avec cette méthode `nextPutAll`: Evaluée par *show it*, elle renverra la collection (Picasso Vinci Dali Picasso David David Vinci Monnet VanGogh VanGogh FraAngelico Raphael).

Nous avons vu que les instances de la classe *Array* ont une taille fixe. Pourtant, la collection parcourue et complétée par le flux *expo* est une instance de la classe *Array*. La classe *WriteStream* gère les augmentations de taille des collections parcourues par ses instances. C'est un des avantages qu'elle offre. Pour agrandir un tableau dans un programme, il est pratiquement plus facile d'utiliser un flux que de calculer soi-même les modifications de taille nécessaires en fonction du nombre d'objets insérés dans le tableau. Si nous complétons la séquence de la figure 5.10 par l'expression « *galerie* », nous verrons s'afficher, après évaluation avec l'option *show it*, le contenu de cette variable : tous les objets y sont, mais ils sont suivis par huit objets *nil*. La méthode `nextPut`: (comme `nextPutAll:`) vérifie si la variable d'instance *collection* peut absorber un nouvel objet. Si ce n'est pas le cas, elle lui envoie le message *grow* (Cf 4.9.3). A la réception de ce message, *collection* s'accroît, mais par paliers. Ceci explique l'apparition des nombreux objets *nil* à la suite de nos salles d'exposition.

```
|galerie expo|
    galerie := #(Picasso Vinci Dali Picasso David David Vinci Monnet).
    expo := WriteStream with:galerie.
    expo nextPutAll:#{(VanGogh VanGogh FraAngelico Raphael).
    expo contents "renvoie la nouvelle collection"
```

<caption>Figure 5.10 : modification de la collection après un accès direct</caption>

Au cours de cette visite technique, le conservateur décide de modifier la salle *Vinci* pour y placer quelques Toulouse-Lautrec. Il dicte donc dans la foulée cette modification. Pour vérifier l'aspect général de sa galerie, il demande aussitôt à son collaborateur un état actuel de l'exposition (figure 5.11).

L'évaluation de la séquence de la figure 5.11 ne donne pas la totalité des salles de la galerie, mais (Picasso Toulouse-Lautrec). La variable *galerie* n'est pas perdue et contient toujours l'exposition au grand complet, y compris la modification de salle *Toulouse-Lautrec* et une suite d'objets *nil* en fin d'énumération. Nous pouvons nous en assurer en examinant la variable *galerie* à l'issue de la séquence de la figure 5.11. Pour comprendre ce comportement particulier, il sera nécessaire de regarder plus en détail la manière dont est gérée la variable d'instance *collection*. Auparavant, citons deux dernières méthodes utilisables sur les instances de la classe *WriteStream* (et ses sous-classes).
```java
|galerie expo|
    galerie := #*(Picasso Vinci Dali Picasso David David
                      Vinci Monnet).
    expo := WriteStream with:galerie.
    expo nextPutAll:#{VanGogh VanGogh FraAngelico Raphael}.
    expo position:1. "se place juste avant Vinci pour le remplacer"
    expo nextPut:#{Toulouse-Lautrec}.
    expo contents "devrait renvoyer la nouvelle collection"
```

<caption>Figure 5.11 : modification d'un élément au milieu de la collection</caption>

**show: aCollection**
comme `nextPutAll()`, rajoute les éléments de `aCollection` à la suite de ceux parcourus par le récepteur (la méthode `show` diffère de `nextPutAll` lorsqu'elle est appliquée à un éditeur de texte : elle provoque alors l'affichage immédiat de `aCollection` dans la fenêtre, contrairement à `nextPutAll()`).

**next: integer put: object**
écrit `integer` fois `object` dans la collection parcourue par le flux receveur, juste après la position courante définie par la variable d'instance `position`.

### 5.3.3 - La variable d'instance collection et son emploi selon les sous-classes de Stream.

La variable *collection* est définie dans la classe *Stream*. Elle est donc héritée par toutes ses sous-classes. Nous venons de voir qu'il est possible de la modifier en ouvrant sur elle un flux en écriture. Que le flux soit une instance des classes `WriteStream` ou `ReadWriteStream`, il faut retenir quelques points :

- si le flux est créé avec le message `with:` (ou `with:from:to:`), alors l'écriture commence en fin de collection ;

- si le flux est créé avec le message `on:` (ou `on:from:to:`), alors l'écriture commence en début de collection ;

- `on` peut positionner le flux sur n'importe quel objet de la collection par la méthode `position:` ;

- l'écriture d'objets dans la collection se fait par les méthodes `nextPut:`, `nextPutAll`: ou `show`. Ces écritures ont lieu juste après l'endroit défini par la variable d'instance `position`, et peuvent donc remplacer un objet déjà existant (ou plusieurs) à partir de cette position ;

- les méthodes `on:from:to:` et `with:from:to:` parcourent des copies de la collection argument. Celle-ci n'est donc jamais modifiée par les opérations d'écriture qui pourront avoir lieu ensuite.

Smalltalk utilise fréquemment les flux en écriture pour la construction de ses méthodes, dès qu'il s'agit de transférer des objets. La figure 5.12 reprend la méthode `next: anInteger` qui retourne les `anInteger` objets suivants la position courante d'un flux en lecture (méthode définie dans la classe *Stream*).
Le message `next: anInteger` a pour effet d'ouvrir un nouveau flux en écriture sur une collection de taille `anInteger`. Les objets sont ensuite lus dans le flux récepteur (message `self next`) pour être aussitôt transférés dans le nouveau flux en écriture (`aStream nextPut:`). Le résultat est obtenu sous la forme d'une collection renvoyée par la méthode `contents`.

`next: anInteger`

"Answer the next anInteger number of items from the receiver, returned in a collection of the same species as the collection being streamed over."

| aStream |
|---|
| aStream := WriteStream on: |
| (collection species new: anInteger). |
| anInteger timesRepeat: [ aStream nextPut: self next]. |
| ^aStream contents |

<caption>Figure 5.12 : méthode `next:anInteger` dans `Stream`</caption>

A part la possibilité de parcourir en lecture sa variable d'instance *collection*, un flux de *ReadWriteStream* présente des différences de comportement importantes avec une instance de *WriteStream* :

- un flux en lecture/écriture est supposé pouvoir parcourir en tous sens sa variable d'instance *collection*, pour remplacer ou lire un élément. Quelle que soit la position du flux à un instant donné, c'est la collection toute entière qui intéresse l'utilisateur. La méthode *contents* renvoie donc le contenu de la collection parcourue par le flux, du premier élément au dernier (non compris les derniers espaces vides occupés par *nil* si la collection a dû s'accroître pendant une opération d'écriture).

- un flux en écriture est normalement utilisé séquentiellement : n'ayant pas la possibilité de lire les objets, il n'est pas très facile à utiliser pour des remplacements ponctuels d'objets. On s'intéresse essentiellement aux objets qui viennent d'être écrits jusqu'à présent : la méthode *contents* renvoie donc le contenu de la collection parcourue depuis le premier objet jusqu'à la position courante du flux (enregistrée dans la variable d'instance *position*). Si le programmeur a forcé le déplacement du flux vers une position *i* (par le message *position:i*), alors le message *contents* renverra un extrait de la collection parcourue allant des objets de rang *l* à *i*. La figure 5.13 illustre cette différence de comportement des classes *ReadWriteStream* et *WriteStream*.

Il est possible, dans la classe *ReadWriteStream* de limiter la collection renvoyée par la méthode *contents* aux objets compris entre le début de la collection et la position courante. Le message *truncate* signale au flux récepteur que sa position courante devient aussi sa limite de lecture. C'est une manière détournée de simuler le comportement de la classe *WriteStream* chez une instance de *ReadWriteStream* (figure 5.13).
```java
 expoFixe galerie expoModifie |
 galerie := # (Vinci Raphael MichelAnge FraAngelico)
 expoFixe := WriteStream with:galerie.
 "expoFixe est déclarée en écriture seule"
 expoModifie := ReadWriteStream with:galerie.
 "expoModifie est déclarée en lecture/écriture"
 expoFixe position:2.
 expoFixe contents printOn:Transcript.
 "affiche (Vinci Rafael)"

 expoModifie position:2.
 expoModifie contents printOn:Transcript.
 "affiche (Vinci Rafael MichelAnge FraAngelico)"
 expoModifie truncate.
 "la position courante de expoModifie devient aussi sa limite de lecture"
 expoModifie contents printOn:Transcript.
 "affiche (Vinci Rafael)"
 galerie printOn:Transcript.
 "affiche (Vinci Rafael MichelAnge FraAngelico)"
 "la variable galerie n'a pas été modifiée"
```

<caption>Figure 5.13 : différences entre ReadWriteStream et WriteStream</caption>

Toute cette gestion des déplacements dans un flux en écriture ou en lecture/écriture utilise trois variables d'instance. Nous allons les aborder plus en détail dans le paragraphe suivant.

### 5.3.4 - Les variables d'instance de position

Les variables d'instance de position dans les classes WriteStream et ReadWriteStream sont au nombre de trois :

- *position* enregistre la position courante du flux (c'est à dire la position du dernier objet venant d'être lu ou écrit dans la collection parcourue);

- *readLimit* enregistre la position du dernier objet pouvant être lu dans la collection. Nous avons entrevu son rôle avec la méthode *truncate*;

- *writeLimit* enregistre la position du dernier objet qui peut être écrit dans la collection (en fonction de sa taille actuelle).

Ces trois variables d'instance sont initialisées lors de la création du flux par un appel à une méthode privée, *setLimits*. La figure 5.14 montre les différences d'implémentation de cette méthode selon les sous-classes de *Stream*.

Cas de la classe *ReadStream* :

la seule opération possible est la lecture. La variable d'instance *writeLimit* n'existe pas (elle n'apparaît qu'au niveau de *WriteStream*). La variable *readLimit* prend la taille de la collection parcourue. Le flux est placé en début de collection (*position* := 0). Il est ainsi prêt à lire la collection.
```perl
setLimits "Méthode de la classe ReadStream"
    position := 0.
    readLimit := collection size

setLimits "Méthode de la classe WriteStream"
    readLimit := position := 0.
    writeLimit := collection size

setLimits " Méthode de la classe ReadWriteStream"
    position := 0.
    readLimit := writeLimit := collection size
```

<caption>Figure 5.14 : la méthode `setLimits` pour les classes `ReadStream`, `WriteStream` et `ReadWriteStream`</caption>

Cas de la classe `WriteStream` :

la seule opération possible est l'écriture. La variable d'instance `readLimit`, héritée de `Stream`, est initialisée à 0. Par contre, la limite d'écriture (`writeLimit`) prend comme valeur la taille de la collection parcourue. Si l'écriture d'éléments dans la collection nécessite d'augmenter sa taille, alors le message `grow` est envoyé à la variable d'instance `collection` et `writeLimit` prend ensuite la nouvelle taille de la collection (figure 5.15). Sauf appel à la méthode `setToEnd` qui déplacerait la position du flux en fin de collection (par le message `position:readLimit`), la position du flux est initialisée en début de collection. Rappelons que l'appel à la méthode `setToEnd` n'a lieu qu'avec la méthode de classe `with`: (ou `with.from:to:`).

Cas de la classe `ReadWriteStream` :

l'écriture et la lecture sont possibles. Les variables `readLimit` et `writeLimit` prennent donc comme valeur la taille de la collection. La variable `writeLimit` enregistre les augmentations successives de la taille de collection si elles ont lieu. Quant à `readLimit`, elle suit la position courante en cas d'écriture de nouveaux objets en fin de collection (on doit évidemment pouvoir lire ce qui vient d'être écrit). Elle ne bouge pas lors des opérations de lecture (elle sert au contraire de borne pour certaines méthodes, comme `contents` ou `setToEnd`) sauf en cas d'appel à `truncate`: dans ce cas, elle prend la valeur de la position courante.

La méthode `nextPut`: décrite figure 5.15 permet d'illustrer le rapport parfois complexe qui existe entre les primitives en langages machine et les instructions Smalltalk. L'écriture d'un objet dans la collection est une action de bas niveau : il faut enregistrer physiquement un nouvel objet en mémoire. La primitive 66 est chargée de ce travail. Mais elle peut échouer dans un cas bien précis : si la position du flux est arrivée en fin de collection, il n'y a plus de place prévue en mémoire pour placer ce nouvel objet. Les instructions Smalltalk qui suivent traitent alors ce cas particulier. Après avoir vérifié (avec la variable `index`) que le flux est en bout de collection, le message `grow` permet d'agrandir celle-ci, et la variable `writeLimit` est réinitialisée. Ce n'est plus une primitive en langage
machine qui écrit l'objet dans la collection, mais une méthode de la classe *Collection (at:put:)*. Remarquons que seul l'échec de la primitive 66 dû à un dépassement de capacité de la collection est traité. Si la primitive échoue pour une autre raison, Smalltalk laisse le soin de gérer cette erreur à la méthode *at:put:*, puisque l'expression *collection at: index put: anObject* est systématiquement évaluée en cas d'échec de la primitive 66.

```pascal
nextPut: anObject
    "Write anObject to the receiver stream. Answer anObject."
  | index |
<primitive: 66>
  index := position + 1.
  index > writeLimit
    ifTrue:[collection grow.
              writeLimit := collection size].
  position := index.
  collection at: index put: anObject.
  ^anObject
```

<caption>Figure 5.15 : méthode *nextPut* : redéfinie dans *WriteStream*</caption>

La méthode *nextPut* : est redéfinie dans la classe *ReadWriteStream* pour mettre à jour *readLimit* : cette variable d'instance prend en compte l'ajout d'un objet et sa valeur augmente d'une unité si nécessaire (si l'objet est ajouté en fin de collection).

## 5.4 - Travailler sur un fichier avec卧流Stream

### 5.4.1 - Comment accéder à un fichier sous Smalltalk ?

#### Créer un fichier texte à partir de l'environnement Smalltalk

La lecture et l'écriture de fichiers passent aussi par des instances issues de la classe *Stream*. La sous-classe *卧流Stream* est spécialisée dans ces opérations. Fille de *ReadWriteStream*, ses instances peuvent donc écrire et lire dans les collections qu'elles parcourent.

Le codage des fichiers ayant pour base l'octet, ou le caractère, nombre de méthodes utilisables dans *FileStream* sont spécialisées dans la manipulation d'octets, de caractères et de mots-machine. Nous aborderons, en plus des méthodes de *FileStream*, toutes celles des classes *ReadStream* et *WriteStream* qui concernent plus particulièrement les flux de caractères.

Auparavant, nous allons créer un fichier texte qui servira de base à tous nos exemples. Smalltalk V offre, dans son environnement de travail, une fenêtre spécialisée dans la gestion des fichiers. Cette fenêtre est accessible par le menu principal (cliquer sur l'écran avec le bouton droit de la souris, dans une zone libre de toute autre fenêtre), en choisissant l'option *Browse Disk*. Selon les versions, un
dialogue plus ou moins évolué permet ensuite de choisir le disque sur lequel la fenêtre doit travailler. Une fois le disque choisi et les dimensions de la fenêtre validées avec le bouton gauche de la souris, un espace de travail divisé en trois sous-fenêtres principales s'affiche à l'écran (figure 5.16).

- Le quart en haut et à gauche est réservé à l'affichage et au défilement des répertoirs. Un menu particulier permet de créer ou détruire un répertoire et de remettre à jour la fenêtre (si l'on a changé de disquette entre-temps, ou modifié les répertoirs par des commandes extérieures à Smalltalk).

- Le quart en haut et à droite contient tous les fichiers du répertoire sélectionné. Un menu permet de créer, renommer, supprimer un fichier, ou de mettre à jour la fenêtre.

- La moitié inférieure de la fenêtre affiche la liste détaillée des fichiers du répertoire étudié tant qu'aucun fichier n'a été sélectionné dans la fenêtre précédente. Si un fichier a été sélectionné (bouton gauche de la souris), cette fenêtre affiche alors le contenu du fichier (en entier si elle le peut, ou bien les premiers et derniers caractères si le fichier est trop important pour tenir en entier dans une fenêtre d'édition de texte). Il est possible de modifier le contenu du fichier. Le menu propre à cette fenêtre permet de sauvegarder, d'effacer, de copier le texte ou de s'y déplacer (cas des gros fichiers).

L'exemple que nous avons choisi figure 5.16 est le fichier *stream.ex1*, appartenant au répertoire *luc* du disque *E:*. Nous remarquons sur la barre de titre que le nombre d'octets disponibles dans le disque est affiché à côté du nom du répertoire de travail (8273920). Le fichier étant sélectionné, son contenu est affiché dans la sous-fenêtre inférieure.

<figure><img src="image_31.png" /><figcaption>Figure 5.16 : contenu du fichier *stream.ex1*</figcaption></figure>
## Représentation « objets » d'un fichier dans Smalltalk

L'accès aux fichiers est une opération de bas niveau. Pour la réaliser, Smalltalk doit échanger des informations avec les modules du système d'exploitation qui gèrent les entrées/sorties. Pour cela, plusieurs classes ont été créées dans l'environnement de base de Smalltalk. Connaître le contenu de ces classes comme le fonctionnement exact de leurs méthodes n'est pas indispensable pour travailler sur des fichiers. Il est quand même utile d'avoir quelques notions de base.

Quatre classes sont associées à la gestion de fichiers :

**Directory** rassemble les objets répertoires. Les méthodes de classe sont utilisées pour gérer le répertoire courant, créer ou supprimer un répertoire donné etc. Les méthodes d'instance permettent de créer, supprimer, traiter un fichier etc.

**File** rassemble les objets fichiers. Ces objets ont une représentation en mémoire particulière (il ne s'agit pas du contenu du fichier lui-même mais d'un descriptif permettant à Smalltalk d'accéder au fichier). Plusieurs méthodes permettent de créer, effacer, ouvrir, fermer, sauvegarder ou renommer un fichier.

### FileControlBlock

L'accès physique au contenu du fichier passe par les instances de cette classe, structurées en tableaux d'octets (cette classe hérite de `byteArray`). Cette classe est réservée aux fichiers de format défini par MS-DOS. L'écriture (ou la lecture) d'un fichier se fait bloc par bloc et non caractère par caractère. Les instances de la classe `FileControlBlock` assurent les opérations de lecture et d'écriture sur disque, selon le format requis.

**文件流** Les trois classes précédentes sont le support des instances de `文件流`, qui sont les seuls objets permettant de consulter et de modifier le contenu d'un fichier. Les méthodes de création ou d'ouverture de fichier des classes `Directory` et `File` renvoient d'ailleurs un objet de la classe `文件流`.

Pour travailler sur un fichier existant, comme nous l'avons fait sur des collections (par exemple `galerie`), la première chose à faire est d'ouvrir une instance de `文件流` sur ce fichier. Pour cela, nous pouvons envoyer une séquence de messages à la classe `Directory` :

```
(Directory pathname:'e:\luc') file:'stream.ex1'
```

qui ouvre un fichier de nom `stream.ex1` (file:'stream.ex1') dans le répertoire représenté par l'instance de la classe `Directory` que nous avons créée sur le répertoire `e:\luc` (message pathname:'e:\luc'). Le même résultat serait obtenu en utilisant la classe `File` :

```
(File pathname:'e:\luc\stream.ex1')
```

où le message `patheName:'e:\luc\stream.ex1'` ouvre le fichier dont le nom est donné en argument.
Ces deux expressions renverront une instance de *FileStream*. Il n'est pas possible de créer directement une instance par une méthode de classe dans *FileStream*. La méthode *on*: héritée de *Stream* est d'ailleurs redéfinie pour signaler une erreur.

Pour créer un nouveau fichier (et s'assurer qu'un éventuel fichier de même nom sera détruit et remplacé par le nouveau), nous utiliserons la classe *Directory* :

```
(Directory pathname:'e:\luc') newFile:'stream.ex1'
```

ou la classe *File*, avec précaution :

```
(File pathname:'e:\luc\stream.ex1') reset
```

car cette séquence crée le fichier `e:\luc\stream.ex1` s'il n'existe pas, mais l'ouvre sans le détruire s'il existe. Le message `reset` place ensuite le flux en début de fichier. Comme pour les flux sur collections, l'instance de *FileStream* se contente de remplacer les caractères au fur et à mesure de leur écriture. Supposons que le fichier `stream.ex1` existe et contienne la ligne *Première version du fichier*. Rouvert avec la séquence précédente, nous voulons remplacer son contenu par *Nouveau fichier*. Après la sauvegarde, le fichier contient *Nouveau fichiern du fichier*. Les derniers caractères de la première version sont toujours là.

### 5.4.2 - Copier un fichier existant

#### Dupliquer un fichier

Nous allons ouvrir deux flux, l'un en lecture sur le fichier `stream.ex1`, l'autre en écriture sur un fichier cible (`stream.bak`). Evaluons maintenant la séquence reprise figure 5.17.

```perl
|sortie entree|
entree := File pathname:'e:\luc\stream.ex1'.
sortie := (Directory pathname:'e:\luc')
          newFile:'stream.bak'.
entree reset. "positionne le flux en début de fichier"
[entree atEnd]
whileFalse:[sortie nextPut:(entree next)].
sortie close. "ferme le fichier stream.bak"
entree close. "ferme le fichier stream.ex1"
```

<caption>Figure 5.17 : copier un fichier en utilisant deux instances de *FileStream*</caption>

L'examen du fichier `stream.bak` dans le *Disk Browser* nous montrera que nous avons créé une copie conforme de notre fichier source.

La séquence que nous venons d'évaluer ne présente pas de méthodes nouvelles, si ce n'est close, dont le rôle est de sauvegarder et fermer le fichier correspondant au flux recevant ce message : le flux en lui-même ne se ferme pas, mais il envoie un message de fermeture à l'instance de la classe *File* qui lui correspond. A la réception de ce message, cette instance force l'écriture sur disque des quelques caractères pouvant rester en suspens dans la mémoire (nous avons vu que les
écritures sur disques se font par blocs de caractères : un bloc n'est écrit sur le disque que lorsqu'il est plein, sauf si un message force cette écriture). Le bloc de contrôle correspondant au fichier enregistre ensuite la fermeture (le fichier n'est plus signalé en ouverture et, en cas d'écriture sur le fichier, les informations comme la date, l'heure et la taille sont mises à jour).

## Modifier le contenu d'un fichier avant de le copier

Découvrons maintenant quelques méthodes particulières à la classe *FileStream*.

La méthode *nextLine* renvoie la suite de caractères compris entre la position courante et la prochaine marque de fin de ligne. Cette marque de fin de ligne est un caractère spécial et varie selon les systèmes d'exploitation (sous MS-DOS, ce caractère a pour code ASCII 13). Lorsque nous écrivons du texte, chaque retour à la ligne (frappe de la touche *Entrée*) est donc chargé dans le fichier correspondant sous forme d'un caractère de fin de ligne.

Pour ne pas redéfinir à chaque changement de système toutes les méthodes utilisant les marques de fin de ligne, Smalltalk utilise une abréviation pour ce caractère (*Lf*). Cette abréviation est répertoriée dans un dictionnaire partagé auquel ont accès les instances de *FileStream*. Il s'appelle *CharacterConstants*. La correspondance entre l'abréviation *Lf* et le caractère qui sera réellement utilisé est enregistrée dans ce dictionnaire. Il est quand même possible de préparer un fichier pour un autre système d'exploitation que celui sur lequel on travaille en imposant un nouveau caractère comme marque de fin de ligne. C'est le rôle du message *lineDelimiter*: *aCharacter*. Pour retrouver le caractère de fin de ligne en cours, on emploiera la méthode *lineDelimiter*.

Nous allons maintenant copier le fichier *stream.ex1* dans la fenêtre *System Transcript*. Nous rajouterons, lors de l'affichage, le rang de chaque ligne. La figure 5.18 montre une façon de réaliser cet affichage.

```
|numLigne entree|
entree := File pathname:'e:\luc\stream.ex1'.
entree reset. "positionne le flux en début de fichier"
numLigne := 1."initialise la variable des numéros de ligne"
[entree atEnd] whileFalse:
    ['numLigne printOn:Transcript.
     numLigne printOn:Transcript. "imprime le numéro de ligne"
     Transcript tab."ajoute une tabulation"
     entree nextLine printOn:Transcript.
     "imprime la ligne suivante du fichier"
     Transcript cr. "saute à la ligne"
     numLigne := numLigne + 1 "incrémente le numéro de ligne"].
entree close."referme le fichier"
```

<caption>Figure 5.18 : afficher un fichier ligne par ligne</caption>
Nous voyons successivement s'afficher sur la fenêtre *System Transcript* les lignes numérotées du fichier source. Ce texte annoté n'est pas conservé dans un fichier. Modifications légèrement nos instructions pour le récupérer dans un fichier *stream.num* (figure 5.19).

```
|numLigne entree sortie|
entree := File pathname:'e:\luc\stream.ex1'.
sortie := (Directory pathname:'e:\luc')
newFile:'e:\luc\stream.num'
entree reset.
numLigne := 1.
[entree atEnd] whileFalse:
    [numLigne printOn:sortie.
     "le numéro de ligne est écrit dans le fichier"
     sortie tab. "une tabulation est écrite dans le fichier"
     "écrit la ligne suivante du fichier entree dans le fichier sortie :"
     sortie nextPutAll: (entree nextLine).
     sortie cr. "ajoute une marque de fin de ligne au fichier sortie"
     numLigne := numLigne + 1].
entree close.sortie close.
```

<caption>Figure 5.19 : lire un fichier tout en écrivant une version modifiée</caption>

L'évaluation par l'option *do it* crée le fichier *stream.num*, qui pourra être consulté dans la fenêtre du *Disk Browser*. Dans ces derniers exemples, nous avons fait appel à trois nouvelles méthodes :

*printOn*: cette méthode s'applique à pratiquement tous les objets, et imprime leur représentation en caractères dans un flux. Nous l'appliquons ici sur un entier (*numLigne*). Cette méthode est d'ailleurs le moyen le plus simple de récupérer la représentation d'un nombre (*entier*, réel, ou autre) sous forme de caractères.

*tab* envoie une marque de tabulation dans le flux récepteur.

*cr* envoie une marque de fin de ligne dans le flux récepteur. Remarquons que la chaîne de caractères obtenue par le message *nextLine* n'intègre pas les marques de fin de ligne. Il faut rajouter ce code de fin de ligne avec le message *cr*.

Récupérant le fichier source ligne après ligne, nous l'avons copié dans le fichier destination par la méthode *nextPutAll: aCollection*.

Illustrons maintenant l'importance des caractères séparateurs en lisant mot à mot (méthode *nextWord*) le fichier *stream.ex1* avant de le copier, toujours mot à mot, dans le fichier *stream.mot* (figure 5.20).

Après évaluation par l'option *do it* de la séquence de la figure 5.20 nous examinerons le contenu de *stream.mot* dans la fenêtre du *Disk Browser*.

ExempledefichierdestintesterlasousclasseFileStreamIlnefau
tpasconfondresteamavecstreamIlyaautantd... etc.
Les espaces et tous les caractères accentués ont disparus. La méthode `nextWord` lit une chaîne de caractères mot à mot, mais elle est construite pour un environnement anglo-saxon. Elle interprète comme caractères séparateurs tous les caractères accentués français (entre autres), en plus des espaces ou marques de tabulation. Pour aérer un peu notre ligne, nous aurions pu ajouter le message *sortie space*. Nous aurions récupéré les blancs entre chaque mot, mais perdu définitivement les caractères accentués, comme les marques de fin de ligne (à moins de les ajouter avec *sortie cr*).

```
| entree sortie|
entree := File pathname:'e:\luc\stream.ex1'.
sortie := (Directory pathname:'e:\luc')
          newFile:'stream.mot'.
entree reset.
[entree atEnd] whileFalse:[
  "nous ajoutons au fichier sortie chaque mot lu dans le fichier entree :"
  sortie nextPutAll:(entree nextWord)].
entree close.sortie close.
```

<caption>Figure 5.20 : importance des caractères séparateurs</caption>

Nous pourrions aussi compter nos blancs avec la méthode `countBlanks`, qui rendra un total correspondant au nombre de blancs et tabulations présents dans le flux (1 par blanc et 4 par tabulation).

Jouer sur les codes de retour de ligne, espace et tabulation offre parfois des possibilités insoupçonnées. Témoin, cet essai de traduction d'un texte en auvergnat (figure 5.21).

```
| entree sortie|
entree := File pathname:'e:\luc\stream.ex1'.
sortie := (Directory pathname:'e:\luc')
          newFile:'stream.sch'.
entree reset.
entree lineDelimiter:$s."définit le caractère s comme signal de fin de
ligne"
  [entree atEnd] whileFalse:[
    sortie nextPutAll:(entree nextLine ).
    "lit une ligne (de s en s). Le caractère de fin de ligne est maintenant interprété
    comme un caractère normal"
    sortie nextPutAll:'ch'].
    "ajoute les caractères ch en fin de ligne dans le fichier destinataire"
  entree close. sortie close.
```

<caption>Figure 5.21 : jouer sur les codes de fin de ligne</caption>
La séquence de la figure 5.21 nous donne, évaluée avec *do it* le contenu du fichier `stream.sch` suivant :

Exemple de fichier, dechtiné à techter
la clachcheFEILEM.

Il ne faut pas confondre chteam avec chtream. Il y a
autant de rapport entre un chteamer et un
chtreamer qu'entre un chteak de chtar et chtar
chtreck,...

Nous avons perturbé la méthode *nextLine* appliquée à *entree*, en choisissant le caractère $\$s$ comme marque de fin de ligne. Les chaînes de caractères stockées dans *sortie* allaient donc de s en s, considérant l'ancien code de retour de ligne (*Lf*) comme un caractère normal. Le fichier *stream.sch* a donc gardé tous les codes de retour de ligne *Lf* de *stream.exl*. Il suffisait d'avoir auparavant remplacé le s par le ou les caractères désirés (*sortie nextPutAll:'ch'*) pour obtenir notre nouveau texte.

5.4.3 - Manipuler des octets

Les fichiers sont écrits sur les disques comme une suite d'octets ou de mots-
machine. Disposer de méthodes ne permettant de traiter que des flux de
caractères est insuffisant. Smalltalk V propose deux méthodes (héritées de
WriteStream), réservées à l'écriture d'octets dans un flux :

nextBytePut: anInteger

charge l'octet de valeur *anInteger* à la position courante dans le flux et
déplace cette position d'un octet (un caractère). La valeur de *anInteger*
doit être comprise entre 0 et 255. Il peut être sous forme binaire, octale,
décimale ou hexadecimal.

nextTwoBytesPut: anInteger

charge le mot de deux octets de valeur *anInteger* à la position courante
et déplace celle-ci de deux octets (deux caractères). Conformément aux
règles courantes du système MS-DOS et des processeurs INTEL, l'octet
de poids faible est chargé en première position, suivi de l'octet de poids
fort. La valeur de *anInteger* doit être comprise entre 0 et 65535. Si
*anInteger* dépasse ces limites, seuls les deux derniers octets sont pris en
considération.

Illustrons l'utilisation de ces deux méthodes par l'exemple suivant, appliqué à
une instance de WriteStream (figure 5.22).
```
|flux|
flux := WriteStream on: (String new).
"On ouvre un flux en écriture sur une chaîne de caractères vide"
flux nextBytePut: 49. "le premier octet écrit correspond à 49"
flux nextTwoBytesPut: 50.
"les deux octets suivants correspondent à 50 (en base décimale)"
flux nextBytePut: 60.
"l'octet suivant à 60 (en base décimale)"
flux nextTwoBytesPut: 12594.
"les deux octets suivants à 12594 (en base décimale)"
flux contents.
```

<caption>Figure 5.22 : charger des octets et des mots machines dans un flux</caption>

Une fois évaluée par *show it* la séquence de la figure 5.22 nous renvoie :
'12 <21'.

Le code correspondant à 49 est le caractère 1. Celui de 50 est le caractère 2.
La méthode `nextTwoBytesPut`: s'attend à recevoir un mot de deux octets en
paramètre. Comme la valeur décimale 50 se code sur un seul octet, la méthode
considère que l'octet de poids fort est égal à 0. L'octet de poids faible représentera
50. Les deux caractères suivants de notre chaîne sont donc 2 et ' ' (l'octet 0
apparaît comme un espace). Le code de 60 est <.

Reste la représentation en octets du nombre 12594. En base hexadecimal, ce
nombre est égal à 3132h. L'octet de poids faible (31h ou 49 décimal) est le caractère 2.
De cette façon, Smalltalk permet de stocker des nombres codés sous forme
binaire dans un fichier. Il reste ensuite à les lire, pour réutiliser ce fichier.

L'exemple de la figure 5.23 montre comment créer une table des codes ASCII
dans un fichier *ascii cod*. Le numéro du code est imprimé dans le fichier par le
message *code printOn:sortie*. L'octet correspondant est chargé par l'instruction
*sortie nextBytePut:code*. Pour améliorer la présentation de notre table, nous avons
inséré des retours à la ligne tous les dix codes ASCII.

```
|sortie |
sortie := (Directory pathname:'e:\luc')
newFile:'ascii.cod'.
0 to:255 do:[:code| code printOn:sortie.
    sortie nextPutAll:' '.
    sortie nextBytePut:code.
    sortie space.
    sortie nextBytePut:179.
    (code \
```

<caption>Figure 5.23 : création d'une table des codes ASCII dans un fichier</caption>

La figure 5.24 montre le résultat après évaluation par *show it*.
<figure><img src="image_32.png" /><figcaption>Figure 5.24 : affichage de la table des codes ASCII</figcaption></figure>

### 5.4.4 - Comment fonctionne le traitement des flux sur des fichiers

Nous aborderons dans ce paragraphe les méthodes concernant le traitement des flux de caractères, dont une bonne partie est implémentée dans la classe `WriteStream`, puis les méthodes spécifiques aux traitements des fichiers.

#### Recours à des abréviations

Nous avons utilisé pour modifier nos textes plusieurs méthodes améliorant la présentation (ajouter une tabulation, un espace, une marque de fin de ligne).

La méthode `cr` n'envoie pas un caractère de fin de ligne mais la valeur correspondant à l'abréviation `Lf` :

`cr`

"Write the line terminating character (line-feed) to the receiver stream."

`self nextPut: Lf`

Cette abréviation est définie dans le dictionnaire partagé `CharacterConstants` (Cf 2.9.2), auquel les instances de `WriteStream` ont accès.

Dans la définition de la classe, le nom du dictionnaire partagé doit être mentionné après le mot-clé `poolDictionary`: (figure 5.25). Toute instance de `WriteStream` peut ainsi retrouver les caractères les plus courants par un équivalent mnémonique (*Lf*, *Space*, *Tab*).