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;

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;

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. Entre les deux, tout aussi valide, est la syntaxe utilisée pour initialiser vals; en pratique, la syntaxe utilisée pour initialiser mots est une forme raccourcie (mais identique en termes d'effet) de celle utilisée pour initialiser vals. Notez enfin que aussiOk est un équivalent plus bref de vals.

float[] tab = new float[10];
int [] vals = new int[]{ 2,3,5,7,11 };
int [] aussiOk = new []{ 2,3,5,7,11 };
string [] mots =
{
   "J'aime", "mon", "prof"
};

C# traite les tableaux différemment des scalaires au sens de l'initialisation. L'exemple à droite montre cette hétérogénéité :

  • La variable n n'est pas considérée comme initialisée, bien que le compilateur y insère techniquement la valeur zéro. Il est possible d'y écrire, mais pas de l'utiliser en lecture. Par exemple, afficher n à la console provoquerait une erreur à la compilation. Pour éviter ceci, vous pouvez simplement l'initialiser (par exemple : écrire int n=0; au lieu de int n;)
  • La variable x est dans la même situation. Elle est techniquement initialisée à null, mais c'est une erreur de l'utiliser sans l'avoir initialisée. Ici encore, il est possible de l'initialiser explicitement si tel est votre souhait
  • Par contre, pour les tableaux, une initialisation par défaut est considérée comme étant faite lors d'un new. Ainsi, vals est un tableau de dix int initialisés à zéro (vous pouvez les afficher à la console si tel est votre désir), et xs est un tableau de dix références nulles sur des X

En effet, 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. Le tableau xs à droite ne contient aucun X, mais bien des références sur des X, et celle-ci sont null jusqu'à preuve du contraire

int n; // pas initialisée; c'est une erreur de l'utiliser en lecture
X x; // idem
int [] vals = new int[10]; // initialisés à zéro
X [] xs = new X[10]; // initialisés à null

class X
{
   public int Valeur { get; private set; }
   public X(int valeur)
   {
      Valeur = valeur;
   }
}

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 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;
}

Un ennui que les gens débutant avec les tableaux tendent à avoir (tous langages confondus) est la confusion de genre, de type vraiment, entre un tableau et ses éléments.

Dans l'exemple à droite (https://dotnetfiddle.net/PPguA3), on trouve d'ailleurs un ensemble de fonctions manipulant un tableau et ses éléments :

  • Remarquez que ce programme comprend deux méthodes Afficher(), soit une acceptant en paramètre un float et une autre acceptant en paramètre un float[]; c'est sur la base du type du paramètre que le compilateur les distinguera
  • Le programme principal, Main(), crée un tableau de 10 float (donc un float[]) à l'aide de CréerTableau(int), puis affiche ce tableau à l'aide de Afficher(float[])
  • La fonction Afficher(float[]) parcourra le tableau et appellera Afficher(float) pour chaque élément du tableau
  • Enfin, la fonction Afficher(float) fera le travail d'afficher chaque valeur qu'on lui aura passé en paramètre
using System;
using static OutilsAffichage;

float [] tab = CréerTableau(10);
Afficher(tab); // appellera Afficher(float[])

static void Remplir(float[] tab, float val)
{
   for (int i = 0; i < tab.Length; ++i)
   {
      tab[i] = val;
   }
}
static float[] CréerTableau(int taille)
{
   float[] tab = new float[taille];
   Remplir(tab, -1.0f);
   return tab;
}
static class OutilsAffichage
{
   public static void Afficher(float val)
   {
      Console.Write($"{val} ");
   }
   public static void Afficher(float [] tab)
   {
      foreach(float x in tab)
      {
         Afficher(x); // appellera Afficher(float)
      }
   }
}

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;

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.

//
// Sympathique jeu de Tic Tac Toe®
//
using System;

//
// nous représenterons une case vide par un blanc
//
const char CASE_VIDE = ' ';

const int DIM = 3;
Random prng = new ();
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($"{qui} a choisi ({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 {tour} tours");
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;
   }

//
// 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);
}
//
// À 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 EstLigneGagnante(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 EstColonneGagnante(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 EstDiagonaleGagnante(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 (EstLigneGagnante(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 (EstColonneGagnante(grille, colonne))
         {
            résultat = DéterminerGagnantSelon(grille[0, colonne]);
         }
      }
   }
   //
   // Diagonale gagnante?
   //
   if (résultat == État.EnCours)
   {
      if (EstDiagonaleGagnante(grille))
      {
         résultat = DéterminerGagnantSelon(grille[1,1]);
      }
   }
   //
   // Nulle?
   //
   if (résultat == État.EnCours && EstPleine(grille))
   {
      résultat = État.Nulle;
   }
   return résultat;
}
//
// 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 init; }
   public int Y { get; private init; }
   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 }

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 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;

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);
}

static bool Trouver<T>(T[] tab, T elem)
{
   foreach (T val in tab)
   {
      if (val.Equals(elem))
         return true;
   }
   return false;
}

Valid XHTML 1.0 Transitional

CSS Valide !