csharp delete完全ガイド:C#でファイル・フォルダ・要素・オブジェクトを安全に削除する方法

はじめに

C#で「delete」と検索する人の多くは、ファイルやフォルダ、コレクションの要素、オブジェクト、データベースのレコードなどを削除する方法を探しているでしょう。

ただし、C#にはあらゆる対象を削除できる共通のdeleteキーワードはありません。削除対象に応じて、File.DeleteDirectory.DeleteRemoveClearDisposeなどを使い分けます。

削除処理はデータを失う可能性があるため、単にAPIを呼び出すだけでは不十分です。対象の存在確認、権限、パス、使用状態、復元方法、ログ記録まで考慮する必要があります。

本記事では、C#でファイル・フォルダ・要素・オブジェクト・データベースレコードを安全に削除する方法を、実用的なコード例とともに解説します。

1. C#のdeleteとは?削除対象ごとの基本を理解する

1-1. C#に共通のdeleteキーワードが存在しない理由

C++では、動的に確保したメモリを解放するためにdeleteを使用します。一方、C#のマネージドオブジェクトはガベージコレクターによって管理されるため、開発者がオブジェクトのメモリを直接削除する必要はありません。

また、ファイル、フォルダ、コレクション、データベースレコードは、それぞれ保存場所や管理方法が異なります。そのため、C#では対象ごとに専用の削除メソッドが用意されています。

C#
File.Delete(filePath);                 // ファイル
Directory.Delete(directoryPath); // フォルダ
list.Remove(item); // Listの要素
dictionary.Remove(key); // Dictionaryの要素
dbContext.Remove(entity); // EF Coreのエンティティ

1-2. ファイル・フォルダ・要素・オブジェクトで削除方法が異なる

代表的な削除方法は次のとおりです。

削除対象主な方法
ファイルFile.Delete
フォルダDirectory.Delete
Listの要素RemoveRemoveAtRemoveAll
全要素Clear
Dictionaryの要素Remove
配列の要素新しい配列を作成
マネージドオブジェクト参照を外し、GCに回収を任せる
外部リソースDispose
データベースレコードSQLのDELETE、EF CoreのRemoveExecuteDelete

「C#でdeleteする」という目的を実現するには、最初に何を削除したいのかを明確にすることが重要です。

1-3. 削除処理を実装する前に確認すべき権限・存在・使用状態

削除前には、少なくとも次の点を確認します。

  • 対象が存在するか

  • アプリケーションに削除権限があるか

  • ファイルやフォルダが別のプロセスで使用されていないか

  • パスやIDが想定した範囲内か

  • 削除後に復元できるか

  • 関連するデータまで削除されないか

存在確認だけでは、削除の成功は保証されません。存在確認後、実際に削除するまでの間に別の処理が対象を変更する可能性があるため、最終的には例外処理も必要です。

1-4. Delete・Remove・Clear・Disposeの違い

Deleteは、ファイル、フォルダ、データベースレコードなどを削除するときに使われる名称です。

Removeは、コレクションから特定の要素を取り除く場合によく使われます。

Clearは、コレクション内の全要素を取り除きます。ただし、コレクションオブジェクト自体は残ります。

Disposeは削除ではありません。ファイルハンドル、データベース接続、ストリームなどが保持している外部リソースを解放するための処理です。マネージドオブジェクトのメモリそのものは、ガベージコレクターが管理します。Microsoft Learn+1

2. C#でファイルを削除する方法

2-1. File.Deleteで単一ファイルを削除する基本構文

単一ファイルを削除するには、System.IO.File.Deleteを使用します。

C#
using System.IO;

string filePath = @"C:\work\sample.txt";

File.Delete(filePath);

File.Deleteは削除結果を返しません。削除できなかった場合は、原因に応じた例外が発生します。

相対パスも指定できますが、実行時のカレントディレクトリによって対象が変わるため、重要な削除処理では絶対パスを使用するほうが安全です。Microsoft Learn

2-2. File.Existsでファイルの存在を確認してから削除する

ファイルの存在を確認するにはFile.Existsを使用します。

C#
string filePath = @"C:\work\sample.txt";

if (File.Exists(filePath))
{
File.Delete(filePath);
}

ただし、File.Deleteは指定したファイルが存在しない場合、通常は例外を発生させません。そのため、削除するだけなら存在確認を省略できます。

存在確認は、「存在しなかったことをログに残したい」「ユーザーに結果を表示したい」といった場合に有効です。

C#
if (!File.Exists(filePath))
{
Console.WriteLine("削除対象のファイルがありません。");
return;
}

File.Delete(filePath);
Console.WriteLine("ファイルを削除しました。");

2-3. 複数ファイルを条件指定で一括削除する

特定のフォルダ内にある複数ファイルを削除する場合は、Directory.EnumerateFilesで列挙します。

C#
string directoryPath = @"C:\work";

foreach (string filePath in Directory.EnumerateFiles(directoryPath))
{
File.Delete(filePath);
}

Directory.GetFilesは対象パスを配列としてまとめて取得します。一方、Directory.EnumerateFilesは列挙しながら処理できるため、大量のファイルを扱う場合にメモリ使用量を抑えやすくなります。

ファイル単位で失敗を処理する場合は、それぞれにtry-catchを設けます。

C#
foreach (string filePath in Directory.EnumerateFiles(directoryPath))
{
try
{
File.Delete(filePath);
Console.WriteLine($"削除成功: {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"削除失敗: {filePath} / {ex.Message}");
}
}

本番環境ではExceptionをまとめて捕捉するだけでなく、UnauthorizedAccessExceptionIOExceptionなどを個別に処理しましょう。

2-4. 拡張子・更新日時・ファイル名を条件に削除する

拡張子を指定する場合は、検索パターンを使用します。

C#
foreach (string filePath in Directory.EnumerateFiles(
@"C:\logs",
"*.log",
SearchOption.TopDirectoryOnly))
{
File.Delete(filePath);
}

更新日時が一定期間より古いファイルだけを削除する例です。

C#
string directoryPath = @"C:\logs";
DateTime threshold = DateTime.UtcNow.AddDays(-30);

foreach (string filePath in Directory.EnumerateFiles(directoryPath, "*.log"))
{
DateTime lastWriteTime = File.GetLastWriteTimeUtc(filePath);

if (lastWriteTime < threshold)
{
File.Delete(filePath);
}
}

ファイル名を条件にする場合は、Path.GetFileNameを使用します。

C#
foreach (string filePath in Directory.EnumerateFiles(@"C:\work"))
{
string fileName = Path.GetFileName(filePath);

if (fileName.StartsWith("temp_", StringComparison.OrdinalIgnoreCase))
{
File.Delete(filePath);
}
}

大文字・小文字の扱いはOSやファイルシステムによって異なるため、文字列比較方法を明示すると意図が伝わりやすくなります。

2-5. 読み取り専用ファイルを削除する方法

読み取り専用属性が設定されたファイルは、環境によってUnauthorizedAccessExceptionが発生することがあります。

削除前に属性を変更します。

C#
string filePath = @"C:\work\readonly.txt";

if (File.Exists(filePath))
{
FileAttributes attributes = File.GetAttributes(filePath);

if ((attributes & FileAttributes.ReadOnly) != 0)
{
File.SetAttributes(
filePath,
attributes & ~FileAttributes.ReadOnly);
}

File.Delete(filePath);
}

FileAttributes.Normalを直接設定する方法もありますが、既存の属性をすべて失う可能性があります。読み取り専用属性だけを解除するほうが安全です。

2-6. 使用中・ロック中のファイルを安全に処理する

他のプロセスがファイルを排他的に開いている場合、File.DeleteIOExceptionが発生することがあります。

まず、自分のコードで開いたストリームを確実に閉じます。

C#
string filePath = @"C:\work\data.txt";

using (FileStream stream = File.OpenRead(filePath))
{
// ファイルを使用する処理
}

// usingブロックを抜けた時点でストリームがDisposeされる
File.Delete(filePath);

一時的なロックが想定される場合は、回数と待機時間を制限した再試行を実装できます。

C#
static async Task DeleteWithRetryAsync(
string filePath,
int maxRetries,
CancellationToken cancellationToken)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();

try
{
File.Delete(filePath);
return;
}
catch (IOException) when (attempt < maxRetries)
{
await Task.Delay(
TimeSpan.FromMilliseconds(200 * attempt),
cancellationToken);
}
}
}

無制限に再試行すると処理が終了しなくなるため、必ず最大回数を設定します。

2-7. File.Deleteで発生する例外と対処法

代表的な例外は次のとおりです。

例外主な原因
UnauthorizedAccessException権限不足、読み取り専用、パスがフォルダ
IOExceptionファイルが使用中、I/Oエラー
DirectoryNotFoundException親フォルダが存在しない
PathTooLongException実行環境で扱えない長いパス
ArgumentExceptionパス形式の問題
NotSupportedExceptionサポートされないパス形式
SecurityExceptionセキュリティ上の制限

例外を利用者向けメッセージと開発者向けログに分けると、安全性と保守性が向上します。

C#
try
{
File.Delete(filePath);
}
catch (UnauthorizedAccessException ex)
{
logger.LogError(ex, "削除権限がありません。Path: {Path}", filePath);
}
catch (IOException ex)
{
logger.LogWarning(ex, "ファイルを削除できません。Path: {Path}", filePath);
}

2-8. 削除したファイルを復元できるごみ箱へ移動する方法

File.Deleteは通常、ファイルをごみ箱へ移動せず直接削除します。

Windowsでごみ箱へ送る場合は、Microsoft.VisualBasic.FileIO.FileSystem.DeleteFileを利用できます。

C#
using Microsoft.VisualBasic.FileIO;

string filePath = @"C:\work\sample.txt";

FileSystem.DeleteFile(
filePath,
UIOption.OnlyErrorDialogs,
RecycleOption.SendToRecycleBin);

この方法はWindowsのごみ箱機能に依存します。クロスプラットフォームアプリでは、削除予定ファイルをアプリ独自の退避フォルダへ移動する方法も検討しましょう。Microsoft Learn+1

3. C#でフォルダを削除する方法

3-1. Directory.Deleteで空のフォルダを削除する

空のフォルダを削除するには、Directory.Deleteを使用します。

C#
using System.IO;

string directoryPath = @"C:\work\empty";

Directory.Delete(directoryPath);

中にファイルやサブフォルダがある場合、引数を1つだけ指定するこの呼び出しでは削除できず、IOExceptionが発生します。Microsoft Learn+1

3-2. Directory.Deleteのrecursive指定で中身ごと削除する

フォルダ内のファイルやサブフォルダをまとめて削除するには、第2引数にtrueを指定します。

C#
string directoryPath = @"C:\work\target";

Directory.Delete(directoryPath, recursive: true);

この操作は対象フォルダ以下を一括削除します。パスを間違えると大きなデータ損失につながるため、ユーザー入力を未検証のまま渡してはいけません。

3-3. Directory.Existsでフォルダの存在を確認する

フォルダの存在を確認するにはDirectory.Existsを使用します。

C#
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, recursive: true);
}

存在確認後に別の処理がフォルダを削除する可能性もあるため、必要に応じて例外処理を追加します。

C#
try
{
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, recursive: true);
}
}
catch (DirectoryNotFoundException)
{
// 確認後に別処理が削除した場合など
}

3-4. フォルダ内のファイルだけを削除してフォルダを残す

フォルダ自体を残し、直下のファイルだけを削除する例です。

C#
foreach (string filePath in Directory.EnumerateFiles(directoryPath))
{
File.Delete(filePath);
}

サブフォルダ内のファイルも対象にする場合は、SearchOption.AllDirectoriesを指定します。

C#
foreach (string filePath in Directory.EnumerateFiles(
directoryPath,
"*",
SearchOption.AllDirectories))
{
File.Delete(filePath);
}

このコードではサブフォルダ自体は残ります。

3-5. サブフォルダを含めて条件付きで削除する

一定期間より古いファイルを削除し、その結果空になったサブフォルダだけを削除する例です。

C#
static void DeleteOldFilesAndEmptyDirectories(
string rootPath,
DateTime thresholdUtc)
{
foreach (string filePath in Directory.EnumerateFiles(
rootPath,
"*",
SearchOption.AllDirectories))
{
if (File.GetLastWriteTimeUtc(filePath) < thresholdUtc)
{
File.Delete(filePath);
}
}

string[] directories = Directory
.EnumerateDirectories(rootPath, "*", SearchOption.AllDirectories)
.OrderByDescending(path => path.Length)
.ToArray();

foreach (string directoryPath in directories)
{
if (!Directory.EnumerateFileSystemEntries(directoryPath).Any())
{
Directory.Delete(directoryPath);
}
}
}

サブフォルダは深い階層から処理します。親フォルダを先に削除しようとすると、中に空の子フォルダが残っているため削除できません。

3-6. 空ではないフォルダを削除できない原因と解決策

主な原因は次のとおりです。

  • Directory.Delete(path)を使用している

  • 非表示ファイルやシステムファイルが残っている

  • サブフォルダが残っている

  • ファイルが使用中である

  • 読み取り専用属性が設定されている

  • 別プロセスがフォルダをカレントディレクトリとして使用している

内容をすべて削除してよい場合は、Directory.Delete(path, true)を使用します。ただし、「強制削除」という名称のAPIではないため、権限不足や使用中のファイルまで無条件に削除できるわけではありません。

3-7. アクセス拒否・使用中・パス不正エラーへの対処法

権限不足の場合は、実行ユーザーに必要最小限の削除権限を与えます。安易に管理者権限で実行するのではなく、対象フォルダのアクセス制御を見直すことが基本です。

使用中の場合は、開いているFileStreamStreamReaderDisposeし、外部プロセスによるロックなら時間を置いて再試行します。

パスについては、Path.GetFullPathで正規化し、許可したルートディレクトリ内にあることを検証してから削除します。

4. C#で配列・List・コレクションの要素を削除する方法

4-1. List.Removeで指定した値を削除する

List<T>.Removeは、指定した値と一致する最初の要素を削除します。

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

bool removed = names.Remove("Alice");

実行後の内容はBobAliceです。重複する値があっても、1回の呼び出しで削除されるのは最初の1件だけです。

戻り値は、削除できた場合にtrue、対象が存在しなかった場合にfalseとなります。Microsoft Learn+1

4-2. List.RemoveAtで指定したインデックスの要素を削除する

指定位置の要素を削除するにはRemoveAtを使用します。

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

names.RemoveAt(1);

実行後はAliceCarolとなります。

インデックスが0未満、またはCount以上の場合はArgumentOutOfRangeExceptionが発生します。

C#
int index = 1;

if (index >= 0 && index < names.Count)
{
names.RemoveAt(index);
}

要素を削除すると後続要素のインデックスが繰り上がります。Microsoft Learn

4-3. List.RemoveAllで条件に一致する要素を一括削除する

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

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

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

偶数がすべて削除され、removedCountには削除件数の3が入ります。

複数条件も指定できます。

C#
users.RemoveAll(user =>
user.IsDeleted ||
user.LastLoginAt < DateTime.UtcNow.AddYears(-1));

4-4. List.Clearで全要素を削除する

全要素を削除するにはClearを使用します。

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

names.Clear();

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

Clearを呼び出しても、namesというList<string>オブジェクト自体は使用できます。

参照型の要素が入っていた場合、リストから参照が外れるだけです。他の場所から参照されていないオブジェクトは、後でガベージコレクターの回収対象になります。

4-5. 配列から要素を削除した新しい配列を作成する

配列の長さは作成時に固定されるため、要素を直接削除して長さを縮めることはできません。

不要な要素を除外した新しい配列を作成します。

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

numbers = numbers
.Where(number => number != 3)
.ToArray();

インデックスを指定して除外する例です。

C#
int removeIndex = 2;

if (removeIndex >= 0 && removeIndex < numbers.Length)
{
numbers = numbers
.Where((_, index) => index != removeIndex)
.ToArray();
}

要素の追加や削除を頻繁に行う場合は、配列ではなくList<T>の使用を検討しましょう。

4-6. Dictionary.Removeでキーと値を削除する

Dictionary<TKey, TValue>から要素を削除するときは、キーを指定します。

C#
var users = new Dictionary<int, string>
{
[1] = "Alice",
[2] = "Bob"
};

bool removed = users.Remove(1);

キーが存在して削除された場合はtrue、存在しない場合はfalseが返ります。Microsoft Learn

削除した値も取得したい場合、対応する環境では次のオーバーロードを使用できます。

C#
if (users.Remove(2, out string? removedName))
{
Console.WriteLine($"削除したユーザー: {removedName}");
}

4-7. HashSet・Queue・Stackから要素を削除する

HashSet<T>ではRemoveを使用します。

C#
var tags = new HashSet<string> { "csharp", "dotnet", "sql" };

tags.Remove("sql");

Queue<T>では、先頭の要素をDequeueで取り出しながら削除します。

C#
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");

string item = queue.Dequeue();

要素がない場合の例外を避けるには、TryDequeueを使用します。

C#
if (queue.TryDequeue(out string? item))
{
Console.WriteLine(item);
}

Stack<T>では、末尾に追加された要素をPopで取り出しながら削除します。

C#
var stack = new Stack<string>();
stack.Push("first");
stack.Push("second");

if (stack.TryPop(out string? item))
{
Console.WriteLine(item);
}

Queue<T>は先入れ先出し、Stack<T>は後入れ先出しで要素を取り除きます。Microsoft Learn

4-8. foreach中に要素を削除すると発生する例外と回避方法

一般的なList<T>foreachで列挙している最中に内容を変更すると、InvalidOperationExceptionが発生します。

C#
foreach (int number in numbers)
{
if (number % 2 == 0)
{
numbers.Remove(number); // 例外の原因
}
}

条件削除ならRemoveAllを使用するのが簡単です。

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

インデックスが必要な場合は、後ろからforで処理します。

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

コピーを列挙する方法もあります。

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

ただし、コピー分のメモリが必要になります。

4-9. LINQのWhereで不要な要素を除外する方法

元のコレクションを変更せず、必要な要素だけを取得するにはWhereを使用します。

C#
var activeUsers = users
.Where(user => user.IsActive)
.ToList();

Whereは元のコレクションから要素を削除する処理ではありません。条件に一致する要素だけを列挙する新しいシーケンスを作ります。

元の変数を置き換える場合は、次のように記述できます。

C#
users = users
.Where(user => !user.IsDeleted)
.ToList();

5. C#でオブジェクトを削除・解放する方法

5-1. C#でオブジェクトを直接deleteできない理由

C#のクラスインスタンスは、CLRのガベージコレクターによって管理されます。

開発者が任意のタイミングでオブジェクトのメモリを解放すると、別の変数が同じオブジェクトを参照している場合に不正なアクセスが発生する可能性があります。そのため、C#には一般的なオブジェクトを直接削除するdelete演算子がありません。

オブジェクトがどこからも参照されなくなると、ガベージコレクターによる回収対象になります。

5-2. 参照をnullにした場合の動作

変数へnullを代入すると、その変数からオブジェクトへの参照が外れます。

C#
MyClass? instance = new MyClass();

instance = null;

ただし、別の変数が同じオブジェクトを参照している場合、そのオブジェクトは引き続き使用できます。

C#
MyClass first = new MyClass();
MyClass second = first;

first = null!;

// secondは同じオブジェクトを参照している
second.Execute();

nullを代入しても、メモリがその場で解放されるわけではありません。

5-3. ガベージコレクションが不要なオブジェクトを回収する仕組み

ガベージコレクターは、アプリケーションから到達できなくなったマネージドオブジェクトを検出し、適切なタイミングでメモリを回収します。

回収タイミングは、割り当て量、世代、メモリ状況などに基づいてランタイムが判断します。そのため、特定のオブジェクトがいつ回収されるかを通常のアプリケーションコードから保証することはできません。

ファイルハンドルやOSハンドルなど、早期解放が必要な外部リソースはGC任せにせず、Disposeを使用します。Microsoft Learn+1

5-4. IDisposableとDisposeでリソースを解放する

外部リソースを保持するクラスは、一般的にIDisposableを実装します。

C#
FileStream stream = File.OpenRead(@"C:\work\data.txt");

try
{
// ストリームを使用する
}
finally
{
stream.Dispose();
}

Disposeは、そのオブジェクトが保持するリソースを解放するためのメソッドです。オブジェクトのメモリを直接削除するものではありません。

代表的なIDisposable実装型には、次のものがあります。

  • FileStream

  • StreamReader

  • StreamWriter

  • SqlConnection

  • SqlCommand

  • HttpResponseMessage

  • CancellationTokenSource

5-5. using文・using宣言で確実にDisposeする

using文を使うと、正常終了時だけでなく例外発生時にもDisposeが呼ばれます。

C#
using (FileStream stream = File.OpenRead(@"C:\work\data.txt"))
{
// ファイルを使用する
}

using宣言を使うと、現在のスコープを抜けるときに解放されます。

C#
using FileStream stream = File.OpenRead(@"C:\work\data.txt");

// ファイルを使用する

非同期の解放に対応する型ではawait usingを使用します。

C#
await using FileStream stream = new(
@"C:\work\data.txt",
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
useAsync: true);

usingは、例外が発生した場合にも破棄可能なインスタンスを確実に解放します。Microsoft Learn+1

5-6. GC.Collectを安易に使用すべきでない理由

GC.Collectを呼び出すと、ガベージコレクションを明示的に要求できます。

C#
GC.Collect();

しかし、通常は使用すべきではありません。ランタイムが判断した最適なタイミングを崩し、アプリケーションの停止時間やCPU負荷を増やす可能性があるためです。

大量処理後にメモリが減らない問題がある場合も、最初に次の点を調査します。

  • 不要な参照が残っていないか

  • 静的コレクションが増え続けていないか

  • イベント購読が解除されているか

  • IDisposableを適切に解放しているか

  • キャッシュに上限があるか

5-7. イベント・静的参照によるメモリリークを防ぐ方法

長期間生存するオブジェクトのイベントを購読すると、購読者への参照が残り続けることがあります。

C#
publisher.Updated += OnUpdated;

不要になった時点で購読を解除します。

C#
publisher.Updated -= OnUpdated;

IDisposableと組み合わせる方法も有効です。

C#
public sealed class Subscriber : IDisposable
{
private readonly Publisher _publisher;

public Subscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.Updated += OnUpdated;
}

private void OnUpdated(object? sender, EventArgs e)
{
}

public void Dispose()
{
_publisher.Updated -= OnUpdated;
}
}

静的フィールドやシングルトンがオブジェクトを保持している場合もGCの回収対象になりません。参照の寿命を設計し、不要になった要素をコレクションから削除することが重要です。

6. C#でデータベースのレコードを削除する方法

6-1. SQLのDELETE文をC#から実行する基本例

SQL Serverへ接続してレコードを削除する例です。

C#
using Microsoft.Data.SqlClient;

string connectionString = "...";
int userId = 100;

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

const string sql = """
DELETE FROM Users
WHERE Id = @Id;
""";

await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", userId);

int affectedRows = await command.ExecuteNonQueryAsync();

ExecuteNonQueryAsyncの戻り値を確認すると、削除された行数を把握できます。

6-2. パラメーター化クエリで安全にレコードを削除する

ユーザー入力をSQL文字列へ直接連結してはいけません。

C#
// 危険な例
string sql = $"DELETE FROM Users WHERE Name = '{userInput}'";

SQLインジェクションを防ぐため、パラメーター化します。

C#
const string sql = """
DELETE FROM Users
WHERE Name = @Name;
""";

await using var command = new SqlCommand(sql, connection);
command.Parameters.Add("@Name", System.Data.SqlDbType.NVarChar, 100)
.Value = userInput;

int affectedRows = await command.ExecuteNonQueryAsync();

型と長さを明示すると、意図しない型変換を避けやすくなります。

6-3. Entity Framework CoreのRemoveでエンティティを削除する

EF Coreでは、対象エンティティを取得してRemoveを呼び出し、SaveChangesAsyncで反映します。

C#
User? user = await dbContext.Users.FindAsync(userId);

if (user is not null)
{
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync();
}

主キーだけ分かっている場合は、削除用エンティティを作成する方法もあります。

C#
var user = new User { Id = userId };

dbContext.Users.Attach(user);
dbContext.Users.Remove(user);

await dbContext.SaveChangesAsync();

ただし、同じ主キーのエンティティがすでに追跡されている場合は競合するため、変更追跡の状態を確認してください。

6-4. RemoveRangeで複数レコードを一括削除する

複数エンティティを削除するにはRemoveRangeを使用します。

C#
List<User> inactiveUsers = await dbContext.Users
.Where(user => !user.IsActive)
.ToListAsync();

dbContext.Users.RemoveRange(inactiveUsers);

await dbContext.SaveChangesAsync();

RemoveRangeはエンティティを削除状態として追跡し、実際のデータベース削除はSaveChangesまたはSaveChangesAsyncで行われます。大量データでは、対象をメモリへ読み込むコストに注意が必要です。Microsoft Learn

6-5. ExecuteDeleteで直接DELETEを実行する

対応するEF Coreでは、ExecuteDeleteAsyncを使って対象を読み込まずに削除できます。

C#
int deletedCount = await dbContext.Users
.Where(user => !user.IsActive)
.ExecuteDeleteAsync();

この処理は条件をSQLのDELETEへ変換し、データベース上で直接実行します。変更追跡を使用せず、SaveChangesAsyncも必要ありません。

一方で、追跡中のエンティティには削除結果が自動反映されません。また、複数回のExecuteDeleteをまとめるトランザクションが自動的に作られるわけではありません。必要なら明示的なトランザクションを使用します。Microsoft Learn+1

6-6. 関連データを削除するカスケード削除の注意点

親レコードを削除したとき、関連する子レコードも削除する仕組みがカスケード削除です。

C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasMany(order => order.Items)
.WithOne(item => item.Order)
.OnDelete(DeleteBehavior.Cascade);
}

便利ですが、親を1件削除しただけで大量の関連データが削除される可能性があります。

設定時には次の点を確認します。

  • データベース側の外部キー設定

  • EF Core側のDeleteBehavior

  • 必須関係か任意関係か

  • 循環する削除経路がないか

  • 監査上残す必要のあるデータがないか

カスケード削除では、親との関係を失った子データを削除するか、外部キーをnullにするかを関係設定に応じて選択します。Microsoft Learn+1

6-7. トランザクションを使って削除処理を安全に実行する

複数の削除処理をすべて成功またはすべて失敗させたい場合は、トランザクションを使用します。

C#
await using var transaction =
await dbContext.Database.BeginTransactionAsync();

try
{
await dbContext.OrderItems
.Where(item => item.OrderId == orderId)
.ExecuteDeleteAsync();

await dbContext.Orders
.Where(order => order.Id == orderId)
.ExecuteDeleteAsync();

await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}

削除順序は外部キー制約を考慮します。カスケード削除を使用しない場合は、通常、子レコードを先に削除します。

6-8. 論理削除と物理削除の違い・使い分け

物理削除は、DELETE文でレコード自体を消す方法です。

論理削除は、削除フラグや削除日時を更新して、通常の検索対象から除外する方法です。

C#
user.IsDeleted = true;
user.DeletedAt = DateTime.UtcNow;

await dbContext.SaveChangesAsync();

論理削除が適している例は次のとおりです。

  • 誤削除から復元したい

  • 監査履歴を残したい

  • 関連データとの整合性を維持したい

  • 一定期間後に物理削除したい

一方、個人情報の消去要件や保存容量の削減など、データを残すべきでない場合は物理削除が必要です。システム要件、法令、社内規定に基づいて選択します。

7. C#の削除処理を安全に実装するポイント

7-1. 削除前に対象パス・ID・条件を検証する

削除対象を受け取ったら、次の項目を検証します。

  • 空文字列やnullではないか

  • 許可された形式か

  • 対象が削除可能な状態か

  • 現在の利用者が削除権限を持つか

  • テナントや所有者が一致するか

  • 削除件数が想定範囲内か

データベースでは、IDだけでなく所有者条件も含めます。

C#
int deletedCount = await dbContext.Documents
.Where(document =>
document.Id == documentId &&
document.OwnerId == currentUserId)
.ExecuteDeleteAsync();

if (deletedCount == 0)
{
throw new InvalidOperationException(
"削除対象が存在しないか、削除権限がありません。");
}

7-2. Path.GetFullPathで意図しないパスへの削除を防ぐ

相対パスや..を含むパスをそのまま削除に使用すると、許可範囲外のファイルを削除する危険があります。

ルートフォルダと対象パスを正規化し、相対位置を検証します。

C#
static string GetValidatedPath(string rootPath, string targetPath)
{
string fullRoot = Path.GetFullPath(rootPath);
string fullTarget = Path.GetFullPath(targetPath);

string relativePath = Path.GetRelativePath(fullRoot, fullTarget);

bool isOutside =
relativePath == ".." ||
relativePath.StartsWith(
$"..{Path.DirectorySeparatorChar}",
StringComparison.Ordinal) ||
Path.IsPathRooted(relativePath);

if (isOutside)
{
throw new InvalidOperationException(
"許可されたフォルダの外は削除できません。");
}

if (string.Equals(
fullRoot.TrimEnd(Path.DirectorySeparatorChar),
fullTarget.TrimEnd(Path.DirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"ルートフォルダ自体は削除できません。");
}

return fullTarget;
}

単純なStartsWithだけでは、C:\dataC:\databaseを誤判定する可能性があります。ルートからの相対パスとして検証するほうが安全です。

7-3. ユーザー入力をそのまま削除条件に使用しない

ファイルパスだけでなく、SQL、検索条件、コマンドライン引数でもユーザー入力の直接使用を避けます。

データベースではパラメーター化クエリを使い、EF CoreではLINQ式として条件を指定します。

ファイル名を受け取る場合は、ディレクトリ部分を除去する方法もあります。

C#
string safeFileName = Path.GetFileName(userInput);
string targetPath = Path.Combine(allowedDirectory, safeFileName);

ただし、ファイル名の長さ、拡張子、使用可能文字、重複なども検証してください。

7-4. 削除前の確認画面と確認ダイアログを実装する

重要なデータを削除する操作では、対象を明示した確認画面を表示します。

確認文には、次の情報を含めます。

  • 削除対象の名称

  • 削除件数

  • 関連データへの影響

  • 元に戻せるか

  • 実行後の状態

単に「削除しますか?」と表示するより、「注文1234と明細10件を完全に削除します。元に戻せません」のように具体的に示すほうが安全です。

Webアプリでは、確認画面だけに依存せず、CSRF対策、認可、対象IDの再検証も行います。

7-5. バックアップ・ごみ箱・論理削除で復元手段を確保する

削除ミスの影響を抑えるには、次の方法があります。

  • ファイルをごみ箱へ移動する

  • 退避フォルダへ移動する

  • 削除前にバックアップを作成する

  • データベースで論理削除する

  • 一定期間後に物理削除する

  • バージョン履歴を保存する

バックアップは作成するだけでなく、復元テストも必要です。復元できないバックアップは、削除事故への対策になりません。

7-6. 削除件数・対象・実行者をログに記録する

削除ログには、次の情報を残します。

  • 実行日時

  • 実行者

  • 対象の識別子

  • 削除件数

  • 成功または失敗

  • 失敗理由

  • リクエストIDや処理ID

C#
logger.LogInformation(
"ユーザー削除を実行しました。UserId: {UserId}, " +
"ExecutedBy: {ExecutedBy}, DeletedCount: {DeletedCount}",
userId,
currentUserId,
deletedCount);

パスワード、アクセストークン、機密性の高い個人情報をログへそのまま記録しないように注意します。

7-7. キャンセル可能な非同期削除を実装する

大量のファイルを削除する場合は、ファイル間でキャンセルを確認します。

C#
static async Task DeleteFilesAsync(
string directoryPath,
CancellationToken cancellationToken)
{
foreach (string filePath in Directory.EnumerateFiles(directoryPath))
{
cancellationToken.ThrowIfCancellationRequested();

File.Delete(filePath);

await Task.Yield();
}
}

File.Delete自体は同期APIであり、実行途中の1ファイル削除をキャンセルすることはできません。キャンセルできるのは、次のファイルへ進む前などの処理境界です。

並列処理を行う場合は、同時実行数を制限します。

C#
var options = new ParallelOptions
{
CancellationToken = cancellationToken,
MaxDegreeOfParallelism = 4
};

await Parallel.ForEachAsync(
Directory.EnumerateFiles(directoryPath),
options,
(filePath, _) =>
{
File.Delete(filePath);
return ValueTask.CompletedTask;
});

7-8. 大量削除時のパフォーマンスとメモリ使用量を改善する

大量削除では、次の対策が有効です。

  • GetFilesではなくEnumerateFilesを使用する

  • 一度に全対象をメモリへ読み込まない

  • 並列度を制限する

  • ファイル単位または一定件数単位でログをまとめる

  • データベースではExecuteDeleteなどの集合操作を利用する

  • 長時間トランザクションを避ける

  • 削除件数に上限を設ける

ファイル削除を無制限に並列化すると、ストレージへの負荷が増えて逆に遅くなることがあります。実際の保存先やファイル数に合わせて測定しましょう。

8. C#のdeleteでよくあるエラーと解決方法

8-1. UnauthorizedAccessExceptionが発生する場合

主な原因は次のとおりです。

  • 実行ユーザーに削除権限がない

  • ファイルが読み取り専用

  • File.Deleteへフォルダのパスを渡している

  • 保護されたシステム領域を操作している

  • アクセス制御リストで拒否されている

まず、対象がファイルかフォルダかを確認します。

C#
if (File.Exists(path))
{
File.Delete(path);
}
else if (Directory.Exists(path))
{
Directory.Delete(path, recursive: true);
}

必要な権限だけを対象フォルダへ付与し、アプリケーション全体を安易に管理者権限で実行しないようにします。

8-2. IOExceptionでファイルやフォルダを削除できない場合

ファイルが別のプロセスで使用されている、フォルダが空ではない、ストレージでI/Oエラーが発生した、といった原因が考えられます。

自分のコードで開いているストリームをusingで解放し、一時的なロックには制限付きの再試行を実装します。

フォルダを中身ごと削除する場合は、次の呼び出しになっているか確認します。

C#
Directory.Delete(directoryPath, recursive: true);

8-3. DirectoryNotFoundException・FileNotFoundExceptionが発生する場合

File.Deleteは対象ファイルが存在しない場合、通常はそのまま終了します。ただし、削除前に実行したFile.OpenFile.ReadAllTextFile.GetAttributesなど、別のAPIからFileNotFoundExceptionが発生することがあります。

親フォルダが存在しない場合は、DirectoryNotFoundExceptionが発生する可能性があります。

パスをログに記録し、次の点を確認します。

  • 相対パスの基準

  • スペルミス

  • ドライブや共有フォルダの接続状態

  • 環境ごとの設定値

  • 削除前に別処理が移動または削除していないか

8-4. PathTooLongExceptionや不正なパスが原因の場合

長いパスの扱いは、OS、ランタイム、アプリケーション設定によって異なります。

次の対策を検討します。

  • 不要に深いフォルダ構造を避ける

  • ファイル名を短くする

  • 絶対パスをログで確認する

  • Path.GetFullPathで正規化する

  • 実行環境の長いパス対応を確認する

パス文字の検証だけに.NETの例外を利用しないようにします。.NET Core 2.1以降では、一部のパスAPIが無効文字を事前検証せず、OS側の処理結果に委ねる動作へ変更されています。Microsoft Learn

8-5. ArgumentOutOfRangeExceptionでListの要素を削除できない場合

RemoveAtへ無効なインデックスを渡すと発生します。

C#
if (index < 0 || index >= list.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}

list.RemoveAt(index);

複数要素をインデックスで削除すると、削除のたびに後続要素が移動します。複数のインデックスを削除する場合は、大きいインデックスから処理します。

C#
foreach (int index in indexes.OrderByDescending(value => value))
{
if (index >= 0 && index < list.Count)
{
list.RemoveAt(index);
}
}

8-6. InvalidOperationExceptionがforeach中に発生する場合

列挙中のコレクションを変更している可能性があります。

次のいずれかへ変更します。

C#
list.RemoveAll(item => item.ShouldDelete);
C#
for (int index = list.Count - 1; index >= 0; index--)
{
if (list[index].ShouldDelete)
{
list.RemoveAt(index);
}
}
C#
foreach (var item in list.ToList())
{
if (item.ShouldDelete)
{
list.Remove(item);
}
}

マルチスレッドで共有コレクションを扱っている場合は、ロックや並行コレクションも検討してください。

8-7. Entity Framework Coreで削除が反映されない場合

主な確認ポイントは次のとおりです。

  • SaveChangesまたはSaveChangesAsyncを呼んでいるか

  • 削除対象が正しいDbContextで追跡されているか

  • トランザクションがコミットされているか

  • 例外を握りつぶしていないか

  • グローバルクエリフィルターで見え方が変わっていないか

  • 論理削除処理へ置き換えられていないか

  • データベーストリガーが影響していないか

Removeは削除状態にするだけで、通常はSaveChangesAsyncが必要です。

C#
dbContext.Users.Remove(user);

int affectedRows = await dbContext.SaveChangesAsync();

一方、ExecuteDeleteAsyncは呼び出した時点でSQLを実行するため、SaveChangesAsyncは不要です。ただし、変更追跡中のオブジェクトには結果が反映されない点に注意します。Microsoft Learn+1

9. C#のdeleteに関するよくある質問

9-1. File.Deleteはファイルが存在しない場合にエラーになる?

指定したファイルが存在しない場合、File.Deleteは通常、例外を発生させずに終了します。

ただし、親フォルダの問題、権限不足、不正なパス、使用中のファイルなど、別の原因があれば例外が発生します。

存在しなかったことを処理結果として区別したい場合は、事前にFile.Existsを使用します。

9-2. 削除したファイルを元に戻すことはできる?

File.Deleteで直接削除したファイルを、C#の標準的な復元APIで元に戻すことはできません。

復元可能にしたい場合は、次の方法を使用します。

  • Windowsのごみ箱へ送る

  • アプリ専用の退避フォルダへ移動する

  • バックアップを作成する

  • クラウドストレージのバージョン管理を利用する

削除後の復元ソフトに依存する設計は、安全な削除処理とはいえません。

9-3. フォルダを中身ごと強制削除するには?

中身ごと削除する基本構文は次のとおりです。

C#
Directory.Delete(directoryPath, recursive: true);

ただし、権限不足、読み取り専用属性、使用中のファイル、パスの問題がある場合は失敗します。あらゆる状態を無視して削除できる「強制削除」ではありません。

例外の原因を確認し、権限やロック状態を適切に解決してください。

9-4. Listから重複する要素をすべて削除するには?

指定した値をすべて削除するにはRemoveAllを使用します。

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

重複そのものを除去して各値を1件ずつ残す場合は、Distinctを使用します。

C#
list = list.Distinct().ToList();

独自クラスでは、比較方法に応じてIEqualityComparer<T>を指定します。

C#
users = users
.DistinctBy(user => user.Id)
.ToList();

9-5. 配列の要素を直接削除できないのはなぜ?

配列は作成時に長さが固定されるデータ構造だからです。

要素を除外したい場合は、新しい配列を作成します。

C#
array = array
.Where(item => item != target)
.ToArray();

追加や削除が多い処理では、List<T>のほうが適しています。

9-6. オブジェクトをnullにすればメモリはすぐ解放される?

すぐには解放されません。

nullを代入すると、その変数からの参照が外れます。ほかに参照がなければGCの回収対象になりますが、実際の回収時期はランタイムが決定します。

外部リソースをすぐ解放する必要がある場合は、nullではなくDisposeusingを使用します。

9-7. Disposeとdeleteは同じ意味?

同じではありません。

deleteは一般に対象を削除する意味で使われますが、Disposeはオブジェクトが保持する外部リソースを解放するための処理です。

Disposeを呼び出しても、そのオブジェクトへの参照が残っていれば変数自体は存在します。ただし、破棄後のメソッド呼び出しではObjectDisposedExceptionが発生する場合があります。

9-8. データベースでは物理削除と論理削除のどちらを選ぶべき?

復元、監査、履歴管理が必要なら論理削除が適しています。

個人情報の消去、不要データの完全削除、保存容量の削減が必要なら物理削除を検討します。

実務では、最初に論理削除し、保持期間が過ぎたデータをバッチ処理で物理削除する方式もよく使われます。どちらを選ぶ場合も、関連データ、法令、監査要件、バックアップからの復元可能性まで含めて設計しましょう。

まとめ

C#には、すべての対象に共通して使えるdeleteキーワードはありません。削除対象に応じて、適切なAPIを選ぶ必要があります。

ファイルはFile.Delete、フォルダはDirectory.Delete、Listの要素はRemoveRemoveAll、全要素はClearを使用します。マネージドオブジェクトのメモリ管理はガベージコレクターに任せ、ファイルや接続などの外部リソースはDisposeusingで確実に解放します。

データベースでは、SQLのDELETE、EF CoreのRemoveRemoveRangeExecuteDeleteを用途に応じて使い分けます。

削除処理で最も重要なのは、APIの呼び出し方だけではありません。パスやIDの検証、権限確認、例外処理、トランザクション、バックアップ、論理削除、監査ログを組み合わせ、誤削除が起きても影響を抑えられる設計にすることが重要です。