C#を利用してアプリケーションを開発する際、データの集合を扱うために「配列」は非常に頻繁に使用されるデータ構造です。

しかし、C#の配列には「一度作成するとサイズを変更できない」という重要な特性があります。

そのため、他のプログラミング言語のように「特定の要素を物理的に削除して配列を詰める」という操作を直接行うことはできません。

配列から要素を削除したい場合には、要素を取り除いた新しい配列を再作成するか、動的なサイズ変更が可能な別のコレクションクラスへ変換するといった工夫が必要になります。

本記事では、初心者から中級者の方に向けて、C#の配列から要素を削除するための具体的な手法を、パフォーマンスや使い勝手の観点から詳しく解説します。

C#における配列の基本的な性質

C#の配列は、メモリ上に連続した領域を確保するデータ構造であり、その長さはインスタンス化された時点で固定されます。

この制約は、メモリ管理の効率化や高速なアクセスを可能にするためのものですが、要素の追加や削除といった動的な操作には不向きです。

例えば、5つの要素を持つ配列から1つの要素を削除したい場合、内部的には「4つの要素を持つ新しい配列を作成し、必要なデータをコピーする」という手順を踏む必要があります。

C#にはこの操作を簡潔に記述するためのライブラリや機能が豊富に用意されているため、状況に応じて最適な手法を選択することが重要です。

配列とList<T>の違い

要素の削除が頻繁に発生する場合には、最初から配列ではなくList<T>を使用するのが一般的です。

特徴配列 (T[])List<T>
サイズ固定 (不変)可変 (動的)
要素の削除新しい配列の作成が必要Removeメソッドで可能
メモリ効率非常に高い内部で予備の領域を確保する
パフォーマンスランダムアクセスが最速配列に匹敵するが僅かにオーバーヘッドあり

List<T>へ変換して削除する方法

最も一般的かつ直感的な方法は、配列を一度List<T>に変換し、要素を削除してから再び配列に戻す手法です。

この方法はコードの可読性が高く、削除条件の指定も容易であるため、パフォーマンスが極限まで求められる場面以外では第一選択となります。

特定の値で削除する

特定の値を指定して削除する場合は、Removeメソッドを使用します。

このメソッドは、最初に見つかった一致する要素を削除します。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // 元の配列
        string[] fruits = { "Apple", "Banana", "Cherry", "Banana" };

        // Listに変換
        List<string> list = fruits.ToList();

        // "Banana"を削除(最初の1つだけ削除される)
        list.Remove("Banana");

        // 配列に戻す
        fruits = list.ToArray();

        // 結果の出力
        Console.WriteLine("削除後の配列:");
        foreach (var item in fruits)
        {
            Console.WriteLine(item);
        }
    }
}
実行結果
削除後の配列:
Apple
Cherry
Banana

インデックスを指定して削除する

削除したい位置(インデックス)がわかっている場合は、RemoveAtメソッドを利用します。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 30, 40, 50 };

        List<int> list = numbers.ToList();

        // インデックス2(30)を削除
        list.RemoveAt(2);

        numbers = list.ToArray();

        Console.WriteLine(string.Join(", ", numbers));
    }
}
実行結果
10, 20, 40, 50

LINQを使用して特定の条件で削除する

C#の強力な機能であるLINQ (Language Integrated Query)を使用すると、条件に合致する要素を除外した新しい配列を非常に簡潔に生成できます。

実質的には「削除」ではなく、「残したい要素だけを抽出(フィルタリング)する」という考え方になります。

Whereメソッドによるフィルタリング

Whereメソッドを使用すると、複雑な条件指定が可能です。

例えば、「30以上の値を持つ要素をすべて削除(=30未満の要素だけを残す)」といった操作が1行で記述できます。

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] scores = { 55, 80, 42, 95, 30, 60 };

        // 80点以上の要素を削除(80点未満のみを抽出)
        scores = scores.Where(s => s < 80).ToArray();

        Console.WriteLine("80点未満のスコア:");
        foreach (var score in scores)
        {
            Console.WriteLine(score);
        }
    }
}
実行結果
80点未満のスコア:
55
42
30
60

LINQを使用する際の注意点

LINQは非常に便利ですが、内部的には反復処理と新しい配列の確保が行われています。

そのため、ループの中で何度もLINQによる削除を繰り返すと、パフォーマンスが著しく低下する恐れがあります。

大量のデータを扱うループ処理内では、後述する別の手法を検討してください。

Array.Clearで要素をリセットする

「配列のサイズはそのままで、特定の中身だけを消したい」という場合には、Array.Clearメソッドが有効です。

これは厳密には要素の「削除」ではなく、「規定値(デフォルト値)へのリセット」を行います。

Array.Clearの使い方

数値型であれば「0」、参照型(クラスや文字列)であれば「null」に置き換わります。

C#
using System;

class Program
{
    static void Main()
    {
        string[] data = { "A", "B", "C", "D", "E" };

        // インデックス1から2つの要素をクリアする
        Array.Clear(data, 1, 2);

        Console.WriteLine("クリア後の配列内容:");
        for (int i = 0; i < data.Length; i++)
        {
            // nullの場合は(null)と表示
            Console.WriteLine($"[{i}]: {data[i] ?? "(null)"}");
        }
    }
}
実行結果
クリア後の配列内容:
[0]: A
[1]: (null)
[2]: (null)
[3]: D
[4]: E

この手法は、配列の長さを変えずに「データが無効であること」を示したい場合に適しています。

また、メモリ領域をそのまま再利用するため、新しい配列を生成するよりも高速に動作します。

インデックスと範囲(Indices and Ranges)による削除

C# 8.0以降では、「インデックス」と「範囲(レンジ)」という構文が導入されました。

これを利用すると、配列の一部を切り出して結合する操作が直感的に記述できます。

特定のインデックスをスキップして結合

例えば、中央の要素を除いた「前半」と「後半」を結合することで、擬似的に要素を削除できます。

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] original = { 1, 2, 3, 4, 5 };
        int indexToRemove = 2; // '3'を削除したい

        // 範囲演算子 [..] を使用して分割し、Concatで結合
        int[] result = original[..indexToRemove]
                        .Concat(original[(indexToRemove + 1)..])
                        .ToArray();

        Console.WriteLine(string.Join(", ", result));
    }
}
実行結果
1, 2, 4, 5

original[..indexToRemove] は「最初から指定インデックスの前まで」、original[(indexToRemove + 1)..] は「指定インデックスの次から最後まで」を意味します。

非常にモダンな書き方ですが、これも内部的には新しい配列を作成している点に留意してください。

パフォーマンスを重視した手動コピー

非常に大きな配列を扱う場合や、ガベージコレクション(GC)の発生を最小限に抑えたいハイパフォーマンスな場面では、Array.Copyを使用して手動で配列を再構築する方法が最も効率的です。

Array.Copyによる効率的な実装

以下のコードは、特定のインデックスを削除する処理を手動で実装した例です。

C#
using System;

class Program
{
    static void Main()
    {
        int[] source = { 100, 200, 300, 400, 500 };
        int removeIndex = 1; // 200を削除

        // 1つ小さいサイズの配列を作成
        int[] destination = new int[source.Length - 1];

        // 削除位置より前の部分をコピー
        Array.Copy(source, 0, destination, 0, removeIndex);

        // 削除位置より後の部分をコピー
        Array.Copy(source, removeIndex + 1, destination, removeIndex, source.Length - removeIndex - 1);

        Console.WriteLine("手動コピーによる削除後:");
        Console.WriteLine(string.Join(", ", destination));
    }
}
実行結果
手動コピーによる削除後:
100, 300, 400, 500

Array.Copyは、メモリブロックのコピーを低レベルで高速に行うため、LINQやList変換よりも高速に動作する傾向があります。

Span<T>を活用した非破壊的な「削除」

C# 7.2で導入されたSpan<T>ReadOnlySpan<T>を使用すると、新しい配列を作成することなく、元の配列の「特定の範囲」だけを参照できます。

これは厳密には削除ではありませんが、「特定の要素を除外して処理を進める」場合には最強のパフォーマンスを発揮します。

スライシングによる範囲指定

C#
using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 0, 1, 2, 3, 4, 5 };

        // 最初の要素と最後の要素を「除外」したビューを作成
        // 新しい配列の割り当て(アロケーション)は発生しない
        Span<int> span = numbers.AsSpan(1, 4);

        Console.WriteLine("Spanによる参照(1〜4):");
        foreach (var n in span)
        {
            Console.Write(n + " ");
        }
    }
}
実行結果
Spanによる参照(1〜4):
1 2 3 4

Span<T>はスタック上に割り当てられる構造体であり、ヒープメモリを汚さないため、GCの負荷を完全にゼロに抑えつつ部分的なデータ操作が可能です。

重複要素を削除して一意にする方法

配列から重複した要素を削除して、ユニークな値だけの配列にしたい場合は、LINQのDistinctメソッド、もしくはHashSet<T>を利用するのが効率的です。

HashSet<T>を利用した高速な重複削除

HashSet<T>は重複を許さないコレクションであり、検索や追加が非常に高速です。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        string[] tags = { "C#", "Java", "C#", "Python", "Java", "Ruby" };

        // HashSetに渡すことで重複が自動的に排除される
        HashSet<string> set = new HashSet<string>(tags);

        // 配列に戻す
        string[] uniqueTags = set.ToArray();

        Console.WriteLine("重複削除後:");
        Console.WriteLine(string.Join(", ", uniqueTags));
    }
}
実行結果
重複削除後:
C#, Java, Python, Ruby

状況別の最適な選択肢

ここまで紹介した手法を、どのような場面で使うべきか整理しました。

目的推奨される手法理由
手軽に削除したいList<T>へ変換メソッドが豊富で分かりやすい
複雑な条件で消したいLINQ (Where)宣言的に書けてバグが混入しにくい
パフォーマンスが最優先Array.Copyオーバーヘッドが最小限
GCを避けたいSpan<T>メモリ割り当てが発生しない
サイズを変えたくないArray.Clearnullや0で埋めるだけで済む
重複を消したいHashSet<T>処理速度が速く、一意性が保証される

まとめ

C#の配列は固定長であるため、要素を「削除」するという操作は、本質的に「条件に合う要素で構成された新しい配列を作り直す」ことを意味します。

モダンなC#開発においては、可読性と生産性を重視してLINQのWhereList<T>への変換を利用するのが一般的です。

一方で、ゲーム開発やリアルタイムシステムなどの高負荷な環境では、Array.Copyによる手動コピーや、Span<T>によるメモリ効率化が大きな武器となります。

それぞれの特徴を正しく理解し、プロジェクトの要件に合わせて最適な削除方法を選択しましょう。

もし頻繁に要素の増減が発生するデータ構造が必要な場合は、設計段階でList<T>LinkedList<T>Queue<T>といった動的コレクションの採用を検討することも忘れないでください。