インデクサのしくみを 実際の C# コード で覗いてみよう

「インデクサって名前は聞いたことがあるけど、結局 list[0] の“角かっこ”は中で何をしているの?」——そんな疑問を抱いたことはありませんか?

本記事では、配列アクセスのように見える [] 記法=インデクサ が、List の内部でどんなメソッドを呼び出し、どのようにデータを取り出しているのかを 最小構成のサンプルコード で確認します。

  • 対象読者: C# の基礎文法(変数・配列・メソッド)が分かりはじめた初学者
  • ゴール:
    1. インデクサの正体が「名前のないプロパティ」だと理解する
    2. List が 固定長配列+自動拡張 というシンプルな仕組みで動いていることを把握する
    3. 自作クラスにインデクサを実装して「配列のような使い心地」を再現できると知る

記事ではまず SimpleList という超軽量コレクションを実装し、[] での読み書き・サイズ拡張・境界チェックを 60 行ほどのコードで追跡します。実務で List をそのまま使う場合でも、内部構造を一度イメージしておくと“なぜ高速なのか・いつ遅くなるのか”を説明できるようになります。

それでは、角かっこの裏側を一緒に覗いてみましょう。

以下は 極端にシンプル化 した自作コレクション SimpleList<T>。

内部に 配列 _items を持ち、[] 記法(インデクサ)で要素を読み書きできます。

SimpleListIndexerDemoプロジェクトとして作成しましょう

using System;

public class SimpleList<T>
{
    private T[] _items = new T[4];   // 初期容量
    private int _size = 0;           // 現在の要素数

    /// 要素数を返す (List<T>.Count 相当)
    public int Count => _size;

    /// ===== インデクサ =====
    public T this[int index]
    {
        get
        {
            if (index < 0 || index >= _size)
                throw new ArgumentOutOfRangeException(nameof(index));
            return _items[index];
        }
        set
        {
            if (index < 0 || index >= _size)
                throw new ArgumentOutOfRangeException(nameof(index));
            _items[index] = value;
        }
    }

    /// 要素を末尾に追加 (List<T>.Add 相当)
    public void Add(T value)
    {
        if (_size == _items.Length)
            Resize();               // 配列が満杯なら 2 倍に拡張

        _items[_size++] = value;    // 代入してサイズを 1 増やす
    }

    private void Resize()
    {
        int newCap = _items.Length * 2;
        T[] bigger = new T[newCap];
        Array.Copy(_items, bigger, _items.Length);
        _items = bigger;
    }
}
要素説明
public外部から参照できる公開プロパティ
int返す値の型は 整数
Countプロパティ名。「現在格納されている要素数」を示す慣例的な名前
=> _size;式(expression-bodied)プロパティ。get { return _size; } を 1 行で書いた省略形

なぜ _sizeをそのまま返すだけで良いのか

  • _size は Add() や要素の削除メソッド(今回省略)が値を増減させるたびに更新される「現在の要素数」フィールド。
  • Count はそれを読み取る “読み取り専用インターフェース” として機能する。
  • 書き込み (set) が不要なので 読み取り専用 にしている。

普通の書き方との比較

// 従来のフル構文
public int Count
{
    get { return _size; }
}

⬇︎

// C# 6 以降の式形式プロパティ
public int Count => _size;
  • 振る舞いは全く同じ。可読性向上と行数削減のために後者を採用。

補足

  • Count が常に O(1)(一定時間)で読めるのは、単にフィールドを返しているだけで計算コストがないため。
  • List など標準コレクションも同様に Count プロパティで要素数を公開している。

これで public int Count => _size; が「現在の要素数を返す読み取り専用プロパティ」を簡潔に表現していることがわかります。

使い方サンプル

var nums = new SimpleList<int>();
nums.Add(10);
nums.Add(20);
nums.Add(30);

Console.WriteLine(nums[0]);   // 10
nums[1] = 99;                 // 書き換え
Console.WriteLine(nums[1]);   // 99
Console.WriteLine($"Count = {nums.Count}"); // 3

// 範囲外なら例外
// Console.WriteLine(nums[3]); // ← ArgumentOutOfRangeException
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleListIndexerDemo
{
    public class SimpleList<T>
    {
        private T[] _items = new T[4];   // 初期容量
        private int _size = 0;           // 現在の要素数

        /// 要素数を返す (List<T>.Count 相当)
        public int Count => _size;

        /// ===== インデクサ =====
        public T this[int index]
        {
            get
            {
                if (index < 0 || index >= _size)
                    throw new ArgumentOutOfRangeException(nameof(index));
                return _items[index];
            }
            set
            {
                if (index < 0 || index >= _size)
                    throw new ArgumentOutOfRangeException(nameof(index));
                _items[index] = value;
            }
        }

        /// 要素を末尾に追加 (List<T>.Add 相当)
        public void Add(T value)
        {
            if (_size == _items.Length)
                Resize();               // 配列が満杯なら 2 倍に拡張

            _items[_size++] = value;    // 代入してサイズを 1 増やす
        }

        private void Resize()
        {
            int newCap = _items.Length * 2;
            T[] bigger = new T[newCap];
            Array.Copy(_items, bigger, _items.Length);
            _items = bigger;
        }
    }
    internal class Program
    {
        static void Main(string[] args)
        {
            var nums = new SimpleList<int>();
            nums.Add(10);
            nums.Add(20);
            nums.Add(30);

            Console.WriteLine(nums[0]);   // 10
            nums[1] = 99;                 // 書き換え
            Console.WriteLine(nums[1]);   // 99
            Console.WriteLine($"Count = {nums.Count}"); // 3

            // 範囲外なら例外
            // Console.WriteLine(nums[3]); // ← ArgumentOutOfRangeException
        }
    }
}

ここだけ押さえよう

ポイント一行まとめ
内部構造固定長配列 を隠し持ち、足りなくなると 2 倍にコピー
インデクサthis[int index] によって obj[index] 記法を提供
速度読み書きは配列アクセスなので O(1)(とても速い)
例外範囲外アクセスは 必ず例外。Count を確認して防ぐ

これが List<T> の核心部分をギュッと凝縮したモデルです。

「中は配列」「[] はインデクサで get_Item / set_Item を呼んでいる」という仕組みさえ覚えれば、実務で List<T> を安心して使えます。

訪問数 4 回, 今日の訪問数 4回

C#,インデクサ

Posted by hidepon