CodeGym /课程 /C# SELF /安全地读取和写入文件

安全地读取和写入文件

C# SELF
第 38 级 , 课程 4
可用

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++)。这可能导致错误。默认情况下 StreamReaderStreamWriter 使用基于 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. 使用临时文件做原子写入

经典问题:在写文件时程序崩溃,导致文件损坏。专业程序常用“原子”写入策略:

  1. 把内容写到临时文件(例如 "file.txt.tmp")。
  2. 把临时文件移动(在文件系统层面通常是原子操作)替换目标文件(使用 File.Replace)。
  3. 旧文件要么被完整替换,要么完全不变 —— 不会出现半坏的数据。

示例:

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.ReadAllTextFile.WriteAllText 会自动打开、读/写并关闭文件。但即便如此也要用 try-catch 包裹:

try
{
    string text = File.ReadAllText("settings.json", Encoding.UTF8);
    // 处理数据...
}
catch (Exception ex)
{
    Console.WriteLine("文件操作错误: " + ex.Message);
}

对于大文件,用流分块读取,别一次性把整个文件读到内存里。

1
调查/小测验
常见异常第 38 级,课程 4
不可用
常见异常
处理文件操作时的错误
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION