Si vous ne connaissez pas les propriétés en C#, mieux vaut lire Proprietes-Methodes.html avant d'aborder le présent article.
Il est difficile de réaliser une encapsulation correcte en C# étant donné que tous les objets y sont manipulés de manière indirecte, à travers des références. Ce problème est exacerbé dans le cas d'objets référant à des collections comme des tableaux ou des List<T>, comme le montre l'exemple suivant, où quelqu'un pense (naïvement) avoir construit une classe CodeSecret cachant, dans un int[], un code secret difficile à découvrir :
using System;
const int TAILLE_TEST = 5;
int [] valeurs = new int[TAILLE_TEST];
for(int i = 0; i != valeurs.Length; ++i)
valeurs[i] = i + 1;
CodeSecret codeSecret = new (valeurs);
//
// ICI : peut-on briser le code secret en insérant une ou deux lignes?
//
int [] essai = new int[valeurs.Length];
do
{
Console.WriteLine($"Entrez {essai.Length} entiers, un par ligne");
for(int i = 0; i != essai.Length; ++i)
essai[i] = int.Parse(Console.ReadLine());
}
while(!codeSecret.Essayer(essai));
if (codeSecret.NbEssais == 1)
Console.WriteLine("Stupéfiant, vous l'avez trouvé du premier coup!");
else
Console.WriteLine($"Bravo, vous l'avez trouvé en {codeSecret.NbEssais} coups");
class CodeSecret
{
public const int MIN = 1,
MAX = 100;
private int [] Valeurs{ get; }
public int NbEssais { get; private set; }
public CodeSecret(int [] valeurs)
{
Random r = new ();
Valeurs = valeurs;
for(int i = 0; i < Valeurs.Length; ++i)
{
Valeurs[i] = r.Next(MIN, MAX + 1);
}
NbEssais = 0;
}
public bool Essayer(int [] tentative)
{
++NbEssais;
if (tentative.Length != Valeurs.Length)
return false;
for(int i = 0; i != Valeurs.Length; ++i)
if(tentative[i] != Valeurs[i])
return false;
return true;
}
}
Ici, le fait que valeurs dans le programme principal et Valeurs dans codeSecret réfèrent tous deux au même objet a pour conséquence qu'il suffise d'ajouter, à la place du commentaire débutant par ICI dans le programme principal, quelque chose comme :
for(int i = 0; i != valeurs.Length; ++i)
valeurs[i] = 3;
... pour tricher et mettre dans le tableau référé par codeSecret.Valeurs des valeurs qui ne sont pas secrètes du tout.
Il est difficile de se prémunir correctement contre de telles situations. Au minimum, il faut procéder à quelques ajustements :
Il faut tout d'abord faire en sorte que l'objet (l'instance de CodeSecret) ne tienne pas de référence sur un tableau provenant de l'extérieur, mais crée plutôt son propre tableau interne dans lequel elle copiera les valeurs du tableau original. Ceci évite le partage du tableau auquel les deux réfèreraient alors, et règle une partie du problème. De toute manière, il n'était pas utile de passer le tableau valeurs en paramètre à CodeSecret, car les valeurs de ce tableau n'étaient jamais utilisées; seule la taille de ce tableau était pertinente au problème. Nous avons donc ici deux améliorations pour le prix d'une seule |
|
Notez que cette section vise des étudiant(e)s en début de formation, et escamote des tas de techniques pertinentes tout en exposant sous forme de méthodes des services qu'il vaudrait mieux implémenter sous d'autres formes. J'implore votre tolérance.
Quittons le cas du CodeSecret et imaginons une classe TableauEntiers faite maison et offrant quelques services au code client : initialiser les éléments du tableau à une valeur autre que zéro, par exemple; trier les éléments du tableau en ordre croissant ou en ordre décroissant de valeur; inverser l'ordre des éléments, etc.
Une implémentation simpliste pourrait être la suivante :
Présumons tout d'abord une classe static nommée Algos logeant des algorithmes d'ordre général, et une méthode de classe Permuter capable de permuter deux entiers. Ceci nous sera utile pour inverser l'ordre des éléments du tableau; de plus, il n'y a pas de raison pertinente pour en faire un service de TableauEntiers, son rôle étant bien plus général. |
|
La classe TableauEntiers en soi n'est pas particulièrement difficile à rédiger :
Un problème demeure entier cependant : comment devrions-nous permettre l'accès aux éléments d'une instance de TableauEntiers? Après tout, pour le moment, nous avons un tableau partiellement immuable, donc non-modifiable une fois construit (outre pour ce qui a trait à l'ordre de ses éléments). Il serait embêtant, avec la classe TableauEntiers que nous avons présentement, d'écrire quelque chose d'aussi simple qu'une fonction capable d'afficher les éléments du tableau à la console. |
|
Si nous souhaitons permettre l'accès en lecture et en écriture à un élément, une manière de procéder est d'exposer une paire de méthodes en ce sens. À droite, vous trouverez donc une méthode GetValeur, retournant une copie de la valeur à un indice donné, de même qu'une méthode SetValeur, insérant une valeur spécifiée par la fonction appelante à un indice donné dans le tableau. Si nous souhaitions ne permettre que la consultation, sans permettre la modification, nous pourrions qualifier SetValeur de private... ou simplement ne pas l'implémenter. |
|
Équipé de cette paire de méthodes, nous sommes en mesure d'accéder aux éléments d'un TableauEntiers en lecture, comme le fait Afficher(TableauEntiers) à droite, de même qu'en écriture, comme le démontre le programme principal (Main). Clairement, cette approche fonctionne. Dans certains langages, c'est d'ailleurs essentiellement la meilleure solution disponible. |
|
On peut toutefois se questionner sur l'élégance ce cette solution. Après tout, comparez le code permettant d'accéder à l'élément situé à l'indice 3 dans un int[] :
int [] tab = new int[10];
// ...
tab[3] = -1; // écriture
// ...
int n = tab[3]; // lecture
// ...
... avec celui requis pour accéder à l'élément situé à l'indice 3 dans un TableauEntiers :
TableauEntiers tab = new (10);
// ...
tab.SetValeur(3, -1); // écriture
// ...
int n = tab.GetValeur(3); // lecture
// ...
Nous conviendrons que ce n'est pas tout à fait homogène
En pratique, pour en arriver à une solution plus homogène que celle proposée ci-dessus avec les méthodes GetValeur et SetValeur, ce que nous aimerions faire est exposer un opérateur [] pour TableauEntiers, un peu comme on surcharge d'autres opérateurs. Cela ne fonctionnera toutefois pas; C# n'offre pas un traitement aussi complet à la surcharge d'opérateurs que ne le font d'autres langages comme C++ par exemple.
Heureusement, C# offre une syntaxe spéciale pour l'utilisation de [] sur un objet avec les indexeurs, ou propriétés indexées. Cette syntaxe peut surprendre au premier abord, car elle « détonne » un peu dans le décor, mais on finit par s'y habituer
Un indexeur en C# est une notation hybride entre celle de la propriété (il y a une partie get et une partie set, respectivement pour les accès en lecture et en écriture, incluant le paramètre « magique » value dans le cas du set) et de la méthode, au sens où elle s'applique à l'objet référé (this) plutôt que de se présenter comme un état de cet objet. Notez aussi que les paramètres (ici, l'indice) d'un indexeur sont placés entre crochets plutôt qu'entre parenthèses. Exprimé plus simplement, là où la propriété Valeurs d'un TableauEntiers s'accéderait comme suit (pour qui y a accès) :
... l'indexeur d'un TableauEntiers s'utilise quant à lui comme suit :
|
|
Le code client proposé plus haut, mais adapté pour profiter de l'indexeur, devient alors :
// ...
TableauEntiers tab = new (10, -1);
Afficher(tab);
Random r = new ();
for (int i = 0; i != tab.Taille; ++i)
tab[i] = r.Next(0, 100); // <-- ICI
Afficher(tab);
tab.InverserÉléments();
Afficher(tab);
tab.TrierCroissant();
Afficher(tab);
tab.TrierDécroissant();
Afficher(tab);
static void Afficher(TableauEntiers tab)
{
for (int i = 0; i != tab.Taille; ++i)
Console.Write($"{tab[i]} "); // <-- ICI
Console.WriteLine();
}
Un indexeur en C# a toujours au moins un indice, mais il est possible d'écrire des indexeurs à plusieurs indices. Par exemple, toute instance de la classe Matrice3x3 ci-dessous est construite à partir d'un double[,] de taille , et est immuable (donc non-modifiable une fois construite). Il est toutefois possible d'accéder aux éléments en utilisant la notation usuelle avec un tableau à deux dimensions (ici, ligne puis colonne) :
using System;
class Matrice3x3
{
private double [,] Valeurs { get; }
public Matrice3x3(double [,] src)
{
if (src.GetLength(0) != 3 || src.GetLength(1) != 3)
throw new ArgumentException();
Valeurs = new double[3, 3]
{
{ src[0,0], src[0,1], src[0,2] },
{ src[1,0], src[1,1], src[1,2] },
{ src[2,0], src[2,1], src[2,2] }
};
}
public double this[int ligne, int colonne]
{
get => Valeurs[ligne, colonne];
private set
{
Valeurs[ligne, colonne] = value;
}
}
// ...
}
Les indexeurs permettent aussi de proposer des métaphores d'accès autres que celle, positionnelle, d'un indice entier. Imaginons par exemple un Registre de taille fixée à la construction, contenant des paires {nom,valeur} où le nom est une string et la valeur est un Jour.
Pour illustrer l'utilisation (et l'utilité) de ce petit type, un programme est proposé à droite. Ce programme insère des corresponsances français / anglais pour les jours de la semaine, puis affiche ces correspondances en ordre croissant des noms anglais de ces jours. À l'affichage, nous obtiendrons :
|
|
Pour les besoins de l'exemple, donc, les valeurs du Registre seront des instances de Jour, un type énuméré. Nous aurions pu utiliser des types à la fois plus simples (des int, par exemple) ou plus complexes (des classes), mais je souhaitais un exemple de petite taille pour aller à l'essentiel. |
|
Un Registre, comme annoncé plus haut, contiendra un tableau Éléments d'instances du type Entrée, où chaque Entrée fera correspondre à un nom (une string) une valeur (un Jour). La capacité de tableau Éléments sera fixée à la construction, mais le nombre d'éléments réellement insérés (NbÉléments) démarrera à zéro et croîtra à chaque ajout au registre. Tenter d'insérer trop d'éléments mènera à une levée d'exception. Pour fins de simplicité, j'ai inséré les éléments dans Éléments en ordre d'insertion, et j'ai fait une fouille linéaire dans l'indexeur pour retrouver la valeur associée à un certain nom. Notez que nous aurions pu être plus sophistiqués, et trier les éléments en ordre croissant de nom par exemple pour permettre une fouille dichotomique dans l'indexeur. Notre indexeur dans ce cas-ci prend en paramètre une string (pas un int), et fouille dans Éléments pour trouver la valeur (le Jour) correspondant à cette string. Si aucune Entrée dans Éléments ne correspond à ce qui est demandé, une exception est levée. |
|
Quelques liens pour en savoir plus.