C# – Tableaux

Quelques raccourcis :

Ce qui suit donne quelques exemples illustrant les tableaux en C#, en utilisant au passage un peu de généricité. Vous pourrez comparer par vous-mêmes avec des programmes équivalents dans les langages que vous connaissez.

Tableaux – bases

De prime abord, un tableau est une séquence, contiguë en mémoire, d'éléments d'un même type. Ainsi, pour lire dix nombres et les afficher en ordre inverse de celui dans lequel ils ont été lus, mieux vaut l'exemple de droite ci-dessous que celui de gauche :

Sans tableau Avec tableau
using System;

namespace z
{
   class Program
   {
      static void Main(string[] args)
      {
         int nb0,
             nb1,
             nb2,
             nb3,
             nb4,
             nb5,
             nb6,
             nb7,
             nb8,
             nb9;
         nb0 = int.Parse(Console.ReadLine());
         nb1 = int.Parse(Console.ReadLine());
         nb2 = int.Parse(Console.ReadLine());
         nb3 = int.Parse(Console.ReadLine());
         nb4 = int.Parse(Console.ReadLine());
         nb5 = int.Parse(Console.ReadLine());
         nb6 = int.Parse(Console.ReadLine());
         nb7 = int.Parse(Console.ReadLine());
         nb8 = int.Parse(Console.ReadLine());
         nb9 = int.Parse(Console.ReadLine());
         Console.WriteLine(nb9);
         Console.WriteLine(nb8);
         Console.WriteLine(nb7);
         Console.WriteLine(nb6);
         Console.WriteLine(nb5);
         Console.WriteLine(nb4);
         Console.WriteLine(nb3);
         Console.WriteLine(nb2);
         Console.WriteLine(nb1);
         Console.WriteLine(nb0);
      }
   }
}
using System;

namespace z
{
   class Program
   {
      static void Main(string[] args)
      {
         const int NB_NOMBRES = 10;
         int[] nombres = new int[NB_NOMBRES];
         for (int i = 0; i < nombres.Length; ++i)
         {
            nombres[i] = int.Parse(Console.ReadLine());
         }
         for (int i = nombres.Length - 1; i >= 0 ; --i)
         {
            Console.WriteLine(nombres[i]);
         }
      }
   }
}

La version de droite n'a que des avantages sur celle de gauche :

Utiliser un tableau en C# (survol)

Un tableau en C# est un type référence, une sorte d'objet.

À titre d'exemple, pour un tableau tab d'un certain type (ici, float), le type du tableau sera float[] et le type de chacun de ses éléments sera float. Puisqu'il s'agit d'un type référence, il peut être initialisé à null. Il peut bien sûr aussi être instancié directement avec new, ce qui est souhaitable en général. À droite, tab est un tableau de dix float, qui restent à initialiser.

Le compilateur C# supporte aussi une écriture compacte initialisant un tableau directement à partir d'une séquence de valeurs. Le tableau mots à droite en est un exemple.

Si les éléments d'un tableau sont d'un type référence, alors il ne faut pas oublier d'initialiser ces éléments (avec new) sur une base individuelle.

float[] tab = new float[10];
string [] mots =
{
   "J'aime", "mon", "prof"
};

On manipule typiquement les éléments d'un tableau à l'aide de répétitives à compteurs. La boucle à droite dépose les valeurs dans les éléments aux positions de tab. Notez que les positions des éléments dans un tableau commencent à zéro, et que le nombre d'éléments d'un tableau est connu du tableau et accessible à travers sa propriété Length. Accéder à un élément d'un tableau tab hors de l'intervalle [0..tab.Length) est une erreur et provoquera une levée d'exception.

for(int i = 0; i < tab.Length; ++i)
{
   tab[i] = i + 0.5f;
}

Un tableau est un type référence, et il faut le manipuler en conséquence. Le code à droite copie les éléments de tab dans un autre tableau autreTab. Remarquez que ceci implique créer un nouveau tableau de la bonne taille, puis rédiger une répétitive qui réalisera une copie élément par élément.

float[] autreTab = new float[tab.Length];
for(int i = 0; i < tab.Length; ++i)
{
   autreTab[i] = tab[i];
}

En retour, le code à droite ne copie pas le contenu de tab dans oups; il fait simplement référer oups au même objet que celui auquel réfère tab. Conséquemment, la modification apportée à oups[2] modifie tab[2] puisque ces deux notations mènent au même endroit en mémoire. On nomme cette façon de faire aliasing, et il s'agit souvent d'un bogue – ne le faites que si cela exprime réellement votre intention.

float[] oups = tab; // aliasing!
oups[2] = -oups[2];
Console.WriteLine(tab[2]); // modifié

Enfin, passer un tableau par copie à une fonction copie ... la référence qu'est le tableau. Ainsi, dans la fonction, modifier les éléments du tableau modifie en pratique les éléments du tableau au point d'appel. À droite, la fonction CréerTableau() crée un tableau de taille éléments et les initialise tous à la valeur -1.0f, puis retourne le tableau ainsi initialisé.

void Remplir(float[] tab, float val)
{
   for (int i = 0; i < tab.Length; ++i)
   {
      tab[i] = val;
   }
}
float[] CréerTableau(int taille)
{
   float[] tab = new float[taille];
   Remplir(tab, -1.0f);
   return tab;
}

Quelques exercices

Pour vous pratiquer, voici quelques exercices simples. Dans chaque cas, assurez-vous de tester votre proposition de solution.

EX00 – Écrivez la fonction CalculerSommeÉléments() acceptant en paramètre un tableau d'éléments de type int, et retournant la somme de ces éléments. Si le tableau contient 2,3,5,7,11 alors sa somme devrait être 28.

EX01 – Écrivez la fonction CalculerMoyenneÉléments() acceptant en paramètre un tableau d'éléments de type int, et retournant la moyenne de ces éléments (attention au choix des types!). Si le tableau contient 2,3,5,7,11 alors sa moyenne devrait être 5.6.

EX02 – Écrivez la fonction TrouverÉlément() acceptant en paramètre un tableau d'éléments de type int de même qu'une valeur de type int, et retournant true seulement si la valeur apparaît au moins une fois dans le tableau.

EX03 – Écrivez la fonction SontÉgaux() acceptant en paramètre deux tableaux d'éléments de type int, et retournant true seulement si les deux tableaux ont le même nombre d'éléments et ont des éléments de même valeur aux mêmes positions.

Tableaux 2D

En réponse à une question d'Étienne Raby, étudiant au programme SIM du Collège Lionel-Groulx à l'automne 2014, voici deux petits exemples de manipulation de tableaux 2D avec C#. Notez que je n'ai pas l'intention de faire le tour de la question; ce qui suit ne fait que montrer la syntaxe pour créer un tableau 2D dans ce langage, puis our accéder à l'un de ses éléments.

Une première approche pour créer un tableau 2D est d'utiliser la syntaxe [,], comme le montre l'extrait à droite avec la variable mat0. Dans ce cas, l'instanciation du tableau 2D fixera à la fois la hauteur et la largeur, et toutes les lignes d'un même tableau auront la même largeur.

Avec cette notation, l'accès à l'élément à la ligne i et à la colonne j dans mat0 s'écrit mat0[i,j].

Pour découvrir dynamiquement les tailles des diverses dimensions d'un tableau sous cette forme, utilisez GetLength() en lui passant en paramètre la dimension qui vous intéresse. Les dimensions sont numérotées à partir de zéro. Plus concrètement, avec mat0 dans l'exemple à droite :

  • L'expression mat0.GetLength(0) vaudra 10, et
  • L'expression mat0.GetLength(1) vaudra 5
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Matrices2DGenre
{
   class Program
   {
      static void Main(string[] args)
      {
         const int HAUTEUR = 10,
                   LARGEUR = 5;
         int[,] mat0 = new int[HAUTEUR, LARGEUR];
         for (int i = 0; i < HAUTEUR; ++i)
            for (int j = 0; j < LARGEUR; ++j)
               mat0[i, j] = -1;

L'alternative est d'utiliser la notation [][], comme on le voit dans l'extrait de code à droite avec la variable mat1. Dans ce cas, mat1 est littéralement un tableau de tableaux. Ainsi :

  • L'instanciation de mat1 crée un tableau de HAUTEUR tableaux de int
  • Chaque ligne de mat1 est instanciée individuellement

Cette approche a le désavantage d'éparpiller les divers tableaux en mémoire, ce qui peut nuire à la vitesse d'exécution du programme, mais a le bon côté de permettre de définir un tableau qui de soit pas rectangulaire, au sens où on pourrait avoir des lignes de longueurs distinctes.

Avec cette notation, l'accès à l'élément à la ligne i et à la colonne j dans mat0 s'écrit mat0[i][j].

// ...
         int[][] mat1 = new int[HAUTEUR][] ;
         for(int i = 0; i < mat1.Length; ++i)
            mat1[i] = new int[LARGEUR];
         for (int i = 0; i < mat1.Length; ++i)
            for (int j = 0; j < mat1[i].Length; ++j)
               mat1[i][j] = -1;
      }
   }
}

Petite activité

Complétez le programme suivant pour obtenir un jeu de Tic Tac Toe® tout simplement transcendant! Les fonctions à compléter sont marquées À COMPLÉTER en commentaires; vous trouverez aussi chaque fois en commentaires des consignes quant au travail à réaliser.

using System;

namespace z
{
   class Program
   {
      //
      // Une Case représente une position x,y dans la grille de jeu. Nous ne faisons pas de validation
      //
      class Case
      {
         public int X { get; private set; }
         public int Y { get; private set; }
         public Case(int x, int y)
         {
            X = x;
            Y = y;
         }
      }
      //
      // Un protagoniste est soit un joueur, soit un ordinateur
      //
      enum Protagoniste { Joueur, Ordi };
      //
      // Un état représente l'état actuel d'une partie
      //
      enum État{ EnCours, XGagne, OGagne, Nulle };
      //
      // ChoisirProtagoniste() pigera un protagoniste au hasard parmi ceux disponibles. Cette
      // fonction a pour principal rôle de déterminer qui jouera en premier
      //
      static Protagoniste ChoisirProtagoniste(Random prng)
      {
         return (Protagoniste)prng.Next((int)Protagoniste.Joueur, ((int)Protagoniste.Ordi) + 1); // borne sup. exclue
      }
      //
      // ProchainProtagoniste() retourne le prochain protagoniste suivant celui reçu en paramère. Le
      // principal rôle de cette fonction est de passer d'un joueur à l'autre
      //
      static Protagoniste ProchainProtagoniste(Protagoniste protagoniste)
      {
         const int NB_PROTAGONISTES = 2;
         return (Protagoniste) ((((int)protagoniste) + 1) % NB_PROTAGONISTES);
      }
      //
      // nous représenterons une case vide par un blanc
      //
      const char CASE_VIDE = ' ';
      //
      // À COMPLÉTER
      //
      // Cette fonction doit déposer CASE_VIDE dans chaque case du tableau grille
      //
      static void InitialiserGrille(char[,] grille)
      {
         // VOTRE CODE VA ICI
      }
      //
      // À COMPLÉTER
      //
      // Cette fonction doit retourner true seulement si la case à la position décrite par zeCase est vide
      //
      static bool EstDisponible(char[,] grille, Case zeCase)
      {
         // VOTRE CODE VA ICI
      }
      //
      // Cette fonction choisit une case de manière pseudoaléatoire. Tant que la case «choisie» n'est pas libre,
      // la fonction choisit une nouvelle case
      //
      static Case ChoisirCaseLibre(char[,] grille, Random prng)
      {
         Case zeCase = new Case(prng.Next(0, grille.GetLength(0)), prng.Next(0, grille.GetLength(1)));
         //
         // ceci est naïf et peut être long!
         //
         while (!EstDisponible(grille, zeCase))
            zeCase = new Case(prng.Next(0, grille.GetLength(0)), prng.Next(0, grille.GetLength(1)));
         return zeCase;
      }
      //
      // Cette fonction lit les coordonnées d'une case au clavier, et retourne la case correspondante.
      // Elle ne fait pas de validation
      //
      static Case LireCase()
      {
         int x,
             y;
         Console.Write("Entrez la coordonnée 'x' de votre choix : ");
         x = int.Parse(Console.ReadLine());
         Console.Write("Entrez la coordonnée 'y' de votre choix : ");
         y = int.Parse(Console.ReadLine());
         return new Case(x, y);
      }
      //
      // Cette fonction lit une case choisie par l'usager, et recommence tant que la case choisie n'est
      // pas disponible
      //
      static Case LireCaseLibre(char[,] grille)
      {
         Case zeCase = LireCase();
         while (!EstDisponible(grille, zeCase))
            zeCase = LireCase();
         return zeCase;
      }
      //
      // À COMPLÉTER
      //
      // Cette fonction doit afficher la grille à l'écran. On veut que la grille se présente sous la
      // forme suivante (contenu donné à titre d'exemple seulement). Le coin 0,0 est en haut et à gauche
      //
      // +---+---+---+
      // | X |   | O |
      // +---+---+---+
      // |   |   |   |
      // +---+---+---+
      // |   | X |   |
      // +---+---+---+
      //
      static void AfficherGrille(char[,] grille) // on s'attend à une 3x3
      {
         // VOTRE CODE VA ICI
      }
      //
      // À COMPLÉTER
      //
      // Cette fonction ne doit retourner true que si la ligne «ligne» de la grille est
      // «gagnante», au sens où toutes les cases de cette ligne ont la même valeur (dans
      // la mesure où cette valeur n'est pas vide, évidemment) 
      //
      static bool LigneGagnante(char[,] grille, int ligne)
      {
         // VOTRE CODE VA ICI
      }
      //
      // À COMPLÉTER
      //
      // Cette fonction ne doit retourner true que si la colonne «colonne» de la grille est
      // «gagnante», au sens où toutes les cases de cette colonne ont la même valeur (dans
      // la mesure où cette valeur n'est pas vide, évidemment) 
      //
      static bool ColonneGagnante(char[,] grille, int colonne)
      {
         // VOTRE CODE VA ICI
      }
      //
      // Cette fonction ne retourne true que si l'une des diagonales de grille est «gagnante»,
      // au sens où toutes les cases de cette colonne ont la même valeur (dans la mesure où
      // cette valeur n'est pas vide, évidemment) 
      //
      static bool DiagonaleGagnante(char[,] grille) // on suppose une grille carrée
      {
         // diagonale 0,0 ; 1,1 ; 2,2
         char symbole = grille[0, 0];
         bool gagnant = symbole != CASE_VIDE;
         for (int i = 1; i < grille.GetLength(0) && gagnant; ++i)
         {
            if (grille[i, i] != symbole)
            {
               gagnant = false;
            }
         }
         if (!gagnant)
         {
            // diagonale 2,0 ; 1,1 ; 0,2
            symbole = grille[2, 0];
            gagnant = symbole != CASE_VIDE;
            for (int i = 1; i < grille.GetLength(0) && gagnant; ++i)
            {
               if (grille[grille.GetLength(0)-i, i] != symbole)
               {
                  gagnant = false;
               }
            }
         }
         return gagnant;
      }
      //
      // Fonction retournant la pièce gagnante sur la base du contenu d'une case.
      // On présume que le fait qu'il y ait un gagnant a été établi au préalable
      //
      static État DéterminerGagnantSelon(char contenuCase)
      {
         État résultat;
         if (contenuCase == 'X')
         {
            résultat = État.XGagne;
         }
         else
         {
            résultat = État.OGagne;
         }
         return résultat;
      }
      //
      // À COMPLÉTER
      //
      // Cette fonction doit retourner le nombre d'occurrences de la valeur val dans la grille
      //
      static int Compter (char[,] grille, char val)
      {
         // VOTRE CODE VA ICI
      }
      //
      // Prédicat retournant true seulement si la grille est pleine
      //
      static bool EstPleine(char[,] grille)
      {
         return Compter(grille, CASE_VIDE) == 0;
      }
      //
      // Fonction analysant la grille et déterminant quel est désormais l'état du jeu, à savoir:
      // la partie est-elle encoure en cours? S'est-elle terminée par un verdict nul? Y a-t-il un(e)
      // gagnant(e) et, le cas échéant, de qui s'agit-il?
      //
      static État AnalyserGrille(char[,] grille)
      {
         État résultat = État.EnCours;
         //
         // Ligne gagnante?
         //
         for (int ligne = 0; ligne < grille.GetLength(0) && résultat == État.EnCours; ++ligne)
         {
            if (LigneGagnante(grille, ligne))
            {
               résultat = DéterminerGagnantSelon(grille[ligne, 0]);
            }
         }
         //
         // Colonne gagnante?
         //
         if (résultat == État.EnCours)
         {
            for (int colonne = 0; colonne < grille.GetLength(1) && résultat == État.EnCours; ++colonne)
            {
               if (ColonneGagnante(grille, colonne))
               {
                  résultat = DéterminerGagnantSelon(grille[0, colonne]);
               }
            }
         }
         //
         // Diagonale gagnante?
         //
         if (résultat == État.EnCours)
         {
            if (DiagonaleGagnante(grille))
            {
               résultat = DéterminerGagnantSelon(grille[1,1]);
            }
         }
         //
         // Nulle?
         //
         if (résultat == État.EnCours && EstPleine(grille))
         {
            résultat = État.Nulle;
         }
         return résultat;
      }
      //
      // Sympathique jeu de Tic Tac Toe®
      //
      static void Main(string[] args)
      {
         const int DIM = 3;
         Random prng = new Random();
         char[,] grille = new char[DIM, DIM];
         char[] pièces = { 'X', 'O' };
         int tour = 0;
         État état = État.EnCours;
         Protagoniste qui = ChoisirProtagoniste(prng);
         InitialiserGrille(grille);
         while (état == État.EnCours)
         {
            AfficherGrille(grille);
            Case zeCase = null;
            if (qui == Protagoniste.Ordi)
            {
               zeCase = ChoisirCaseLibre(grille, prng);
            }
            else
            {
               zeCase = LireCaseLibre(grille);
            }
            Console.WriteLine("{0} a choisi ({1},{2})", qui, zeCase.X, zeCase.Y);
            grille[zeCase.Y, zeCase.X] = pièces[tour % 2];
            état = AnalyserGrille(grille);
            ++tour;
            qui = ProchainProtagoniste(qui);
         }
         Console.WriteLine("La partie s'est terminée en {0} tours", tour);
         AfficherGrille(grille);
         switch(état)
         {
            case État.Nulle:
               Console.WriteLine("Partie nulle");
               break;
            case État.OGagne:
               Console.WriteLine("Les 'O' ont gagné");
               break;
            case État.XGagne:
               Console.WriteLine("Les 'X' ont gagné");
               break;
         }
      }
   }
}

Une exécution possible de ce programme (une fois les fonctions manquantes complétées) serait :


+---+---+---+
|   |   |   |
+---+---+---+
|   |   |   |
+---+---+---+
|   |   |   |
+---+---+---+
Entrez la coordonnée 'x' de votre choix : 1
Entrez la coordonnée 'y' de votre choix : 1
Joueur a choisi (1,1)
+---+---+---+
|   |   |   |
+---+---+---+
|   | X |   |
+---+---+---+
|   |   |   |
+---+---+---+
Ordi a choisi (1,2)
+---+---+---+
|   |   |   |
+---+---+---+
|   | X |   |
+---+---+---+
|   | O |   |
+---+---+---+
Entrez la coordonnée 'x' de votre choix : 0
Entrez la coordonnée 'y' de votre choix : 1
Joueur a choisi (0,1)
+---+---+---+
|   |   |   |
+---+---+---+
| X | X |   |
+---+---+---+
|   | O |   |
+---+---+---+
Ordi a choisi (2,0)
+---+---+---+
|   |   | O |
+---+---+---+
| X | X |   |
+---+---+---+
|   | O |   |
+---+---+---+
Entrez la coordonnée 'x' de votre choix : 2
Entrez la coordonnée 'y' de votre choix : 1
Joueur a choisi (2,1)
La partie s'est terminée en 5 tours
+---+---+---+
|   |   | O |
+---+---+---+
| X | X | X |
+---+---+---+
|   | O |   |
+---+---+---+
Les 'X' ont gagné
Appuyez sur une touche pour continuer...

Exemple avec généricité

La généricité s'exprime par l'apposition des types sur lesquels elle s'applique, placés entrechevrons. Ici, la méthode de classe Trouver() est générique sur la base d'un type T, et reçoit en paramètre une référence sur un tableau de T et une valeur de type T à y chercher.

Le mot clé foreach permet d'itérer à travers des collections énumérables, dont font partie les tableaux. Dans Trouver(), la variable val et de type T et prendra successivement chaque valeur trouvée dans le tableau tab (la méthode Equals() est utilisée puisqu'elle est définie, à tout le moins de manière abstraite, dès object).

évidemment, une boucle for typique, comme on en trouve dans d'autres langages (syntaxe de C) aurait pu être utilisée si les indices des valeurs avaient été requis.

Le tableau instancié dans Main() utilise une syntaxe abrégée; la forme complète aurait impliqué un new et une série d'initialisations, une case à la fois.

L'invocation de Trouver() est cohérente, en ce sens qu'un string[] et un string sont passés en paramètre, instanciant la méthode pour le type string.

Un passage d'un tableau de string à une List<string>, et inversement, est utilisé pour montrer une manière d'ajouter des éléments à une collection. Il aurait été possible d'itérer à travers la List<string> directement avec foreach puisqu'elle est elle aussi énumérable.

using System;
using System.Collections.Generic;
using System.Linq;

namespace ExempleTableaux
{
   class Program
   {
      private static bool Trouver<T>(T[] tab, T elem)
      {
         foreach (T val in tab)
         {
            if (val.Equals(elem))
               return true;
         }
         return false;
      }
      static void Main(string[] args)
      {
         string[] légumes = { "Patate", "Carotte", "Rabiole" };
         foreach (string s in légumes)
         {
            Console.WriteLine("{0} est un légume", s);
         }
         string nom = "Tomate";
         if (Trouver(légumes, nom))
            Console.WriteLine("{0} est un légume", nom);
         else
            Console.WriteLine("{0} n'est pas un légume", nom);
         List<string> temp = légumes.ToList<string>();
         temp.Add("Fenouil");
         légumes = temp.ToArray();
         foreach (string s in légumes)
         {
            Console.WriteLine("{0} est un légume", s);
         }
      }
   }
}

Valid XHTML 1.0 Transitional

CSS Valide !