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.
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.
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 :
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.
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.
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 :
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 :
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.