CodeGym /コース /C# SELF /Extension Members: インデクサ

Extension Members: インデクサ

C# SELF
レベル 18 , レッスン 4
使用可能

1. はじめに

例えば、みんな大好きなクラス DogShelter があって、犬のコレクションを持ってるとするよ。前のレクチャーでは、インデクサを追加して、番号で犬を取得できるようにしたよね:Dog firstDog = myShelter[0];。これ、便利だよね!

でも、もしユーザーが番号じゃなくて、例えば名前で犬を取得したい場合は?それとも犬種で?もしくは複数条件の組み合わせとか?もちろん、GetDogByName("Buddy")GetDogByBreedAndAge("Labrador", 5) みたいなメソッドを追加するのもアリ。

でも、もっと「配列っぽく」直感的にアクセスしたい時もあるよね。例えば:Dog buddy = myShelter["Buddy"];Dog oldLab = myShelter["Labrador", 8]; みたいに書きたい!

DogShelter が自作クラスなら、普通にインデクサを追加すればいい。でも、DogShelter が外部ライブラリのクラスで、編集できない場合は?あるいは、超ニッチなアクセス方法を追加したいけど、元のクラスを「汚したくない」場合は?

そんな時に登場するのが拡張インデクサ(Extension Indexersだよ!

2. "角括弧"を外から追加する

前回のレクチャーで DisplayName って拡張プロパティを Dog に追加したの覚えてる?インデクサもほぼ同じノリでいける!

拡張インデクサは、staticクラスの中で定義するstaticなインデクサで、obj[インデックス] みたいな構文を、もともとインデクサがなかった型や、違う型のパラメータで使えるようにしてくれるんだ。

例えるなら、冷蔵庫を買ってきて、特定の場所を叩くとコーラが出てくる仕組みを後付けする感じ。冷蔵庫自体はそのままだけど、外から機能を追加できるってわけ!

拡張インデクサの構文


public static class MyExtensionClass
{
    extension(ObjectType インスタンス)
    {    
        public static ReturnType this[インデックスタイプ index ]
        {
            get
            {
                // インスタンスindex を使って値を取得するロジック
                return ...;
            }
            set
            {
                // インスタンスindex、キーワード 'value' を使う
                // 'value' は新しい値だよ
            }
        }
    }        
}
拡張インデクサ(Extension Indexer)の構文

this 拡張対象オブジェクトの型 インスタンス に注目してね。この構文は、拡張メソッドや拡張プロパティで見たのと全く同じ。インスタンス は、getやsetの中で拡張するオブジェクトを指す名前だよ。

3. Extension Indexerの宣言方法(頭が爆発しないように!)

構文は前回やったExtension Propertiesとほぼ同じだけど、インデックス用のパラメータがあるだけ。最小限の例を見てみよう:


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;
            }
        }
    }        
}
DogShelter用の名前検索Extension Indexer

見覚えある要素たち:

  • this が最初のパラメータの前にある ― これはExtension Members(拡張対象オブジェクト)のお約束。
  • クラス名の後に、角括弧で使うパラメータリストが続く。
ほぼ普通のインデクサと同じだけど、元のクラスをいじらなくていいのがポイント!

実践:DogShelterに名前検索インデクサを追加しよう

じゃあ、実際にやってみよう。犬のシェルターがあって、犬は名前でユニークだとするよ:

DogShelterクラス(ライブラリ/他人のコード)

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

    // 以前からある番号インデクサ
    public Dog this[int index]
    {
        get => dogs[index];
        set => dogs[index] = value;
    }

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

やりたいこと: shelter["ブーシャ"]

前はメソッドでしかできなかった:

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

今は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("犬が見つかりません");
            }
        }
    }        
}

これでメインのコードがめっちゃスッキリするよ:

var shelter = new DogShelter();
shelter.AddDog(new Dog { Name = "ブーシャ", Age = 3 });
shelter.AddDog(new Dog { Name = "トゥジク", Age = 5 });

// extensionインデクサを使う!
Dog busya = shelter["ブーシャ"]!;
Console.WriteLine(busya.Age);

shelter["ブーシャ"] = new Dog { Name = "ブーシャ", Age = 4 };

ビジュアルで理解しよう:何が起きてる?

操作 昔のやり方 Extension Indexer
名前で検索 shelter.FindByName("X") shelter["X"]
名前で犬を更新 shelter.UpdateName("X", ..) shelter["X"] = ...

4. Extension Indexersの細かい話と特徴

コンパイラとスコープ

  • Extension Indexerはpublicなstaticクラスで宣言しないとダメ(普通のextension methodsと同じ)。
  • 必要なusingを忘れずに!忘れるとコンパイラは何も言わず、コードがコンパイルされない。
  • もし元のクラスに同じインデクサがあったら、拡張できない(シグネチャが違う必要あり)。

setアクセサの実装

get だけでもOK(読み取り専用インデクサになる)。set も追加すれば(上の例みたいに)、読み書き両方できるよ。

値渡しと参照渡し

Extension Indexerは、拡張を付けるインスタンス(thisが最初のパラメータ)で動く。オブジェクトが参照型なら、その状態を直接変えられるよ。

1つのクラスに複数インデクサ

全然OK!パラメータの組み合わせを変えて、いくつでもextensionインデクサを宣言できる。例えば年齢で検索:shelter[5](昔からあるやつ)、shelter["ブーシャ"](新しいやつ)、shelter[age: 3](さらに追加したいやつ)とか。

例:DogShelterに2つのインデクサを追加


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {       
        // 名前で
        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!;
            }
        }

        // 年齢で ― 最初に見つかったその年齢の犬を返す
        public static Dog? this[int age]
        {
            get => shelter.FirstOrDefault(d => d.Age == age);
        }
    }        
}

これでこんな書き方ができる:

var youngDog = shelter[1];           // 年齢で
var tony = shelter["トニー"];          // 名前で
shelter["トゥジク"] = new Dog { Name = "トゥジク", Age = 9 };

リアルなユースケース

  • 外部ライブラリ: サードパーティのクラスに追加のインデクサを付けたい時、元のソースをいじらずに済む。例えば注文コレクションを番号や日付、ステータスで検索したい時、ラッパーメソッドを増やさずに済む。
  • 「アダプタパターン」: 古い「イケてない」APIのコレクションを、今風でC#っぽいAPIに変換できる。しかも互換性は壊さない。
  • レガシーコードの移行: 既存の型に新機能を追加したいけど、既存コードやテストは触りたくない時に便利。
  • テストのしやすさ: テスト用に一時的なインデクサを付けて、テスト固有の条件で検索できる。メインのクラスを汚さずに済む。

5. Extension Indexersでよくあるミスや落とし穴

もし元のクラスに全く同じシグネチャのインデクサがあったら、extensionインデクサは呼ばれない ― 基本のインデクサが優先されるよ。

Extensionインデクサも他のextension memberと同じで、using(名前空間のインポート)がないと拡張が見えない。

ありがちなミス:null を返して、ユーザーに何も伝えないこと。もし間違って存在しない要素にアクセスして、extensionインデクサがnullを返すと、他の場所でNullReferenceExceptionが発生するかも。ちゃんと考えて、例外を投げるか、ダミーオブジェクトを返すか、nullでいいのか決めよう。

複数のextensionインデクサを持つ場合は、パラメータの型や数がユニークになるように注意。全く同じシグネチャのインデクサは作れない ― コンパイラがエラーを出すよ。

Extensionインデクサはインスタンスオブジェクトにしか使えない。static型には使えないよ。

2
タスク
C# SELF, レベル 18, レッスン 4
ロック未解除
数値リスト用の拡張インデクサーを追加しよう
数値リスト用の拡張インデクサーを追加しよう
1
アンケート/クイズ
インデクサー入門、レベル 18、レッスン 4
使用不可
インデクサー入門
インデクサーとExtension Members
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION