filesystemwatcher仅单向通知文件变更,双向同步需自行实现读取→比对→写入闭环,并解决冲突、原子写入、编码差异及事件抖动等问题。

文件系统监听器只能单向触发,FileSystemWatcher 不提供内容比对或自动写入能力
很多人以为 FileSystemWatcher 能“绑定”两个文件,其实它只负责通知“哪个文件变了”,不关心内容、不处理冲突、也不帮你写回另一个文件。双向同步必须自己实现读取→比对→写入的闭环,而且得决定谁当权威源(否则改A时触发改B,B又触发改A,无限循环)。
常见错误现象:FileSystemWatcher 触发两次(创建+写入)、事件丢失(高频修改)、路径大小写敏感导致监听失败、没处理好文件被占用时的 IOException。
- 监听前先用
File.GetLastWriteTimeUtc()拿初始状态,避免启动时漏掉“已存在但未改动”的差异 - 在
Changed事件里加Thread.Sleep(10)或使用防抖逻辑(比如记录最后触发时间,200ms内重复触发只执行一次),防止 Windows 写入分多批次导致多次误判 - 务必用
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,别加FileName或DirectoryName,否则移动/重命名会干扰同步逻辑
手动比对内容必须绕过编码和换行符陷阱
直接 File.ReadAllText(a) == File.ReadAllText(b) 看似简单,实际在跨平台或编辑器混用时极易误判:UTF-8 with BOM vs UTF-8 no BOM、\r\n vs \n、末尾空格、甚至隐藏的零宽字符都会让字符串比较失败,但用户认为“内容没变”。
推荐做法是比对哈希值而非原始字符串:
- 用
using var fs = File.OpenRead(path);+SHA256.HashData(fs)(.NET 5+)或SHA256.Create().ComputeHash(fs)(旧版),避开编码解析环节 - 不要用
File.ReadAllBytes()加载大文件进内存,容易 OOM;流式哈希更安全 - 如果业务允许“逻辑等价”(比如忽略空白行),那就得走文本解析,但此时必须统一 Normalize:先用
File.ReadAllLines(),再.Select(l => l.TrimEnd()).Where(l => !string.IsNullOrEmpty(l)),而不是盲目Replace
写入目标文件前必须处理权限、占用和原子性
直接 File.WriteAllText(target, content) 在生产环境大概率失败:目标文件正被记事本打开、目录无写权限、杀毒软件锁住文件、甚至 NTFS 权限继承异常。更麻烦的是——写入中途崩溃,会导致目标文件被清空或截断。
- 永远用
File.WriteAllBytes(target + ".tmp", bytes)先写临时文件,再用File.Move(tmpPath, target, true)原子替换(Windows 下Move是原子操作,且true表示覆盖) - 捕获
UnauthorizedAccessException和IOException,记录具体Exception.Message(比如 “The process cannot access the file because it is being used by another process”),别只打“写入失败” - 同步前检查目标目录是否存在且可写:
Directory.GetAccessControl(dir).GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier))太重,简单点用try { File.OpenWrite(path).Close(); }预检
双向同步的核心难点是冲突检测与决策,不是技术实现
技术上把 A 改完写 B、B 改完写 A 并不难,难的是:A 和 B 同时被改了怎么办?用户希望保留哪边?要不要弹窗?日志里怎么标记“已丢弃 B 的修改”?这些都不是 FileSystemWatcher 能回答的。
最轻量的可行方案是引入时间戳权威:每次写入时在文件末尾追加注释 // synced_at: 2024-06-12T14:23:01Z,比对前先提取这个时间,谁新谁赢。但要注意——本地时钟不准、NTP 不同步、手动改系统时间都会让这机制失效。
真正稳定的方案得加一个外部协调文件(如 sync.state.json),记录每个文件的最后同步版本号和来源标识。不过这就超出“两个文件直连”的范畴了。
多数人卡在这一步不是因为不会写代码,而是没想清楚:你到底要的是“镜像同步”(强制以某一方为准),还是“合并同步”(需要 diff 工具介入)。前者能用上面三步搞定,后者就得集成 diff-match-patch 或调用 git diff,复杂度立刻翻倍。










