.Net 4.0 : les nouveautés et évolutions de System.IO

Ronald Vasseur

Avec la nouvelle version du Framework 4.0 il y a eu de petites modifications dans le System.IO qui sont un peu passées inaperçues dans le flot des nouvelles fonctionnalités plus vendeuses et attrayantes. System.IO est présent depuis le tout début du Framework .Net, c'est un des éléments de base puisqu'il permet de communiquer avec le système de fichiers, de lire et d'écrire des fichiers ou encore de gérer les flux. Ce namespace compte environ une quarantaine de classes avec beaucoup de méthodes statiques que nous utilisons tous les jours pour interagir avec des fichiers. Ces classes se nomment File, FileInfo, Directory, FileStream, Path, MemoryStream… pour ne citer que les plus connues.

Voyons sans plus attendre quels sont les changements et nouveautés majeurs de ce namespace essentiel de la BCL (Base Class Library).

- Titre : .Net 4.0 : les nouveautés et évolutions de System.IO

- Auteur : Ronald Vasseur

- Parution : 15 octobre 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Ronald Vasseur. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. La méthode File.ReadLines

- II. La méthode AppendAllLines

- III. La méthode WriteAllLines

- IV. La méthode Path.Combine

- V. La méthode EnumerateFiles

- VI. Les Memory Mapped Files

- VII. Récapitulatif des principales évolutions

I - La méthode File.ReadLines

Non content d'avoir amélioré certaines méthodes l'équipe en charge de la BCL s'est aussi attachée au développement de nouvelles méthodes pour compléter certains manques fonctionnels ou du moins pour offrir de vraies solutions dans des situations précises. En écrivant cela, je pense immédiatement à la lecture de gros fichiers.

Microsoft .Net

En effet, jusqu'à maintenant si l'on souhaitait lire un fichier long (plusieurs dizaines ou centaines de milliers de lignes, voir plus) nous n'avions pas tellement le choix. La seule solution consistait à utiliser la classe TextReader qui permet de lire ligne à ligne, de manière séquentielle le contenu d'un fichier. Certes cela fonctionne, mais c'est rageant de voir que la méthode statique ReadAllLines de la classe File, plus simple d'utilisation (elle ne demande qu'une ligne de code au lieu de quatre ou cinq pour la méthode ReadLine de TextReader), ne nous est d'aucun secours vu ses piètres performances sur de gros fichiers.

Avant .Net 4.0 il fallait utiliser le TextReader

using (TextReader reader = new StreamReader(@"C:\Private\largeFile.txt"))
{
int i = 0;
string line;
while ((line = reader.ReadLine()) != null)
{
i++;
}
Console.ReadLine();
}

Arrêtons-nous un instant sur le fonctionnement de la méthode ReadAllLines. Cette méthode comme nous le savons, lit tout le contenu d'un fichier et le mets en mémoire. Cela s'avère extrêmement pratique sur de petits fichiers, mais c'est une horreur sur ceux de plus grandes tailles. Nous devons attendre que l'intégralité du fichier ait été parcourue et ses lignes placées dans un tableau en mémoire avant de pouvoir travailler sur son contenu. En plus d'être terriblement lent, cela consomme une quantité de mémoire astronomique. Cerise sur le gâteau, en plus d'une performance indigne, cette méthode nous retourne… un tableau de chaînes de caractères, wow, cela sent quand même la méthode au top de la technologie :).

Juste pour vous donner un ordre d'idée j'ai réalisé un rapide test avec un fichier texte de 17 millions de lignes pour un poids de 2,3 Go, bref, un beau bébé. Avec l'ancienne méthode il faut environ 62 secondes pour lire et parcourir toutes les lignes du fichier et l'application console consomme environ 5,4 Go de RAM (autant dire qu'en 32 bits ça ne passera pas… :)). Si l'on utilise la nouvelle méthode avec le même fichier, il ne faut plus que 37 secondes pour réaliser la même opération et l'on utilise plus que 25 Mo de RAM (la consommation habituelle d'une application console), la solution qui visait à contourner le problème avec TextReader affiche aussi 37 secondes et 25 Mo, rien de plus normal car c'est à peu de chose prés le même mécanisme qui est encapsulé dans la nouvelle méthode. Voilà, une vitesse d'exécution quasiment doublée et une consommation de RAM divisée par 215… Pas la peine d'en dire plus, l'avantage est indiscutable. Voici deux petites captures d'écran réalisées au passage :

Méthodes qui retourne un Array : beaucoup de RAM pour pas grand chose !

La nouvelle méthode qui retourne un IEnumerable de String fait des miracles :)

Le code suivant montre comment utiliser cette nouvelle méthode. Comme on peut le voir elle retourne un type Enumerable, l'on va donc pouvoir facilement itérer dessus, et cela, dès le début du parcours du fichier par la méthode ReadLines, plus la peine d'attendre que tout le fichier ait été chargé en mémoire et retourné dans un tableau pour commencer à le parcourir.

La nouvelles méthodes : ReadLines()

int i = 0;
IEnumerable<string> lines2 = File.ReadLines(@"C:\Private\largeFile.txt");
foreach (var line2 in lines2)
{
i++;
}

Console.ReadLine();

Un autre avantage de cette nouvelle méthode est de pouvoir sortir de l'itération si nécessaire sans avoir à attendre de parcourir l'ensemble du fichier, ce qui était obligatoire avec RealAllLines du fait du chargement préalable de toutes les lignes. Le fait de retourner un IEnumerable de String et de ne pas avoir à attendre le chargement complet du tableau avant de l'exploiter, sont les deux avantages majeurs de ces améliorations dans la lecture de fichiers.

II - La méthode AppendAllLines

La méthode AppendAllLines est entièrement nouvelle. Elle permet d'écrire à la suite d'un fichier existant en prenant en paramètre un IEnumerable de String qui contient les lignes du fichier. Une surcharge prenant également en compte l'encoding est disponible. La grande nouveauté est donc de prendre un IEnumerable de String en lieu et place d'un Array de String, ce qui est en terme de performance, une grande amélioration. Une fois l'ajout au fichier terminé la méthode va le libérer automatiquement.

Exemple d'utilisation d'AppendAllLines

private void UtilisationDeAppendAllLines()
{
List<string> myLines = new List<string>();
myLines.Add("A line about...");
myLines.Add("A line about something else");
myLines.Add("Another line...");
File.AppendAllLines(@"C:\Private\TestAppendAllLines.txt", myLines);
}

III - La méthode WriteAllLines

Cette méthode elle, n'est pas nouvelle mais une surcharge prenant un IEnumerable de String en paramètre apparaît. On passe donc directement notre collection de lignes sans avoir à se tracasser avec la préparation d'un Array que l'on utilise plus vraiment depuis longtemps.

private static void UtilisationDeWriteAllLines()
{
List<string> myLines = new List<string>();
myLines.Add("A line about...");
myLines.Add("A line about something else");
myLines.Add("Another line...");
File.WriteAllLines(@"C:\Private\TestWriteAllLines.txt", myLines);
}

IV - La méthode Path.Combine

Une petite évolution, mais qui a quand même sa place dans cet article, ce sont les trois nouvelles surcharge de la méthode Path.Combine qui permettent de passer maintenant de une à quatre chaines paramètre pour combiner des éléments dans un seul chemin. Il est même possible d'en combiner une infinité en passant un tableau de chaines à la méthode Combine. Ainsi nous n'avons plus besoin de faire des Path.Combine imbriqués qui sont très désagréables à écrire mais surtout à relire.

private static void UtilisationDePathCombine()
{
// Nouvelles surcharges de Path.Combine
string path1 = System.IO.Path.Combine("folder", "test.txt");
string path2 = System.IO.Path.Combine("folder", "subfolder", "text2.txt");
string path3 = System.IO.Path.Combine("folder", "subfolder", "anotherSubFolder", "text3.txt");

// Nous pouvons passer autant de chaîne que l'on souhaite à PAth.Combine en lui passant directement
// un tableau de chaînes comme suit :
string[] stringArray = { "folder", "subfolder", "anotherSubFolder", "anotherSubFolder", "anotherSubFolder", "test.txt" } ;
string resultat = Path.Combine(stringArray);
}

V - La méthode EnumerateFiles

La classe DirectoryInfo n'est pas nouvelle mais a subit une sérieuse remise à niveau avec l'apparition de la méthode EnumerateFiles. Prenons le cas de la méthode GetFiles qui permet de récupérer la liste des fichiers contenus dans un dossier sous la forme d'un Array d'objets FileInfo. Le fait de retourner un Array oblige à attendre que le listing de tous les fichiers soit terminé et que le tableau soit complétement instancié avant de retourner quoique ce soit. Si le dossier ne contient que peu de fichiers alors la consommation de ressources reste raisonnable par contre, quand le dossier contient de très nombreux fichiers la situation devient très vite compliquée et impossible à gérer.

De plus lorsque l'on veut accéder aux propriétés de l'objet FileInfo nous sommes obligés de faire appel au système de fichiers une fois de plus, en effet les propriétés de celui-ci sont initialisées lors du premier appel à l'une d'entre elle. Cela signifie que nous devrons faire appel au système de fichiers une fois de plus, alors que l'on pourrait raisonnablement penser que l'instance de FileInfo contient déjà toutes les données nécessaires.

La nouvelle méthode EnumerateFiles apporte une solution à ces deux problèmes, tout d'abord elle retourne un IEnumerable de FileInfo et les propriétés sont initialisées lors de l'instanciation de l'objet, nous ne devons plus rappeler le système de fichiers pour les obtenir ce qui représente un gain de temps qui peut être considérable et la mémoire consommée n'est plus démesurée comme c'était le cas avec l'utilisation d'Array.

Voici un exemple montrant comment utiliser ces nouveautés :

private static void UtilisationDeEnumerateFiles()
{
DirectoryInfo myDirectory = new DirectoryInfo(@"C:\private");
IEnumerable<FileInfo> myFiles = myDirectory.EnumerateFiles();
foreach (var file in myFiles)
{
Console.WriteLine("Nom du fichier : {0}, Taille du fichier : {1} octets.", file.Name, file.Length);
}
}

Il existe également un équivalent en ce qui concerne cette fois-ci les répertoires, cette méthode se nomme EnumerateDirectories tout simplement, je vous renvoie à la MSDN pour plus de précisions.

VI - Les Memory Mapped Files

Les Memory-Mapped Files sont un nouveau mécanisme apparu avec .Net 4.0 (mais qui existe dans Windows depuis très longtemps) qui permet comme son nom l'indique, de mapper le contenu d'un fichier physique directement en mémoire et de travailler dessus.

Cela revient à placer un fichier facilement et directement dans la mémoire et non plus sur le système de fichiers avec tous les avantages que vous pouvez imaginer. C'est idéal pour de gros fichiers et lorsque l'on doit faire de très nombreux accès à ceux-ci.

Un autre avantage de cet objet est le fait de pouvoir être partagé entre différents processus, oui vous avez bien lu, entre processus, nous n'étions pas vraiment habitués à cela avec la plateforme .Net :). Ces processus peuvent être .Net ou non, tout est géré par le système d'exploitation.

Voyons d'abord comment créer un tel objet et réaliser des opérations basiques :

Réaliser les opérations de base de Memory-Mapped File

Comme vous le voyez, nous spécifions la taille que nous allons donner à notre fichier en mémoire, il faut être prudent dans la manière de gérer cela pour faire en sorte que l'espace soit suffisant pour accueillir notre fichier en entier.

Voyons maintenant comment accéder à un Memory-Mapped File créé et partagé par un autre processus :

Accéder au Memory Mapped Files d'un autre processus

Les Memory Mapped Files étant une nouveauté importante de .Net et un petit peu complexe, je leur consacrerais bientôt un article complet pour aller plus en profondeur sur ce sujet qui est un peu inhabituel pour les développeurs de Managed Code que nous sommes :), "stay tuned".

VII - Récapitulatif des principales évolutions

- System.IO.File

- public static IEnumerable<string>ReadLines(string path)

- public static void WriteAllLines(string path, IEnumerable<string> contents)

- public static void AppendAllLines(string path, IEnumerable<string> contents)

- System.IO.Directory

- public static Enumerable<string>EnumerateDirectories(string path)

- public static IEnumerable<string>EnumerateFiles(string path)

- public staticIEnumerable<string>EnumerateFileSystemEntries(string path)

- System.IO.DirectoryInfo

- publicIEnumerable<DirectoryInfo>EnumerateDirectories()

- publicIEnumerable<FileInfo>EnumerateFiles()

- publicIEnumerable<FileSystemInfo>EnumerateFileSystemInfos()

- System.IO.Path

- public static Combine(String[])

- public static Combine(String, String, String)

- public static Combine(String, String, String, String)

- System.IO.MemoryMappedFiles (tout le namespace est nouveau)

Comme vous pouvez le voir il n'y a pas de révolution dans le System.IO (bien que les Memory Mapped Files soit un gros progrès), ce sont surtout des améliorations qui étaient nécessaires, il n'était plus vraiment acceptable pour une plateforme mature comme l'était .Net d'avoir des méthodes comme ReadFile, qui consommaient de la RAM démesurément ou encore le mécanisme de double accès au Système de fichiers réalisé par FileInfo. De trop nombreuses applications sont tombées en production à cause de tels problèmes, certes avant tout, elles étaient mal conçues mais quand même… Il est très appréciable que Microsoft ait enfin apporté les changements nécessaires par le biais de nouvelles méthodes et de nouvelles surcharges. Rendez-vous dans un prochain article avec un dossier complet sur les Memory-Mapped files.

Denier point, mais le moindre, cette utilisation de IEnumerable permet également d'employer "LINQ to Objects" pour manipuler nos collections, c'est un point trés interessant, et il serait dommage de vous en priver quand vous essayerai ces nouvelles méthodes.