Utiliser des indexeurs (Guide de programmation C#)

Les indexeurs simplifient, d’un point de vue syntaxique, la création d’une classe, d’un struct ou d’une interface auxquels les applications clientes peuvent accéder comme à un tableau. Le compilateur génère une propriété Item (ou une propriété nommée autrement si IndexerNameAttribute est présent) et les méthodes d’accesseur appropriées. Le plus souvent, les indexeurs sont implémentés dans les types dont l’objectif premier est d’encapsuler une collection ou un tableau interne. Prenons l’exemple d’une classe TempRecord qui représente la température, en Fahrenheit, enregistrée à 10 moments différents sur une période de 24 heures. La classe contient un tableau temps de type float[] pour stocker les valeurs de température. En implémentant un indexeur dans cette classe, les clients peuvent accéder aux températures dans une instance TempRecord sous la forme float temp = tempRecord[4] et non sous la forme float temp = tempRecord.temps[4]. La notation d’indexeur non seulement simplifie la syntaxe pour les applications clientes, mais elle permet aussi aux autres développeurs de comprendre de façon plus intuitive la classe et son objectif.

Pour déclarer un indexeur sur une classe ou un struct, utilisez le mot clé this, comme dans l’exemple suivant :

// Indexer declaration
public int this[int index]
{
    // get and set accessors
}

Important

La déclaration d’un indexeur génère automatiquement une propriété nommée Item sur l’objet. La propriété Item n’est pas directement accessible depuis l’expression d’accès au membre de l’instance. De plus, si vous ajoutez votre propre propriété Item à un objet avec un indexeur, vous obtenez une erreur du compilateur CS0102. Pour éviter cette erreur, utilisez IndexerNameAttribute et renommez l’indexeur comme indiqué ci-dessous.

Remarques

Le type d’un indexeur et le type de ses paramètres doivent être au moins aussi accessibles que l’indexeur lui-même. Pour plus d’informations sur les niveaux d’accessibilité, consultez Modificateurs d’accès.

Pour plus d’informations sur l’utilisation d’indexeurs avec une interface, consultez Indexeurs d’interface.

La signature d’un indexeur est composée du nombre et des types de ses paramètres formels. Elle ne comporte ni le type de l’indexeur ni le nom des paramètres formels. Si vous déclarez plusieurs indexeurs dans la même classe, ils doivent avoir des signatures différentes.

Un indexeur n’est pas classé en tant que variable, c’est pourquoi une valeur d’indexeur ne peut pas être passée par référence (en tant que paramètre ref ou out), sauf si sa valeur est une référence (c’est-à-dire qu’elle est retournée par référence).

Pour affecter à l’indexeur un nom exploitable dans d’autres langages, utilisez System.Runtime.CompilerServices.IndexerNameAttribute, comme dans l’exemple suivant :

// Indexer declaration
[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{
    // get and set accessors
}

Cet indexeur a le nom TheItem, car il est remplacé par l’attribut de nom d’indexeur. Par défaut, le nom de l’indexeur est Item.

Exemple 1

L’exemple suivant montre comment déclarer un champ de tableau privé temps, et un indexeur. L’indexeur permet d’accéder directement à l’instance tempRecord[i]. Comme alternative à l’utilisation de l’indexeur, vous pouvez déclarer le tableau comme membre public et accéder directement à ses membres, tempRecord.temps[i].

public class TempRecord
{
    // Array of temperature values
    float[] temps =
    [
        56.2F, 56.7F, 56.5F, 56.9F, 58.8F,
        61.3F, 65.9F, 62.1F, 59.2F, 57.5F
    ];

    // To enable client code to validate input
    // when accessing your indexer.
    public int Length => temps.Length;
    
    // Indexer declaration.
    // If index is out of range, the temps array will throw the exception.
    public float this[int index]
    {
        get => temps[index];
        set => temps[index] = value;
    }
}

Notez que quand l’accès à un indexeur est évalué, par exemple dans une instruction Console.Write, l’accesseur get est appelé. C’est pourquoi une erreur de compilation se produit s’il n’existe aucun accesseur get.

var tempRecord = new TempRecord();

// Use the indexer's set accessor
tempRecord[3] = 58.3F;
tempRecord[5] = 60.1F;

// Use the indexer's get accessor
for (int i = 0; i < 10; i++)
{
    Console.WriteLine($"Element #{i} = {tempRecord[i]}");
}

Indexation avec d’autres valeurs

C# ne limite pas le type de paramètre d’indexeur au type entier. Par exemple, il peut être utile d’utiliser une chaîne avec un indexeur. Il est possible d’implémenter un tel indexeur en recherchant la chaîne dans la collection et en retournant la valeur appropriée. Comme les accesseurs peuvent être surchargés, les versions string et integer peuvent coexister.

Exemple 2

L’exemple suivant déclare une classe qui stocke les jours de la semaine. Un accesseur get prend une chaîne, le nom d’un jour, et retourne l’entier correspondant. Par exemple, « Sunday » retourne 0, « Monday » retourne 1 et ainsi de suite.

// Using a string as an indexer value
class DayCollection
{
    string[] days = ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"];

    // Indexer with only a get accessor with the expression-bodied definition:
    public int this[string day] => FindDayIndex(day);

    private int FindDayIndex(string day)
    {
        for (int j = 0; j < days.Length; j++)
        {
            if (days[j] == day)
            {
                return j;
            }
        }

        throw new ArgumentOutOfRangeException(
            nameof(day),
            $"Day {day} is not supported.\nDay input must be in the form \"Sun\", \"Mon\", etc");
    }
}

Exemple de consommation 2

var week = new DayCollection();
Console.WriteLine(week["Fri"]);

try
{
    Console.WriteLine(week["Made-up day"]);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine($"Not supported input: {e.Message}");
}

Exemple 3

L’exemple suivant déclare une classe qui stocke les jours de la semaine à l’aide de l’enum System.DayOfWeek. Un accesseur get prend un DayOfWeek, la valeur d’un jour, et retourne l’entier correspondant. Par exemple, DayOfWeek.Sunday retourne 0, DayOfWeek.Monday retourne 1, et ainsi de suite.

using Day = System.DayOfWeek;

class DayOfWeekCollection
{
    Day[] days =
    [
        Day.Sunday, Day.Monday, Day.Tuesday, Day.Wednesday,
        Day.Thursday, Day.Friday, Day.Saturday
    ];

    // Indexer with only a get accessor with the expression-bodied definition:
    public int this[Day day] => FindDayIndex(day);

    private int FindDayIndex(Day day)
    {
        for (int j = 0; j < days.Length; j++)
        {
            if (days[j] == day)
            {
                return j;
            }
        }
        throw new ArgumentOutOfRangeException(
            nameof(day),
            $"Day {day} is not supported.\nDay input must be a defined System.DayOfWeek value.");
    }
}

Exemple de consommation 3

var week = new DayOfWeekCollection();
Console.WriteLine(week[DayOfWeek.Friday]);

try
{
    Console.WriteLine(week[(DayOfWeek)43]);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine($"Not supported input: {e.Message}");
}

Programmation fiable

La sécurité et la fiabilité des indexeurs peuvent être améliorées de deux manières principales :

  • N’oubliez pas d’incorporer une stratégie de gestion des erreurs au cas où le code client passerait une valeur d’index non valide. Dans le premier exemple décrit plus haut dans cette rubrique, la classe TempRecord fournit une propriété Length qui permet au code client de vérifier l’entrée avant de la passer à l’indexeur. Vous pouvez également placer le code de gestion des erreurs à l’intérieur de l’indexeur lui-même. N’oubliez pas d’indiquer aux utilisateurs toutes les exceptions que vous levez dans un accesseur d’indexeur.

  • Définissez pour les accesseurs get et set une accessibilité aussi restrictive que possible. Cela est particulièrement important dans le cas de l’accesseur set. Pour plus d’informations, consultez Restriction d’accessibilité de l’accesseur.

Voir aussi