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' は新しい値だよ
}
}
}
}
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;
}
}
}
}
見覚えある要素たち:
- 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型には使えないよ。
GO TO FULL VERSION