Réaliser des entrées / sorties sur des fichiers textes est une tâche commune en programmation, et la vaste majorité des langages de programmation offrent des mécanismes pour y arriver. C# ne fait pas exception à la règle.
Ce qui suit se veut une brève introduction à certains mécanismes permettant de lire d'un fichier texte ou d'écrire dans un fichier texte. On parle d'ailleurs bien d'une introduction sommaire : l'objectif de cet article est d'aider les lectrices et les lecteurs à démarrer, sans plus. La plateforme .NET offre une vaste gamme d'outils d'entrée / sortie, et vous bénéficierez sans doute d'une exploration de ces options par vous-mêmes.
L'outil de base pour lire d'un fichier texte est probablement le type TextReader. Ainsi, le programme suivant lira le texte entier d'un fichier et l'affichera à l'écran :
using System;
using System.IO;
string nom = "../../../Program.cs";
var lecteur = new StreamReader(nom);
Console.Write(lecteur.ReadToEnd());
lecteur.Close();
Ici, le type StreamReader est une classe capable de consommer des données d'un fichier (ici, celui identifié par la variable nom), quoi que contienne ce fichier. Elle offre des services pour consommer des données d'un fichier texte. Un de ces services, ReadToEnd, lit la totalité d'un fichier dans une seule et même string et retourne cette dernière. C'est simple, bien que pas nécessairement rapide si le fichier est de grande taille.
Notez le Close en fin de fonction. En l'absence de finalisation déterministe, C# offre un support incomplet de l'encapsulation. Il existe toutefois un mécanisme permettant de simplifier cet aspect du programme, soit les blocs using, qui insèrent un try...finally autour d'une variable (si celle-ci est d'un type implémentant IDisposable), et appellent Dispose sur cette variable dans le bloc finally (pour un flux, Dispose appellera Close).
Gestion manuelle de la fermeture | Gestion implicite de la fermeture |
---|---|
|
|
Supposons que nous ayons pour tâche de compter les lignes dans un fichier texte. Nous définirons une ligne comme une séquence de caractères délimitée à la fin par un saut de ligne (caractère '\n') ou par la fin du fichier.
Une version « manuelle » serait la suivante :
using System;
using System.IO;
string nom = "../../../Program.cs";
Console.WriteLine($"CompterLignes manuel : {CompterLignesManuel(nom)}");
static int CompterLignesManuel(string nomFichier)
{
int n = 0;
using(var lecteur = new StreamReader(nomFichier))
{
string ligne = lecteur.ReadLine();
while (ligne != null)
{
++n;
ligne = lecteur.ReadLine();
}
}
return n;
}
Quelques explications :
Ce serait un peu plus joli avec une boucle for :
Avec un while | Avec un for |
---|---|
|
|
Une version profitant de la méthode Split de string serait la suivante :
using System;
using System.IO;
string nom = "../../../Program.cs";
Console.WriteLine($"CompterLignes Split : {CompterLignesSplit(nom)}");
static int CompterLignesSplit(string nomFichier)
{
using (var lecteur = new StreamReader(nomFichier))
return lecteur.ReadToEnd().Split(Environment.NewLine).Length;
}
Quelques explications :
Supposons que nous ayons pour tâche de compter les mots dans un fichier texte. Nous définirons un mot comme une séquence de caractères non-blancs contigus les uns aux autres (cela signifie que "if(x==3)" serait un mot selon notre définition).
Une version « manuelle » serait la suivante :
using System;
using System.IO;
string nom = "../../../Program.cs";
Console.WriteLine($"CompterMots manuel : {CompterMotsManuel(nom)}");
static int CompterMotsManuel(string nomFichier)
{
int n = 0;
using (var lecteur = new StreamReader(nomFichier))
{
int c = lecteur.Read();
bool dansMot = false;
while(c != -1)
{
if (!dansMot && !char.IsWhiteSpace((char)c))
{
dansMot = true;
++n;
}
else if (dansMot && char.IsWhiteSpace((char)c))
{
dansMot = false;
}
c = lecteur.Read();
}
}
return n;
}
Quelques explications :
Ce serait un peu plus joli avec une boucle for :
Avec un while | Avec un for |
---|---|
|
|
Une version profitant de la méthode Split de string serait la suivante :
using System;
using System.IO;
string nom = "../../../Program.cs";
Console.WriteLine($"CompterMots Split : {CompterMotsSplit(nom)}");
static int CompterNonVides(string [] strs)
{
int n = 0;
foreach (string s in strs)
if (s.Length != 0)
++n;
return n;
}
static int CompterMotsSplit(string nomFichier)
{
TextReader lecteur = new StreamReader(nomFichier);
int n = CompterNonVides(lecteur.ReadToEnd().Split(null));
lecteur.Close();
return n;
}
Quelques explications :
Si vous souhaitez vous amuser, on peut passer un paramètre supplémentaire à Split pour que les éléments vides soient épurés par la fonction. Je vous laisse explorer la question!
L'outil de base pour écrire dans un fichier texte est probablement le type TextWriter. Ainsi, le programme suivant lira des lignes au clavier jusqu'à ce qu'une ligne vide soit rencontrée, et écrira ce texte dans le fichier sortie.txt (le dossier où ce fichier sera écrit sera celui où se trouve l'exécutable de votre programme) :
using System;
using System.IO;
string nom = "sortie.txt";
var scripteur = new StreamWriter(nom);
string ligne = Console.ReadLine();
while(ligne != null)
{
scripteur.WriteLine(ligne);
ligne = Console.ReadLine();
}
scripteur.Close();
Notez le Close en fin de fonction. En l'absence de finalisation déterministe, C# offre un support incomplet de l'encapsulation. Il existe toutefois des mécanismes permettant de simplifier cet aspect du programme, mais nous ne les couvrirons pas ici (ces mécanismes sont à la charge du code client; l'encapsulation demeure incomplète). Ici, si nous négligeons le Close, il se peut que le fichier dans lequel nous écrivons soit incomplet lorsque le programme terminera son exécution.
Le même programme est plus joli et plus concis avec une boucle for :
using System;
using System.IO;
string nom = "sortie.txt";
var scripteur = new StreamWriter(nom);
for(string ligne = Console.ReadLine(); ligne != null; ligne = Console.ReadLine())
scripteur.WriteLine(ligne);
scripteur.Close();
... et il est encore plus joli avec un bloc using pour automatiser la fermeture du flux :
using System;
using System.IO;
string nom = "sortie.txt";
using(var scripteur = new StreamWriter(nom))
for(string ligne = Console.ReadLine(); ligne != null; ligne = Console.ReadLine())
scripteur.WriteLine(ligne);
Ici, le type StreamWriter est une classe capable de produire des données dans un fichier (ici, celui identifié par la variable nom).
Supposons que nous souhaitions écrire un programme acceptant au démarrage autant de noms de fichiers que voulu (paramètre args de Main), et pour chaque fichier, que nous :
Une solution possible serait :
using System;
using System.IO;
// args est implicite avec le « Top-Level Main »
foreach(string nom in args)
Transformer(nom);
static void Transformer(string nomFichier)
{
using (var lecteur = new StreamReader(nomFichier))
using(var scripteur = new StreamWriter(nomFichier + ".maj"))
scripteur.Write(lecteur.ReadToEnd().ToUpper());
}
Supposons que nous souhaitions écrire un programme générant des données pour une sinusoïdale, et que nous souhaitons exporter ces données pour générer un graphique dans un tableau un chiffrier électronique), une donnée par ligne.
Une solution possible serait :
using System;
using System.IO;
using (var scripteur = new StreamWriter("sinus.txt"))
foreach(double x in GénérerDonnées(1000, 4000))
scripteur.WriteLine(x);
static double [] GénérerDonnées(int granularité, int nbDonnées)
{
double tranche = 2 * Math.PI / granularité;
double [] données = new double[nbDonnées];
for(int i = 0; i < nbDonnées; ++i)
données[i] = Math.Sin(tranche * i);
return données;
}
Ne reste plus qu'à ouvrir votre tableur favori, y ouvrir le fichier sinus.txt, et générer un graphique avec les données en question.
Supposons que nous souhaitions sauvegarder l'état d'une partie d'un jeu très sophistiqué pour la reprendre ultérieurement. Supposons que ce jeu soit un Tic-Tac-Toe, et que l'état d'une partie soit la configuration du plateau (les 'X', les 'O' et les ' ') de même que quelque chose identifiant quelle est la joueuse ou quel est le joueur destiné à poser le prochain geste.
Une solution (simpliste) à ce problème serait :
using System;
using System.IO;
Configuration cfg = new ();
// ... on joue quelques tours
cfg.Jouer(1, 1); // un X au milieu
cfg.Afficher(Console.Out);
cfg.Jouer(0, 0); // un O en haut à gauche
cfg.Afficher(Console.Out);
cfg.Jouer(1, 2); // un X en bas au centre
cfg.Afficher(Console.Out);
// ... on sent la souper chaude, heure de sauver la partie
cfg.Sauvegarder("partie.tic"); // disons
// ... on prend une tasse de thé
// ... la partie reprend
cfg = Configuration.Charger("partie.tic");
cfg.Jouer(2, 2); // un O en bas à droite
cfg.Afficher(Console.Out);
cfg.Jouer(1, 0); // un X en haut au centre
cfg.Afficher(Console.Out);
// les X ont gagné
class Configuration
{
const int LARGEUR = 3,
HAUTEUR = LARGEUR;
const char LIBRE = ' ';
static public int NbJoueurs => Symboles.Length;
private static char[] Symboles { get; } = new char[] { 'X', 'O' };
public int ProchainJoueur { get; private set; }
private char [,] Config { init; }
public Configuration()
{
Config = new char[HAUTEUR, LARGEUR];
for (int i = 0; i != Config.GetLength(0); ++i)
for (int j = 0; j != Config.GetLength(1); ++j)
Config[i, j] = LIBRE;
ProchainJoueur = 0;
}
private Configuration(int prochainJoueur, char [,] config)
{
Config = config;
ProchainJoueur = prochainJoueur;
}
public bool Jouer(int x, int y)
{
if (Config[y, x] != LIBRE)
return false;
Config[y, x] = Symboles[ProchainJoueur];
ProchainJoueur = (ProchainJoueur + 1) % NbJoueurs;
return true;
}
// ... bla bla
public void Sauvegarder(string nomFichier)
{
using (var scripteur = new StreamWriter(nomFichier))
{
scripteur.WriteLine(ProchainJoueur);
for (int i = 0; i != Config.GetLength(0); ++i)
for (int j = 0; j != Config.GetLength(1); ++j)
scripteur.WriteLine(Config[i, j]);
}
}
public static Configuration Charger(string nomFichier)
{
using (var lecteur = new StreamReader(nomFichier))
{
int prochainJoueur = int.Parse(lecteur.ReadLine());
char[,] config = new char[HAUTEUR, LARGEUR];
for (int i = 0; i != config.GetLength(0); ++i)
for (int j = 0; j != config.GetLength(1); ++j)
config[i, j] = lecteur.ReadLine()[0];
}
return new (prochainJoueur, config);
}
public void Afficher(TextWriter sortie)
{
for (int i = 0; i != Config.GetLength(0); ++i)
{
for (int j = 0; j != Config.GetLength(1); ++j)
sortie.Write($"{Config[i, j]} ");
sortie.WriteLine();
}
sortie.WriteLine(new string('-', 70));
}
}
Quelques exercices pour vous faire la main. Des solutionnaires possibles sont disponibles sur e_s_texte_solutionnaires.html. Pour les exercices touchant des mots, considérez qu'un mot est une séquence de caractères non-blancs contigus les uns aux autres.
EX00 – Écrivez la fonction TrouverPlusLongueLigne qui trouvera et retournera la ligne la plus longue d'un fichier texte. S'il y a plusieurs lignes de cette longueur, retournez n'importe laquelle d'entre elles. Écrivez une version « manuelle » et une version utilisant Split.
EX01 – Écrivez la fonction TrouverPlusLongMot qui trouvera et retournera le mot le plus long d'un fichier texte. S'il y a plusieurs mots de cette longueur, retournez n'importe laquelle d'entre elles. Écrivez une version « manuelle » et une version utilisant Split.
EX02 – Écrivez la fonction TrouverMotPlusVoyelleux qui trouvera et retournera le mot contenant le plus de voyelles d'un fichier texte. S'il y a plusieurs mots avec le même nombre de voyelles, retournez n'importe lequel d'entre eux. Écrivez une version « manuelle » et une version utilisant Split.
EX03 – Écrivez la fonction TrouverMotPlusFréquent qui trouvera et retournera le mot apparaissant le plus grand nombre de fois dans un fichier texte. S'il y a plusieurs mots avec le même nombre d'occurrences, retournez n'importe lequel d'entre eux. Écrivez une version « manuelle » et une version utilisant Split.
Quelques liens pour en savoir plus.