配列やリストは arr[0] のように [] でアクセスできますが、自作クラスでも同じ記法を使えると便利なことがあります。C# の インデクサー(Indexer) は、そのための仕組みです。
var container = new WordBook();
container["apple"] = "リンゴ";
Console.WriteLine(container["apple"]); // リンゴ
この記事では次の 4 つのトピックに沿って、インデクサーの使い方を解説します。
- 文字列キーによるインデックスアクセス
- インデクサーメソッドのオーバーロード
- 多次元インデックス
- インターフェース型でのインデクサー定義
インデクサーの基本構文
インデクサーは this[...] というキーワードで定義します。プロパティと同様に get / set アクセサーを持てます。
class MyList
{
private string[] _data = new string[10];
// インデクサーの定義
public string this[int index]
{
get => _data[index];
set => _data[index] = value;
}
}
var list = new MyList();
list[0] = "Hello";
Console.WriteLine(list[0]); // Hello
通常のメソッドと違い、呼び出し側から見れば配列と同じ [] 記法 で値の読み書きができます。
1. 文字列キーによるインデックスアクセス
インデクサーのパラメーターは int に限りません。string をキーに使うことで、辞書(Dictionary)のような読み書きを自作クラスに持たせられます。
using System.Collections.Generic;
class WordBook
{
private readonly Dictionary<string, string> _store = new();
// string キーで読み書きするインデクサー
public string this[string word]
{
get
{
if (_store.TryGetValue(word, out var meaning))
return meaning;
return $"('{word}' は未登録です)";
}
set => _store[word] = value;
}
public int Count => _store.Count;
}
var book = new WordBook();
book["apple"] = "リンゴ";
book["banana"] = "バナナ";
book["cherry"] = "さくらんぼ";
Console.WriteLine(book["apple"]); // リンゴ
Console.WriteLine(book["grape"]); // ('grape' は未登録です)
Console.WriteLine(book.Count); // 3
Dictionary<TKey, TValue> 自体もインデクサーで実装されているため、ラッパークラスとして追加ロジックを差し込みたいときにこのパターンが役立ちます。
2. インデクサーメソッドのオーバーロード
同一クラスにパラメーターの型や数が異なる複数のインデクサーを定義できます。これをインデクサーのオーバーロードといいます。
class DataStore
{
private readonly string[] _array = new string[100];
private readonly Dictionary<string, string> _map = new();
// int インデックスでアクセス
public string this[int index]
{
get => _array[index];
set => _array[index] = value;
}
// string キーでアクセス
public string this[string key]
{
get => _map.TryGetValue(key, out var v) ? v : string.Empty;
set => _map[key] = value;
}
// int + string の組み合わせでアクセス(後述の多次元と組み合わせた例)
public string this[int index, string suffix]
{
get => $"{_array[index]}-{suffix}";
}
}
var store = new DataStore();
// int インデクサー
store[0] = "zero";
Console.WriteLine(store[0]); // zero
// string インデクサー
store["name"] = "Alice";
Console.WriteLine(store["name"]); // Alice
// int + string インデクサー(getter のみ)
Console.WriteLine(store[0, "tag"]); // zero-tag
オーバーロードを活用すると、同一の [] 構文でさまざまな検索軸に対応できます。ただし増やしすぎると可読性が落ちるため、設計に応じて使い分けてください。
3. 多次元インデックス
パラメーターを複数個にすれば、2 次元・3 次元の行列や表形式データを自然に表現できます。
class Matrix
{
private readonly int[,] _data;
public int Rows { get; }
public int Cols { get; }
public Matrix(int rows, int cols)
{
Rows = rows;
Cols = cols;
_data = new int[rows, cols];
}
// 2 次元インデクサー
public int this[int row, int col]
{
get
{
if (row < 0 || row >= Rows || col < 0 || col >= Cols)
throw new IndexOutOfRangeException($"インデックスが範囲外です: [{row}, {col}]");
return _data[row, col];
}
set
{
if (row < 0 || row >= Rows || col < 0 || col >= Cols)
throw new IndexOutOfRangeException($"インデックスが範囲外です: [{row}, {col}]");
_data[row, col] = value;
}
}
}
var m = new Matrix(3, 3);
// 値の書き込み
m[0, 0] = 1; m[0, 1] = 2; m[0, 2] = 3;
m[1, 0] = 4; m[1, 1] = 5; m[1, 2] = 6;
m[2, 0] = 7; m[2, 1] = 8; m[2, 2] = 9;
// 値の読み取り
for (int r = 0; r < m.Rows; r++)
{
for (int c = 0; c < m.Cols; c++)
Console.Write($"{m[r, c],3}");
Console.WriteLine();
}
// 出力:
// 1 2 3
// 4 5 6
// 7 8 9
多次元インデクサーは C# の組み込み多次元配列(int[,])と同じ [row, col] 記法になるため、利用者が直感的に使えます。
4. インターフェース型でのインデクサー定義
インデクサーはインターフェースにも定義できます。実装クラスに対してインデクサーの シグネチャ(型・アクセサー) を強制できるため、依存性の注入(DI)や単体テストの差し替えに役立ちます。
インターフェースの定義
public interface IStringContainer
{
// int インデックスで string を読み書きできることを要求
string this[int index] { get; set; }
}
{ get; set; } の代わりに { get; } だけにすれば読み取り専用インデクサーも定義できます。
インターフェースを実装するクラス
public class SomeClass : IStringContainer
{
private readonly List<string> _items = new();
// IStringContainer のインデクサーを実装
public string this[int index]
{
get
{
if (index < 0 || index >= _items.Count)
throw new ArgumentOutOfRangeException(nameof(index));
return _items[index];
}
set
{
// 必要に応じてリストを拡張
while (_items.Count <= index)
_items.Add(string.Empty);
_items[index] = value;
}
}
public void Add(string item) => _items.Add(item);
public int Count => _items.Count;
}
別の実装クラス(テスト用スタブなど)
public class StubStringContainer : IStringContainer
{
private readonly Dictionary<int, string> _store = new();
public string this[int index]
{
get => _store.TryGetValue(index, out var v) ? v : string.Empty;
set => _store[index] = value;
}
}
利用例
static void PrintAll(IStringContainer container, int count)
{
for (int i = 0; i < count; i++)
Console.WriteLine($"[{i}] = {container[i]}");
}
// 本番クラスで使う
var real = new SomeClass();
real.Add("Alice");
real.Add("Bob");
real.Add("Carol");
PrintAll(real, real.Count);
// [0] = Alice
// [1] = Bob
// [2] = Carol
// スタブで差し替える
var stub = new StubStringContainer();
stub[0] = "Stub-A";
stub[1] = "Stub-B";
PrintAll(stub, 2);
// [0] = Stub-A
// [1] = Stub-B
PrintAll メソッドは IStringContainer に依存しているため、実装クラスを差し替えても変更不要です。
読み取り専用インデクサー
C# 8.0 以降では readonly 修飾子をインデクサーの get アクセサーに付けられます。また、init アクセサー(C# 9.0)を使えばオブジェクト初期化子({ } 構文)での一度限りの書き込みも可能です。
class ReadOnlyGrid
{
private readonly int[,] _data = { { 1, 2 }, { 3, 4 } };
// 読み取り専用インデクサー
public int this[int r, int c] => _data[r, c];
}
まとめ
| トピック | ポイント |
|---|---|
| 基本構文 | public T this[型 param] { get; set; } で定義 |
| 文字列キー | string をパラメーターにすれば辞書風アクセスが可能 |
| オーバーロード | パラメーターの型・数が違えば複数定義できる |
| 多次元 | this[int r, int c] のようにパラメーターを増やすだけ |
| インターフェース | string this[int index] { get; set; } と宣言し実装クラスに強制 |
インデクサーを使いこなすと、自作コレクションや設定クラスが 組み込み配列・辞書と同じ直感的な記法 で扱えるようになります。インターフェースとの組み合わせで依存性の逆転(DIP)も実現しやすくなるので、ぜひ積極的に活用してください。