FileSystemWatcher重复触发事件:如何实现文件仅复制一次?
问题解答
1. 事件连续触发两次的原因
- 文件系统写入机制:多数程序创建文件时会分两步执行——先创建临时文件写入内容,完成后重命名为目标文件。这两个操作都会触发
Created事件,导致两次通知。 - FileSystemWatcher的通知特性:操作系统会针对文件创建的不同阶段(比如文件句柄创建、内容写入完成)发送多个通知,Watcher会全部捕获。
- 自身操作的连锁触发:如果你的
FileSystemWatcher开启了IncludeSubdirectories=true,那么事件中创建子目录、复制文件的操作,可能会触发额外的Created通知(针对子目录或复制后的文件)。 - 时间格式潜在问题:你用
DateTime.Now.ToString("dddd, dd MMMM yyyy")生成目录名,若两次触发间隔跨日期(比如深夜23:59到00:00)会生成不同目录名;若第二次触发时源文件已被系统释放/删除,也会引发异常。
2. 实现仅复制一次的解决方案
方案1:添加已处理文件缓存
用线程安全集合记录已处理的文件路径,避免重复操作:
// 线程安全集合存储已处理文件路径 private readonly HashSet<string> _processedFiles = new HashSet<string>(); private readonly object _lockObj = new object(); private void OnCreated(object sender, FileSystemEventArgs e) { this.Invoke(new Action(delegate () { lock (_lockObj) { // 跳过已处理文件或不存在的源文件 if (_processedFiles.Contains(e.FullPath) || !File.Exists(e.FullPath)) return; _processedFiles.Add(e.FullPath); } RichTextBoxExtensions.AppendText(richTextBox1, $"Created: {e.FullPath}", Color.GreenYellow); RichTextBoxExtensions.AppendText(richTextBox1, $" On: {DateTime.Now}", Color.Yellow); richTextBox1.AppendText(Environment.NewLine); // 用Path.Combine替代字符串拼接,避免路径错误 string folderName = Path.Combine(savedGamesPath, $"Save Game {DateTime.Now:dddd, dd MMMM yyyy}"); Directory.CreateDirectory(folderName); // 无需提前判断目录是否存在 string destFile = Path.Combine(folderName, e.Name); try { File.Copy(e.FullPath, destFile); } catch (Exception ex) { RichTextBoxExtensions.AppendText(richTextBox1, $"复制失败: {ex.Message}", Color.Red); richTextBox1.AppendText(Environment.NewLine); // 移除缓存,允许后续重试(可选) lock (_lockObj) { _processedFiles.Remove(e.FullPath); } } })); }
方案2:延迟处理,等待文件稳定
第一次触发可能是临时文件,延迟几秒再处理,确保文件完全创建完成:
private void OnCreated(object sender, FileSystemEventArgs e) { // 延迟1秒处理,避免捕获临时文件触发的事件 Task.Delay(1000).ContinueWith(_ => { this.Invoke(new Action(delegate () { if (!File.Exists(e.FullPath)) return; RichTextBoxExtensions.AppendText(richTextBox1, $"Created: {e.FullPath}", Color.GreenYellow); RichTextBoxExtensions.AppendText(richTextBox1, $" On: {DateTime.Now}", Color.Yellow); richTextBox1.AppendText(Environment.NewLine); string folderName = Path.Combine(savedGamesPath, $"Save Game {DateTime.Now:dddd, dd MMMM yyyy}"); Directory.CreateDirectory(folderName); string destFile = Path.Combine(folderName, e.Name); try { File.Copy(e.FullPath, destFile); } catch (Exception ex) { RichTextBoxExtensions.AppendText(richTextBox1, $"复制失败: {ex.Message}", Color.Red); richTextBox1.AppendText(Environment.NewLine); } })); }); }
方案3:排除自身操作的触发
若Watcher监控了子目录,判断当前触发的文件是否是自己复制的文件,直接跳过:
private void OnCreated(object sender, FileSystemEventArgs e) { this.Invoke(new Action(delegate () { // 跳过备份目录内的文件触发的事件 if (e.FullPath.StartsWith(Path.Combine(savedGamesPath, "Save Game "), StringComparison.OrdinalIgnoreCase)) return; // 后续处理代码... })); }
额外优化建议
- 始终用
Path.Combine拼接路径,避免手动拼接带来的分隔符错误。 Directory.CreateDirectory无需提前判断Directory.Exists,该方法会自动处理目录已存在的情况。- 添加异常捕获逻辑,避免单个文件处理失败导致整个Watcher逻辑崩溃。
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

