前回は、ストリームとリーダー/ライターを使った読み書きを見ました。
今回は、ファイルの変更を監視する FileSystemWatcher と、オブジェクトを保存可能な形へ変換する考え方を扱います。
ログ収集、設定ファイルの再読み込み、インポートフォルダーの監視など、ファイルの変化を起点に処理を動かしたい場面はよくあります。
また、アプリケーション内のオブジェクトをファイルへ保存するには、オブジェクトをテキストやバイナリの形へ変換する必要があります。
ファイル変更を監視する
FileSystemWatcher を使うと、指定したディレクトリ内のファイル変更を監視できます。
using System.IO;
using FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = "watch";
watcher.Filter = "*.txt";
watcher.IncludeSubdirectories = false;
watcher.EnableRaisingEvents = true;
watcher.Created += (sender, e) =>
{
Console.WriteLine($"作成: {e.FullPath}");
};
watcher.Changed += (sender, e) =>
{
Console.WriteLine($"変更: {e.FullPath}");
};
watcher.Deleted += (sender, e) =>
{
Console.WriteLine($"削除: {e.FullPath}");
};
watcher.Renamed += (sender, e) =>
{
Console.WriteLine($"名前変更: {e.OldFullPath} -> {e.FullPath}");
};
Directory.CreateDirectory("watch");
Console.WriteLine("監視中です。Enter キーで終了します。");
Console.ReadLine();
EnableRaisingEvents = true にすると、監視イベントが発生するようになります。
監視対象を絞る
Filter で対象ファイルを絞れます。
watcher.Filter = "*.json";
複数の拡張子を扱いたい場合、.NET のバージョンによっては Filters コレクションを使えます。
watcher.Filters.Add("*.json");
watcher.Filters.Add("*.xml");
サブディレクトリも含めたい場合は、次のようにします。
watcher.IncludeSubdirectories = true;
ただし、監視範囲を広げるほどイベント数が増えます。
必要な範囲だけを監視する設計にしましょう。
監視イベントの注意点
FileSystemWatcher は便利ですが、次の点に注意が必要です。
- 1 つの操作で複数イベントが発生することがある
- ファイル作成イベント直後は、まだ書き込み中の場合がある
- 大量変更時にイベントを取りこぼす可能性がある
- イベントハンドラーは短く保つ方がよい
たとえば、ファイルが作成された直後に読み込もうとしても、別プロセスがまだ書き込み中で失敗することがあります。
watcher.Created += async (sender, e) =>
{
await Task.Delay(500);
try
{
string text = await File.ReadAllTextAsync(e.FullPath);
Console.WriteLine(text);
}
catch (IOException ex)
{
Console.WriteLine($"読み込み失敗: {ex.Message}");
}
};
実務では、少し待つ、リトライする、処理キューへ積む、といった工夫を入れることが多いです。
監視処理をキューに分離する
イベントハンドラーの中で重い処理を直接行うと、イベント処理が詰まりやすくなります。
簡単なキューへ渡す形にすると、構造が整理されます。
using System.Collections.Concurrent;
using System.IO;
var queue = new BlockingCollection<string>();
Task worker = Task.Run(async () =>
{
foreach (string path in queue.GetConsumingEnumerable())
{
try
{
await Task.Delay(300);
string text = await File.ReadAllTextAsync(path);
Console.WriteLine($"処理: {Path.GetFileName(path)}");
Console.WriteLine(text);
}
catch (Exception ex)
{
Console.WriteLine($"処理失敗: {ex.Message}");
}
}
});
using FileSystemWatcher watcher = new FileSystemWatcher("watch", "*.txt");
watcher.Created += (sender, e) => queue.Add(e.FullPath);
watcher.EnableRaisingEvents = true;
Console.WriteLine("Enter キーで終了します。");
Console.ReadLine();
queue.CompleteAdding();
await worker;
この形にすると、監視と処理を分離できます。
後からリトライ、重複排除、ログ出力なども追加しやすくなります。
オブジェクトを保存するとはどういうことか
アプリケーション内のオブジェクトは、そのままではファイルに保存できません。
public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
この Person オブジェクトを保存するには、XML や JSON などのテキスト形式、または独自のバイナリ形式へ変換する必要があります。
このように、オブジェクトを保存・送信しやすい形へ変換することを、一般にシリアライズと呼びます。
逆に、保存されたデータからオブジェクトを復元することをデシリアライズと呼びます。
オブジェクトグラフ
オブジェクトは、単独で存在するとは限りません。
別のオブジェクトを参照していることがあります。
public class Department
{
public string Name { get; set; } = "";
public List<Person> Members { get; set; } = new();
}
Department は Person のリストを持っています。
このように、あるオブジェクトから参照でつながる一連のオブジェクト群をオブジェクトグラフと考えます。
var department = new Department
{
Name = "Development",
Members =
{
new Person { Name = "Alice", Age = 30 },
new Person { Name = "Bob", Age = 28 }
}
};
保存形式化では、このつながりをどのように表現するかが重要です。
保存形式を選ぶ観点
オブジェクトを保存するときは、形式を選ぶ必要があります。
JSON
Web API、設定ファイル、ログ、外部連携でよく使われます。
人間にも比較的読みやすく、現在の .NET では標準的な選択肢です。
XML
古くから使われており、スキーマ、属性、名前空間などの表現に強みがあります。
既存システム連携や文書寄りのデータで使われることがあります。
独自バイナリ
サイズや速度を重視する場合に使うことがあります。
ただし、互換性やデバッグしやすさの面では慎重な設計が必要です。
保存対象に向いたクラス設計
保存形式化しやすいクラスには、いくつかの特徴があります。
- public なプロパティで状態を表す
- 循環参照を避ける
- 保存したくない値を明確にする
- バージョン変更に備える
- ドメインロジックと保存形式を混ぜすぎない
たとえば、計算結果として求められる値は、保存せずに復元後に再計算する設計もあります。
public class Order
{
public List<OrderLine> Lines { get; set; } = new();
public decimal Total => Lines.Sum(line => line.Price * line.Quantity);
}
Total は保存する値ではなく、Lines から計算できる値として扱えます。
まとめ
FileSystemWatcher を使うと、ファイルやディレクトリの変更を監視できます。
ただし、イベントの重複や書き込み途中の読み込みなど、実務では注意点も多いため、処理キューやリトライを組み合わせると安定します。
後半では、オブジェクトを保存可能な形へ変換する考え方を導入しました。
次回は、XML と JSON を使って、実際にオブジェクトを保存・復元する方法を見ていきます。