C#のRemoveメソッド完全ガイド|文字列・リスト・ファイル削除の使い方とエラー対策
はじめに
C#でデータを削除するときは、対象によって使用するメソッドが異なります。文字列の一部を削除する場合はstring.Remove、リストの要素を削除する場合はList<T>.Remove、ファイルを削除する場合はFile.Deleteを使用します。
同じ「削除」という処理でも、値を指定するのか、インデックスを指定するのか、条件に一致する要素をまとめて削除するのかによって、適切なメソッドは変わります。誤ったメソッドや引数を使用すると、削除できないだけでなく、ArgumentOutOfRangeExceptionやInvalidOperationExceptionなどの例外が発生することもあります。
本記事では、C#のRemoveメソッドを中心に、文字列、リスト、Dictionary、HashSet、ファイル、フォルダーを削除する方法をコード例付きで解説します。エラーの原因や、ループ処理中に要素を安全に削除する方法も確認していきましょう。
1. C#のRemoveとは?削除対象ごとの使い分け
1-1. Removeメソッドで削除できるもの
C#では、複数のクラスにRemoveという名前のメソッドが定義されています。代表的な削除対象は次のとおりです。
string.Remove:文字列の一部を削除するList<T>.Remove:指定した値と一致する要素を削除するList<T>.RemoveAt:指定インデックスの要素を削除するList<T>.RemoveAll:条件に一致する要素をすべて削除するList<T>.RemoveRange:連続する複数の要素を削除するDictionary<TKey,TValue>.Remove:指定キーの要素を削除するHashSet<T>.Remove:指定した値を削除する
一方、ファイルやフォルダーの削除では、RemoveではなくDeleteを使用します。
1-2. 文字列・コレクション・ファイルで使用するメソッドの違い
文字列、コレクション、ファイルでは、削除処理の性質が異なります。
文字列は変更不可能なイミュータブル型です。そのため、string.Removeを実行しても元の文字列は変更されず、削除後の新しい文字列が戻り値として返されます。
C#string text = "ABCDE";
string result = text.Remove(2);
Console.WriteLine(text); // ABCDE
Console.WriteLine(result); // AB
List<T>やDictionary<TKey,TValue>などのコレクションでは、Removeを実行するとコレクション自体の内容が変更されます。
C#var numbers = new List<int> { 10, 20, 30 };
numbers.Remove(20);
Console.WriteLine(string.Join(", ", numbers)); // 10, 30
ファイルの削除ではFile.Deleteを使用します。削除されたファイルは通常、ごみ箱に移動せず直接削除されるため注意が必要です。
C#File.Delete("sample.txt");
1-3. Remove・RemoveAt・RemoveAll・Deleteの使い分け早見表
| メソッド | 主な対象 | 指定するもの | 戻り値 |
|---|---|---|---|
string.Remove | 文字列 | 開始位置、文字数 | 削除後の文字列 |
List<T>.Remove | リスト | 値 | 削除できたかを示すbool |
List<T>.RemoveAt | リスト | インデックス | なし |
List<T>.RemoveAll | リスト | 条件 | 削除した件数 |
List<T>.RemoveRange | リスト | 開始位置、件数 | なし |
Dictionary.Remove | Dictionary | キー | 削除できたかを示すbool |
HashSet.Remove | HashSet | 値 | 削除できたかを示すbool |
File.Delete | ファイル | ファイルパス | なし |
Directory.Delete | フォルダー | フォルダーパス | なし |
2. string.Removeで文字列を削除する方法
2-1. string.Removeの基本構文と戻り値
string.Removeには、主に2種類のオーバーロードがあります。
C#string Remove(int startIndex);
string Remove(int startIndex, int count);
Remove(int startIndex)は、指定した位置から文字列の末尾までを削除します。
Remove(int startIndex, int count)は、指定した位置から指定文字数分を削除します。
どちらのメソッドも元の文字列を変更せず、削除後の新しい文字列を返します。そのため、結果を変数に代入する必要があります。
C#string text = "123456789";
text = text.Remove(3, 2);
Console.WriteLine(text); // 1236789
2-2. 指定した位置から末尾まで削除する
引数を1つだけ指定すると、その位置から末尾までが削除されます。
C#string text = "C# Remove Guide";
string result = text.Remove(2);
Console.WriteLine(result); // C#
文字列のインデックスは0から始まります。
C # R e m o v e
0 1 2 3 4 5 6 7 8
Remove(2)では、インデックス2以降が削除されるため、インデックス0と1の文字だけが残ります。
2-3. 開始位置と文字数を指定して削除する
開始位置と削除する文字数を指定する場合は、2つの引数を渡します。
C#string text = "ABCDEFGHIJ";
string result = text.Remove(3, 4);
Console.WriteLine(result); // ABCHIJ
この例では、インデックス3から4文字分のDEFGが削除されます。
開始位置は0から数える一方、削除文字数は通常の個数として指定する点に注意してください。
2-4. 文字列の先頭・中央・末尾を削除する
文字列の先頭を削除する場合は、開始位置に0を指定します。
C#string text = "Hello World";
string result = text.Remove(0, 6);
Console.WriteLine(result); // World
中央部分を削除する場合は、削除対象の開始インデックスと文字数を指定します。
C#string text = "Hello Beautiful World";
string result = text.Remove(6, 10);
Console.WriteLine(result); // Hello World
末尾を削除する場合は、残したい文字列の長さを開始位置として指定できます。
C#string text = "report.txt";
string result = text.Remove(6);
Console.WriteLine(result); // report
末尾から一定数の文字を削除する場合は、Lengthを利用すると便利です。
C#string text = "ABCDEFG";
string result = text.Remove(text.Length - 3);
Console.WriteLine(result); // ABCD
2-5. 元の文字列が変更されない理由
C#のstringはイミュータブル、つまり作成後に内容を変更できない型です。Remove、Replace、Trimなどを実行すると、変更後の内容を持つ新しい文字列が生成されます。
次のコードでは、Removeの戻り値を代入していないため、textの内容は変わりません。
C#string text = "ABCDE";
text.Remove(2);
Console.WriteLine(text); // ABCDE
削除結果を使用するには、次のように戻り値を代入します。
C#text = text.Remove(2);
Console.WriteLine(text); // AB
または、別の変数に保存します。
C#string original = "ABCDE";
string removed = original.Remove(2);
2-6. IndexOfとRemoveを組み合わせて特定文字列以降を削除する
特定の文字や文字列が現れた位置から末尾まで削除する場合は、IndexOfとRemoveを組み合わせます。
C#string text = "商品名(販売終了)";
int index = text.IndexOf('(');
if (index >= 0)
{
text = text.Remove(index);
}
Console.WriteLine(text); // 商品名
文字列を検索することもできます。
C#string url = "https://example.com/page?category=book";
int index = url.IndexOf('?', StringComparison.Ordinal);
if (index >= 0)
{
url = url.Remove(index);
}
Console.WriteLine(url); // https://example.com/page
IndexOfは対象が見つからない場合に-1を返します。確認せずにRemove(-1)を実行するとArgumentOutOfRangeExceptionが発生するため、必ず0以上か確認してください。
大文字と小文字を区別せずに検索する場合は、StringComparisonを指定します。
C#string text = "Product-END";
int index = text.IndexOf("-end", StringComparison.OrdinalIgnoreCase);
if (index >= 0)
{
text = text.Remove(index);
}
Console.WriteLine(text); // Product
2-7. Substring・Replace・Trimとの違い
Removeと似た文字列操作として、Substring、Replace、Trimがあります。
Substringは、削除する範囲ではなく、取り出したい範囲を指定します。
C#string text = "ABCDEFG";
string byRemove = text.Remove(3); // ABC
string bySubstring = text.Substring(0, 3); // ABC
Replaceは、指定した文字列を別の文字列に置き換えます。空文字列に置き換えることで、該当部分をすべて削除できます。
C#string text = "A-B-C-D";
string result = text.Replace("-", "");
Console.WriteLine(result); // ABCD
Removeがインデックスを基準に削除するのに対し、Replaceは値を基準に置換または削除します。
Trimは、文字列の先頭と末尾にある空白文字などを削除します。
C#string text = " C# Remove ";
string result = text.Trim();
Console.WriteLine(result); // C# Remove
用途に応じて、次のように使い分けます。
位置を指定して削除する:
Remove必要な範囲を取り出す:
Substring特定の文字列を置き換える、またはすべて削除する:
Replace先頭や末尾の空白などを削除する:
Trim
3. List<T>.Removeでリストの要素を削除する方法
3-1. List.Removeの基本構文と戻り値
List<T>.Removeは、指定した値と一致する最初の要素を削除します。
C#bool Remove(T item);
削除に成功するとtrue、指定した要素が存在しなければfalseを返します。
C#var fruits = new List<string>
{
"Apple",
"Banana",
"Orange"
};
bool removed = fruits.Remove("Banana");
Console.WriteLine(removed); // True
Console.WriteLine(string.Join(", ", fruits)); // Apple, Orange
存在しない値を指定しても例外は発生しません。
C#bool removed = fruits.Remove("Grape");
Console.WriteLine(removed); // False
3-2. 指定した値と一致する要素を削除する
整数のリストから指定した値を削除する例は次のとおりです。
C#var numbers = new List<int> { 10, 20, 30, 40 };
bool removed = numbers.Remove(30);
if (removed)
{
Console.WriteLine("30を削除しました。");
}
else
{
Console.WriteLine("30は見つかりませんでした。");
}
Removeに渡すのはインデックスではなく値です。
C#var numbers = new List<int> { 10, 20, 30 };
numbers.Remove(1);
このコードはインデックス1の20を削除するのではなく、値が1の要素を探して削除します。インデックスを指定したい場合はRemoveAtを使用します。
3-3. 同じ値が複数ある場合の削除結果
List<T>.Removeが削除するのは、最初に見つかった1件だけです。
C#var numbers = new List<int> { 10, 20, 20, 30 };
numbers.Remove(20);
Console.WriteLine(string.Join(", ", numbers)); // 10, 20, 30
同じ値をすべて削除したい場合は、RemoveAllを使用します。
C#int removedCount = numbers.RemoveAll(x => x == 20);
Console.WriteLine(removedCount);
Removeを繰り返してすべて削除する方法もありますが、通常はRemoveAllのほうが意図が分かりやすく効率的です。
C#while (numbers.Remove(20))
{
}
3-4. 要素が存在するかを確認してから削除する
Containsで存在を確認してからRemoveすることもできます。
C#if (fruits.Contains("Apple"))
{
fruits.Remove("Apple");
}
ただし、単に削除の成否を確認したいだけであれば、Removeの戻り値を使うほうが効率的です。ContainsとRemoveを続けて実行すると、リスト内を最大2回検索することになるためです。
C#if (fruits.Remove("Apple"))
{
Console.WriteLine("削除しました。");
}
else
{
Console.WriteLine("対象は存在しません。");
}
3-5. 独自クラスの要素をRemoveで削除する
独自クラスを格納したリストでもRemoveを使用できます。
C#public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
同じインスタンスを指定した場合は削除できます。
C#var product = new Product
{
Id = 1,
Name = "Keyboard"
};
var products = new List<Product>
{
product
};
bool removed = products.Remove(product);
Console.WriteLine(removed); // True
一方、プロパティの値が同じでも、別のインスタンスは標準では同一と判定されません。
C#var products = new List<Product>
{
new Product { Id = 1, Name = "Keyboard" }
};
bool removed = products.Remove(
new Product { Id = 1, Name = "Keyboard" }
);
Console.WriteLine(removed); // 通常はFalse
IDを条件に削除したい場合は、RemoveAllやFindを使用できます。
C#products.RemoveAll(product => product.Id == 1);
最初の1件だけを削除したい場合は、対象を取得してからRemoveします。
C#Product? target = products.Find(product => product.Id == 1);
if (target is not null)
{
products.Remove(target);
}
3-6. EqualsとIEquatable<T>が削除判定に与える影響
List<T>.Removeは、要素が一致するかどうかをEqualityComparer<T>.Defaultで判定します。
独自クラスで「IDが同じなら同じ商品」と判定したい場合は、IEquatable<T>を実装できます。
C#public sealed class Product : IEquatable<Product>
{
public int Id { get; init; }
public string Name { get; init; } = "";
public bool Equals(Product? other)
{
return other is not null && Id == other.Id;
}
public override bool Equals(object? obj)
{
return obj is Product other && Equals(other);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
このクラスでは、IDが同じ別インスタンスを指定しても削除できます。
C#var products = new List<Product>
{
new Product { Id = 1, Name = "Keyboard" }
};
bool removed = products.Remove(
new Product { Id = 1, Name = "別の商品名" }
);
Console.WriteLine(removed); // True
Equalsをオーバーライドする場合は、同じ等価性ルールに基づいてGetHashCodeもオーバーライドしてください。特にHashSet<T>やDictionary<TKey,TValue>では、ハッシュコードの整合性が重要です。
4. RemoveAt・RemoveAll・RemoveRangeの使い方
4-1. RemoveAtで指定インデックスの要素を削除する
RemoveAtは、指定したインデックスにある要素を削除します。
C#var colors = new List<string>
{
"Red",
"Green",
"Blue"
};
colors.RemoveAt(1);
Console.WriteLine(string.Join(", ", colors)); // Red, Blue
インデックスは0から始まります。先頭を削除する場合はRemoveAt(0)、末尾を削除する場合はRemoveAt(list.Count - 1)です。
C#if (colors.Count > 0)
{
colors.RemoveAt(colors.Count - 1);
}
存在しないインデックスを指定するとArgumentOutOfRangeExceptionが発生します。
4-2. RemoveAllで条件に一致する要素を一括削除する
RemoveAllは、指定した条件に一致する要素をすべて削除します。
C#var numbers = new List<int>
{
1, 2, 3, 4, 5, 6
};
int removedCount = numbers.RemoveAll(number => number % 2 == 0);
Console.WriteLine(string.Join(", ", numbers)); // 1, 3, 5
Console.WriteLine(removedCount); // 3
引数には、要素を受け取ってboolを返す条件式を指定します。条件式がtrueになった要素が削除対象です。
4-3. ラムダ式を使って複数条件で削除する
論理演算子を使えば、複数の条件を組み合わせて削除できます。
C#public class User
{
public int Id { get; init; }
public string Name { get; init; } = "";
public bool IsActive { get; init; }
public int Age { get; init; }
}
無効なユーザー、または18歳未満のユーザーを削除する例です。
C#int removedCount = users.RemoveAll(
user => !user.IsActive || user.Age < 18
);
無効かつ18歳未満のユーザーだけを削除する場合は、&&を使用します。
C#users.RemoveAll(
user => !user.IsActive && user.Age < 18
);
条件が複雑な場合は、別のメソッドに分けると読みやすくなります。
C#static bool ShouldRemove(User user)
{
return !user.IsActive || user.Age < 18;
}
int removedCount = users.RemoveAll(ShouldRemove);
4-4. RemoveRangeで連続する複数要素を削除する
RemoveRangeは、指定した開始インデックスから連続する複数の要素を削除します。
C#var numbers = new List<int>
{
10, 20, 30, 40, 50
};
numbers.RemoveRange(1, 3);
Console.WriteLine(string.Join(", ", numbers)); // 10, 50
第1引数は開始インデックス、第2引数は削除する要素数です。この例では、インデックス1から3件の20、30、40を削除しています。
リストの途中から末尾まですべて削除する場合は、件数を計算します。
C#int startIndex = 2;
if (startIndex >= 0 && startIndex <= numbers.Count)
{
numbers.RemoveRange(startIndex, numbers.Count - startIndex);
}
4-5. Remove・RemoveAt・RemoveAll・RemoveRangeの違い
各メソッドの違いは次のとおりです。
C#var values = new List<int> { 10, 20, 20, 30, 40 };
値が20の最初の1件を削除します。
C#values.Remove(20);
インデックス1の要素を削除します。
C#values.RemoveAt(1);
値が20の要素をすべて削除します。
C#values.RemoveAll(value => value == 20);
インデックス1から3件を削除します。
C#values.RemoveRange(1, 3);
削除対象が値なのか、位置なのか、条件なのか、連続範囲なのかを確認して選択しましょう。
4-6. 削除した件数や成否を取得する方法
Removeは、削除に成功したかをboolで返します。
C#bool removed = values.Remove(20);
RemoveAllは、削除した要素数をintで返します。
C#int removedCount = values.RemoveAll(value => value < 0);
RemoveAtとRemoveRangeには戻り値がありません。削除件数を記録したい場合は、削除前後のCountを比較できます。
C#int beforeCount = values.Count;
values.RemoveRange(1, 2);
int removedCount = beforeCount - values.Count;
ただし、RemoveRangeでは第2引数が削除件数なので、呼び出しが正常に完了すればその値が削除件数になります。
5. Dictionary・HashSetなどのコレクションから要素を削除する方法
5-1. Dictionary.Removeで指定キーの要素を削除する
Dictionary<TKey,TValue>.Removeは、指定したキーに対応する要素を削除します。
C#var scores = new Dictionary<string, int>
{
["Alice"] = 90,
["Bob"] = 75,
["Carol"] = 88
};
bool removed = scores.Remove("Bob");
Console.WriteLine(removed); // True
存在しないキーを指定した場合はfalseが返り、例外は発生しません。
C#bool removed = scores.Remove("David");
Console.WriteLine(removed); // False
削除前にContainsKeyを実行することもできますが、成否を確認するだけならRemoveの戻り値を利用するほうが簡潔です。
5-2. Dictionary.Removeで削除した値を同時に取得する
対応する.NET環境では、Removeのout引数を使って、削除した値を同時に取得できます。
C#var scores = new Dictionary<string, int>
{
["Alice"] = 90,
["Bob"] = 75
};
if (scores.Remove("Bob", out int removedScore))
{
Console.WriteLine($"削除した点数: {removedScore}");
}
else
{
Console.WriteLine("指定したキーは存在しません。");
}
キーの検索、値の取得、削除を1回の呼び出しで行えるため、TryGetValueとRemoveを別々に実行するより簡潔です。
使用している.NETのバージョンでこのオーバーロードが利用できない場合は、次のように記述します。
C#if (scores.TryGetValue("Bob", out int removedScore))
{
scores.Remove("Bob");
Console.WriteLine($"削除した点数: {removedScore}");
}
5-3. HashSet.Removeで指定要素を削除する
HashSet<T>は、重複しない要素を管理するコレクションです。Removeを使うと、指定した要素を削除できます。
C#var tags = new HashSet<string>
{
"C#",
".NET",
"ASP.NET Core"
};
bool removed = tags.Remove(".NET");
Console.WriteLine(removed); // True
HashSet<T>.Removeも、削除に成功するとtrue、要素が存在しなければfalseを返します。
複数の要素をまとめて取り除く場合は、ExceptWithも利用できます。
C#tags.ExceptWith(new[] { "C#", "ASP.NET Core" });
5-4. QueueやStackから要素を取り出して削除する
Queue<T>では、先頭の要素を取り出して削除するためにDequeueを使用します。
C#var queue = new Queue<string>();
queue.Enqueue("A");
queue.Enqueue("B");
queue.Enqueue("C");
string first = queue.Dequeue();
Console.WriteLine(first); // A
Console.WriteLine(queue.Count); // 2
空のキューに対してDequeueを実行すると例外が発生します。対応する環境では、TryDequeueを使うと安全に取得できます。
C#if (queue.TryDequeue(out string? item))
{
Console.WriteLine($"取り出した要素: {item}");
}
Stack<T>では、最後に追加した要素をPopで取り出して削除します。
C#var stack = new Stack<string>();
stack.Push("A");
stack.Push("B");
stack.Push("C");
string top = stack.Pop();
Console.WriteLine(top); // C
Console.WriteLine(stack.Count); // 2
安全に処理する場合はTryPopを使用します。
C#if (stack.TryPop(out string? item))
{
Console.WriteLine($"取り出した要素: {item}");
}
5-5. Clearでコレクション内の全要素を削除する
コレクション内の要素をすべて削除する場合は、Clearを使用します。
C#var numbers = new List<int> { 1, 2, 3 };
numbers.Clear();
Console.WriteLine(numbers.Count); // 0
DictionaryやHashSetでも同様です。
C#scores.Clear();
tags.Clear();
Clearを実行しても、コレクション変数そのものがnullになるわけではありません。空のコレクションとして引き続き使用できます。
5-6. 配列から要素を削除できない理由と代替方法
C#の配列は作成時に長さが決まり、後から要素数を変更できません。そのため、配列にはRemoveメソッドがありません。
C#int[] numbers = { 10, 20, 30 };
特定の要素を除外した新しい配列を作る場合は、LINQのWhereとToArrayを使用できます。
C#int[] result = numbers
.Where(number => number != 20)
.ToArray();
Console.WriteLine(string.Join(", ", result)); // 10, 30
削除処理を頻繁に行う場合は、配列ではなくList<T>を使用するほうが適しています。
C#var list = numbers.ToList();
list.Remove(20);
6. File.Deleteでファイルを削除する方法
6-1. File.Deleteの基本構文
ファイルを削除するには、System.IO.File.Deleteを使用します。
C#using System.IO;
File.Delete("sample.txt");
File.Deleteの戻り値はありません。削除に成功すると処理が完了し、アクセス権限やファイル使用中などの問題がある場合は例外が発生します。
指定したファイルが存在しない場合、通常は例外を発生させずに処理を終了します。ただし、パス自体が不正な場合や、パスの一部に問題がある場合は例外が発生することがあります。
6-2. ファイルの存在を確認してから削除する
File.Existsを使用すると、ファイルが存在する場合だけ削除できます。
C#string path = "sample.txt";
if (File.Exists(path))
{
File.Delete(path);
Console.WriteLine("ファイルを削除しました。");
}
else
{
Console.WriteLine("ファイルが存在しません。");
}
存在しないファイルに対するFile.Deleteは通常何もしないため、削除だけが目的なら存在確認は必須ではありません。
ただし、次のような処理を行う場合はFile.Existsが役立ちます。
ファイルが存在しなかったことをログに記録する
ユーザーに削除結果を表示する
削除前に確認画面を出す
バックアップ処理を行う
なお、File.Existsの確認後、File.Deleteを実行するまでに、別の処理がファイルを変更または削除する可能性があります。存在確認だけで安全性を保証できるわけではないため、最終的には例外処理も必要です。
6-3. 相対パスと絶対パスを指定して削除する
相対パスでは、アプリケーションの現在の作業ディレクトリを基準にファイルが検索されます。
C#File.Delete("data/sample.txt");
絶対パスでは、ドライブやルートからの完全なパスを指定します。
C#File.Delete(@"C:\work\data\sample.txt");
文字列の先頭に@を付けると、\をエスケープせずに記述できます。
C#string path1 = @"C:\work\data\sample.txt";
string path2 = "C:\\work\\data\\sample.txt";
OSに依存しにくいパスを作るには、Path.Combineを使用します。
C#string path = Path.Combine(
AppContext.BaseDirectory,
"data",
"sample.txt"
);
File.Delete(path);
意図しないファイルを削除しないように、削除前にPath.GetFullPathで絶対パスを確認する方法もあります。
C#string fullPath = Path.GetFullPath(path);
Console.WriteLine($"削除対象: {fullPath}");
6-4. 複数ファイルを条件指定して一括削除する
特定の拡張子を持つファイルをまとめて削除する場合は、Directory.EnumerateFilesを使用できます。
C#string directoryPath = @"C:\work\temp";
foreach (string filePath in Directory.EnumerateFiles(
directoryPath,
"*.tmp",
SearchOption.TopDirectoryOnly))
{
File.Delete(filePath);
}
サブフォルダーも検索する場合は、SearchOption.AllDirectoriesを指定します。
C#foreach (string filePath in Directory.EnumerateFiles(
directoryPath,
"*.log",
SearchOption.AllDirectories))
{
File.Delete(filePath);
}
複数の拡張子を対象にする場合は、LINQで条件を追加できます。
C#string[] targetExtensions = { ".tmp", ".bak", ".log" };
IEnumerable<string> files = Directory
.EnumerateFiles(directoryPath)
.Where(filePath =>
targetExtensions.Contains(
Path.GetExtension(filePath),
StringComparer.OrdinalIgnoreCase
)
);
foreach (string filePath in files)
{
File.Delete(filePath);
}
一括削除では対象範囲を誤ると多くのファイルを失う可能性があります。実際に削除する前に、対象パスをログへ出力して確認すると安全です。
6-5. Directory.Deleteでフォルダーを削除する
空のフォルダーを削除するにはDirectory.Deleteを使用します。
C#Directory.Delete(@"C:\work\empty-folder");
フォルダー内にファイルやサブフォルダーが存在すると、通常は削除できません。中身も含めて削除する場合は、第2引数にtrueを指定します。
C#Directory.Delete(
@"C:\work\target-folder",
recursive: true
);
この処理はフォルダー内のデータをまとめて削除するため、実行前に削除対象のパスを十分に確認してください。
C#string targetDirectory = Path.GetFullPath(
@"C:\work\target-folder"
);
Console.WriteLine($"削除対象: {targetDirectory}");
if (Directory.Exists(targetDirectory))
{
Directory.Delete(targetDirectory, recursive: true);
}
6-6. ごみ箱を経由せず完全削除される点に注意する
File.DeleteやDirectory.Deleteで削除したデータは、通常のエクスプローラー操作とは異なり、ごみ箱を経由せず削除されます。
そのため、重要なファイルを扱う処理では、次のような対策が必要です。
削除前に確認画面を表示する
削除対象を専用フォルダーに限定する
削除前にバックアップを作成する
削除対象の絶対パスをログに残す
本番環境と開発環境の保存先を分ける
いきなり削除せず、退避フォルダーへ移動する
復元可能な運用が必要な場合は、File.DeleteではなくFile.Moveを使って、一時的な退避フォルダーへ移動する方法も検討してください。
7. RemoveやDeleteで発生するエラーと対処法
7-1. ArgumentOutOfRangeExceptionが発生する原因
ArgumentOutOfRangeExceptionは、指定したインデックスや件数が有効範囲外の場合に発生します。
代表例は次のとおりです。
string.Removeに負の開始位置を指定した文字列の長さを超える開始位置を指定した
開始位置と削除文字数の合計が文字列の長さを超えた
List<T>.RemoveAtに存在しないインデックスを指定したRemoveRangeの範囲がリストの要素数を超えた
インデックスや件数を外部入力から受け取る場合は、実行前の検証が重要です。
7-2. string.Removeの開始位置や文字数が範囲外になる場合
次のコードでは、文字列の長さを超える位置を指定しているため例外が発生します。
C#string text = "ABC";
string result = text.Remove(5);
開始位置は、0以上かつ文字列の長さ以下である必要があります。
C#int startIndex = 2;
if (startIndex >= 0 && startIndex <= text.Length)
{
text = text.Remove(startIndex);
}
2つの引数を指定する場合は、次の条件を確認します。
C#int startIndex = 1;
int count = 2;
bool isValid =
startIndex >= 0 &&
count >= 0 &&
startIndex <= text.Length &&
count <= text.Length - startIndex;
if (isValid)
{
text = text.Remove(startIndex, count);
}
startIndex + count <= text.Lengthでも確認できますが、count <= text.Length - startIndexと書くと、非常に大きな数値を扱う場合の加算オーバーフローを避けやすくなります。
7-3. RemoveAtで存在しないインデックスを指定した場合
RemoveAtで有効なインデックスは、0からCount - 1までです。
C#var numbers = new List<int> { 10, 20, 30 };
numbers.RemoveAt(3); // 例外
要素数が3の場合、有効なインデックスは0、1、2です。
安全に削除するには範囲を確認します。
C#int index = 2;
if (index >= 0 && index < numbers.Count)
{
numbers.RemoveAt(index);
}
末尾の要素を削除する場合は、リストが空でないことを確認します。
C#if (numbers.Count > 0)
{
numbers.RemoveAt(numbers.Count - 1);
}
7-4. null参照によるNullReferenceExceptionを防ぐ方法
リスト変数がnullの状態でRemoveを呼び出すと、NullReferenceExceptionが発生します。
C#List<string>? names = null;
names.Remove("Alice"); // 例外
null条件演算子を使えば、nullの場合はメソッドを実行しません。
C#bool removed = names?.Remove("Alice") ?? false;
初期化できる場合は、空のリストとして用意しておく方法が基本です。
C#var names = new List<string>();
文字列がnullの可能性がある場合も確認が必要です。
C#string? text = GetText();
if (!string.IsNullOrEmpty(text))
{
text = text.Remove(0, 1);
}
ただし、空文字列に対してRemove(0)を実行することは可能です。1文字以上を削除する場合は、必要な長さがあるかも確認してください。
7-5. foreach中のRemoveでInvalidOperationExceptionが発生する原因
foreachで列挙しているコレクションに対して、直接Removeを実行すると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を使用するfor文で末尾から処理するWhereで新しいリストを作るToListでコピーを作ってから元のリストを変更する
7-6. File.DeleteでUnauthorizedAccessExceptionが発生する場合
UnauthorizedAccessExceptionは、ファイルやフォルダーへのアクセス権限が不足している場合などに発生します。
主な原因は次のとおりです。
削除権限がない
読み取り専用ファイルを扱っている
保護されたシステムフォルダーを操作している
ファイルではなくフォルダーのパスを
File.Deleteに渡している実行ユーザーに必要な権限が付与されていない
読み取り専用属性を解除してから削除する例は次のとおりです。
C#string path = "readonly.txt";
if (File.Exists(path))
{
FileAttributes attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.ReadOnly) != 0)
{
File.SetAttributes(
path,
attributes & ~FileAttributes.ReadOnly
);
}
File.Delete(path);
}
ただし、アプリケーションが権限設定を勝手に変更してよいかは、運用要件を確認する必要があります。
例外を捕捉してログを残すことも重要です。
C#try
{
File.Delete(path);
}
catch (UnauthorizedAccessException ex)
{
Console.Error.WriteLine(
$"削除権限がありません: {ex.Message}"
);
}
7-7. IOExceptionやファイル使用中エラーへの対処法
別のアプリケーションや自分のプログラムがファイルを使用している場合、IOExceptionが発生することがあります。
次のコードでは、ファイルを開いたまま削除しようとしています。
C#string path = "sample.txt";
using FileStream stream = File.OpenRead(path);
File.Delete(path);
ファイル操作が完了してから、ストリームを確実に破棄する必要があります。
C#string path = "sample.txt";
using (FileStream stream = File.OpenRead(path))
{
// ファイルを読み取る
}
File.Delete(path);
一時的に他のプロセスが使用している可能性がある場合は、短い間隔で限定的に再試行する方法があります。
C#static bool TryDeleteFile(
string path,
int maxAttempts = 3,
int delayMilliseconds = 200)
{
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
File.Delete(path);
return true;
}
catch (IOException) when (attempt < maxAttempts)
{
Thread.Sleep(delayMilliseconds);
}
}
return false;
}
無制限に再試行すると処理が終了しなくなるため、試行回数には上限を設けてください。
7-8. パス不正や長すぎるパスに関するエラー対策
ファイルパスに問題があると、ArgumentException、NotSupportedException、DirectoryNotFoundException、PathTooLongExceptionなどが発生する可能性があります。実際に発生する例外は、使用している.NETやOS、パスの内容によって異なります。
パスを組み立てるときは、文字列連結ではなくPath.Combineを使用します。
C#string path = Path.Combine(
baseDirectory,
fileName
);
ユーザー入力をファイル名として使用する場合は、不正な文字を確認します。
C#static bool ContainsInvalidFileNameChars(string fileName)
{
return fileName.IndexOfAny(
Path.GetInvalidFileNameChars()
) >= 0;
}
意図したフォルダーの外側を指していないか確認することも重要です。
C#string baseDirectory = Path.GetFullPath(@"C:\app\data");
string targetPath = Path.GetFullPath(
Path.Combine(baseDirectory, userInput)
);
string basePrefix =
baseDirectory.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar
) + Path.DirectorySeparatorChar;
bool isInsideBaseDirectory =
targetPath.StartsWith(
basePrefix,
StringComparison.OrdinalIgnoreCase
);
if (!isInsideBaseDirectory)
{
throw new InvalidOperationException(
"許可されたフォルダー外のパスです。"
);
}
パスの大文字と小文字の扱いはOSによって異なるため、実行環境に応じた比較方法を選択してください。
8. ループ処理中に要素を安全に削除する方法
8-1. foreach中にList.Removeを実行してはいけない理由
foreachはコレクションの列挙中に構造が変わらないことを前提としています。Remove、Add、Clearなどを実行すると、列挙子が無効になり、通常は次の反復時にInvalidOperationExceptionが発生します。
C#foreach (User user in users)
{
if (!user.IsActive)
{
users.Remove(user); // 実行してはいけない
}
}
削除条件が明確であれば、RemoveAllが最も簡潔です。
C#users.RemoveAll(user => !user.IsActive);
8-2. for文で末尾から削除する
インデックスを使って削除する場合は、リストの末尾から先頭へ向かって処理します。
C#for (int i = users.Count - 1; i >= 0; i--)
{
if (!users[i].IsActive)
{
users.RemoveAt(i);
}
}
末尾から削除すれば、まだ確認していない前方の要素のインデックスは変わりません。
先頭から削除すると、削除後に後続要素が左へ詰まり、要素を飛ばす可能性があります。
C#for (int i = 0; i < users.Count; i++)
{
if (!users[i].IsActive)
{
users.RemoveAt(i);
}
}
この書き方では、削除直後に新しくインデックスiへ移動した要素が確認されない場合があります。
8-3. RemoveAllを使って条件に一致する要素を削除する
List<T>から条件に一致する要素をまとめて削除する場合は、RemoveAllが適しています。
C#int removedCount = users.RemoveAll(
user => !user.IsActive
);
Console.WriteLine(
$"{removedCount}件削除しました。"
);
RemoveAllは元のリストを直接変更します。削除件数も戻り値として取得でき、通常は手動でRemoveAtを繰り返すより読みやすいコードになります。
8-4. Whereで削除対象を除外した新しいリストを作る
元のリストを変更せず、条件に一致しない要素から新しいリストを作る場合はWhereを使用します。
C#List<User> activeUsers = users
.Where(user => user.IsActive)
.ToList();
この方法ではusersは変更されません。
変数に再代入すれば、削除後と同等のリストを得られます。
C#users = users
.Where(user => user.IsActive)
.ToList();
ただし、新しいリストが作成されるため、元のList<T>インスタンスを他の箇所が参照している場合、その参照先の内容は変わりません。
C#List<User> sharedReference = users;
users = users
.Where(user => user.IsActive)
.ToList();
// sharedReferenceは元のリストを参照したまま
同じリストインスタンスを維持する必要がある場合はRemoveAllを使用します。
8-5. ToListでコピーを作成してから削除する
列挙対象のコピーを作れば、コピーをforeachで処理しながら元のリストから削除できます。
C#foreach (User user in users.ToList())
{
if (!user.IsActive)
{
users.Remove(user);
}
}
ToListで作成したコピーを列挙しているため、元のusersを変更しても列挙子は無効になりません。
ただし、リスト全体のコピーを作成するため、要素数が多い場合は追加のメモリが必要です。また、Removeを繰り返すことで検索や要素移動のコストが増える可能性があります。
条件削除だけが目的なら、通常はRemoveAllのほうが適しています。
8-6. 安全性とパフォーマンスを考慮した選び方
削除方法は、目的とデータ量に応じて選択します。
List<T>を直接変更して条件削除する:RemoveAllインデックスを使った個別処理が必要:末尾からの
for元のリストを保持する:
Where(...).ToList()削除中に個別の副作用処理を行う:コピーを作るか、末尾からの
for連続範囲をまとめて削除する:
RemoveRange値が分かっている1件だけを削除する:
Remove
List<T>の途中から要素を削除すると、後続要素を詰める処理が必要です。大量の要素に対してRemoveAtを何度も繰り返すと、処理時間が大きくなる可能性があります。
単純な条件削除なら、1回の走査で処理できるRemoveAllを優先するとよいでしょう。
9. C#のRemoveを使った実践的なコード例
9-1. 入力文字列から不要な部分を削除する
入力文字列の#以降をコメントとして削除する例です。
C#static string RemoveComment(string input)
{
ArgumentNullException.ThrowIfNull(input);
int commentIndex = input.IndexOf('#');
if (commentIndex < 0)
{
return input;
}
return input.Remove(commentIndex).TrimEnd();
}
使用例は次のとおりです。
C#string input = "timeout=30 # 秒単位";
string result = RemoveComment(input);
Console.WriteLine(result); // timeout=30
URLからクエリ文字列とフラグメントを削除する場合は、最初に現れる?または#の位置を調べます。
C#static string RemoveQueryAndFragment(string url)
{
ArgumentNullException.ThrowIfNull(url);
int queryIndex = url.IndexOf('?');
int fragmentIndex = url.IndexOf('#');
int removeIndex;
if (queryIndex < 0)
{
removeIndex = fragmentIndex;
}
else if (fragmentIndex < 0)
{
removeIndex = queryIndex;
}
else
{
removeIndex = Math.Min(queryIndex, fragmentIndex);
}
return removeIndex >= 0
? url.Remove(removeIndex)
: url;
}
9-2. リストから指定IDのデータを削除する
指定IDのデータを1件削除する例です。
C#public sealed class Product
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
C#static bool RemoveProductById(
List<Product> products,
int targetId)
{
ArgumentNullException.ThrowIfNull(products);
int index = products.FindIndex(
product => product.Id == targetId
);
if (index < 0)
{
return false;
}
products.RemoveAt(index);
return true;
}
使用例は次のとおりです。
C#var products = new List<Product>
{
new() { Id = 1, Name = "Keyboard" },
new() { Id = 2, Name = "Mouse" },
new() { Id = 3, Name = "Monitor" }
};
bool removed = RemoveProductById(products, 2);
Console.WriteLine(removed); // True
同じIDのデータが複数存在する可能性があり、すべて削除したい場合はRemoveAllを使用します。
C#int removedCount = products.RemoveAll(
product => product.Id == 2
);
9-3. 条件に一致する重複データを一括削除する
同じIDを持つデータが複数ある場合に、最初の1件だけを残して重複を削除する例です。
C#var seenIds = new HashSet<int>();
int removedCount = products.RemoveAll(
product => !seenIds.Add(product.Id)
);
HashSet<T>.Addは、値を新しく追加できた場合にtrue、すでに存在する場合にfalseを返します。そのため、2件目以降の同一IDだけが削除対象になります。
次のデータに対して実行します。
C#var products = new List<Product>
{
new() { Id = 1, Name = "Keyboard A" },
new() { Id = 2, Name = "Mouse" },
new() { Id = 1, Name = "Keyboard B" },
new() { Id = 3, Name = "Monitor" },
new() { Id = 2, Name = "Mouse B" }
};
削除後は、各IDについて最初に出現した要素だけが残ります。
重複を除外した新しいリストを作る場合は、対応する.NET環境でDistinctByを使用できます。
C#List<Product> uniqueProducts = products
.DistinctBy(product => product.Id)
.ToList();
9-4. Dictionaryから不要なキーを安全に削除する
Dictionaryを列挙しながら直接削除する方法は、実行環境や処理内容による差を避けるためにも使用せず、削除対象のキーを先にリスト化すると安全です。
C#var scores = new Dictionary<string, int>
{
["Alice"] = 90,
["Bob"] = 40,
["Carol"] = 75,
["David"] = 35
};
List<string> keysToRemove = scores
.Where(pair => pair.Value < 50)
.Select(pair => pair.Key)
.ToList();
foreach (string key in keysToRemove)
{
scores.Remove(key);
}
削除件数を取得する場合は、Removeの戻り値を数えます。
C#int removedCount = 0;
foreach (string key in keysToRemove)
{
if (scores.Remove(key))
{
removedCount++;
}
}
9-5. 拡張子や更新日時を条件にファイルを削除する
指定フォルダー内にある、30日以上前の.logファイルを削除する例です。
C#static int DeleteOldLogFiles(
string directoryPath,
int retentionDays)
{
ArgumentException.ThrowIfNullOrWhiteSpace(
directoryPath
);
if (retentionDays < 0)
{
throw new ArgumentOutOfRangeException(
nameof(retentionDays)
);
}
if (!Directory.Exists(directoryPath))
{
return 0;
}
DateTime thresholdUtc =
DateTime.UtcNow.AddDays(-retentionDays);
int deletedCount = 0;
foreach (string filePath in Directory.EnumerateFiles(
directoryPath,
"*.log",
SearchOption.TopDirectoryOnly))
{
DateTime lastWriteTimeUtc =
File.GetLastWriteTimeUtc(filePath);
if (lastWriteTimeUtc < thresholdUtc)
{
File.Delete(filePath);
deletedCount++;
}
}
return deletedCount;
}
更新日時の比較には、タイムゾーンの影響を受けにくいUTCを使用しています。
使用例は次のとおりです。
C#int deletedCount = DeleteOldLogFiles(
@"C:\app\logs",
retentionDays: 30
);
Console.WriteLine(
$"{deletedCount}件のログを削除しました。"
);
9-6. 例外処理を含めてファイルを安全に削除する
ファイル削除の結果を列挙型で返すと、呼び出し側で処理を分岐しやすくなります。
C#public enum FileDeleteResult
{
Deleted,
NotFound,
AccessDenied,
InUseOrIoError,
InvalidPath,
UnexpectedError
}
C#static FileDeleteResult DeleteFileSafely(
string path,
Action<string>? log = null)
{
if (string.IsNullOrWhiteSpace(path))
{
return FileDeleteResult.InvalidPath;
}
try
{
string fullPath = Path.GetFullPath(path);
if (!File.Exists(fullPath))
{
return FileDeleteResult.NotFound;
}
File.Delete(fullPath);
log?.Invoke($"削除しました: {fullPath}");
return FileDeleteResult.Deleted;
}
catch (UnauthorizedAccessException ex)
{
log?.Invoke($"アクセス拒否: {ex.Message}");
return FileDeleteResult.AccessDenied;
}
catch (IOException ex)
{
log?.Invoke($"I/Oエラー: {ex.Message}");
return FileDeleteResult.InUseOrIoError;
}
catch (ArgumentException ex)
{
log?.Invoke($"パスが不正です: {ex.Message}");
return FileDeleteResult.InvalidPath;
}
catch (NotSupportedException ex)
{
log?.Invoke($"未対応のパス形式です: {ex.Message}");
return FileDeleteResult.InvalidPath;
}
catch (Exception ex)
{
log?.Invoke($"予期しないエラー: {ex.Message}");
return FileDeleteResult.UnexpectedError;
}
}
使用例は次のとおりです。
C#FileDeleteResult result = DeleteFileSafely(
"sample.txt",
Console.WriteLine
);
switch (result)
{
case FileDeleteResult.Deleted:
Console.WriteLine("削除に成功しました。");
break;
case FileDeleteResult.NotFound:
Console.WriteLine("ファイルが存在しません。");
break;
case FileDeleteResult.AccessDenied:
Console.WriteLine("削除権限がありません。");
break;
default:
Console.WriteLine("ファイルを削除できませんでした。");
break;
}
File.Existsで確認した直後にファイルの状態が変わる可能性は残ります。そのため、存在確認と例外処理の両方を組み合わせることが重要です。
10. Removeを正しく使うための注意点
10-1. 削除対象が値かインデックスかを確認する
RemoveとRemoveAtでは、引数の意味が異なります。
C#var numbers = new List<int>
{
10, 20, 30
};
numbers.Remove(1);
このコードは、インデックス1の20ではなく、値が1の要素を削除しようとします。
インデックス1の要素を削除する場合は、RemoveAtを使用します。
C#numbers.RemoveAt(1);
整数リストでは値とインデックスを混同しやすいため、特に注意が必要です。
10-2. 文字列のインデックスが0から始まる点に注意する
C#の文字列とリストのインデックスは0から始まります。
C#string text = "ABCDE";
各文字のインデックスは次のようになります。
A B C D E
0 1 2 3 4
Remove(2, 1)で削除されるのは3文字目のCです。
C#string result = text.Remove(2, 1);
Console.WriteLine(result); // ABDE
外部から「3文字目」という指定を受け取る場合は、必要に応じて1を引いてインデックスへ変換します。
C#int position = 3;
int index = position - 1;
10-3. 削除前にnull・件数・存在有無を確認する
削除処理では、対象に応じて次の点を確認します。
文字列の場合は、nullかどうか、必要な長さがあるかを確認します。
C#if (!string.IsNullOrEmpty(text) && text.Length >= 3)
{
text = text.Remove(0, 3);
}
リストの場合は、nullかどうか、インデックスが範囲内かを確認します。
C#if (items is not null &&
index >= 0 &&
index < items.Count)
{
items.RemoveAt(index);
}
値を削除する場合は、Removeの戻り値を利用します。
C#if (!items.Remove(target))
{
Console.WriteLine("対象は存在しません。");
}
ファイルの場合は、必要に応じて存在確認を行い、最終的には例外処理で失敗に備えます。
C#try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (IOException ex)
{
Console.Error.WriteLine(ex.Message);
}
10-4. 大量データ削除時のパフォーマンスを考慮する
List<T>の途中から要素を削除すると、後続要素を前へ移動する処理が発生します。先頭付近のRemoveAtを大量に繰り返すと、処理量が大きくなる可能性があります。
条件に一致する要素をまとめて削除する場合は、RemoveAllが適しています。
C#items.RemoveAll(item => item.IsDeleted);
削除対象以外を新しいリストへ集める方法もあります。
C#items = items
.Where(item => !item.IsDeleted)
.ToList();
キーを使った検索と削除を頻繁に行う場合は、List<T>よりDictionary<TKey,TValue>やHashSet<T>が適していることがあります。
C#var productsById =
new Dictionary<int, Product>();
productsById.Remove(targetId);
コレクションの選択段階から、検索方法、順序の必要性、重複の可否、削除頻度を考慮しましょう。
10-5. 復元できないファイル削除では確認処理を入れる
File.Deleteや再帰的なDirectory.Deleteは、重要なデータを失う原因になります。特に次のようなコードでは、変数の値を誤ると広範囲のファイルが削除されます。
C#Directory.Delete(targetDirectory, recursive: true);
安全性を高めるには、削除対象を許可されたベースディレクトリ内に限定します。
C#static bool IsPathInsideDirectory(
string targetPath,
string allowedDirectory)
{
string fullTargetPath =
Path.GetFullPath(targetPath);
string fullAllowedDirectory =
Path.GetFullPath(allowedDirectory)
.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar
) + Path.DirectorySeparatorChar;
return fullTargetPath.StartsWith(
fullAllowedDirectory,
StringComparison.OrdinalIgnoreCase
);
}
さらに、削除前に対象件数とパスを表示する、確認フラグを必要とする、バックアップを作るといった対策を組み合わせると安全です。
10-6. 用途別に最適な削除メソッドを選ぶ
C#の削除処理では、対象と条件に合わせてメソッドを選択することが重要です。
文字列の指定位置から削除する:
string.Removeリストから指定した値を1件削除する:
List<T>.Removeリストから指定位置の要素を削除する:
RemoveAt条件に一致する要素をすべて削除する:
RemoveAll連続する範囲を削除する:
RemoveRangeDictionaryから指定キーを削除する:
Dictionary.RemoveHashSetから指定値を削除する:
HashSet.RemoveQueueの先頭を取り出して削除する:
DequeueまたはTryDequeueStackの末尾を取り出して削除する:
PopまたはTryPopコレクションを空にする:
Clearファイルを削除する:
File.Deleteフォルダーを削除する:
Directory.Delete
メソッド名だけでなく、引数が値なのかインデックスなのか、戻り値から成否や件数を取得できるのかも確認してください。
まとめ
C#のRemoveは、文字列やコレクションからデータを削除するための基本的なメソッドです。ただし、対象によって動作や戻り値が異なります。
string.Removeは、開始位置や文字数を指定して文字列の一部を削除し、削除後の新しい文字列を返します。文字列自体は変更されないため、戻り値の代入を忘れないことが重要です。
List<T>.Removeは値と一致する最初の要素を削除し、成功したかどうかをboolで返します。インデックスを指定する場合はRemoveAt、条件に一致する要素をまとめて削除する場合はRemoveAll、連続範囲を削除する場合はRemoveRangeを使用します。
DictionaryやHashSetでもRemoveを利用できますが、Dictionaryでは値ではなくキーを指定します。QueueやStackでは、それぞれDequeueとPopを使って要素を取り出しながら削除します。
ファイルやフォルダーの削除には、RemoveではなくFile.DeleteとDirectory.Deleteを使用します。これらの処理は通常ごみ箱を経由しないため、削除対象のパス、アクセス権限、ファイルの使用状態を確認し、例外処理やログ記録を組み合わせることが大切です。
削除処理を実装するときは、「値」「インデックス」「条件」「範囲」「パス」のどれを指定する処理なのかを明確にし、目的に合ったメソッドを選びましょう。

