CodeGym /Cours /C# SELF /Membres d’extension : indexeurs

Membres d’extension : indexeurs

C# SELF
Niveau 18 , Leçon 4
Disponible

1. Introduction

Imagine, on a notre classe préférée DogShelter, qui stocke une collection de chiens. Dans les leçons précédentes, on a déjà pu lui ajouter un indexeur pour récupérer un chien par son numéro dans le refuge : Dog firstDog = myShelter[0];. C’est cool !

Mais si notre utilisateur veut choper un chien non pas par numéro, mais, genre, par surnom ? Ou par race ? Ou même par une combinaison de critères ? Bien sûr, on pourrait ajouter des méthodes du genre GetDogByName("Buddy") ou GetDogByBreedAndAge("Labrador", 5). Et franchement, c’est une approche tout à fait normale.

Mais parfois, on aimerait bien que l’accès soit plus “array-like” et intuitif. Genre, pouvoir écrire : Dog buddy = myShelter["Buddy"]; ou Dog oldLab = myShelter["Labrador", 8];.

Si DogShelter est notre propre classe, on peut juste ajouter de nouveaux indexeurs dedans. Mais si DogShelter vient d’une lib externe qu’on ne peut pas modifier ? Ou peut-être qu’on veut ajouter un accès super spécifique qui ne doit pas “polluer” la classe principale ?

C’est là que débarquent les indexeurs d’extension (Extension Indexers) !

2. Les “crochets” depuis l’extérieur

Tu te souviens, dans la leçon précédente, on avait ajouté une propriété d’extension DisplayName pour Dog ? Avec les indexeurs, c’est pareil !

Un indexeur d’extension, c’est un indexeur statique défini dans une classe statique, qui te permet d’utiliser la syntaxe obj[index] sur des objets de types déjà existants, même si ces types n’avaient pas d’indexeur à la base, ou si tu veux ajouter un indexeur avec un autre type de paramètre.

C’est comme si t’achetais un frigo, et qu’après tu trouvais comment faire pour qu’en tapant à un endroit précis, il te file une bouteille de coca. Le frigo reste le même, mais tu lui as ajouté une fonctionnalité “de l’extérieur” !

Syntaxe d’un indexeur d’extension


public static class MyExtensionClass
{
    extension(ObjectType instance)
    {    
        public static ReturnType this[TypeIndex index ]
        {
            get
            {
                // Logique de lecture, en utilisant instance et index
                return ...;
            }
            set
            {
                // En utilisant instance, index et le mot-clé 'value'
                // 'value' — c’est la nouvelle valeur
            }
        }
    }        
}
Syntaxe d’un indexeur d’extension (Extension Indexer)

Regarde bien le this TypeObjetAÉtendre instance. Cette syntaxe est exactement la même que pour les méthodes et propriétés d’extension. instance, c’est comment on va appeler l’objet qu’on étend, à l’intérieur de nos accesseurs get et set.

3. Comment déclarer un Extension Indexer (et pas se cramer le cerveau) ?

La syntaxe ressemble aux Extension Properties dont on a parlé la dernière fois, mais avec des paramètres d’index. Voilà un exemple minimal :


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {      
        public static Dog this[string name]
        {
            get
            {
                foreach (var dog in shelter)
                {
                    if (dog.Name == name)
                        return dog;
                }
                return null;
            }
        }
    }        
}
Extension Indexer par surnom pour DogShelter

Éléments familiers :

  • this devant le premier paramètre — c’est obligatoire pour les Extension Members (objet à étendre).
  • Après le nom de la classe, tu mets la liste des paramètres qui seront utilisés dans les crochets.
Ça marche presque comme les indexeurs classiques, sauf que tu ne touches pas à la classe d’origine !

Pratique : On étend DogShelter avec un indexeur par surnom

On va modifier notre projet d’exemple. Imagine qu’on a un refuge pour chiens, et que chaque chien a un surnom unique :

Classe DogShelter (librairie/code externe)

public class Dog
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class DogShelter : IEnumerable<Dog>
{
    private List<Dog> dogs = new List<Dog>();

    public void AddDog(Dog dog) => dogs.Add(dog);

    // Ancien indexeur par numéro
    public Dog this[int index]
    {
        get => dogs[index];
        set => dogs[index] = value;
    }

    public IEnumerator<Dog> GetEnumerator() => dogs.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Ce qu’on veut : shelter["Busya"]

Avant — seulement via une méthode :

// Avant C# 14 :
public static Dog? FindByName(this DogShelter shelter, string name) { ... }

Maintenant — via un Extension Indexer :


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {    
        public static Dog? this[string name]
        {
            get
            {
                foreach (var dog in shelter)
                    if (dog.Name == name)
                        return dog;
                return null; 
            }
            set
            {
                for (int i = 0; i < shelter.Count; i++)
                {
                    if (shelter[i].Name == name)
                    {
                        shelter[i] = value!;
                        return;
                    }
                }
                throw new ArgumentException("Chien non trouvé");
            }
        }
    }        
}

Maintenant, notre code principal est bien plus stylé :

var shelter = new DogShelter();
shelter.AddDog(new Dog { Name = "Busya", Age = 3 });
shelter.AddDog(new Dog { Name = "Tuzik", Age = 5 });

// On utilise l’extension-indexeur !
Dog busya = shelter["Busya"]!;
Console.WriteLine(busya.Age);

shelter["Busya"] = new Dog { Name = "Busya", Age = 4 };

Visualisation : qu’est-ce qui se passe ?

Opération Comment ça marchait avant Avec Extension Indexer
Recherche par surnom shelter.FindByName("X") shelter["X"]
Modification d’un chien par surnom shelter.UpdateName("X", ..) shelter["X"] = ...

4. Subtilités et particularités des Extension Indexers

Le compilateur et la portée

  • L’Extension Indexer doit être déclaré dans une classe statique publique (comme les extension methods classiques).
  • N’oublie pas d’ajouter le bon using. Si tu oublies — le compilateur ne dit rien, mais ton code ne compile pas.
  • Si la classe de base a déjà un indexeur comme ça — tu ne peux pas l’étendre (les signatures doivent être différentes).

Implémentation du set-accessor

Tu peux déclarer seulement le get (alors l’indexeur sera en lecture seule). Ou ajouter aussi le set (comme dans l’exemple plus haut) — du coup tu pourras lire et écrire via ton indexeur.

Passage par valeur et par référence

L’Extension Indexer bosse avec l’instance de l’objet sur lequel tu ajoutes l’extension (this devant le premier paramètre). Si l’objet est un type référence, tu modifies son état.

Plusieurs indexeurs dans une même classe

Aucun souci — tu peux déclarer plusieurs extension-indexers avec des ensembles de paramètres différents ! Par exemple, chercher par âge : shelter[5] (l’ancien), shelter["Busya"] (le nouveau), shelter[age: 3] (encore un autre, si tu veux).

Exemple : on ajoute deux indexeurs à DogShelter


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {       
        // Par surnom
        public static Dog? this[string name]
        {
            get => shelter.FirstOrDefault(d => d.Name == name);
            set
            {
                for (int i = 0; i < shelter.Count; i++)
                    if (shelter[i].Name == name)
                        shelter[i] = value!;
            }
        }

        // Par âge — retourne le premier chien trouvé de cet âge
        public static Dog? this[int age]
        {
            get => shelter.FirstOrDefault(d => d.Age == age);
        }
    }        
}

Maintenant tu peux écrire :

var youngDog = shelter[1];           // Par âge
var tony = shelter["Tony"];          // Par surnom
shelter["Tuzik"] = new Dog { Name = "Tuzik", Age = 9 };

Scénarios réels

  • Librairies externes : Tu veux ajouter d’autres façons d’indexer à une classe externe, sans toucher au code source. Par exemple, bosser avec une collection de commandes, les trouver par numéro, date, statut, etc., sans dupliquer des méthodes wrappers.
  • “Pattern adaptateur” : Tu transformes une vieille collection avec une API “nulle” en une API moderne, concise, plus “C#-like”, sans casser la compatibilité.
  • Migration de code legacy : Tu ajoutes des nouvelles features à des types déjà écrits, sans toucher au code existant ni aux tests.
  • Pratique pour les tests : Tu peux ajouter des indexeurs temporaires pour tes besoins (genre, chercher par des critères uniques pour le test), sans polluer la classe principale.

5. Erreurs typiques et pièges avec les Extension Indexers

Si la classe de base a déjà un indexeur avec exactement la même signature, l’extension-indexeur ne sera pas appelé — c’est l’indexeur de base qui a la priorité.

L’extension-indexeur, c’est toujours un extension member, et sans le bon using (import du namespace), l’extension ne sera pas visible.

Autre erreur fréquente — retourner null sans prévenir l’utilisateur. Si quelqu’un, par erreur, demande un élément qui n’existe pas, et que l’extension-indexeur retourne null, ça peut causer un NullReferenceException ailleurs dans le code. C’est une bonne pratique de réfléchir à ce que ta méthode doit faire : lancer une exception, retourner un objet spécial “bouchon”, ou juste retourner null.

Si tu as plusieurs extension-indexers, fais gaffe à leur unicité par types et nombre de paramètres. Par exemple, tu ne peux pas créer deux indexeurs avec la même signature — le compilateur va râler.

Les extension-indexers ne marchent qu’avec des instances d’objets, pas avec des types statiques.

1
Étude/Quiz
Découverte des indexeurs, niveau 18, leçon 4
Indisponible
Découverte des indexeurs
Indexeurs et Extension Members
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION