C#を使用した開発において、ファイルのコピーは非常に頻繁に行われる操作の一つです。

ログファイルのバックアップや、ユーザーがアップロードしたデータの配置など、さまざまなシーンでファイル操作の知識が求められます。

本記事では、初心者の方でもすぐに使える基本的な方法から、大規模なシステムで必要となる非同期処理や高速化のテクニックまでを詳しく解説します。

現在の.NET環境において最適な実装方法を選択できるよう、具体的なソースコードとともに学んでいきましょう。

C#におけるファイルコピーの基本

C#で最も手軽にファイルをコピーする方法は、System.IO名前空間に含まれるFileクラスを使用することです。

このクラスは静的メソッドを提供しており、インスタンスを生成せずに直接呼び出すことが可能です。

File.Copyメソッドによるシンプルなコピー

最も一般的な手法は、File.Copyメソッドを利用することです。

このメソッドには、コピー元のパスとコピー先のパスを引数として渡します。

C#
using System;
using System.IO;

class Program
{
    static void Main()
    {
        string sourceFile = "source.txt";
        string destFile = "destination.txt";

        try
        {
            // 基本的なファイルコピー
            File.Copy(sourceFile, destFile);
            Console.WriteLine("ファイルのコピーが完了しました。");
        }
        catch (IOException e)
        {
            Console.WriteLine($"エラーが発生しました: {e.Message}");
        }
    }
}
実行結果
ファイルのコピーが完了しました。

デフォルトの状態では、コピー先に同名のファイルが存在する場合、IOExceptionが発生します。

既存のファイルを上書きする方法

コピー先にファイルが既に存在していても強制的に上書きしたい場合は、第3引数にtrueを指定します。

これにより、事前にファイルを削除する手間を省き、コードを簡潔に保つことができます。

C#
using System;
using System.IO;

class Program
{
    static void Main()
    {
        string sourceFile = "source.txt";
        string destFile = "destination.txt";

        // 第3引数をtrueに設定して上書きを許可
        File.Copy(sourceFile, destFile, true);
        
        Console.WriteLine("ファイルを上書きコピーしました。");
    }
}

このフラグを指定しないと、プログラムが意図せず停止する原因になるため、要件に合わせて適切に設定してください。

FileInfoクラスを使用したファイルコピー

ファイルパスを文字列として扱うのではなく、一つのオブジェクトとして扱いたい場合にはFileInfoクラスが便利です。

FileInfoクラスを使用すると、ファイルサイズや作成日時などの属性情報にも簡単にアクセスできます。

FileInfo.CopyToメソッドの使い方

FileInfoクラスのCopyToメソッドを使用することで、インスタンス化されたファイル情報を基にコピー処理を行えます。

C#
using System;
using System.IO;

class Program
{
    static void Main()
    {
        FileInfo fileSource = new FileInfo("example.txt");

        if (fileSource.Exists)
        {
            // 指定したパスへコピー
            fileSource.CopyTo("example_backup.txt", true);
            Console.WriteLine($"{fileSource.Name} をコピーしました。");
        }
        else
        {
            Console.WriteLine("コピー元のファイルが存在しません。");
        }
    }
}

この手法は、特定のディレクトリ内にある複数のファイルをループ処理でコピーする場合などに、オブジェクト指向的な書き方ができるため好まれます。

非同期処理によるファイルコピー

サイズの大きいファイルをコピーする場合、同期的なメソッドを使用すると処理が終わるまでUIスレッドや呼び出し元がブロックされてしまいます。

ユーザーエクスペリエンスを向上させるためには、async/awaitを活用した非同期コピーが推奨されます。

FileStreamとCopyToAsyncの活用

残念ながら、File.Copyには直接的な非同期版のFile.CopyAsyncというメソッドは存在しません(2026年現在の標準APIにおいても同様の傾向です)。

そのため、FileStreamを使用してストリーム経由でデータを転送する方法が一般的です。

C#
using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string sourcePath = "large_video.mp4";
        string destinationPath = "large_video_copy.mp4";

        await CopyFileAsync(sourcePath, destinationPath);
        
        Console.WriteLine("非同期でのコピーが完了しました。");
    }

    static async Task CopyFileAsync(string sourcePath, string destinationPath)
    {
        // 読み取りストリームと書き込みストリームを開く
        using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
        using (FileStream destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write))
        {
            // 非同期でストリームの内容をコピー
            await sourceStream.CopyToAsync(destinationStream);
        }
    }
}

この方法を使用することで、コピー中にアプリケーションがフリーズするのを防ぐことができます。

進捗状況を表示する非同期コピー

さらに高度な実装として、コピーの進捗(パーセント表示)を取得したい場合があります。

この場合は、バッファサイズを指定してループ内で少しずつデータを読み書きし、進捗を報告します。

C#
using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        IProgress<double> progress = new Progress<double>(percent => {
            Console.WriteLine($"進行状況: {percent:F2}%");
        });

        await CopyWithProgressAsync("large_data.bin", "target_data.bin", progress);
    }

    static async Task CopyWithProgressAsync(string source, string dest, IProgress<double> progress)
    {
        using var sourceStream = File.OpenRead(source);
        using var destStream = File.Create(dest);
        
        byte[] buffer = new byte[81920]; // 80KBのバッファ
        long totalBytes = sourceStream.Length;
        long copiedBytes = 0;
        int bytesRead;

        while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await destStream.WriteAsync(buffer, 0, bytesRead);
            copiedBytes += bytesRead;
            progress?.Report((double)copiedBytes / totalBytes * 100);
        }
    }
}

進捗表示を行うことで、ユーザーは処理が正常に継続していることを確認でき、安心感につながります。

大量・大容量ファイルのための高速化手法

数ギガバイトを超えるファイルや、数万個のファイルをコピーする場合、単純な実装ではパフォーマンスが不足することがあります。

ここでは、システム負荷を抑えつつ高速にコピーを行うためのヒントを紹介します。

バッファサイズの最適化

先ほどのストリームコピーではバッファサイズを指定しましたが、この値が小さすぎるとシステムコールの回数が増え、遅くなります。

逆に大きすぎるとメモリを圧迫するため、4KBから128KB程度の間で調整するのが一般的です。

現代のSSD環境では、80KB(81,920バイト)以上が効率的であると言われています。

OSの機能を直接利用する(Windows環境)

WindowsOSで動作する場合、CopyFileExといったWin32 APIを直接呼び出すことで、OSレベルの最適化を享受できる場合があります。

しかし、.NETのFile.Copy自体が内部でネイティブAPIを効率的に呼び出しているため、通常の用途では無理にP/Invoke(プラットフォーム呼び出し)を使用する必要はありません。

ディレクトリ(フォルダ)の再帰的コピー

C#の標準ライブラリには「ディレクトリを丸ごとコピーする」という直接的なメソッドが用意されていません。

そのため、再帰処理を実装して、サブディレクトリも含めた全てのファイルをコピーする必要があります。

再帰的なコピーの実装例

以下のコードは、フォルダ構造を維持したまま全てのコンテンツをコピーする汎用的なメソッドです。

C#
using System;
using System.IO;

public static class DirectoryHelper
{
    public static void CopyDirectory(string sourceDir, string destDir, bool recursive)
    {
        // コピー元のディレクトリ情報を取得
        var dir = new DirectoryInfo(sourceDir);

        if (!dir.Exists)
            throw new DirectoryNotFoundException($"コピー元が見つかりません: {dir.FullName}");

        // コピー先のディレクトリが存在しない場合は作成
        Directory.CreateDirectory(destDir);

        // ファイルをコピー
        foreach (FileInfo file in dir.GetFiles())
        {
            string targetFilePath = Path.Combine(destDir, file.Name);
            file.CopyTo(targetFilePath, true);
        }

        // 再帰的にサブディレクトリを処理
        if (recursive)
        {
            foreach (DirectoryInfo subDir in dir.GetDirectories())
            {
                string newDestDir = Path.Combine(destDir, subDir.Name);
                CopyDirectory(subDir.FullName, newDestDir, true);
            }
        }
    }
}

このコードを使用する際は、循環参照や極端に深い階層構造によるスタックオーバーフローに注意してください。

エラーハンドリングと注意点

ファイル操作は、外部要因(ディスク容量不足、アクセス権限、他プロセスによるロック)によって失敗する可能性が非常に高い処理です。

堅牢なアプリケーションを作るためには、例外処理を適切に記述することが不可欠です。

よく発生する例外(Exception)

ファイルコピー時にキャッチすべき主な例外は以下の通りです。

例外名発生する主な原因
FileNotFoundExceptionコピー元のファイルが存在しない。
DirectoryNotFoundException指定されたパスのディレクトリが存在しない。
UnauthorizedAccessException書き込み権限がない、または読み取り専用ファイルである。
IOExceptionファイルが別のプロセスで使用中、またはディスクフル。
PathTooLongExceptionパスの文字列がシステム制限を超えている。

ファイルロックの回避

他のアプリケーションがファイルを開いている場合、コピーに失敗することがあります。

その際、リトライ処理(数秒待ってから再試行する)を実装するか、読み取り専用モードでストリームを開くといった工夫が必要です。

FileShare.ReadWriteを指定してストリームを開くことで、他のプロセスが書き込み中でも読み取りを試みることができます。

最新の.NETにおけるファイル操作のトレンド

2026年現在の.NET環境では、メモリ使用量を抑えるためのSpan<T>やMemory<T>を活用した低レイヤーの操作も注目されています。

また、クラウドストレージ(Azure Blob StorageやAWS S3)との連携においては、ローカルのファイルコピーと同様のインターフェースを持つ抽象化ライブラリを使用することが増えています。

しかし、ローカル環境におけるファイルコピーの基本は変わらず、OSのファイルシステムをいかに効率よく叩くかが重要です。

まとめ

C#でのファイルコピーは、シンプルなFile.Copyから、自由度の高いFileStreamまで多彩な方法が用意されています。

小規模なツールであればFile.Copyの静的メソッドで十分ですが、大規模なデータを扱う場合は非同期処理やバッファサイズの調整を検討してください。

また、フォルダごとのコピーが必要な場合は、今回紹介した再帰処理のロジックをテンプレートとして活用するとスムーズです。

適切なエラーハンドリングを組み合わせることで、ユーザーにとって信頼性の高いファイル操作機能を実装できるでしょう。

今回解説したテクニックを駆使して、より高度で効率的なC#プログラミングを実践してみてください。