CodeGym /コース /C# SELF /新しいLINQメソッド: Index

新しいLINQメソッド: Index

C# SELF
レベル 32 , レッスン 3
使用可能

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. 使う時のコツや注意点

使える範囲

IndexIEnumerable<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も要素にアクセスする時にインデックスを計算する。つまり、巨大なコレクションや無限シーケンスでもメモリを爆発させずに正しくインデックスが付くよ。

2
タスク
C# SELF, レベル 32, レッスン 3
ロック未解除
リストの行番号付け
リストの行番号付け
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION