C# Tips | コレクション・LINQ:インデックス付きSelect

C# C#
スポンサーリンク

はじめに:「インデックス付き Select」は“要素の値+位置情報”を同時に扱える便利ワザ

LINQ の Select は「値を変換する」ためのメソッドですが、
実務では「値だけでなく、その要素が何番目か(インデックス)も使いたい」場面がよくあります。

「行番号を付けたい」
「奇数番目だけ処理したい」
「インデックスをキーにして別データと突き合わせたい」
「CSV の 1 行目はヘッダー扱いにしたい」

こういうときに使えるのが インデックス付き Select です。

LINQ の Select には、実は 2 つの引数を取るオーバーロード があり、

Select((value, index) => ...)
C#

という形で「値」と「インデックス」を同時に扱えます。

ここから、初心者向けに

インデックス付き Select の基本
実務でよくある使い方
インデックスの注意点(0 始まり・順序保証)
ユーティリティ化してもっと使いやすくする方法

を、例題付きで丁寧に解説していきます。


インデックス付き Select の基本:Select((value, index) => …)

まずは動きをコードで確認する

using System;
using System.Linq;

var names = new[] { "Alice", "Bob", "Charlie" };

var result = names
    .Select((name, index) => $"{index}: {name}");

foreach (var line in result)
{
    Console.WriteLine(line);
}
C#

出力はこうなります。

0: Alice
0: Bob
2: Charlie

ここでの重要ポイントは、

インデックスは 0 始まり
Select の中で「値」と「インデックス」を同時に使える

ということです。


実務でよくある使い方 1:行番号を付ける

CSV やログの行番号を付けたいとき

var lines = File.ReadAllLines("data.csv");

var numbered = lines
    .Select((line, index) => new
    {
        LineNumber = index + 1,  // 1 始まりにしたい場合
        Text = line
    });

foreach (var x in numbered)
{
    Console.WriteLine($"{x.LineNumber}: {x.Text}");
}
C#

業務では「1 行目はヘッダー」「2 行目以降がデータ」という扱いが多いので、
index + 1 として 1 始まりにするのが定番です。

ここでの重要ポイントは、

「行番号を付ける」という処理を、LINQ の中で自然に書ける

ということです。


実務でよくある使い方 2:インデックスを使ってフィルタリング

偶数行だけ処理したい/奇数行だけ処理したい

var evenLines = lines
    .Select((line, index) => new { line, index })
    .Where(x => x.index % 2 == 0)
    .Select(x => x.line);
C#

ここでの重要ポイントは、

インデックスを使った条件分岐が簡単に書ける

ということです。


実務でよくある使い方 3:インデックスをキーにして別データと突き合わせる

例:2 つのリストを「位置」でマージする

var keys = new[] { "A", "B", "C" };
var values = new[] { 10, 20, 30 };

var merged = keys
    .Select((key, index) => new
    {
        Key = key,
        Value = values[index]
    });
C#

ここでの重要ポイントは、

「同じ位置にあるデータ同士を結びつける」処理が簡単に書ける

ということです。


実務でよくある使い方 4:エラー時に「何番目のデータか」をログに残す

例:データ変換中に例外が出たら「何行目か」を記録したい

var converted = lines
    .Select((line, index) =>
    {
        try
        {
            return ConvertLine(line);
        }
        catch (Exception ex)
        {
            throw new Exception($"Error at line {index + 1}: {ex.Message}");
        }
    })
    .ToList();
C#

ここでの重要ポイントは、

インデックスがあると、エラーの特定が圧倒的に楽になる

ということです。


インデックス付き Select の注意点

インデックスは「0 始まり」

LINQ のインデックスは必ず 0 始まりです。

1 始まりにしたい場合は index + 1 を使います。

並列 LINQ(AsParallel)ではインデックスが保証されない

var result = items
    .AsParallel()
    .Select((value, index) => ...)
C#

これは 危険 です。

PLINQ(並列 LINQ)は順序を保証しないため、
インデックスが期待通りの順番にならないことがあります。

順序が必要なら必ず

.AsParallel()
.AsOrdered()
C#

を付ける必要があります。

ここでの重要ポイントは、

インデックス付き Select は「順次処理」で使うのが基本

ということです。


インデックス付き Select をもっと使いやすくするユーティリティ

「Index()」のような拡張メソッドを作る

毎回 (value, index) と書くのが面倒なら、
「インデックスを付けるだけのユーティリティ」を作ると便利です。

public static class IndexExtensions
{
    public static IEnumerable<(T item, int index)> WithIndex<T>(
        this IEnumerable<T> source)
    {
        return source.Select((item, index) => (item, index));
    }
}
C#

使い方はこうです。

var result = names.WithIndex();

foreach (var (name, index) in result)
{
    Console.WriteLine($"{index}: {name}");
}
C#

ここでの重要ポイントは、

タプルを使うと、インデックス付きデータがとても扱いやすくなる

ということです。


まとめ:「インデックス付き Select」は“位置情報を自然に扱うための道具”

インデックス付き Select の本質は、

「値」と「位置」を同時に扱えるようにすることで、
業務ロジックをシンプルに書けるようにする

ことです。

押さえておきたいポイントをまとめると、

Select((value, index) => ...) でインデックスが使える
行番号付け・奇数行/偶数行処理・エラーログなど実務で大活躍
インデックスは 0 始まり(1 始まりにしたいなら index + 1)
並列 LINQ ではインデックスが保証されないので注意
タプルを返す WithIndex() 拡張メソッドを作るとさらに便利

ここまで理解できれば、
「インデックスが必要な処理」を for 文で書く必要がなくなり、
LINQ のパイプラインの中で自然に表現できるようになります。

タイトルとURLをコピーしました