Dans cette séquence nous allons parler de la bibliothèque de Stream qui est fournie de base avec Pharo. Nous verrons ce que sont les streams et comment les utiliser, dans quels cas ils peuvent être utiles.
Alors qu'est-ce qu'un stream ? Un stream, c'est un objet qui permet d'itérer sur une séquence d'éléments. Cette séquence, ça peut être:
Un stream garde en mémoire la position courante, et au fur à mesure de l'utilisation du stream on va pouvoir avancer ou reculer pour pouvoir lire ou écrire des éléments dans le stream.
Pour créer un stream, il y a quelques objets sur lesquels on peut utiliser les messages readStream
et writeStream
c'est notamment le cas des fichiers ou les collections.
Le message streamContents:
s'envoie à une collection et prend un block en paramètre qui reçoit un stream. L'utilisation de ce stream au sein du block va créer une collection qui sera finalement retournée par la méthode. On verra l'utilisation de cette méthode-là à la fin.
Les messages readStream
, soit writeStream
, soit readWriteStream
envoyés à une classe permettent de créer une nouvelle instance par rapport à une collection.
Les 3 messages:
next
upTo: anObject
upToEnd
Permettent de lire des éléments, donc un ou plusieurs jusqu'à une certaine limite.
nextPut: anElement
nextPutAll: aCollection
Permettent d'écrire un élément ou une collection d'éléments dans le stream.
Alors voici quelques exemples permettant de lire dans un Stream. Première étape, on crée un stream. Ici, on crée un stream en lecture à partir d'une collection.
|stream| stream := #( $a $b $c $d $e $f ) readStream. "A l'initialisation le curseur est à la position 0" "le message next permet de récupérer le premier élément du flux" stream next. "> $a" "Après le premier appel à next le curseur a avancé d'un élément" stream position. "> 1" stream upTo: $d. "> #($b $c)" stream position. "> 4" stream upToEnd. "> #($e $f)"
La collection contient les caractères de l'alphabet entre 'a' et 'f'. On créé un Stream
sur cette collection-là (ligne 2) et on va regarder les caractères un par un.
Une fois qu'on a notre Stream, la première chose qu'on peut faire, c'est envoyer le message next (ligne 6) qui va nous retourner l'élément juste après la position courante.
Dès l'initialisation du stream, la position courante c'est 0 donc on se situe au début du stream, next
retourne au début le premier élément, c'est-à-dire le caractère 'a'.
Si j'envoie successivement next
à l'objet stream je vais obtenir 'b', puis 'c', puis 'd', etc en faisant évoluer la position du curseur d'un cran à chaque fois.
Dans l'exemple j'envoie le message upTo:
et un élément (ligne 14). La méthode retourne tous les éléments entre la position courante et l'élément que j'ai passé en paramètre. Donc là si avant j'avais 'a', je me situe en position 1 et si j'envoie le message upTo: $d
, il va me retourner tout ce qu'il y a entre la position courante et 'd', c'est-à-dire une collection contenant les éléments 'b' et 'c'. 'd' est consommé par le stream, c'est-à-dire que maintenant le stream se situe juste après 'd' mais 'd' n'est pas retourné.
Le message unaire position
nous donne la position en cours (lignes 11 et 17). La position du curseur commence à 0. À ce moment-là si j'envoie le message upToEnd
au stream (ligne 20), je vais obtenir une collection avec tous les éléments qui se situent entre la position courante et la fin du stream, c'est-à-dire 'e' et 'f'.
Donc on voit que le Stream maintient une position courante que je peux faire avancer grâce à quelques méthodes.
De la même façon on peut écrire dans un stream. La première étape encore une fois, c'est créer le stream.
|myStream| myStream := (Array new: 6) writeStream. myStream nextPut: 1; contents. "> #(1)" myStream nextPutAll: #( 4 8 2 6 7 ); contents. "> #(1 4 8 2 6 7)"
(Array new: 6)
me permet de créer un tableau vide mais de taille 6. J'envoie le message writeStream
dessus pour créer un stream sur ce tableau-là, de façon à pouvoir remplir le tableau petit à petit grâce à mon Stream.
Je stocke ce stream dans la variable myStream
et je commence par envoyer le message nextPut: 1
, qui prend un élément et ajoute cet élément en position courante dans le stream.
Maintenant, mon tableau contient un 1 suivi de 5 cases vides. Le message nextPutAll:
, quant à lui, prend une collection d'objets à mettre les uns après les autres dans le stream. Après l'envoi de ce message nextPutAll:
à mon stream, j'obtiens le tableau qui contient un résultat de mon nextPut:
précédent suivi de 4, 8, 2, 6, 7, résultats du nextPutAll: (ligne 11).
Donc les streams sont particulièrement utiles et efficaces pour lire et écrire dans les collections d'objets.
Je peux aussi lire et écrire dans des fichiers. Là, je montre un exemple de comment écrire dans un nouveau fichier qui n'existe pas encore.
|fileStream| fileStream := 'hello.txt' asFileReference writeStream. fileStream nextPutAll: 'Hello Pharo!'; close.
Dans la chaîne de caractères ici j'indique le nom du fichier. En envoyant à une chaîne de caractères le message asFileReference
, je créé une référence vers un fichier. Là c'est un fichier qui n'existe pas encore mais je peux quand même avoir une référence dessus. J'ai une référence vers un fichier 'hello.txt' et j'envoie le message writeStream
sur ce fichier histoire d'avoir un stream en écriture vers ce fichier qui n'existe pas encore.
Dès qu'on va écrire dans le stream, le fichier va être créé. Maintenant que j'ai mon stream, j'envoie le message nextPutAll:
avec une chaîne de caractères et ce message nextPutAll:
va écrire caractère après caractère chaque élément de ma chaîne de caractères. Il va écrire le 'H', puis le 'e', puis le 'l', etc. A la fin, je ferme mon stream pour indiquer au système d'exploitation que j'ai fini d'écrire dans le fichier, et qu'il peut lui écrire l'ensemble sur le support de stockage et fermer le pointeur sur le fichier.
Maintenant que j'ai écrit dans ce fichier, je peux avoir envie de le lire.
|fileStream| fileStream := 'hello.txt' asFileReference readStream. fileStream next. "> $H" fileStream upToEnd. "> 'ello Pharo!'"
De la même façon, j'ai le nom du fichier sous forme d'une chaînes de caractères, le message asFileReference
qui me permet de créer une référence vers ce fichier-là, et readStream
qui me permet d'ouvrir un flux en lecture.
Avec next, je récupère le premier élément dans le stream. J'avais écrit “Hello Pharo”, le premier élément, c'est 'H'. Et avec le message upToEnd:
, je récupère tous les caractères entre la position courante, c'est-à-dire juste après le 'H' et avant le 'e' et jusqu'à la fin du stream, jusqu'à la fin du fichier: j'obtiens 'ello Pharo!' sans le 'H' puisque je l'ai déjà récupéré grâce au next
précédent.
On peut créer des collections en utilisant des streams. Ça c'est très utile quand on veut créer des collections et qu'il nous faut du code pour choisir ce qu'on va mettre dans la collection petit à petit.
myStream := OrderedCollection new writeStream. myStream nextPut: 1; contents. "Equivalent " OrderedCollection streamContents: [ :stream | stream nextPut: 1 ]
Là, je veux créer une OrderedCollection en envoyant des messages à un stream. À partir de ma classe OrderedCollection, je fais une nouvelle instance, j'en fais un flux en écriture et avec le message nextPut:
, j'ajoute l'élément 1 dans mon stream.
Maintenant quand je vais envoyer le message contents
(ligne 4), je vais obtenir une instance de la classe OrderedCollection qui contient uniquement la valeur 1.
Ces 3 expressions peuvent s'écrire plus simplement comme on le voit en-dessous avec l'équivalent présenté lignes 7 et 8. En envoyant le message streamContents:
à la classe de collection qui nous intéresse, donc ici c'est OrderedCollection, j'envoie ce message “streamContents” à OrderedCollection
, je lui passe en paramètre un block qui prend un stream en paramètre. Au sein de ce block, je vais utiliser le stream pour remplir ma collection petit à petit et quand le block se termine j'obtiens ma collection.
Dans le block, je fais stream nextPut: 1
, j'ajoute 1 dans mon stream qui va l'ajouter dans la collection. Quand streamContents
se termine, je vais obtenir une OrderedCollection qui contient 1.
StreamContents c'est utile pour créer des collections à partir de rien.
Dans cette séquence, nous avons découvert l'API de stream. Il y a énormément de méthodes dans cette API que je vous invite à aller découvrir en naviguant dans les classes grâce au navigateur de codes Nautilus.
Un stream peut lire et écrire dans des collections d'éléments en mémoire, dans des fichiers, sur le réseau et d'autres éléments encore.
Un stream a toujours une position courante, c'est important. La position courante sépare les éléments du passé des éléments du futur, et en fait évoluer cette position courante à chaque fois qu'on va vouloir lire ou écrire dans le flux. Enfin, les streams peuvent servir à créer des nouvelles collections.