1. 痛みの進化
きっとみんなも経験あると思うけど、コレクションをただループするだけじゃなくて、各要素の番号(インデックス)も知りたい時ってあるよね。例えば、番号付きリストを出力したい時とか、インデックスが0じゃない要素だけ変えたい時とか、3つごとに何かしたい時とか。普通のforなら超簡単:
// いつものやつ、C#のクラシック:
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine($"{i}: {list[i]}");
}
でもLINQで書き始めると…インデックスが消えちゃうんだよね!あのキレイな.Whereや.Select、.OrderByたちは要素しかくれない。インデックスはどこ?ってなる。やりたいのはこういうの:
list.SelectWithIndex((item, index) => ...);
でもSelectWithIndexみたいな標準メソッドはなかったんだよね。Selectのオーバーロードでインデックスも取れるけど…インデックスだけ使いたいとか、変換したくない時は、余計な.Selectを重ねるしかなくて、LINQコードの美しさが台無しになっちゃう。
.NET 9以前のサバイバル術
昔のC#プログラマーは色々なハックを使ってたよ:
var result = list.Select((item, index) => new { item, index });
他にもLINQメソッドの組み合わせとか。でも「組み込み」っぽくないし、やっぱり美しくないんだよね。
2. 新しいLINQメソッド:Indexって何?なんで必要?
公式の説明
.NET 9で開発チームは何万人ものプログラマーの叫び(まあ、Twitterで何万件もあったから十分だよね)を聞いて、LINQに新しい拡張メソッドIndexを追加したんだ。公式ドキュメントの.NET 9ページにもこう書いてある:
Index()は各要素に0から始まるインデックスを付けて、(値, インデックス)のペアを返してくれる。匿名型を自分で作ったり、.Select((item, idx) => new { item, idx })みたいなコードを書かなくていいんだ。
これ、めっちゃ便利!Indexは各要素に対して「要素+インデックス」のペアを返してくれるよ。
メソッドのシグネチャ
public static IEnumerable<(T Element, int Index)> Index<T>(this IEnumerable<T> source);
ざっくり言うと: コレクションの各要素に「Element」とその「Index」のタプルがもらえる。それだけ。もう匿名型を作る必要なし!
3. Indexの使い方例
めっちゃシンプルな例
好きなフルーツのリストを使ってみよう。
var fruits = new List<string> { "りんご", "バナナ", "オレンジ", "キウイ" };
foreach (var (fruit, idx) in fruits.Index())
{
Console.WriteLine($"{idx}: {fruit}");
}
出力:
0: りんご
1: バナナ
2: オレンジ
3: キウイ
こんな感じで、インデックスと値のペアが簡単に取れるし、LINQクエリの中でも使えるよ。
他のLINQメソッドとの連携
IndexはLINQファミリーの一員!普通にチェーンの中に入れられるよ。
例1:インデックスが奇数の要素だけフィルタ
var numbers = Enumerable.Range(10, 10); // 10, 11, ... 19
var oddIndexes = numbers.Index()
.Where(pair => pair.Index % 2 == 1)
.Select(pair => pair.Element);
Console.WriteLine(string.Join(", ", oddIndexes));
出力:
11, 13, 15, 17, 19
例2:インデックスが偶数の要素だけ大文字に変換
var users = new List<string> { "アンナ", "イゴール", "カーチャ", "デニス" };
var modified = users.Index()
.Select(pair => pair.Index % 2 == 0 ? pair.Element.ToUpper() : pair.Element.ToLower());
foreach (var name in modified)
Console.WriteLine(name);
出力:
アンナ
いごーる
カーチャ
でにす
例3:他のコレクションとインデックスで結合(zipっぽい)
var ids = new[] { 101, 102, 103 };
var names = new[] { "Alice", "Bob", "Charlie" };
var merged = names.Index()
.Join(ids.Index(),
namePair => namePair.Index,
idPair => idPair.Index,
(namePair, idPair) => (idPair.Element, namePair.Element));
foreach (var (id, name) in merged)
Console.WriteLine($"{id}: {name}");
出力:
101: Alice
102: Bob
103: Charlie
4. Indexを実アプリで使う
じゃあ、よくある「学習用」アプリに新機能を追加しよう。コレクションの全生徒を番号付きで表示して、ユーザーが番号で生徒を選べるようにする感じ。
例:生徒リストを番号付きで表示
前の例で使ったStudentクラスがあるとする:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
生徒のリストを作るよ:
var students = new List<Student>
{
new Student { Name = "ダーシャ", Grade = 5 },
new Student { Name = "ペーチャ", Grade = 3 },
new Student { Name = "ヴォーヴァ", Grade = 4 },
new Student { Name = "オリャ", Grade = 5 }
};
で、Indexを使って番号付きで表示:
foreach (var (student, idx) in students.Index())
{
Console.WriteLine($"{idx + 1}. {student.Name} — 成績: {student.Grade}");
}
ここで
idx + 1は、インデックスを1から始めたいからだよ(0からだと違和感あるしね)。
結果:
1. ダーシャ — 成績: 5
2. ペーチャ — 成績: 3
3. ヴォーヴァ — 成績: 4
4. オリャ — 成績: 5
実用的なメリット:ユーザーが番号で生徒を選びたい時もバッチリ!手動カウンター不要、読みやすいしバグも減るよ。
5. Indexが昔のやり方よりイケてる理由
.NET 9以前:古いスタイル
前は要素とインデックスを一緒に取りたかったら、.Select((item, index) => ...)のオーバーロードで匿名型を作るしかなかった:
var withIndexes = students.Select((student, index) => new { student, index });
必要なフィールドを取るたびに.studentや.indexって書かないといけないし、型も匿名型で名前もつけられない。
.NET 9以降:21世紀スタイル
今はフィールドや匿名型を気にしなくてOK。超シンプル:
foreach (var (student, idx) in students.Index())
{
// 箱から出してすぐ使える、直感的でキレイ
}
コードがスッキリ。短くてミスも減る。LINQチェーンが長くなっても余計なこと考えなくていい!
6. 使う時のコツや注意点
使える範囲
IndexはIEnumerable<T>を実装してるものなら何でもOK。普通のコレクション、配列、他のLINQクエリの結果でも使えるよ。
Indexの戻り値の型は?
タプルの列挙型を返すよ。1つ目が要素(Element)、2つ目がIndex(型はint)。C#のタプル構文のおかげで、foreach (var (element, index) in ...)って書くだけで両方の値がすぐ使える。
Query Syntaxでも使える?
ダメ、Indexは拡張メソッドだから、クエリ構文(SQLっぽいLINQ)では直接使えない。つまりこういうのはNG:
// 動かない!
var query = from s in students.Index() select ...;
どうしても組み合わせたい時は、メソッドをカッコで囲んで普通のコレクションとして扱えばOK:
var query = from pair in students.Index()
where pair.Index > 1
select pair.Element;
インデックスは必ず0から
Indexは必ず0から始まるよ。C#の他の多くのものと同じ。1から始めたい時は、必要な場所で+1してね。
8. よくあるミスや注意ポイント
初心者のうちはIndexと.Select((item, index) => ...)の違いで混乱しがち。よくあるミスは:
— Indexをクエリ構文で使おうとして「なんで動かないの?」ってなる。これは拡張メソッドだからだよ。
— インデックスが1から始まると思い込むけど、実際は0から。
— Indexが元のコレクションを変えると思ってる人もいるけど、LINQメソッドは全部新しいシーケンスを返すだけで、元は変わらない(イミュータブル方式)。
もう一つ面白い特徴:コレクションが「遅延評価」なら(LINQクエリはデフォで遅延評価)、Indexも要素にアクセスする時にインデックスを計算する。つまり、巨大なコレクションや無限シーケンスでもメモリを爆発させずに正しくインデックスが付くよ。
GO TO FULL VERSION