C#のList.RemoveAll完全ガイド|条件に一致する要素を一括削除する使い方と注意点

はじめに

C#でList<T>から条件に一致する要素をまとめて削除したい場合は、RemoveAllメソッドが便利です。

RemoveAllを使うと、「偶数をすべて削除する」「特定の文字列を含む要素を削除する」「指定したIDのオブジェクトをまとめて削除する」といった処理を簡潔に記述できます。ループ内で要素を1件ずつ削除する必要がなく、削除漏れやインデックスのずれも防ぎやすくなります。

この記事では、C#のList.RemoveAllの構文、基本的な使い方、オブジェクトの削除方法、ほかの削除メソッドとの違い、注意点、パフォーマンスまで詳しく解説します。

1. C#のList.RemoveAllとは

1-1. 条件に一致する要素を一括削除するメソッド

List<T>.RemoveAllは、指定した条件に一致する要素をリストからすべて削除するメソッドです。

たとえば、次のコードでは数値リストから値が3の要素をすべて削除しています。

C#
var numbers = new List<int> { 1, 3, 2, 3, 4, 3 };

numbers.RemoveAll(number => number == 3);

Console.WriteLine(string.Join(", ", numbers));
// 1, 2, 4

Removeメソッドが最初に見つかった1件だけを削除するのに対し、RemoveAllは条件を満たす要素をすべて削除します。

同じ値が複数含まれている場合や、複雑な条件によって複数の要素を削除したい場合に適しています。

1-2. RemoveAllの構文と引数Predicate<T>

RemoveAllの基本構文は次のとおりです。

C#
int 削除件数 = list.RemoveAll(削除条件);

メソッドの定義は次のような形です。

C#
public int RemoveAll(Predicate<T> match);

引数にはPredicate<T>型の条件判定処理を指定します。

Predicate<T>は、要素を1つ受け取り、その要素を削除するかどうかをbool値で返すデリゲートです。

C#
bool 条件判定(T item)
{
return 削除する場合はtrue;
}

通常はラムダ式を使って、次のように記述します。

C#
numbers.RemoveAll(number => number < 0);

この例では、number < 0trueになる負の数が削除されます。

重要なのは、条件式がtrueになった要素が残るのではなく、削除されるという点です。

1-3. 戻り値として削除した要素数を取得する方法

RemoveAllの戻り値は、実際に削除された要素の件数です。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

int removedCount = numbers.RemoveAll(number => number % 2 == 0);

Console.WriteLine($"削除件数: {removedCount}");
// 削除件数: 3

この例では、246の3件が削除されるため、戻り値は3です。

削除対象が1件も存在しない場合は0が返されます。

C#
int removedCount = numbers.RemoveAll(number => number > 100);

Console.WriteLine(removedCount);
// 0

戻り値を確認すれば、削除処理が実際に行われたかを判定できます。

C#
if (removedCount > 0)
{
Console.WriteLine($"{removedCount}件を削除しました。");
}
else
{
Console.WriteLine("削除対象はありませんでした。");
}

1-4. RemoveAllを利用できるList<T>の基本

RemoveAllは、List<T>が提供しているメソッドです。

C#
var numbers = new List<int>();
var names = new List<string>();
var users = new List<User>();

Tには、intstringなどの組み込み型だけでなく、独自に定義したクラスも指定できます。

一方、配列にはRemoveAllメソッドがありません。

C#
int[] numbers = { 1, 2, 3 };

// 配列では呼び出せない
// numbers.RemoveAll(...);

配列から条件に一致しない要素だけを取得したい場合は、LINQのWhereを利用できます。

C#
int[] filteredNumbers = numbers
.Where(number => number != 2)
.ToArray();

また、変数の型がIList<T>IEnumerable<T>の場合も、インターフェースにはRemoveAllが定義されていないため、そのままでは呼び出せません。

C#
IList<int> numbers = new List<int> { 1, 2, 3 };

// numbers.RemoveAll(number => number == 2); // 呼び出せない

RemoveAllを直接利用する場合は、基本的にList<T>型として扱います。

2. List.RemoveAllの基本的な使い方

2-1. 数値リストから特定の値を削除する

数値リストから特定の値をすべて削除するには、等価比較を使用します。

C#
var numbers = new List<int> { 10, 20, 30, 20, 40 };

numbers.RemoveAll(number => number == 20);

Console.WriteLine(string.Join(", ", numbers));
// 10, 30, 40

値が20である要素は2件ありますが、RemoveAllを使うと両方とも削除されます。

特定の値以外を削除することもできます。

C#
numbers.RemoveAll(number => number != 30);

この場合は、値が30ではない要素がすべて削除され、30だけが残ります。

2-2. 数値リストから指定範囲の要素を削除する

指定した範囲に含まれる数値を削除する場合は、比較演算子と論理演算子を組み合わせます。

次の例では、10以上20以下の要素を削除しています。

C#
var numbers = new List<int> { 5, 10, 15, 20, 25, 30 };

numbers.RemoveAll(number => number >= 10 && number <= 20);

Console.WriteLine(string.Join(", ", numbers));
// 5, 25, 30

範囲外の要素を削除したい場合は、OR条件を使用します。

C#
numbers.RemoveAll(number => number < 10 || number > 20);

この条件では、10以上20以下の要素だけがリストに残ります。

2-3. 文字列リストから一致する要素を削除する

文字列リストでも、数値と同じように条件を指定できます。

C#
var fruits = new List<string>
{
"apple",
"banana",
"orange",
"banana"
};

fruits.RemoveAll(fruit => fruit == "banana");

Console.WriteLine(string.Join(", ", fruits));
// apple, orange

完全一致だけでなく、ContainsStartsWithEndsWithなどを使った部分一致による削除も可能です。

C#
fruits.RemoveAll(fruit => fruit.Contains("app"));

ただし、リストにnullが含まれる可能性がある場合は、文字列メソッドを呼び出す前にnullを考慮する必要があります。

2-4. ラムダ式を使って削除条件を記述する

RemoveAllでは、削除条件をラムダ式で記述するのが一般的です。

C#
list.RemoveAll(item => 条件式);

数値が負数かどうかを判定する場合は、次のように記述します。

C#
var numbers = new List<int> { -2, -1, 0, 1, 2 };

numbers.RemoveAll(number => number < 0);

処理が複数行になる場合は、波かっこを使ったステートメント形式のラムダ式も利用できます。

C#
numbers.RemoveAll(number =>
{
bool isNegative = number < 0;
return isNegative;
});

単純な条件であれば1行のラムダ式、複雑な条件であれば複数行のラムダ式や別メソッドを使うと読みやすくなります。

2-5. 削除件数と処理後のリストを確認する

RemoveAllの実行結果を確認するときは、戻り値と処理後のリストを出力します。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

int removedCount = numbers.RemoveAll(number => number >= 4);

Console.WriteLine($"削除件数: {removedCount}");
Console.WriteLine($"処理後: {string.Join(", ", numbers)}");

実行結果は次のとおりです。

削除件数: 3
処理後: 1, 2, 3

戻り値には削除件数が入り、元のnumbers自体が変更されます。

3. 条件別に見るList.RemoveAllの実践例

3-1. 偶数・奇数の要素を一括削除する

偶数を削除する場合は、値を2で割った余りが0かどうかを判定します。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

numbers.RemoveAll(number => number % 2 == 0);

Console.WriteLine(string.Join(", ", numbers));
// 1, 3, 5

奇数を削除する場合は、余りが0ではないことを判定します。

C#
numbers.RemoveAll(number => number % 2 != 0);

負の奇数も含めて明確に判定したい場合は、絶対値を使う方法もあります。

C#
numbers.RemoveAll(number => Math.Abs(number % 2) == 1);

3-2. 指定した値以上・以下の要素を削除する

指定値以上の要素を削除する例です。

C#
var scores = new List<int> { 45, 60, 75, 80, 95 };

scores.RemoveAll(score => score >= 80);

Console.WriteLine(string.Join(", ", scores));
// 45, 60, 75

指定値以下の要素を削除する場合は、<=を使用します。

C#
scores.RemoveAll(score => score <= 60);

境界値を含めない場合は、>または<を使用してください。

C#
scores.RemoveAll(score => score > 80);
scores.RemoveAll(score => score < 60);

80以上80より大きいでは結果が異なるため、要件に応じて比較演算子を選ぶ必要があります。

3-3. 特定の文字列を含む要素を削除する

文字列に特定のキーワードが含まれているかどうかは、Containsで判定できます。

C#
var files = new List<string>
{
"report.pdf",
"report_backup.pdf",
"photo.jpg",
"memo.txt"
};

files.RemoveAll(file => file.Contains("backup"));

Console.WriteLine(string.Join(", ", files));
// report.pdf, photo.jpg, memo.txt

リスト内にnullが含まれる可能性がある場合は、次のように記述します。

C#
files.RemoveAll(file =>
file != null && file.Contains("backup"));

null自体も削除したい場合は、OR条件を使用します。

C#
files.RemoveAll(file =>
file == null || file.Contains("backup"));

3-4. 前方一致・後方一致する文字列を削除する

指定した文字列で始まる要素を削除するには、StartsWithを使います。

C#
var names = new List<string>
{
"test_user",
"admin",
"test_admin",
"guest"
};

names.RemoveAll(name => name.StartsWith("test_"));

Console.WriteLine(string.Join(", ", names));
// admin, guest

指定した文字列で終わる要素を削除するには、EndsWithを使います。

C#
var files = new List<string>
{
"image.jpg",
"document.pdf",
"photo.jpg",
"memo.txt"
};

files.RemoveAll(file => file.EndsWith(".jpg"));

ファイル拡張子などを比較する場合は、大文字と小文字を区別しない比較を指定すると安全です。

C#
files.RemoveAll(file =>
file.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase));

3-5. 空文字・null・空白だけの文字列を削除する

null、空文字、空白だけの文字列をまとめて削除する場合は、string.IsNullOrWhiteSpaceを使用します。

C#
var values = new List<string?>
{
"apple",
"",
null,
" ",
"banana"
};

values.RemoveAll(value => string.IsNullOrWhiteSpace(value));

Console.WriteLine(string.Join(", ", values));
// apple, banana

string.IsNullOrWhiteSpaceは、次の文字列に対してtrueを返します。

  • null

  • ""

  • 半角スペースだけの文字列

  • タブや改行などの空白文字だけの文字列

空文字とnullだけを削除し、空白だけの文字列は残したい場合は、string.IsNullOrEmptyを使います。

C#
values.RemoveAll(value => string.IsNullOrEmpty(value));

3-6. 大文字と小文字を区別せずに削除する

文字列の大文字と小文字を区別せずに比較する場合は、StringComparisonを指定します。

C#
var names = new List<string>
{
"Alice",
"ALICE",
"alice",
"Bob"
};

names.RemoveAll(name =>
string.Equals(
name,
"alice",
StringComparison.OrdinalIgnoreCase));

Console.WriteLine(string.Join(", ", names));
// Bob

ToLowerToUpperを使った比較もできますが、比較のために新しい文字列が作成される場合があります。

C#
names.RemoveAll(name => name.ToLower() == "alice");

通常は、意図が明確で余計な文字列生成を避けやすいStringComparison.OrdinalIgnoreCaseの利用が適しています。

部分一致で大文字と小文字を区別しない場合は、次のように記述できます。

C#
names.RemoveAll(name =>
name.Contains(
"ali",
StringComparison.OrdinalIgnoreCase));

3-7. 複数条件をAND・ORで組み合わせて削除する

複数条件をすべて満たす要素を削除する場合は、ANDを表す&&を使用します。

C#
var numbers = new List<int> { 5, 10, 15, 20, 25, 30 };

numbers.RemoveAll(number =>
number >= 10 && number <= 20);

Console.WriteLine(string.Join(", ", numbers));
// 5, 25, 30

いずれかの条件を満たす要素を削除する場合は、ORを表す||を使用します。

C#
numbers.RemoveAll(number =>
number < 10 || number > 20);

文字列でも複数条件を組み合わせられます。

C#
var names = new List<string?>
{
null,
"",
"test_user",
"admin",
"guest"
};

names.RemoveAll(name =>
string.IsNullOrWhiteSpace(name) ||
name.StartsWith("test_", StringComparison.OrdinalIgnoreCase));

条件が複雑になる場合は、かっこを使って評価順序を明確にすると読みやすくなります。

C#
users.RemoveAll(user =>
(user.IsInactive && user.LoginCount == 0) ||
user.IsDeleted);

4. オブジェクトのListでRemoveAllを使う方法

以降の例では、次のUserクラスを使用します。

C#
public class User
{
public int Id { get; set; }

public string Name { get; set; } = "";

public int Age { get; set; }

public string Status { get; set; } = "";

public DateTime LastLoginAt { get; set; }
}

4-1. プロパティの値を条件にオブジェクトを削除する

オブジェクトのリストでは、ラムダ式の中でプロパティを参照します。

C#
var users = new List<User>
{
new User { Id = 1, Name = "Alice", Age = 25 },
new User { Id = 2, Name = "Bob", Age = 17 },
new User { Id = 3, Name = "Carol", Age = 30 }
};

users.RemoveAll(user => user.Age < 18);

この例では、Age18未満のユーザーがすべて削除されます。

名前を条件に削除することもできます。

C#
users.RemoveAll(user =>
user.Name == "Alice");

4-2. IDが一致するオブジェクトを削除する

特定のIDを持つオブジェクトを削除する例です。

C#
int targetId = 2;

int removedCount = users.RemoveAll(user =>
user.Id == targetId);

IDが一意であれば通常は1件だけ削除されますが、同じIDのオブジェクトが複数存在する場合は、そのすべてが削除されます。

必ず最初の1件だけを削除したい場合は、FindFindIndexRemoveRemoveAtを組み合わせる方法が適しています。

4-3. 複数のプロパティを条件に一括削除する

年齢とステータスなど、複数のプロパティを組み合わせて削除できます。

C#
users.RemoveAll(user =>
user.Age < 20 &&
user.Status == "Inactive");

このコードでは、20歳未満かつステータスがInactiveのユーザーだけが削除されます。

いずれかの条件に一致したユーザーを削除する場合は、OR条件を使います。

C#
users.RemoveAll(user =>
user.Age < 20 ||
user.Status == "Deleted");

文字列比較で大文字と小文字を区別しない場合は、string.Equalsを利用します。

C#
users.RemoveAll(user =>
user.Age < 20 &&
string.Equals(
user.Status,
"inactive",
StringComparison.OrdinalIgnoreCase));

4-4. 日付やステータスを条件に古いデータを削除する

一定期間ログインしていないユーザーを削除する例です。

C#
DateTime threshold = DateTime.Today.AddMonths(-6);

users.RemoveAll(user =>
user.LastLoginAt < threshold);

この例では、6か月前より古い日時がLastLoginAtに設定されているユーザーを削除します。

ステータスと日付を組み合わせることもできます。

C#
users.RemoveAll(user =>
user.Status == "Inactive" &&
user.LastLoginAt < threshold);

現在日時を条件に使う場合は、条件判定の前に基準日時を変数へ保存しておくと、判定基準が処理中に変化しません。

C#
DateTime now = DateTime.UtcNow;
DateTime threshold = now.AddDays(-30);

users.RemoveAll(user =>
user.LastLoginAt < threshold);

アプリケーション内でUTCを採用している場合は、比較対象の日時もUTCに統一することが重要です。

4-5. 削除対象のID一覧を使って複数要素を削除する

削除対象のIDが複数ある場合は、ID一覧のContainsを使えます。

C#
var targetIds = new List<int> { 2, 4, 7 };

users.RemoveAll(user =>
targetIds.Contains(user.Id));

削除対象のIDが少数であれば、この書き方でも問題ありません。

対象IDが多い場合は、HashSet<int>を利用すると効率的です。

C#
var targetIds = new HashSet<int>
{
2,
4,
7
};

int removedCount = users.RemoveAll(user =>
targetIds.Contains(user.Id));

HashSet<T>は値の存在確認を高速に行えるため、大量のIDと照合する処理に適しています。

4-6. 条件判定をメソッドとして切り出す

削除条件が複雑な場合は、条件判定をメソッドとして切り出すと読みやすくなります。

C#
static bool ShouldRemoveUser(User user)
{
return user.Status == "Inactive" &&
user.LastLoginAt < DateTime.UtcNow.AddMonths(-6);
}

作成したメソッドは、そのままRemoveAllに渡せます。

C#
int removedCount = users.RemoveAll(ShouldRemoveUser);

基準日時を固定したい場合は、引数として直接渡せないため、ローカル関数を利用する方法があります。

C#
DateTime threshold = DateTime.UtcNow.AddMonths(-6);

bool ShouldRemoveUser(User user)
{
return user.Status == "Inactive" &&
user.LastLoginAt < threshold;
}

users.RemoveAll(ShouldRemoveUser);

条件判定を切り出すと、処理の意図が明確になり、同じ条件のテストもしやすくなります。

5. RemoveAllとほかの削除方法の違い

5-1. Removeとの違い|最初の一致要素だけを削除

Removeは、指定した値と一致する最初の要素だけを削除します。

C#
var numbers = new List<int> { 1, 2, 2, 2, 3 };

bool removed = numbers.Remove(2);

Console.WriteLine(string.Join(", ", numbers));
// 1, 2, 2, 3

戻り値はboolで、削除できた場合はtrue、対象がなかった場合はfalseです。

一方、RemoveAllは条件に一致する要素をすべて削除します。

C#
int removedCount = numbers.RemoveAll(number => number == 2);

使い分けは次のとおりです。

  • 指定した値の最初の1件だけを削除する場合はRemove

  • 条件に一致する全件を削除する場合はRemoveAll

5-2. RemoveAtとの違い|インデックスを指定して削除

RemoveAtは、指定したインデックスにある要素を削除します。

C#
var names = new List<string>
{
"Alice",
"Bob",
"Carol"
};

names.RemoveAt(1);

Console.WriteLine(string.Join(", ", names));
// Alice, Carol

RemoveAtには要素ではなく位置を指定します。

範囲外のインデックスを指定すると、ArgumentOutOfRangeExceptionが発生します。

C#
// names.RemoveAt(10);

削除したい要素の位置が分かっている場合はRemoveAt、値やプロパティなどの条件で削除したい場合はRemoveAllが適しています。

5-3. Clearとの違い|すべての要素を削除

Clearは、リスト内のすべての要素を削除します。

C#
var numbers = new List<int> { 1, 2, 3 };

numbers.Clear();

Console.WriteLine(numbers.Count);
// 0

条件に関係なく全件を削除する場合は、RemoveAllよりもClearのほうが意図を明確に表現できます。

C#
// 動作は全件削除だが、意図が分かりにくい
numbers.RemoveAll(number => true);

// 全件削除であることが明確
numbers.Clear();

5-4. LINQのWhereとの違い|元のListを変更するか

RemoveAllは元のリストを直接変更します。

C#
var numbers = new List<int> { 1, 2, 3, 4 };

numbers.RemoveAll(number => number % 2 == 0);

実行後のnumbers1, 3になります。

一方、LINQのWhereは、条件を満たす要素を列挙する新しい結果を返します。元のリストは変更されません。

C#
var numbers = new List<int> { 1, 2, 3, 4 };

List<int> filteredNumbers = numbers
.Where(number => number % 2 != 0)
.ToList();

Console.WriteLine(string.Join(", ", numbers));
// 1, 2, 3, 4

Console.WriteLine(string.Join(", ", filteredNumbers));
// 1, 3

RemoveAllでは「削除する条件」を指定しますが、Whereでは通常「残す条件」を指定する点にも注意が必要です。

5-5. FindAllとの違い|一致要素を抽出するか削除するか

FindAllは、条件に一致する要素を新しいList<T>として取得します。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

List<int> evenNumbers = numbers.FindAll(
number => number % 2 == 0);

evenNumbersには2, 4, 6が格納され、元のnumbersは変更されません。

RemoveAllで同じ条件を指定すると、一致した2, 4, 6が元のリストから削除されます。

C#
numbers.RemoveAll(number => number % 2 == 0);

つまり、同じ条件でも処理結果は反対です。

  • FindAll:条件に一致した要素を抽出する

  • RemoveAll:条件に一致した要素を削除する

5-6. 用途別に選ぶListの削除方法

目的に応じて、次のように使い分けます。

目的使用する方法
指定した値の最初の1件を削除するRemove
条件に一致する全件を削除するRemoveAll
指定位置の1件を削除するRemoveAt
指定範囲を位置で削除するRemoveRange
全要素を削除するClear
元のリストを残して絞り込むWhere
条件に一致する要素を別リストで取得するFindAll

条件に一致する複数の要素を元のリストから削除するなら、基本的にはRemoveAllが最も分かりやすい選択です。

6. List.RemoveAllを使う際の注意点

6-1. RemoveAllは元のListを直接変更する

RemoveAllを実行すると、元のList<T>が変更されます。

C#
var original = new List<int> { 1, 2, 3, 4 };

original.RemoveAll(number => number % 2 == 0);

Console.WriteLine(string.Join(", ", original));
// 1, 3

元のデータを残したい場合は、事前にコピーを作成します。

C#
var original = new List<int> { 1, 2, 3, 4 };
var copied = new List<int>(original);

copied.RemoveAll(number => number % 2 == 0);

または、LINQのWhereで新しいリストを作成します。

C#
List<int> filtered = original
.Where(number => number % 2 != 0)
.ToList();

6-2. 条件式がtrueの要素が削除される

RemoveAllでは、条件式がtrueを返した要素が削除されます。

C#
numbers.RemoveAll(number => number >= 10);

このコードは、10以上の要素を残すのではなく削除します。

残したい条件を先に考えると、条件を逆に書いてしまうことがあります。

たとえば、10以上だけを残したい場合の削除条件は次のとおりです。

C#
numbers.RemoveAll(number => number < 10);

条件が複雑な場合は、shouldRemoveという名前の変数やメソッドを使うと意図が明確になります。

C#
numbers.RemoveAll(number =>
{
bool shouldRemove = number < 10;
return shouldRemove;
});

6-3. 削除対象がない場合の戻り値は0

削除条件に一致する要素がなくても、通常は例外になりません。

C#
var numbers = new List<int> { 1, 2, 3 };

int removedCount = numbers.RemoveAll(
number => number > 100);

Console.WriteLine(removedCount);
// 0

リストの内容も変更されません。

戻り値を使えば、削除対象が存在したかを簡単に確認できます。

C#
if (removedCount == 0)
{
Console.WriteLine("対象の要素は見つかりませんでした。");
}

6-4. nullのListに対して呼び出すと例外が発生する

変数がnullの状態でRemoveAllを呼び出すと、NullReferenceExceptionが発生します。

C#
List<int>? numbers = null;

// NullReferenceException
// numbers.RemoveAll(number => number == 1);

nullの可能性がある場合は、事前に確認します。

C#
if (numbers != null)
{
numbers.RemoveAll(number => number == 1);
}

null条件演算子を利用する方法もあります。

C#
int removedCount =
numbers?.RemoveAll(number => number == 1) ?? 0;

可能であれば、リストをnullにせず空のリストで初期化しておくと扱いやすくなります。

C#
var numbers = new List<int>();

6-5. Predicateにnullを渡すと例外が発生する

RemoveAllの引数であるPredicate<T>nullを渡すと、ArgumentNullExceptionが発生します。

C#
var numbers = new List<int> { 1, 2, 3 };
Predicate<int>? predicate = null;

// ArgumentNullException
// numbers.RemoveAll(predicate);

通常のラムダ式を直接指定する場合は問題になりにくいものの、条件判定を変数や引数として受け取る設計では注意が必要です。

C#
static int RemoveItems<T>(
List<T> list,
Predicate<T> predicate)
{
ArgumentNullException.ThrowIfNull(list);
ArgumentNullException.ThrowIfNull(predicate);

return list.RemoveAll(predicate);
}

6-6. 条件判定中にListを変更してはいけない

RemoveAllに渡す条件判定の中で、対象リストへ要素を追加したり削除したりしないようにします。

C#
var numbers = new List<int> { 1, 2, 3 };

// 避けるべき処理
numbers.RemoveAll(number =>
{
numbers.Add(100);
return number == 2;
});

条件判定中に同じリストを変更すると、処理結果が予測しにくくなり、リストの内部状態や評価対象に影響する可能性があります。

Predicateは、受け取った要素を判定してtrueまたはfalseを返す処理に限定するのが安全です。

C#
numbers.RemoveAll(number => number == 2);

ログ出力など、リスト自体を変更しない副作用も技術的には記述できますが、条件判定はできるだけ単純に保つほうが保守しやすくなります。

6-7. 共有されているListへの影響に注意する

List<T>は参照型です。同じリストを複数の変数が参照している場合、一方でRemoveAllを実行すると、もう一方から見える内容も変わります。

C#
var list1 = new List<int> { 1, 2, 3, 4 };
var list2 = list1;

list2.RemoveAll(number => number % 2 == 0);

Console.WriteLine(string.Join(", ", list1));
// 1, 3

list1list2は同じリストを参照しているためです。

独立したリストとして処理したい場合はコピーを作成します。

C#
var list2 = new List<int>(list1);

また、複数のスレッドから同じList<T>を同時に読み書きする設計にも注意が必要です。List<T>の変更操作は、複数スレッドからの同時実行を前提としていません。必要に応じてロックやスレッドセーフな設計を検討してください。

6-8. 削除前の要素が必要な場合は別途保存する

RemoveAllの戻り値から取得できるのは削除件数だけです。削除された要素そのものは返されません。

削除対象を後から利用する場合は、削除前に保存します。

C#
var usersToRemove = users
.Where(user => user.Status == "Deleted")
.ToList();

int removedCount = users.RemoveAll(
user => user.Status == "Deleted");

同じ条件を重複して書きたくない場合は、条件を変数に保存します。

C#
Predicate<User> shouldRemove =
user => user.Status == "Deleted";

List<User> usersToRemove = users
.Where(user => shouldRemove(user))
.ToList();

int removedCount = users.RemoveAll(shouldRemove);

ただし、条件判定に外部状態や現在時刻を使う場合は、2回の判定結果が変わらないように基準値を固定しておく必要があります。

7. ループ処理で要素を削除するときの問題と対処法

7-1. foreach中に削除すると例外が発生する理由

foreachでリストを列挙している途中に、そのリストの要素を削除すると通常はInvalidOperationExceptionが発生します。

C#
var numbers = new List<int> { 1, 2, 3, 4 };

foreach (int number in numbers)
{
if (number % 2 == 0)
{
numbers.Remove(number);
}
}

foreachは列挙中のコレクションが変更されないことを前提としています。途中で要素を削除すると、列挙子が無効になるためです。

条件に一致する複数要素を削除したい場合は、次のようにRemoveAllを使います。

C#
numbers.RemoveAll(number => number % 2 == 0);

7-2. 前向きのfor文で削除すると要素を飛ばす理由

前から後ろへ進むfor文で要素を削除すると、削除後に要素が前へ詰められるため、次の要素を飛ばすことがあります。

C#
var numbers = new List<int> { 2, 2, 2, 3 };

for (int i = 0; i < numbers.Count; i++)
{
if (numbers[i] == 2)
{
numbers.RemoveAt(i);
}
}

インデックス0の要素を削除すると、元のインデックス1の要素が0へ移動します。しかし、ループ変数iは次に1へ進むため、新しくインデックス0へ移動した要素は判定されません。

削除後にiを減らす方法もあります。

C#
for (int i = 0; i < numbers.Count; i++)
{
if (numbers[i] == 2)
{
numbers.RemoveAt(i);
i--;
}
}

ただし、単純な条件付き一括削除であれば、RemoveAllのほうが簡潔です。

7-3. for文を後ろから回して削除する方法

インデックスが必要な処理では、for文を後ろから回すことで安全に削除できます。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

for (int i = numbers.Count - 1; i >= 0; i--)
{
if (numbers[i] % 2 == 0)
{
numbers.RemoveAt(i);
}
}

後ろの要素を削除しても、それより前のインデックスは変化しません。

削除する要素のインデックスが必要な場合や、位置に応じて条件を変えたい場合は、この方法が役立ちます。

7-4. 条件付き一括削除ではRemoveAllを優先する

単純に要素の値やプロパティを判定して一括削除する場合は、ループよりもRemoveAllを優先するとコードが明確になります。

C#
numbers.RemoveAll(number => number % 2 == 0);

後ろ向きのfor文と比べても、削除の目的が一目で分かります。

C#
for (int i = numbers.Count - 1; i >= 0; i--)
{
if (numbers[i] % 2 == 0)
{
numbers.RemoveAt(i);
}
}

ただし、インデックスを使った判定や、削除する要素ごとに個別の処理が必要な場合は、ループのほうが適切なこともあります。

7-5. 1件だけ削除したい場合の適切な方法

条件に一致する最初の1件だけを削除したい場合、RemoveAllは適していません。

値が分かっている場合はRemoveを使います。

C#
numbers.Remove(10);

オブジェクトを条件で検索して削除する場合は、FindRemoveを組み合わせられます。

C#
User? target = users.Find(user =>
user.Id == targetId);

if (target != null)
{
users.Remove(target);
}

インデックスを検索して削除する方法もあります。

C#
int index = users.FindIndex(user =>
user.Id == targetId);

if (index >= 0)
{
users.RemoveAt(index);
}

FindIndexRemoveAtの組み合わせは、見つかった位置の1件だけを削除することが明確です。

8. List.RemoveAllのパフォーマンス

8-1. RemoveAllの計算量はO(n)

RemoveAllは、リスト内の要素を順番に確認して条件判定を行います。そのため、要素数をnとすると基本的な計算量はO(n)です。

条件に一致しない要素は前方へ詰められ、不要になった領域が整理されます。条件に一致する要素が多い場合でも、リスト全体を何度も先頭から検索する処理ではありません。

ただし、Predicateの内部で重い処理を行っている場合、全体の実行時間はその条件判定にも左右されます。

C#
list.RemoveAll(item => ExpensiveCheck(item));

条件判定は各要素に対して実行されるため、可能な範囲で軽量にすることが重要です。

8-2. Removeを繰り返す処理との違い

削除対象ごとにRemoveを繰り返すと、検索や要素移動が何度も発生する可能性があります。

C#
foreach (int target in targets)
{
numbers.Remove(target);
}

Removeは対象値を探し、削除位置より後ろの要素を前へ詰めます。対象数が多いと、リスト全体を何度も検索することになり、処理量が増える場合があります。

条件に一致する全件を削除するなら、RemoveAllで1回の処理にまとめるほうが効率的です。

C#
var targetSet = new HashSet<int>(targets);

numbers.RemoveAll(number =>
targetSet.Contains(number));

8-3. 大量データを削除するときの考え方

大量のデータを扱う場合は、次の点を確認します。

まず、元のリストを変更してよいかを判断します。変更してよいならRemoveAll、元のリストを残す必要があるならWhereで新しいリストを作成します。

次に、Predicate内の検索処理に注意します。

C#
numbers.RemoveAll(number =>
targetIds.Contains(number));

targetIdsList<int>で要素数が多い場合、Containsの検索にも時間がかかります。削除対象をHashSet<int>に変換すると、存在確認を高速化できます。

また、削除後に必要な容量が大きく減ったとしても、リストが内部で確保している容量が自動的に要素数と同じになるとは限りません。メモリ削減が重要な場面では、処理後に容量を調整する方法もあります。

C#
numbers.TrimExcess();

ただし、今後再び要素を追加する予定がある場合、容量を縮小すると再確保が発生しやすくなるため、常に実行すればよいわけではありません。

8-4. HashSetを使って削除対象の検索を高速化する

複数の削除対象IDと照合する場合、HashSet<T>が有効です。

C#
var targetIds = new HashSet<int>
{
10,
20,
30,
40
};

users.RemoveAll(user =>
targetIds.Contains(user.Id));

一般に、HashSet<T>.Containsは平均的に高速な存在確認ができます。

一方、List<T>.Containsは先頭から順番に値を検索するため、削除対象の候補が増えるほど比較回数が増えます。

C#
var targetIds = new List<int>
{
10,
20,
30,
40
};

users.RemoveAll(user =>
targetIds.Contains(user.Id));

対象が数件だけなら差を意識する必要はありませんが、数千件以上のIDを照合する場合はHashSet<T>への変換を検討するとよいでしょう。

C#
var targetIdSet = targetIds.ToHashSet();

users.RemoveAll(user =>
targetIdSet.Contains(user.Id));

8-5. 元のListを残す場合はWhereで新しいListを作成する

元のリストを変更したくない場合は、LINQのWhereを使います。

C#
var original = new List<int>
{
1,
2,
3,
4,
5
};

List<int> result = original
.Where(number => number % 2 != 0)
.ToList();

originalにはすべての要素が残り、resultには奇数だけが格納されます。

RemoveAllWhereでは条件の意味が逆になりやすいため注意してください。

C#
// 偶数を削除する条件
original.RemoveAll(number => number % 2 == 0);

// 奇数を残す条件
List<int> result = original
.Where(number => number % 2 != 0)
.ToList();

新しいリストを作成すると追加のメモリが必要になりますが、元データを保持できるため、処理前後の比較や再利用が必要な場合に適しています。

9. List.RemoveAllでよくある疑問

9-1. 条件に一致する最初の1件だけを削除できるか

RemoveAllは、条件に一致する要素をすべて削除するメソッドです。最初の1件だけを削除する設定はありません。

最初の一致要素だけを削除したい場合は、FindIndexで位置を取得してからRemoveAtを実行します。

C#
var numbers = new List<int> { 1, 2, 2, 2, 3 };

int index = numbers.FindIndex(number =>
number == 2);

if (index >= 0)
{
numbers.RemoveAt(index);
}

Console.WriteLine(string.Join(", ", numbers));
// 1, 2, 2, 3

オブジェクトの場合も同様です。

C#
int index = users.FindIndex(user =>
user.Status == "Inactive");

if (index >= 0)
{
users.RemoveAt(index);
}

削除したい値やオブジェクト自体が分かっているなら、Removeも利用できます。

9-2. 削除した要素そのものを取得できるか

RemoveAllが返すのは削除件数だけで、削除された要素の一覧は取得できません。

削除した要素が必要な場合は、削除前にWhereFindAllで取得します。

C#
Predicate<int> shouldRemove =
number => number % 2 == 0;

var numbers = new List<int>
{
1,
2,
3,
4,
5,
6
};

List<int> removedNumbers = numbers
.Where(number => shouldRemove(number))
.ToList();

int removedCount = numbers.RemoveAll(shouldRemove);

処理後の内容は次のとおりです。

removedNumbers: 2, 4, 6
numbers: 1, 3, 5
removedCount: 3

条件判定を1回だけにしたい場合は、残す要素と削除する要素を別々に振り分ける方法もあります。

C#
var remainingNumbers = new List<int>();
var removedNumbers = new List<int>();

foreach (int number in numbers)
{
if (number % 2 == 0)
{
removedNumbers.Add(number);
}
else
{
remainingNumbers.Add(number);
}
}

元のリストを直接変更する必要がある場合は、削除対象を保存してからRemoveAllを実行する方法が分かりやすいでしょう。

まとめ

C#のList.RemoveAllは、条件に一致する要素をリストからまとめて削除できるメソッドです。

基本構文は次のとおりです。

C#
int removedCount = list.RemoveAll(
item => 削除条件);

条件式がtrueになった要素が削除され、戻り値として削除件数を取得できます。

C#
var numbers = new List<int>
{
1,
2,
3,
4,
5,
6
};

int removedCount = numbers.RemoveAll(
number => number % 2 == 0);

特定の値、数値範囲、部分一致する文字列、オブジェクトのプロパティ、複数のIDなど、さまざまな条件をラムダ式で指定できます。

条件に一致する全件を削除する場合はRemoveAll、最初の1件だけならRemoveFindIndexRemoveAt、全件削除ならClearを使います。元のリストを変更せずに要素を絞り込みたい場合は、LINQのWhereが適しています。

foreach中の削除や前向きのfor文による削除は、例外や要素の飛ばしにつながることがあります。単純な条件付き一括削除では、簡潔で意図が伝わりやすいRemoveAllを活用しましょう。