C#の配列をインデックス付きforeachする

前置き

C#の配列をインデックス付きでforeachする方法は純粋なC#では2種類あります。シンプルにforで回すか、LinqでSelectするかです(ループの外にインデックスを宣言すればどの方法でも処理できますが、ここではインデックス用の変数(i)が外に見えない形式を指しています)

int[] array = new int[] { 100, 101, 102, 103, 104 };

// 通常のfor文の利用
for (int i = 0; i < array.Length; i++)
{
    Console.WriteLine($"[{i}] = {array[i]}");
    // > 出力:
    // > [0] = 100
    // > [1] = 101
    // > [2] = 102
    // > [3] = 103
    // > [4] = 104
}

また、配列にインデックス付きのforeachするならLinqでも以下のように記述すればできます。

int[] array = new int[] { 100, 101, 102, 103, 104 };

// Tupleを使ったインデックス付きforeachの記述例
foreach ((int item, int i) in array.Select((x, i) => (x, i)))
{
    result.Add(item);
}

ですが何度も上記の処理を毎回記述すると結構面倒です。

また上記記述方法以外でループの外でインデックス用の変数を用いれば上記手法に囚われずループ中にインデックスが利用できます。

しかし、宣言したインデックスの変数スコープがループ外に見える事が邪魔なケースもあり、出来ればインデックス用の変数はループ外部から見えないほうがいいです。

またLinqでSelectすると処理が結構遅い事もあり、わざわざ配列を選択した局面で遅いアルゴリズムを選択することも少し気になります。PC環境では気になることはそれほど無いですがモバイルだと影響が結構出るかもしれません。

そこで拡張メソッドの仕組みを利用してインデックス付きのforeachを実現したいと思います。

確認環境

確認環境は以下の通りです。

  • C# 7.3
  • .Net Core3.2
  • VisualStudio2019

ArrayExtensionクラス

まず、ArrayExtensionクラスを宣言して以下のように拡張メソッドを定義します。

配列を引数にとり内部でforを回しています。

/// <summary>
/// 配列に対する拡張機能を定義します。
/// </summary>
public static class ArrayExtension
{
    /// <summary>
    /// 指定したコレクションに対してインデックス付き foreach を実行します。途中で中断できません。
    /// 
    /// Func:
    ///   T1 : Tuple(i:インデックス, item:配列要素)
    /// </summary>
    public static void ForEach<T>(this T[] array, Action<(int i/*index*/, T item)> func)
    {
        for (int i = 0; i < array.Length; i++)
        {
            func((i, array[i]));
        }
    }

    /// <summary>
    /// 指定した配列に対してインデックス付き foreach を実行します。途中で中断可能版。
    /// 
    /// Func:
    ///   T1 : Tuple(i:インデックス, item:配列要素)
    ///   TRet : ループを継続するかどうかのフラグ, true:継続する / false:ループを終了
    ///   
    /// 戻り値:
    ///   true : 全要素に対して処理完了 / false : 途中でループ終了
    /// </summary>
    public static bool ForEach<T>(this T[] array, Func<(int i/*index*/, T item), bool> func)
    {
        for (int i = 0; i < array.Length; i++)
        {
            if (!func((i, array[i])))
            {
                return false; // 中断終了
            }
        }
        return true;
    }
}

使用方法

上記クラスの使い方は以下の通りです。

配列のインスタンスに対してForEachを使えるように機能が拡張されます。

メソッドに渡すデリゲートにインデックスと配列要素を格納したTupleが渡されるのでループ中はこれを利用します。

public static void Main(string[] args)
{
    // 配列に対する操作
    int[] array = new int[] { 100, 101, 102, 103, 104 };

    // 全部の要素を列挙して処理を行う
    array.ForEach(p =>
    {
        // p.i にインデックス
        // p.item に配列要素が入ってくる
        Console.WriteLine($"[{p.i}] = {p.item}");
    });
    // > 出力:
    // > [0] = 100
    // > [1] = 101
    // > [2] = 102
    // > [3] = 103
    // > [4] = 104

    // ある条件になったらループを途中終了する
    bool ret1 = array.ForEach(p =>
    {
        if (p.item > 102)
        {
            Console.WriteLine($"[{p.i}]で処理を中断");
            return false;
        }
        Console.WriteLine($"[{p.i}] = {p.item}");
        return true;
    });
    // > 出力:
    // > [0] = 100
    // > [1] = 101
    // > [2] = 102
    // [3]で処理を中断
}

上記メソッドはforで記述したときの処理時間を1としたときにForEachメソッドは1.07倍程度のコストで使用できます。

LinqでSelectすると3.1倍程度なので視認性を天秤にかけてまぁ許せる速度かなと思います。

という訳なのでもしよかったら使用してみてください。