C#は、オブジェクト指向プログラミングを強力にサポートする言語として、開発者が直感的にコードを記述できるさまざまな機能を備えています。

その中でも「インデクサ (Indexer)」は、クラスや構造体のインスタンスを配列のように扱うことができる非常に便利な機能です。

独自のコレクションクラスを作成する際や、内部データをカプセル化しつつ簡潔なアクセス手段を提供したい場合に、インデクサは欠かせない存在となります。

本記事では、インデクサの基本概念から具体的な実装方法、さらには応用的な活用シーンまで、テクニカルな視点で徹底的に解説します。

インデクサとは何か

インデクサとは、クラスや構造体のインスタンスに対して 配列と同じような角括弧 [] を用いたアクセスを可能にする機能 です。

通常、クラス内のデータにアクセスするにはプロパティやメソッドを使用しますが、インデクサを定義することで、あたかもそのオブジェクト自体が配列やリストであるかのように振る舞わせることができます。

インデクサは本質的に「引数を持つプロパティ」と考えることができます。

プロパティと同様に get アクセサと set アクセサを持ち、データの取得と代入のロジックを定義します。

インデクサを使用するメリット

インデクサを導入することで、以下のようなメリットを享受できます。

  • コードの可読性向上:データの集合を扱うオブジェクトに対し、直感的な構文でアクセスできるようになります。
  • カプセル化の維持:内部のデータ保持形式 (配列、Dictionary、データベースなど) を隠蔽したまま、外部に簡潔なインターフェースを提供できます。
  • 柔軟な型指定:添字 (インデックス) には整数だけでなく、文字列や独自の型を使用することが可能です。

インデクサの基本構文と書き方

インデクサを定義するには、this キーワードを使用します。

一般的なプロパティとは異なり、名前を自由に付けることはできず、常に this を指定するのがルールです。

基本的な実装例

まずは、もっともシンプルな整数インデックスを用いた例を見てみましょう。

C#
using System;

namespace IndexerSample
{
    public class SimpleCollection
    {
        // 内部でデータを保持する配列
        private string[] _data = new string[5];

        // インデクサの定義
        public string this[int index]
        {
            get
            {
                // インデックスの範囲チェック
                if (index < 0 || index >= _data.Length)
                {
                    throw new IndexOutOfRangeException("インデックスが範囲外です。");
                }
                return _data[index];
            }
            set
            {
                if (index < 0 || index >= _data.Length)
                {
                    throw new IndexOutOfRangeException("インデックスが範囲外です。");
                }
                _data[index] = value;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SimpleCollection collection = new SimpleCollection();

            // インデクサを使用して値を代入
            collection[0] = "C#";
            collection[1] = "インデクサ";
            collection[2] = "解説";

            // インデクサを使用して値を取得
            Console.WriteLine(collection[0]);
            Console.WriteLine(collection[1]);
            Console.WriteLine(collection[2]);
        }
    }
}
実行結果
C#
インデクサ
解説

この例では、SimpleCollection クラスのインスタンスに対して collection[0] のようにアクセスしています。

内部的には _data 配列の要素を操作していますが、利用側からは配列そのものを操作しているかのように見えます。

getアクセサとsetアクセサの挙動

インデクサはプロパティと同様に、読み取り専用にすることも可能です。

set ブロックを記述しなければ、そのインデクサは 読み取り専用 (Read-only) となります。

また、C# 6.0以降であれば、式形式のメンバー (Expression-bodied members) を使用して簡潔に記述することもできます。

C#
// 読み取り専用インデクサの簡潔な書き方
public string this[int index] => _data[index];

インデクサの応用:多様なインデックス型

インデクサの強力な点は、インデックスの型として int 以外も使用できる ことにあります。

文字列をインデックスに使用する

Dictionary<TKey, TValue> のように、文字列をキーにしてデータにアクセスするインデクサを作成できます。

これは、設定情報の管理や、特定の名前に関連付けられたデータを取得する際に非常に便利です。

C#
using System;
using System.Collections.Generic;

public class LocalizationManager
{
    private Dictionary<string, string> _resources = new Dictionary<string, string>();

    // 文字列をインデックスに取るインデクサ
    public string this[string key]
    {
        get
        {
            return _resources.ContainsKey(key) ? _resources[key] : "Key Not Found";
        }
        set
        {
            _resources[key] = value;
        }
    }
}

// 利用例
var lang = new LocalizationManager();
lang["Submit"] = "送信";
lang["Cancel"] = "キャンセル";
Console.WriteLine(lang["Submit"]); // 出力: 送信

このように、データ構造に合わせた最適な型をインデックスとして定義 することで、クラスの利便性は飛躍的に向上します。

インデクサのオーバーロード

C#ではメソッドと同様に、インデクサも オーバーロード が可能です。

引数の数や型が異なれば、一つのクラスに複数のインデクサを定義できます。

C#
public class DataStore
{
    private List<string> _list = new List<string>();

    // 整数インデックス
    public string this[int index]
    {
        get => _list[index];
        set => _list[index] = value;
    }

    // 文字列インデックスによる検索用(オーバーロード)
    public int this[string value]
    {
        get => _list.IndexOf(value);
    }
}

この例では、インデックスに数値を指定すれば要素の内容を取得でき、文字列を指定すればその内容がリストの何番目にあるかを検索できる、といった多機能なクラスを実現しています。

多次元インデクサの実装

配列に2次元配列 (int[,]) があるように、インデクサも複数の引数を取ることができます。

これは、グリッドデータや行列計算、座標系を扱うクラスに最適です。

C#
using System;

public class Matrix
{
    private double[,] _storage = new double[3, 3];

    // 2つの引数を持つインデクサ
    public double this[int row, int col]
    {
        get => _storage[row, col];
        set => _storage[row, col] = value;
    }
}

class Program
{
    static void Main()
    {
        Matrix mat = new Matrix();
        mat[0, 0] = 1.0;
        mat[1, 2] = 5.5;
        
        Console.WriteLine($"[1, 2] の値: {mat[1, 2]}");
    }
}
実行結果
[1, 2] の値: 5.5

多次元インデクサを使用することで、mat[0][0] (ジャグ配列形式) ではなく mat[0, 0] という直感的な形式でアクセスできるようになります。

インデクサとインターフェース

インデクサはインターフェース内で宣言することも可能です。

これにより、特定のアクセス方法を強制する抽象的なコレクション設計が可能になります。

C#
public interface ICustomCollection
{
    // インターフェースでのインデクサ宣言(実装は持たない)
    string this[int index] { get; set; }
    int Count { get; }
}

public class MyList : ICustomCollection
{
    private string[] _items = new string[10];
    public int Count => _items.Length;

    public string this[int index]
    {
        get => _items[index];
        set => _items[index] = value;
    }
}

インターフェースで定義する場合、プロパティと同様に getset の有無のみを指定します。

実装クラスでは、それに基づいて具体的なロジックを記述します。

最新のC#におけるインデクサの進化

近年のC# (C# 8.0以降) では、Index型Range型 が導入されました。

これらに対応するインデクサを実装することで、末尾からのアクセス (^1) や範囲指定 (1..3) といったモダンな構文を自作クラスでもサポートできます。

IndexとRangeへの対応

以下の例は、最新の構文に対応したインデクサの実装方法を示しています。

C#
using System;
using System.Linq;

public class ModernContainer
{
    private string[] _items = { "Apple", "Banana", "Cherry", "Date", "Elderberry" };

    // Index型への対応 (末尾アクセスなど)
    public string this[Index index] => _items[index];

    // Range型への対応 (スライス操作)
    public string[] this[Range range] => _items[range];
}

class Program
{
    static void Main()
    {
        var container = new ModernContainer();

        // 末尾の要素を取得
        Console.WriteLine($"最後: {container[^1]}");

        // 範囲指定で取得
        var subSet = container[1..4];
        Console.WriteLine($"範囲抽出: {string.Join(", ", subSet)}");
    }
}
実行結果
最後: Elderberry
範囲抽出: Banana, Cherry, Date

IndexやRangeを活用することで、従来の Length - 1 のような冗長な記述を排除し、安全かつ簡潔なコードを書くことが可能になります。

インデクサ設計時の注意点とベストプラクティス

インデクサは非常に便利ですが、誤った使い方をするとコードの混乱を招く可能性があります。

以下のガイドラインを意識して設計しましょう。

1. 意味的に適切か検討する

インデクサは、そのオブジェクトが「データの集まり」である場合にのみ使用すべきです。

例えば、Customer クラスに対して customer[0] とアクセスして「顧客名」を取得するような設計は避けるべきです。

この場合は customer.Name というプロパティを使うのが適切です。

2. 予期しない例外を避ける

インデックスが範囲外の場合、C#の標準的な挙動に合わせて IndexOutOfRangeException をスローするのが一般的です。

しかし、Dictionaryのような振る舞いをさせる場合は、キーが存在しないときに例外を投げるのか、あるいは null を返すのか、設計方針を一貫させる必要があります。

3. パフォーマンスに注意する

インデクサ内部で重い処理 (データベースクエリや複雑な計算) を行うのは避けましょう。

利用者は [] によるアクセスが高速 (O(1) または O(log n)) であることを期待しています。

重い処理が必要な場合は、メソッドとして定義する方が、利用者に対してコストがかかる処理であることを明示できます。

4. 適切なアクセシビリティの設定

インデクサ全体を public にしつつ、set アクセサだけを privateprotected に設定することも可能です。

C#
public string this[int index]
{
    get => _data[index];
    private set => _data[index] = value; // クラス内部からのみ更新可能
}

インデクサの内部的な仕組み (CLRの視点)

C#のコンパイラは、インデクサを Item という名前の特別なプロパティとしてコンパイルします。

IL (Intermediate Language) レベルでは、メソッドの引数としてインデックスを受け取るプロパティとして表現されます。

もし、他の言語 (インデクサをサポートしていない言語) からこのC#ライブラリを呼び出す場合、その言語からは get_Itemset_Item というメソッドとして見えることになります。

[IndexerName("MyIndexer")] 属性を使用することで、この内部的な名称を変更することも可能です。

まとめ

インデクサは、C#においてオブジェクトの操作性を大幅に向上させる強力な機能です。

  • 基本this[index] 構文を使い、インスタンスを配列のように扱う。
  • 柔軟性:整数だけでなく、文字列や多次元のインデックス、さらには IndexRange にも対応可能。
  • 設計思想:論理的に「データの集合」と見なせるクラスに適用し、直感的なインターフェースを提供する。

適切に実装されたインデクサは、クラスの利用側にとって非常に使い勝手の良いコードをもたらします。

内部の実装を隠蔽しつつ、外部にはシンプルで標準的なアクセス手段を提供する。

このカプセル化の原則に基づいた設計こそが、C#らしい洗練されたプログラミングの鍵となります。

今回解説した基本から応用までの知識を活用し、より堅牢で美しいC#プログラムの実装に役立ててください。