C# – entrées / sorties et fichiers textes

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.

Lire d'un fichier texte – Outils de base

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;

public class Program
{
   public static void Main()
   {
      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
using System;
using System.IO;
class Program
{
   static string Traiter(string s)
   {
      // ... code mystérieux qui peut lever une exception ...
   }
   static void Main()
   {
      string nom = "../../../Program.cs";
      StreamReader lecteur = null;
      try
      {
         lecteur = new StreamReader(nom);
         Console.Write(Traiter(lecteur.ReadToEnd()));
      }
      finally
      {
         if(lecteur != null)
            lecteur.Close();
      }
   }
}
using System;
using System.IO;
class Program
{
   static string Traiter(string s)
   {
      // ... code mystérieux qui peut lever une exception ...
   }
   static void Main()
   {
      string nom = "../../../Program.cs";
      using(var lecteur = new StreamReader(nom))
         Console.Write(Traiter(lecteur.ReadToEnd()));
   }
}

Compter les lignes dans un fichier texte

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.

Version manuelle

Une version « manuelle » serait la suivante :

using System;
using System.IO;

public class Program
{
   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;
   }
   public static void Main()
   {
      string nom = "../../../Program.cs";
      Console.WriteLine($"CompterLignes manuel : {CompterLignesManuel(nom)}");
   }
}

Quelques explications :

Ce serait un peu plus joli avec une boucle for :

Avec un while Avec un for
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;
}
static int CompterLignesManuel(string nomFichier)
{
   int n = 0;
   using(var lecteur = new StreamReader(nomFichier))
   {
      for(var ligne = lecteur.ReadLine();
          ligne != null;
          ligne = lecteur.ReadLine())
         ++n;
   }
   return n;
}

Version reposant sur la méthode Split de string

Une version profitant de la méthode Split de string serait la suivante :

using System;
using System.IO;

public class Program
{
   static int CompterLignesSplit(string nomFichier)
   {
      using (var lecteur = new StreamReader(nomFichier))
         return lecteur.ReadToEnd().Split(Environment.NewLine).Length;
   }
   public static void Main()
   {
      string nom = "../../../Program.cs";
      Console.WriteLine($"CompterLignes Split  : {CompterLignesSplit(nom)}");
   }
}

Quelques explications :

Compter les mots dans un fichier texte

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

Version manuelle

Une version « manuelle » serait la suivante :

using System;
using System.IO;

public class Program
{
   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;
   }
   public static void Main()
   {
      string nom = "../../../Program.cs";
      Console.WriteLine($"CompterMots manuel   : {CompterMotsManuel(nom)}");
   }
}

Quelques explications :

Ce serait un peu plus joli avec une boucle for :

Avec un while Avec un for
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;
}
static int CompterMotsManuel(string nomFichier)
{
   int n = 0;
   using (var lecteur = new StreamReader(nomFichier))
   {
      bool dansMot = false;
      for(int c = lecteur.Read(); c != -1; c = lecteur.Read())
      {
         if (!dansMot && !char.IsWhiteSpace((char)c))
         {
            dansMot = true;
            ++n;
         }
         else if (dansMot && char.IsWhiteSpace((char)c))
         {
            dansMot = false;
         }
      }
   }
   return n;
}

Version reposant sur la méthode Split de string

Une version profitant de la méthode Split de string serait la suivante :

using System;
using System.IO;

public class Program
{
   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;
   }
   public static void Main()
   {
      string nom = "../../../Program.cs";
      Console.WriteLine($"CompterMots Split    : {CompterMotsSplit(nom)}");
   }
}

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!

Écrire dans un fichier texte – Outils de base

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;

public class Program
{
   public static void Main()
   {
      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;

public class Program
{
   public static void Main()
   {
      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;

public class Program
{
   public static void Main()
   {
      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).

Transformer un fichier

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;

public class Program
{
   static void Transformer(string nomFichier)
   {
      using (var lecteur = new StreamReader(nomFichier))
         using(var scripteur = new StreamWriter(nomFichier + ".maj"))
            scripteur.Write(lecteur.ReadToEnd().ToUpper());
   }
   public static void Main(string [] args)
   {
      foreach(string nom in args)
         Transformer(nom);
   }
}

Exporter des données

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;

public class Program
{
   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;
   }
   public static void Main(string [] args)
   {
      using (var scripteur = new StreamWriter("sinus.txt"))
         foreach(double x in GénérerDonnées(1000, 4000))
            scripteur.WriteLine(x);
   }
}

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.

Sauvegarder des états

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;

public class Program
{
   class Configuration
   {
      const int LARGEUR = 3,
                HAUTEUR = LARGEUR;
      const char LIBRE = ' ';
      static public int NbJoueurs { get => Symboles.Length; }
      private static char[] Symboles { get; } = new char[] { 'X', 'O' };
      public int ProchainJoueur { get; private set; }
      private char [,] Config { get; }
      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 Configuration(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));
      }
   }
   public static void Main(string[] args)
   {
      Configuration cfg = new Configuration();
      // ... 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é
   }
}

Exercices

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.

Lectures complémentaires

Quelques liens pour en savoir plus.


Valid XHTML 1.0 Transitional

CSS Valide !