1. はじめに
.NET でファイルやフォルダを扱うには、哲学的に異なる2つのクラス群があります:
静的クラス: File, Directory — ユーティリティ関数群のように振る舞います:メソッドを呼んでパスを渡せば結果が返ってきます。
インスタンスクラス: FileInfo, DirectoryInfo — 具体的なファイルやフォルダをプロパティとメソッドを持つオブジェクトとして表現します。
なぜ両方あるのか?それは使いやすさ(静的メソッド)とオブジェクト指向の柔軟性(インスタンスクラス)のトレードオフです。シナリオによって使い分けるのが良いです。
2. 静的クラスの哲学
File と Directory は「とにかくやれ」的な設計です。中間オブジェクトを作る必要なくファイルシステム操作を直接実行するインターフェースを提供します。
// ファイルの存在を確認する
if (File.Exists("document.txt"))
{
// 全文を読み込む
string content = File.ReadAllText("document.txt");
// バックアップを作る
File.Copy("document.txt", "document_backup.txt");
// オリジナルを削除
File.Delete("document.txt");
}
これは単発のシンプルな操作に便利です:オブジェクトやライフサイクル、状態を気にせずにパスを渡してメソッドを呼べばいいだけです。
同様に Directory も動作します:
// フォルダを作成
Directory.CreateDirectory(@"C:\MyProject\Data");
// すべてのテキストファイルを取得
string[] textFiles = Directory.GetFiles(@"C:\MyProject", "*.txt");
// フォルダの存在を確認
if (Directory.Exists(@"C:\Temp"))
{
Directory.Delete(@"C:\Temp", recursive: true);
}
3. 実践例での比較
例 1: 単純なファイルコピー
静的アプローチ:
string sourcePath = "original.txt";
string destinationPath = "copy.txt";
if (File.Exists(sourcePath))
{
File.Copy(sourcePath, destinationPath, overwrite: true);
Console.WriteLine("ファイルがコピーされました");
}
else
{
Console.WriteLine("元のファイルが見つかりません");
}
インスタンスアプローチ:
var sourceFile = new FileInfo("original.txt");
var destinationFile = new FileInfo("copy.txt");
if (sourceFile.Exists)
{
sourceFile.CopyTo(destinationFile.FullName, overwrite: true);
Console.WriteLine("ファイルがコピーされました");
}
else
{
Console.WriteLine("元のファイルが見つかりません");
}
ここでは静的アプローチの方が簡潔です。ただし追加情報が必要な場合はどうでしょうか?
例 2: サイズチェック付きコピー
静的アプローチ:
string sourcePath = "largefile.zip";
string destinationPath = "backup.zip";
if (File.Exists(sourcePath))
{
var fileInfo = new FileInfo(sourcePath); // 結局オブジェクトを作る必要がある!
if (fileInfo.Length > 100 * 1024 * 1024) // 100 MB より大きい
{
Console.WriteLine($"注意: 大きなファイルをコピーしています ({fileInfo.Length / 1024 / 1024} MB)");
}
File.Copy(sourcePath, destinationPath, overwrite: true);
}
インスタンスアプローチ:
var sourceFile = new FileInfo("largefile.zip");
if (sourceFile.Exists)
{
if (sourceFile.Length > 100 * 1024 * 1024) // 100 MB より大きい
{
Console.WriteLine($"注意: 大きなファイルをコピーしています ({sourceFile.Length / 1024 / 1024} MB)");
}
sourceFile.CopyTo("backup.zip", overwrite: true);
}
ここではオブジェクト指向の方が自然です:ファイルをオブジェクトとして扱い、そのプロパティを使います。
例 3: フォルダの内容分析
静的アプローチ:
string folderPath = @"C:\Documents";
if (Directory.Exists(folderPath))
{
string[] files = Directory.GetFiles(folderPath);
string[] subdirs = Directory.GetDirectories(folderPath);
Console.WriteLine($"ファイル数: {files.Length}, フォルダ数: {subdirs.Length}");
// サイズを得るには結局 FileInfo オブジェクトが必要
long totalSize = 0;
foreach (string filePath in files)
{
var fileInfo = new FileInfo(filePath);
totalSize += fileInfo.Length;
}
Console.WriteLine($"合計サイズ: {totalSize / 1024} KB");
}
インスタンスアプローチ:
var folder = new DirectoryInfo(@"C:\Documents");
if (folder.Exists)
{
var files = folder.GetFiles();
var subdirs = folder.GetDirectories();
Console.WriteLine($"ファイル数: {files.Length}, フォルダ数: {subdirs.Length}");
long totalSize = files.Sum(f => f.Length); // 情報が既に利用可能!
Console.WriteLine($"合計サイズ: {totalSize / 1024} KB");
}
ここでは DirectoryInfo の方が有利です:FileInfo[] コレクションはメタデータを含んでいます。
4. アプローチ選択の基準
次の場合は静的クラス(File/Directory)を使う:
- 操作が単純で一回きりの場合。 ファイルの存在確認、削除、内容の読み取りなどを素早く行いたいときは静的メソッドが最適です。
// 設定の簡単な読み込み
if (File.Exists("config.json"))
{
string config = File.ReadAllText("config.json");
// 設定を処理...
}
- ファイルのプロパティ情報が不要な場合。 サイズ、日付、属性が重要でないなら静的呼び出しの方が短くて済みます。
- パス文字列を扱うロジックがメインの場合。 ロジックが文字列パスで完結するなら静的メソッドの方が自然です。
次の場合はインスタンスクラス(FileInfo/DirectoryInfo)を使う:
- 一つのファイル/フォルダについて多くの情報が必要な場合。 Length, CreationTime, Attributes などのプロパティが直接使えます。
var logFile = new FileInfo("application.log");
Console.WriteLine($"ログのサイズ: {logFile.Length / 1024} KB");
Console.WriteLine($"最終更新: {logFile.LastWriteTime}");
Console.WriteLine($"場所: {logFile.Directory.FullName}");
- 同じファイルに対して複数の操作を行う場合。 一度オブジェクトを作成して使い回す方が便利です。
var document = new FileInfo("report.docx");
if (document.Exists)
{
var backup = document.CopyTo($"report_backup_{DateTime.Now:yyyyMMdd}.docx");
document.MoveTo("archive/report.docx");
Console.WriteLine($"ドキュメントをアーカイブし、コピーを作成しました {backup.Name}");
}
- ファイルのコレクションを扱う場合。 GetFiles() や GetDirectories() は完全な情報を持つオブジェクトを返します。
- オブジェクト指向のアーキテクチャが必要な場合。 ファイル/フォルダをオブジェクトとして渡したり保持したり、LINQ と組み合わせるのが楽です。
5. パフォーマンスとキャッシュの特徴
インスタンスクラスのキャッシュ
FileInfo/DirectoryInfo の主要な特徴の一つはメタデータのキャッシュです。最初にプロパティ(例えば Length や CreationTime)へアクセスしたときに .NET はシステムコールを行い、情報を読み込んでオブジェクト内部にキャッシュします。
var file = new FileInfo("document.txt");
// 初回アクセス - メタデータをロードするためのシステムコール
long size = file.Length;
// 以降のアクセスはキャッシュを使うので非常に速い
DateTime created = file.CreationTime;
DateTime modified = file.LastWriteTime;
bool readOnly = file.IsReadOnly;
同一ファイルの複数プロパティが必要なら、オブジェクト指向のアプローチの方が効率的です:システムコールが1回で済みます。
キャッシュの陳腐化の問題
逆に、ファイルが外部から変更されると情報が古くなることがあります。これを解決するには Refresh() を使います:
var file = new FileInfo("data.txt");
Console.WriteLine($"サイズ: {file.Length}"); // 例えば 1000 バイト
// この間に別のプロセスがファイルを変更した...
Console.WriteLine($"サイズ: {file.Length}"); // まだキャッシュされた 1000 バイト!
// 強制的に更新
file.Refresh();
Console.WriteLine($"サイズ: {file.Length}"); // 今は最新のサイズ
静的呼び出しは常にファイルシステムへ直接問い合わせるので、この問題は発生しません。
バルク操作(大量ファイル)の場合
大量のファイルを扱うと API の選択がパフォーマンスに大きく影響します:
// 静的アプローチ — 多数のシステムコールが発生
string[] files = Directory.GetFiles(@"C:\Photos");
foreach (string filePath in files)
{
var info = new FileInfo(filePath); // 各ファイルごとにシステムコール
if (info.Length > 10 * 1024 * 1024) // 10 MB より大きい
{
Console.WriteLine($"大きな写真: {info.Name}");
}
}
// インスタンスアプローチ — フォルダ単位で一回のシステムコール
var photosDir = new DirectoryInfo(@"C:\Photos");
foreach (var file in photosDir.GetFiles()) // 情報がまとめて読み込まれる
{
if (file.Length > 10 * 1024 * 1024)
{
Console.WriteLine($"大きな写真: {file.Name}");
}
}
6. API の違いと機能
インスタンスクラスの固有機能
var file = new FileInfo(@"C:\Projects\MyApp\source\Program.cs");
// フォルダ階層のナビゲーション
DirectoryInfo projectDir = file.Directory.Parent; // MyApp
DirectoryInfo sourceDir = file.Directory; // source
// ファイルの詳細情報
Console.WriteLine($"拡張子: {file.Extension}");
Console.WriteLine($"読み取り専用か: {file.IsReadOnly}");
Console.WriteLine($"属性: {file.Attributes}");
// ディレクトリをオブジェクトとして扱う
var dir = new DirectoryInfo(@"C:\Projects");
DirectoryInfo parent = dir.Parent; // C:\
DirectoryInfo root = dir.Root; // C:\
静的クラスの固有機能
// 1行での読み書き
string content = File.ReadAllText("config.txt");
File.WriteAllText("output.txt", "Hello World");
// 文字列操作
string[] lines = File.ReadAllLines("data.txt");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });
// ファイルへの追記
File.AppendAllText("log.txt", $"{DateTime.Now}: Application started\n");
// バイト操作
byte[] data = File.ReadAllBytes("image.jpg");
File.WriteAllBytes("copy.jpg", data);
7. 実践的な推奨
シナリオ 1: バックアップユーティリティ
サイズや日時をチェックしてコピーする場合はインスタンスクラスが便利です:
public void BackupDirectory(string sourcePath, string backupPath)
{
var sourceDir = new DirectoryInfo(sourcePath);
var backupDir = new DirectoryInfo(backupPath);
if (!backupDir.Exists)
backupDir.Create();
foreach (var file in sourceDir.GetFiles())
{
var backupFile = new FileInfo(Path.Combine(backupPath, file.Name));
// ファイルが新しいか存在しない場合のみコピー
if (!backupFile.Exists || file.LastWriteTime > backupFile.LastWriteTime)
{
file.CopyTo(backupFile.FullName, overwrite: true);
Console.WriteLine($"コピーしました: {file.Name} ({file.Length / 1024} KB)");
}
}
}
シナリオ 2: 単純なテキストファイル操作
単純なデータの読み書きなら静的メソッドで十分です:
public void SaveUserPreferences(string username, string theme, bool notifications)
{
string configPath = "user.config";
string[] settings = {
$"Username={username}",
$"Theme={theme}",
$"Notifications={notifications}"
};
File.WriteAllLines(configPath, settings);
}
public Dictionary<string, string> LoadUserPreferences()
{
string configPath = "user.config";
var preferences = new Dictionary<string, string>();
if (!File.Exists(configPath))
{
// デフォルト設定を作成
SaveUserPreferences("User", "Light", true);
return LoadUserPreferences();
}
string[] lines = File.ReadAllLines(configPath);
foreach (string line in lines)
{
if (line.Contains('='))
{
string[] parts = line.Split('=', 2);
preferences[parts[0]] = parts[1];
}
}
return preferences;
}
シナリオ 3: ファイルシステム解析
ディスク使用量レポートにはインスタンスクラスが最適です:
public void AnalyzeDiskUsage(string path)
{
var directory = new DirectoryInfo(path);
var report = new Dictionary<string, long>();
foreach (var file in directory.GetFiles("*", SearchOption.AllDirectories))
{
string extension = file.Extension.ToLower();
if (string.IsNullOrEmpty(extension))
extension = "(拡張子なし)";
if (!report.ContainsKey(extension))
report[extension] = 0;
report[extension] += file.Length;
}
var sortedReport = report.OrderByDescending(kvp => kvp.Value);
foreach (var item in sortedReport.Take(10))
{
Console.WriteLine($"{item.Key}: {item.Value / 1024 / 1024} MB");
}
}
8. よくあるミスと注意点
ミス: 不必要な混在
時々、静的メソッドで始めてからメタデータのためにオブジェクトを作ると、余計な重複した呼び出しが発生します:
// 非効率的 - ファイルシステムへの二重アクセス
if (File.Exists("document.txt"))
{
var fileInfo = new FileInfo("document.txt"); // 存在チェックの重複
Console.WriteLine($"サイズ: {fileInfo.Length}");
}
// 最初からオブジェクトを使う方が良い
var fileInfo = new FileInfo("document.txt");
if (fileInfo.Exists)
{
Console.WriteLine($"サイズ: {fileInfo.Length}");
}
ミス: キャッシュの陳腐化を無視する
外部で変更される可能性がある長時間稼働の処理では Refresh() でキャッシュを更新してください:
var logFile = new FileInfo("application.log");
while (true)
{
logFile.Refresh(); // ファイル情報を更新
if (logFile.Length > 100 * 1024 * 1024) // 100 MB
{
// ログをアーカイブ
logFile.MoveTo($"logs/archived_{DateTime.Now:yyyyMMdd_HHmmss}.log");
break;
}
Thread.Sleep(60000); // 1分ごとにチェック
}
ミス: バルク操作での不適切な選択
何千ものファイルを処理する場合、API 選択はパフォーマンスに直結します:
// 遅い - 小さなファイルシステム呼び出しが多数発生
string[] allFiles = Directory.GetFiles(@"C:\BigFolder", "*", SearchOption.AllDirectories);
var largeFiles = allFiles.Where(path => new FileInfo(path).Length > 1024 * 1024).ToList();
// 速い - 既存の情報を使う
var folder = new DirectoryInfo(@"C:\BigFolder");
var largeFiles = folder.GetFiles("*", SearchOption.AllDirectories)
.Where(file => file.Length > 1024 * 1024)
.ToList();
GO TO FULL VERSION