インデクサのしくみを 実際の C# コード で覗いてみよう
「インデクサって名前は聞いたことがあるけど、結局 list[0] の“角かっこ”は中で何をしているの?」——そんな疑問を抱いたことはありませんか?
本記事では、配列アクセスのように見える [] 記法=インデクサ が、List の内部でどんなメソッドを呼び出し、どのようにデータを取り出しているのかを 最小構成のサンプルコード で確認します。
- 対象読者: C# の基礎文法(変数・配列・メソッド)が分かりはじめた初学者
- ゴール:
- インデクサの正体が「名前のないプロパティ」だと理解する
- List が 固定長配列+自動拡張 というシンプルな仕組みで動いていることを把握する
- 自作クラスにインデクサを実装して「配列のような使い心地」を再現できると知る
記事ではまず 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> を安心して使えます。
ディスカッション
コメント一覧
まだ、コメントがありません