【C#】配列に要素を追加・削除する、中身をシャッフルする

C#の配列に要素を追加したり削除したり中身をランダム化する方法の紹介です。

配列は一度宣言してしまうとサイズ変更は(基本的に)できないです。そういった事がした場合は動的リストの「System.Collections.Generic」名前空間にある「List」を使用しますが、パフォーマンスや制約などで配列でデータを持っているとろへ要素を追加したりしたくなったのでコードを書いてみました。

先に注意点ですが、(基本的に)できないことを行っているのでパフォーマンスが同様の操作をListに対して行うよりだいぶ悪いと思います。もし要素の追加・削除を頻繁に行うようであればListの仕様を検討ください。

実装コード

かなり長いので、このコードはコピペして使い方の項目まで飛ばしても問題ありません。

ArrayExtensionクラス

各操作を配列に対する拡張メソッドの形式で定義しています。

using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 配列に対する拡張機能を提供します。
/// </summary>
public static class ArrayExtension
{
    //
    // (1) 配列の要素に対する基本操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Exists{T}(T[], Predicate{T})"/> を標準の検索方法で拡張メソッド化します。
    /// </summary>
    public static bool Exists<T>(this T[] array, T item) => Array.Exists(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Exists{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// </summary>
    public static bool Exists<T>(this T[] array, Predicate<T> match) => Array.Exists(array, match);

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Find{T}(T[], Predicate{T})"/> を標準の検索方法で拡張メソッド化します。
    /// </summary>
    public static T Find<T>(this T[] array, T item) => Array.Find(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Find{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// </summary>
    public static T Find<T>(this T[] array, Predicate<T> match) => Array.Find(array, match);

    /// <summary>
    /// 配列に対する操作 <see cref="Array.FindIndex{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// (見つからない場合-1)
    /// </summary>
    public static int FindIndex<T>(this T[] array, T item) => Array.FindIndex(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.FindIndex{T}(T[], Predicate{T})"/> を準の検索方法で拡張メソッド化します。
    /// (見つからない場合-1)
    /// </summary>
    public static int FindIndex<T>(this T[] array, Predicate<T> match) => Array.FindIndex(array, match);

    //
    // (2) 配列に要素を追加する
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列の先頭に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertTop<T>(this T[] array, T value)
    {
        var newArray = new T[array.Length + 1];
        newArray[0] = value;
        Array.Copy(array, 0, newArray, 1, array.Length);
        return newArray;
    }

    /// <summary>
    /// 配列の最後に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertLast<T>(this T[] array, T value)
    {
        var newArray = new T[array.Length + 1];
        Array.Copy(array, 0, newArray, 0, array.Length);
        newArray[newArray.Length - 1] = value;
        return newArray;
    }

    /// <summary>
    /// 指定した位置に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] Insert<T>(this T[] array, int index, T value)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        var newArray = new T[array.Length + 1];
        Array.Copy(array, 0, newArray, 0, index); // インデックスより前
        newArray[index] = value;
        Array.Copy(array, index, newArray, index + 1, array.Length - index);

        return newArray;
    }

    /// <summary>
    /// 指定した位置に collection で指定した要素を連続で追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertRange<T>(this T[] array, int index, IEnumerable<T> collection)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        int len = collection.Count();
        var newArray = new T[array.Length + len];

        Array.Copy(array, 0, newArray, 0, index); // インデックスより前

        int i = 0;
        foreach (var item in collection)
        {
            newArray[index + i++] = item;
        }
        Array.Copy(array, index, newArray, index + len, array.Length - index);

        return newArray;
    }

    //
    // (3) 特定の要素を削除
    // - - - - - - - - - - - - - - - - - - - -

    // 補足:
    // 以下の削除処理は、メモリ効率と動作速度がかなり悪いため
    // 頻繁にこのような操作が発生する場合は System.Collections.Generic.List<T> の使用を検討すること。

    /// <summary>
    /// 配列から指定した位置の要素を削除した新しい配列を取得します。
    /// </summary>
    public static T[] RemoveAt<T>(this T[] array, int index)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        var newArray = new T[array.Length - 1];
        Array.Copy(array, 0, newArray, 0, index); // インデックスより前
        Array.Copy(array, index + 1, newArray, index, array.Length - index - 1); // インデックスより後

        return newArray;
    }

    /// <summary>
    /// 配列のいちばん最初に見つかった1つの要素を削除し新しい配列を取得します。削除されなかった場合null を返します。
    /// </summary>
    public static T[] RemoveFirst<T>(this T[] array, T item)
    {
        int index = array.FindIndex(item);
        if (index == -1) return null;

        return array.RemoveAt(index);
    }

    /// <summary>
    /// 指定した条件を満たす配列のいちばん最初に見つかった要素を削除し新しい配列を取得します。
    //// 削除されなかった場合null を返します。
    /// </summary>
    public static T[] RemoveFirst<T>(this T[] array, Predicate<T> match)
    {
        int index = array.FindIndex(match);
        if (index == -1) return null;

        return array.RemoveAt(index);
    }

    /// <summary>
    /// 指定した要素と同じ値を配列から全て削除します。削除されなかった場合 null を返します。
    /// </summary>
    public static T[] RemoveAll<T>(this T[] array, T item) => array.RemoveAll(elem => elem.Equals(item));

    /// <summary>
    /// 指定した条件を満たす要素を配列から全て削除します。削除されなかった場合 null を返します。
    /// </summary>
    public static T[] RemoveAll<T>(this T[] array, Predicate<T> match)
    {
        var list = new List<T>();
        for (int i = 0; i < array.Length; i++)
        {
            if (!match(array[i]))
            {
                list.Add(array[i]);
            }
        }
        return list.Count == array.Length ? null : list.ToArray();
    }

    //
    // (4) 配列に対するランダムな操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列からランダムに要素を1つ取り出します。
    /// </summary>
    public static T PickupOne<T>(this T[] array)
    {
        return array[Rand.Next(0, array.Length)];
    }

    /// <summary>
    /// 配列からランダムに1つ要素を取り出した後その要素を配列から削除します。
    /// </summary>
    public static (T[] /*newArray*/, T /*poppedItem*/) PickupOneAndRemove<T>(this T[] array)
    {
        var newArray = new T[array.Length - 1];
        int index = Rand.Next(0, array.Length);
        T item = array[index];
        for (int i = 0, j = 0; i < array.Length; i++)
        {
            if (i == index)
            {
                continue;
            }
            newArray[j++] = array[i];
        }
        return (newArray, item);
    }

    /// <summary>
    /// 指定した配列をランダムに並び替えます。
    /// </summary>
    public static void Shuffle<T>(this T[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array.Swap(i, Rand.Range(0, array.Length));
        }
    }

    // 元の配列はそのままで新しいランダムな配列を作る取得します。

    /// <summary>
    /// 指定した配列からランダム化された新しい配列を作成・取得します。
    /// </summary>
    public static T[] GetNewRandomArray<T>(this T[] array)
    {
        var newArray = new T[array.Length];
        Array.Copy(array, newArray, array.Length);
        newArray.Shuffle();
        return newArray;
    }

    /// <summary>
    /// 配列の指定した2つのインデックス間の値を入れ替えます。
    /// </summary>
    public static void Swap<T>(this T[] array, int i, int j)
    {
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //
    // (5) Linq風の便利な操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 指定した配列をリストに変換します。
    /// </summary>
    public static List<T> ToList<T>(this T[] array)
    {
        var list = new List<T>();
        for (int i = 0; i < array.Length; i++)
        {
            list.Add(array[i]);
        }
        return list;
    }

    /// <summary>
    /// 指定した配列を述語に従って新しい型の配列に変換します。
    /// </summary>
    public static Dest[] Convert<T, Dest>(this T[] array, Func<T, Dest> func)
    {
        var newArray = new Dest[array.Length];
        for (int i = 0; i < array.Length; i++)
        {
            newArray[i] = func(array[i]);
        }
        return newArray;
    }

    /// <summary>
    /// 配列に対する操作 <see cref="Array.ForEach{T}(T[], Action{T}))"/> を拡張メソッド化します。
    /// </summary>
    public static void ForEach<T>(this T[] array, Action<T> action)
    {
        Array.ForEach(array, action);
    }

    //
    // (6) 配列に対するソート操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// <see cref="Array.Sort(Array)"/> を拡張メソッド化します。
    /// </summary>
    public static void Sort<T>(this T[] array) => Array.Sort(array);

    /// <summary>
    /// <see cref="Array.Sort{T}(T[], Comparison{T})"/> を拡張メソッド化します。
    /// </summary>
    public static void Sort<T>(this T[] array, Comparison<T> comparer) => Array.Sort(array, comparer);
}

使い方

以下、上記クラスの使い方になります。

配列にに要素が含まれているかどうかを確認・検索する(Exist, Find系)

要素の追加・削除などの前に配列に要素が存在するかどうかを確認するExist、要素がどの位置に存在するのかを確認するFindメソッドの使い方になります。

public static void Main(params string[] args)
{
    //
    // (1) 配列の要素に対する基本操作
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 4, 5, 6 };

    // (1-1) 要素が存在するか確認する
    string msg = array.Exists(3) ? "要素は存在します。" : "要素は存在しません。";
    // > msg = 要素は存在します。

    // (1-2) 条件を指定の要素が存在するか確認する
    string msg2 = array.Exists(elem => elem >= 3) ? "要素は存在します。" : "要素は存在しません。";
    // > msg2 = 要素は存在します。

    // (2-1) 指定した値を取得
    var value = array.Find(5);
    // > value = 5

    // (2-2) 条件を指定して要素を取得
    int value2 = array.Find(elem => elem > 5); // 5より大きい最初の値を取得(例としてちょっと微妙
    // > value2 = 6

    // (3-1) 指定した位置の要素を取得
    int index = array.FindIndex(3);
    Console.WriteLine($"3 は index={index} の位置にに存在します。");

    // (3-2) 指定した条件に一致する要素を取得
    int index2 = array.FindIndex(elem => elem >= 3);
    Console.WriteLine($"elem > 3 を満たす要素は index={index2} の位置にに存在します。");
}

配列に値を追加・挿入する(Insert系)

配列に対して値を追加・挿入する操作の説明です。

注意点として、Insertすると配列の要素数が増えますが、元の配列はそのままにして戻り値に要素が追加された配列が帰ってくるので追加したら以降はその配列を使用します。

public static void Main(params string[] args)
{
    //
    // (2) 配列に要素を追加する
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (1) 先頭に要素を追加する
    int[] array1 = array.InsertTop(99);
    // > array1 = 99, 1, 2, 3, 4, 5

    // (2) 末尾に要素を追加する
    int[] array2 = array.InsertLast(99);
    // > array2 = 1, 2, 3, 4, 5, 99

    // (3) 指定した位置に要素を追加する
    int[] array3 = array.Insert(2, 99);
    // > array3 = 1, 2, 99, 3, 4, 5

    // (4) 指定した位置に複数の要素を追加する
    int[] array4 = array.InsertRange(3, new int[] { 99, 98, 97, 96 });
    // > array4 = 1, 2, 3, 99, 98, 97, 96, 4, 5
}

配列から値を削除する(Remove系)

配列から特定の要素を削除する処理の説明です。

これもInsert系と同じく元の配列はそのままで、戻り値に値が削除された配列が戻るので以降これを使用します。

public static void Main(params string[] args)
{
    //
    // (3) 特定の要素を削除
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 3, 4, 4, 5 };

    // (1) 指定した位置の要素を削除
    int[] array1 = array.RemoveAt(3);
    // array1 = 1, 2, 3, 4, 4, 5 (3がひとつ削除された新しい配列が戻る)

    // (2-1) 最初に見つかった要素を削除
    int[] array2 = array.RemoveFirst(2);
    // array2 = 1, 3, 3, 4, 4, 5 (2が削除された新しい配列が戻る)

    // (2-2) 指定した条件を満たす最初に見つかった要素を削除
    int[] array3 = array.RemoveFirst(elem => elem > 3); // 3より大きい最初の要素を削除
    // array3 = 1, 2, 3, 3, 4, 5 (4がひとつ削除された新しい配列が戻る)

    // (3-1) 指定した値に一致する要素をすべて削除
    int[] array4 = array.RemoveAll(3);
    // array4 = 1, 2, 4, 4, 5 (3が全部削除された新しい配列が戻る)

    // (3-2) 指定した条件に一致するすべての要素を削除
    int[] array5 = array.RemoveAll(elem => elem > 3); // 3より大きい要素を全部削除
    // array5 = 1, 2, 3, 3
}

配列に対しランダムな操作を行う

以下は、ゲームでは割とよく使う配列に対するランダムな操作の説明です。

配列からランダムに1つ値を取り出す(PickupOneメソッド)

配列の中からランダムに値を1つ選択して取り出します。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // ランダムに1つ取り出す
    int item = array.PickupOne();
    // > 2
}
配列からランダムに1つ値を取り出して要素を削除する(PickupOneAndRemoveメソッド)

上記のPickupメソッドに似ていますが、取り出した後に値を配列から値を削除します。

削除後の配列と取り出したデータは戻り値のタプルで受け取れます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // ランダムに1つ取り出して要素を削除
    var (newArray, poppedItem) = array.PickupOneAndRemove();
    // > newArray  = 1, 3, 4, 5
    // > popedItem = 2
}
配列の内容をシャッフルする(Shuffleメソッド)

指定した配列の中身を全てランダムにシャッフルします。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // 配列をランダムに並び替える
    array.Shuffle();
    // > array = 3, 5, 1, 4, 2
}
シャッフルした新しい配列を取得する(GetNewRandomArrayメソッド)

上記のShuffleメソッドは元の配列に対してシャッフルしましたが、こちらは元の配列はそのままにしてシャッフル済みの新しい配列を戻り値で受け取れます。

public static void Main(params string[] args)
{
    // ランダムな新しい配列を取得する(元の配列はそのまま)
    var newArray2 = array.GetNewRandomArray();
    // > newArray2 = 2, 1, 5, 4, 3
}

Linq風の便利な操作

以下は、System.Linq風の処理を配列にも適用できるようにした処理です。

配列をリスト(List)に変換する(ToListメソッド)

配列をリスト(List)に変換します。List.ToArray()はLinqにあるのに逆が無いみたいなので実装しています。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (1) 配列をリストに変換する
    List<int> list = array.ToList(); // 同じ型のリストに変換できる
}
配列を別の型の配列に変換する(Convertメソッド)

配列を別の型の配列に変換します。各要素に対してラムダ式で変換方法を指定することによって別の型に変換することができます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (2) 配列を他の型の配列に変換する
    float[] array2 = array.Convert(elem => (float)elem); // ラムダで変換条件を指定する
}
配列をforeachする(ForEachメソッド)

配列は通常のforeach文で使用できますが、このForEachメソッドを使うと処理内容をラムダで指定してLinq風に回す事ができます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (2) 配列を他の型の配列に変換する
    float[] array2 = array.Convert(elem => (float)elem); // ラムダで変換条件を指定する
}

配列に対するソート処理(Sort系)

以下、配列をソートする方法です。指定方法は性的メソッドの「Array.Sort」の方が多彩ですが、このコードを入れておくと配列に対するメソッドとして処理を行うことができるようになります。多分この方が直観的なのかと思います。機能不足の場合は各自実装を追加したほうがいいかもしれません。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };
    array.Shuffle(); // (4)で紹介した内容をランダム化で内容をランダム化しておく
    // > 3, 1, 4, 5, 2

    // (1) 配列をソートする
    array.Sort();
    // > 1, 2, 3, 4, 5

    // (2) 整列条件を指定して配列をソートする
    array.Sort((a, b) => b - a); // この場合逆順の指定
    // > 5, 3, 4, 2, 1
}

リンク

ランダム化で使用しているRandクラスは以下リンクに掲載しているのでそちらも併せて確認ください。

takachan.hatenablog.com

すごく長くなってしましたが以上です。