1. 介绍
对文件的操作几乎总是和程序外部的情况有关:文件可能被删除、移动、被另一个程序锁住,用户权限可能被撤销,或者磁盘上没有空闲空间。所有这些都可能导致异常——即在程序运行时中断并“抛出”一个异常对象(Exception)。如果不捕获并处理这个异常,程序会以错误结束,并向用户显示恐怖的红色控制台文本(或者更糟——悄无声息地崩溃)。
在 C# 中处理这类情况使用的是 try-catch 块(俗称“错误捕获”)。它不仅能“捕获”错误,还能让你优雅地决定接下来的动作:显示友好的消息、提示用户选另一个文件、把错误写入日志或简单地重试。
在真实应用里
如果你写的是一个控制台计算器,可能文件错误根本不会碰到。但如果你写的是企业文档处理程序或保存进度的游戏,不处理这些错误就像在走钢丝却没带安全网:迟早会出事。
2. 回顾异常处理
基本语法 try-catch
在进入具体的文件场景前,回顾一下 try-catch 的基本语法(我们在异常相关的讲座里已经见过):
try
{
// 这里是可能“抛出”异常的代码
}
catch (Exception ex)
{
// 这里我们捕获所有可能的异常... 但最好别这样做!
Console.WriteLine("出了点问题: " + ex.Message);
}
在 try 块里写潜在危险的代码,在 catch 块里处理“如果出了问题该怎么办”。
程序员笑话:“try-catch 就像给错误戴上隐形斗篷。错误存在,但你看不到它!”
文件相关的异常
通过 .NET 操作文件时,最常见的是在创建流、读写时遇到异常。下面是几种典型场景及对应的异常:
- 文件未找到 → FileNotFoundException
- 没有对文件/目录的访问权限 → UnauthorizedAccessException
- 路径问题(例如不允许的字符、路径太长) → PathTooLongException, ArgumentException
- 文件被另一个进程占用 → IOException
- 磁盘空间不足 → IOException
大多数与文件操作相关的异常都是 IOException 的派生类型。它们都继承自基类型 System.Exception。
3. 实践:捕获并处理文件操作错误
我们来看在“正在开发的应用”里的典型案例:假设我们尝试从文件读取一段问候语,处理它并输出给用户。这里我们会用 try-catch 来检测并处理错误。
示例 1:处理“文件未找到”
string filePath = "hello.txt";
try
{
string greeting = File.ReadAllText(filePath);
Console.WriteLine("文件内容: " + greeting);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("未找到文件!请检查文件 " + filePath + " 是否存在。");
// 可以额外输出详细信息
Console.WriteLine("技术细节: " + ex.Message);
}
如果文件 "hello.txt" 不存在,程序不会崩溃,而是输出友好的消息。就这么简单,让程序对用户的“傻错”更有韧性。
示例 2:权限被拒绝
再复杂一点的情况:文件存在,但我们没有读取权限(比如有人只给了写权限,或者文件在受保护的目录中)。
try
{
string secret = File.ReadAllText("C:\\Windows\\System32\\config.txt");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("没有文件访问权限!尝试以管理员身份运行程序。");
Console.WriteLine("技术原因: " + ex.Message);
}
示例 3:通用的 IOException
有些错误可能和锁冲突有关(例如,另一个进程正在占用文件),也可能是磁盘空间不足或硬件问题:
try
{
File.WriteAllText("important.txt", "重要信息!");
}
catch (IOException ex)
{
Console.WriteLine("文件操作出错:很可能被另一个程序占用或磁盘空间不足。");
Console.WriteLine("技术原因: " + ex.Message);
}
4. 捕获多个异常:有选择地处理
有时候需要对不同异常做不同处理。.NET 允许写多个 catch 块——要从更具体的到更通用的顺序写(否则编译器会生气)。
try
{
string content = File.ReadAllText("file.txt");
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("未找到文件。");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("没有文件访问权限!");
}
catch (IOException ex)
{
Console.WriteLine("其他 IO 错误: " + ex.Message);
}
重要:如果把 catch (Exception ex) 放在最前面,后面的 catch 就没意义了,因为基类会先捕获一切!
5. 嵌套 try-catch 和重试
有时你不仅想处理错误,还想给用户“改正”的机会——比如提示用户输入一个存在的文件路径:
string filePath;
string content;
int attempts = 0;
const int maxAttempts = 3;
do
{
Console.Write("请输入文件路径: ");
filePath = Console.ReadLine();
try
{
content = File.ReadAllText(filePath);
Console.WriteLine("文件内容:\n" + content);
break;
}
catch (FileNotFoundException)
{
Console.WriteLine("未找到文件!请再试一次。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("没有文件访问权限!请尝试其他文件。");
}
attempts++;
}
while (attempts < maxAttempts);
if (attempts == maxAttempts)
Console.WriteLine("尝试次数过多。");
这段代码对忘了文件放哪儿的用户来说,是个小小的“友好界面”。
6. 常见错误与注意事项:从懒惰到有意识
新手(甚至有经验的开发者)经常犯的错误是捕获所有异常,写一个简单的 catch (Exception) 然后输出“发生了错误!”,不去考虑原因。这种做法有几个坏处。首先,它掩盖了业务逻辑中的真实问题。其次,和文件无关的其它错误(比如代码笔误或数学错误)可能被误吞,导致排查时间拉长。
更好的做法是只捕获你能有意义地处理的错误。如果不清楚发生了什么,宁可不捕获它——让异常“崩溃”程序,这样你能看到调用栈并更容易找到要修复的问题。
注意:有些异常可能包含内部原因(InnerException)。在写日志时分析它们很方便,能提供更详细的诊断信息。
另一个细节是:如果在 catch 之后程序无法继续(例如无法打开主配置文件),可以用 return 结束执行,或者重新抛出异常(throw;),这样不会留下半残的“僵尸程序”。
GO TO FULL VERSION