1. 介绍
在编程里,文件操作的安全不仅仅是防病毒和黑客,还包括对错误、锁、权限、错误路径和其他坑的正确处理。马虎会导致数据丢失、程序挂起、奇怪的 bug 或臭名昭著的异常 IOException。
下面是我们想要能够应对的典型情况:
- 文件不存在,但我们尝试读取(或相反:已经存在,但我们想以“仅在不存在时创建”的模式写入)。
- 文件被另一个程序打开并锁定。
- 用户没有读取或写入该文件或文件夹的权限。
- 文件路径错误或包含禁止字符。
- 操作被意外中断(比如磁盘满了)。
- 糟糕的习惯:把文件打开着不关闭,流泄露。
幸好,.NET 提供了处理这些问题的所有工具。它们很简单——但就像安全带一样,关键是别忘了系上。
2. 与文件安全操作的基本原则
提前检查文件是否存在和权限
在读取文件之前,先检查它是否存在(比如通过 File.Exists),尤其当路径来自用户输入时:
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("错误:未找到文件。");
return;
}
在写入之前,确认目录存在并且你有写入权限(或者把错误抛到上层处理)。
绝不要把流留着不关
使用关键字 using 自动关闭流——这是最佳实践:
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// 即便发生错误,文件也会在这里关闭!
这能防止“挂起”的锁和资源泄露。
一定要捕获异常
任何文件操作都可能抛出异常。即便文件刚刚存在 —— 在访问时也可能被删除或移动。使用 try-catch 结构:
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("未找到文件。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("没有文件访问权限。");
}
catch (IOException ex)
{
Console.WriteLine($"输入/输出 错误:{ex.Message}");
}
不要信任用户输入
如果路径由用户提供,他们可能输入错误(甚至尝试让程序崩溃)。建议验证路径(参见 Path.GetInvalidPathChars()):
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("路径不能为空!");
return;
}
// 额外:检查禁止字符
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("路径包含不允许的字符。");
return;
}
// 之后安全地处理文件
}
catch (Exception ex)
{
Console.WriteLine("路径验证错误: " + ex.Message);
}
3. 实战示例:用“成熟”的方式读取文件
我们给教学项目稍微升级一下。假设需要读取用户输入名字的文件,并把内容输出到屏幕上。所有这些都要处理错误并注意编码。
Console.Write("请输入文件路径: ");
string? path = Console.ReadLine();
// 路径验证
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("路径不能为空!");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("路径包含不允许的字符。");
return;
}
// 尝试读取文件
try
{
if (!File.Exists(path))
{
Console.WriteLine("未找到文件。");
return;
}
// 明确指定编码(例如 UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("文件内容:");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("没有读取文件的权限。");
}
catch (IOException ex)
{
Console.WriteLine("输入/输出 错误: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("意外错误: " + ex.Message);
}
注意,即使文件在输入名字和读取之间消失了,这段代码也不会崩溃:异常会被捕获。流会在离开 using 块时自动关闭——即便发生错误。
4. 写入文件:避免数据丢失
当以写入模式打开文件,尤其是覆盖模式(在 StreamWriter 构造函数的第二个参数传入 false)时,存在意外擦除重要数据的风险。这里有一些建议:
检查你不会覆盖已有文件
有时如果文件已存在,最好询问用户:
if (File.Exists(path))
{
Console.WriteLine("注意:文件已存在。覆盖吗? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
在需要的地方用追加模式(append)
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": 新的日志条目。");
旧信息不会消失。
5. 防止竞态和冲突
有时文件会被多个程序同时打开(比如你的 C# 客户端和 Notepad++)。这可能导致错误。默认情况下 StreamReader 和 StreamWriter 使用基于 FileShare 的共享模式 —— 要么允许别人读,要么不允许。
可以显式控制它:
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// 读取...
- FileShare.Read:别人只能读取。
- FileShare.None:不允许共享 —— 文件完全“属于”你。
如果需要让文件被不同程序同时读写,可以选择合适的模式,但要小心:这种做法通常只在你非常清楚后果时才用。
6. 意外异常以及如何处理
即使严格遵循建议,也可能出现比如磁盘空间不足、损坏的U盘或病毒导致的故障。这里列出一些“特殊”的异常以及如何捕获它们:
- PathTooLongException — 文件路径太长(在旧版 Windows 中超过 260 个字符)。
- DirectoryNotFoundException — 指定的目录未找到。
- DriveNotFoundException — 例如路径是 "Z:\\file.txt",但驱动器 Z 不存在。
- NotSupportedException — 例如路径包含不支持的组合。
建议对这些异常做单独处理 —— 至少把它们单独记录日志。
7. 使用临时文件做原子写入
经典问题:在写文件时程序崩溃,导致文件损坏。专业程序常用“原子”写入策略:
- 把内容写到临时文件(例如 "file.txt.tmp")。
- 把临时文件移动(在文件系统层面通常是原子操作)替换目标文件(使用 File.Replace)。
- 旧文件要么被完整替换,要么完全不变 —— 不会出现半坏的数据。
示例:
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// 把所有内容写到临时文件
writer.Write(contentForSave);
// 成功写入后替换主文件
File.Replace(tempPath, path, null); // 移动临时文件,替换目标文件(原子操作)
}
catch (Exception ex)
{
Console.WriteLine("保存文件时出错: " + ex.Message);
// 如果不需要,最好删除 tempPath
}
实际中很多编辑器和办公软件都这么做,以保证数据一致性。
8. 使用包装类来安全访问
.NET 有一些辅助方法用于“安全”地执行文件操作。例如 File.ReadAllText 和 File.WriteAllText 会自动打开、读/写并关闭文件。但即便如此也要用 try-catch 包裹:
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// 处理数据...
}
catch (Exception ex)
{
Console.WriteLine("文件操作错误: " + ex.Message);
}
对于大文件,用流分块读取,别一次性把整个文件读到内存里。
GO TO FULL VERSION