Accueil > Articles > Convertir un String en MemoryStream (et inversement)

Convertir un String en MemoryStream (et inversement)

Voici un cas que l’on retrouve assez fréquemment mais dont la solution s’oublie malheureusement assez vite : il s’agit de la conversion d’une chaîne de caractères (String) en flux de données (Stream). Voici un petit rappel des méthodes à utiliser et des pièges à éviter, ainsi qu’une explication sur la différence entre ces deux classes.

 

Un flux est souvent employé lorsque les données doivent être traitées au fur et à mesure et dans des quantités importantes. Dans ce cas, ce traitement s’effectue la plupart du temps sur des données binaires, octet par octet. On retrouve ainsi l’utilisation de ces flux dans les des opérations d’entrées / sorties sur fichier, les communications réseau, la compression, le cryptage, etc.

En général, les objets réalisant ce type de traitement imposent un objet de type Stream en entrée. Cependant, les données sont rarement stockées sous cette forme au moment de l’exécution du programme : pour des données texte par exemple, le stockage habituel sera plutôt un String. Une conversion sera donc nécessaire. Malheureusement, aucune méthode réalisant cette conversion n’est disponible au niveau de ces deux types de données et les classes de traitement proposent rarement les surcharges adéquates.

La classe Stream est une classe abstraite (MustInherit en VB), elle n’est donc utilisable qu’à travers ses classes dérivées comme MemoryStream ou FileStream. La classe MemoryStream permet de gérer un flux de données en mémoire. On l’emploie habituellement dès que l’on souhaite utiliser des données présentes en mémoire dans des objets effectuant certaines opérations d’entrée / sortie spécifiques (chiffrage, compression, etc.), sans être obligé de passer par une étape intermédiaire (stockage sur le disque par exemple). En interne, le MemoryStream est constitué d’un tableau d’octets non signés. Ce tableau n’est pas redimensionnable si la classe à été crée et initialisée avec des données. Par contre si le MemoryStream est vide au départ, le tableau pourra être redimensionné dynamiquement.

 

 

Convertir un String en MemoryStream

 

La classe StreamWriter

La première solution serait d’utiliser la classe StreamWriter, puis la méthode Write pour écrire nos données dans le MemoryStream. Le code ressemblerait alors à celui-ci :

MemoryStream ms = new MemoryStream();
StreamWriter writer = new StreamWriter(ms);
writer.Write("My text");

 

Le StreamWriter possédant une méthode Close, on serait tenté de l’appeler à juste après le Write. Cet appel permettrait, en plus de libérer les ressources allouées par le writer, de s’assurer que toutes les opérations d’écriture dans le flux ont bien été effectuées. Cependant la description de cette méthode nous apprend que cela entraînerait non seulement la fermeture du StreamWriter, mais aussi celle du flux sous-jacent. Dans le cas où le but serait d’écrire les données dans un fichier, cela ne poserait pas vraiment de problème. Mais dans notre cas, fermer le flux signifie aussi libérer ses ressources, donc la mémoire allouée, ce qui est un peu gênant car le but d’un MemoryStream est justement de garder ces données en mémoire jusqu’à ce qu’elles ne soient plus utiles. D’ailleurs le code ci-dessous nous permet de constater que la fermeture du StreamWriter rend impossible la lecture du MemoryStream :

MemoryStream ms = new MemoryStream();
StreamWriter writer = new StreamWriter(ms);
writer.Write("My text");
writer.Close();

Console.WriteLine("Read : " + ms.CanRead);
Console.WriteLine("Write : " + ms.CanWrite);
Read : False
Write : False

 

A noter que la méthode Dispose a le même effet sur le flux. Sinon, pour s’assurer que les données sont bien écrites, on peut utiliser la méthode Flush juste après l’appel du dernier Write. Attention toutefois : le StreamWriter laisse le pointeur à la fin du flux. Il faut donc penser à le repositionner au début pour éviter tout problème. Il faut aussi gérer correctement les exceptions propres à cette classe.

L’utilisation du StreamWriter avec un MemoryStream n’est donc pas très pratique. Heureusement, il est possible de s’en passer en utilisant directement les constructeurs paramétrés du MemoryStream. Ceux-ci demandent un tableau d’octets en entrée, nous allons donc devoir convertir notre texte en tableau d’octets pour pouvoir continuer. Aucune méthode de la classe String ne propose cette conversion, il va falloir la chercher ailleurs.

 

Conversion de la chaîne en tableau d’octets

Deux mêmes chaînes de caractères peuvent avoir des tailles (en mémoire) très différentes suivant l’encodage utilisé. Celui-ci permet notamment de définir le nombre maximal de caractères différents qu’il est possible de représenter. L’UTF-8 par exemple (format souvent utilisé sur Internet), permet de représenter 256 caractères différents stockés chacun sur un octet, tandis que l’Unicode (UTF-16) autorise jusqu’à 65536 caractères différents stockés chacun sur deux octets. La conversion d’une chaîne en tableau d’octets sera donc deux fois plus volumineuse en UTF-16. L’ensemble des méthodes de conversion selon l’encodage sont disponibles dans la classe Encoding et ses propriétés associées. Le plus simple dans notre cas est de choisir l’encodage par défaut :

byte[] data = Encoding.Default.GetBytes("My text");

 

On peut ensuite passer ce tableau au constructeur du MemoryStream, ce qui peut se résumer par l’instruction suivante :

MemoryStream ms = new MemoryStream(Encoding.Default.GetBytes("My text"));

Si des caractères anormaux apparaissent lors de l’utilisation du MemoryStream, pensez dans ce cas à utiliser les encodages prédéfinis plutôt que l’encodage par défaut.

 

Convertir un MemoryStream en String

 

La classe StreamReader

Comme pour la conversion précédente, le plus simple consiste à utiliser la classe StreamReader car elle contient déjà les méthodes nécessaires ainsi que la détection automatique de l’encodage utilisé pour les données texte. Ici, la conversion consistera simplement à appeler la méthode ReadToEnd qui permet de lire l’ensemble du flux et de renvoyer un objet de type String :

StreamReader reader = new StreamReader(ms);
string outputText = reader.ReadToEnd();
reader.Close();

 

Idéalement, pour gérer correctement la libération des ressources dans le cas où des exceptions seraient levées, il vaut mieux mettre l’appel à la méthode Close du StreamReader dans un bloc finally. On peut aussi tester aussi la propriété CanRead pour savoir s’il est utile ou non d’instancier le StreamReader, et repositionner le pointeur au début du flux :

if(ms.CanRead)
{
    StreamReader reader = null;

    try
    {
        ms.Position = 0;
        reader = new StreamReader(ms);

        Console.WriteLine(reader.ReadToEnd());
        reader.Close();
    }
    finally
    {
        if(reader != null)
            reader.Close();
    }
}

 

Cependant, comme pour la classe StreamWriter, l’appel ici de la méthode Close ferme le MemoryStream et libère les ressources associées, qui ne sont alors plus disponibles pour d’autres traitements. On va donc encore une fois chercher un autre moyen de réaliser cette conversion, cette fois-ci directement dans la classe MemoryStream.

 

GetBuffer et ToArray

A défaut d’avoir une méthode GetBytes comme pour les encodeurs, la classe MemoryStream propose deux méthodes qui semblent correspondre à ce que l’on cherche, à savoir récupérer les données du flux sans pour autant le vider ou libérer ses ressources :

GetBuffer :
Retourne le tableau d’octets non signés à partir duquel ce flux a été créé.

ToArray :
Écrit le contenu du flux dans un tableau d’octets, quelle que soit la propriété Position.

La différence entre les deux est assez subtile : La méthode GetBuffer renvoie l’ensemble des octets alloués, y compris les octets inutilisés. Contrairement à la méthode ToArray qui ne renvoie que les octets utiles. Cette seconde méthode réalise aussi une copie des données, ce qui n’est pas très optimisé pour la mémoire, mais qui permet de ne pas se soucier des modifications qui pourraient survenir dans le flux en cours de traitement. La méthode ToArray est donc celle qui convient le mieux ici, d’autant plus que la méthode GetBuffer renvoie curieusement une exception de type UnauthorizedAccessException (Impossible d’accéder à la mémoire tampon interne de MemoryStream) si la classe MemoryStream a été instanciée avec un tableau d’octet, ce qui est malheureusement notre cas ici. Il suffit ensuite d’utiliser à nouveau la classe Encoding et ses méthodes statiques pour convertir ce tableau d’octets en String, suivant l’encodage par défaut ou autre :

byte[] data = ms.ToArray();
string text = Encoding.Default.GetString(data);
Console.WriteLine(text);

Que l’on peut résumer par :

Encoding.Default.GetString(ms.ToArray());

Ce qui est finalement plus simple à utiliser que la classe StreamReader.

 

Résumé et conclusion

 

Pour résumer, voici les méthodes à utiliser dans la plupart des cas pour réaliser les conversions (j’ai ajouté ces méthodes au mémo pour les retrouver plus vite) :

// String => MemoryStream
MemoryStream ms = new MemoryStream(Encoding.Default.GetBytes("My text"));

// MemoryStream => String
string text = Encoding.Default.GetString(ms.ToArray());

 

Les flux sont utilisés dans de nombreuses classes du .NET Framework. Voici une liste de quelques flux spécialisés dont la classe dérive de Stream :

FileStream (System.IO.ISolatedStorage) : Entrées / Sorties sur fichier.
BufferedStream (System.IO) : Mémoire tampon, optimisation des échanges avec d’autres flux.
MemoryStream (System.IO) : Flux de données en mémoire.

DeflateStream et GZipStream (System.IO.Compression) : Compression de données.
CryptoStream (System.Security.Cryptography) : Flux de données chiffrées.
NetworkStream (System.Net.Sockets) : Communications réseau.
AuthentictedStream (System.Net.Security) : Flux authentifiés / communications sécurisées.
PipeStream (System.IO.Pipes) : Communications par canaux.

 

Categories: Articles Tags: , , ,
  1. Pas encore de commentaire
  1. Pas encore de trackbacks