第十一章我们学习了 LINQ,知道了如何优雅地查询数据。但程序运行中总会遇到意外情况:文件不存在、网络断开、用户输入错误......如果不对这些情况做处理,程序就会崩溃。这一章要学的异常处理和调试技巧,就是让程序在遇到问题时能够优雅地处理,而不是直接崩溃。
12.1 什么是异常?
12.1.1 异常的概念
异常 = 程序运行时发生的错误或意外情况
csharp
// 常见的异常情况
int[] arr = new int[3];
arr[5] = 10; // IndexOutOfRangeException(索引越界)
int x = 10 / 0; // DivideByZeroException(除以零)
string s = null;
int len = s.Length; // NullReferenceException(空引用)
int num = int.Parse("abc"); // FormatException(格式错误)
12.1.2 没有异常处理的后果
csharp
// 没有异常处理:程序直接崩溃
Console.Write("请输入数字:");
int number = int.Parse(Console.ReadLine()); // 用户输入"abc" → 程序崩溃
Console.WriteLine($"你输入的是:{number}");
// 输出:
// 请输入数字:abc
// 未处理的异常:System.FormatException: 输入字符串的格式不正确。
// 程序终止
12.1.3 有异常处理的程序
csharp
// 有异常处理:程序可以继续运行
try
{
Console.Write("请输入数字:");
int number = int.Parse(Console.ReadLine());
Console.WriteLine($"你输入的是:{number}");
}
catch (FormatException)
{
Console.WriteLine("错误:请输入有效的数字!");
}
finally
{
Console.WriteLine("程序执行完毕");
}
// 输出:
// 请输入数字:abc
// 错误:请输入有效的数字!
// 程序执行完毕
12.2 try-catch-finally 结构
12.2.1 基本语法
csharp
try
{
// 可能发生异常的代码
}
catch (异常类型1 变量名)
{
// 处理异常类型1
}
catch (异常类型2 变量名)
{
// 处理异常类型2
}
finally
{
// 无论是否发生异常,都会执行的代码(可选)
// 通常用于释放资源
}
12.2.2 try-catch 基础示例
csharp
class Program
{
static void Main()
{
Console.WriteLine("=== 异常处理演示 ===\n");
// 示例1:捕获特定异常
try
{
Console.Write("请输入被除数:");
int dividend = int.Parse(Console.ReadLine());
Console.Write("请输入除数:");
int divisor = int.Parse(Console.ReadLine());
int result = dividend / divisor;
Console.WriteLine($"结果:{result}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"除以零错误:{ex.Message}");
}
catch (FormatException ex)
{
Console.WriteLine($"格式错误:{ex.Message}");
}
catch (Exception ex) // 捕获所有其他异常
{
Console.WriteLine($"未知错误:{ex.Message}");
}
Console.WriteLine("\n程序继续运行...");
}
}
12.2.3 捕获异常的信息
csharp
try
{
int.Parse("abc");
}
catch (Exception ex)
{
// 常用属性
Console.WriteLine($"异常类型:{ex.GetType().Name}");
Console.WriteLine($"错误消息:{ex.Message}");
Console.WriteLine($"堆栈跟踪:{ex.StackTrace}");
Console.WriteLine($"内部异常:{ex.InnerException}");
Console.WriteLine($"来源:{ex.Source}");
}
// 输出示例:
// 异常类型:FormatException
// 错误消息:输入字符串的格式不正确。
// 堆栈跟踪: at System.Int32.Parse(String s)
// at Program.Main(String[] args) in ...
12.2.4 finally 块------资源释放
csharp
class FileProcessor
{
public static void ReadFile(string path)
{
StreamReader reader = null;
try
{
reader = new StreamReader(path);
string content = reader.ReadToEnd();
Console.WriteLine($"文件内容:{content}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"文件未找到:{ex.Message}");
}
catch (IOException ex)
{
Console.WriteLine($"IO错误:{ex.Message}");
}
finally
{
// 无论成功还是失败,都要关闭文件
if (reader != null)
{
reader.Close();
Console.WriteLine("文件已关闭");
}
}
}
}
12.2.5 多个 catch 块的顺序
csharp
// 重要:更具体的异常在前,更通用的在后
try
{
// 代码
}
catch (DivideByZeroException ex) // 具体异常
{
Console.WriteLine("除以零");
}
catch (ArithmeticException ex) // 更宽泛的异常
{
Console.WriteLine("算术错误");
}
catch (Exception ex) // 最通用的异常
{
Console.WriteLine("其他错误");
}
// ❌ 错误顺序:永远捕获不到后面更具体的异常
try
{
// 代码
}
catch (Exception ex) // 会捕获所有异常
{
Console.WriteLine("错误");
}
catch (DivideByZeroException ex) // 永远不会执行
{
Console.WriteLine("除以零");
}
12.3 异常类型体系
12.3.1 异常层次结构
text
System.Object
└── System.Exception
├── System.SystemException
│ ├── System.ArithmeticException
│ │ ├── DivideByZeroException
│ │ └── OverflowException
│ ├── System.ArgumentException
│ │ ├── ArgumentNullException
│ │ └── ArgumentOutOfRangeException
│ ├── System.FormatException
│ ├── System.IndexOutOfRangeException
│ ├── System.InvalidOperationException
│ ├── System.NullReferenceException
│ └── System.IO.IOException
│ ├── FileNotFoundException
│ └── DirectoryNotFoundException
└── System.ApplicationException // 自定义异常通常继承这个
12.3.2 常见的异常类型
| 异常类型 | 触发条件 | 示例 |
|---|---|---|
DivideByZeroException |
除以零 | int x = 10 / 0; |
NullReferenceException |
访问 null 对象的成员 | string s = null; int l = s.Length; |
IndexOutOfRangeException |
数组索引越界 | int[] a = new int[3]; a[5] = 1; |
FormatException |
格式转换错误 | int.Parse("abc"); |
ArgumentException |
参数无效 | string.IsNullOrEmpty(null); |
ArgumentNullException |
参数为 null | 传入 null 但不应为 null |
ArgumentOutOfRangeException |
参数超出范围 | "abc".Substring(5); |
InvalidOperationException |
操作在当前状态下无效 | 枚举集合时修改集合 |
FileNotFoundException |
文件不存在 | File.ReadAllText("notexist.txt"); |
IOException |
IO 错误(网络、磁盘等) | 文件被占用、网络断开 |
OverflowException |
数值溢出(checked 模式) | checked{ int x = int.MaxValue + 1; } |
StackOverflowException |
栈溢出 | 无限递归 |
OutOfMemoryException |
内存不足 | 创建超大数组 |
12.3.3 异常的捕获原则
csharp
// 原则1:只捕获你能处理的异常
try
{
// 代码
}
catch (FileNotFoundException ex)
{
// 能处理:提示用户检查文件路径
Console.WriteLine($"文件 {ex.FileName} 未找到");
}
catch (Exception ex)
{
// 不要这样!捕获了但不处理,会隐藏问题
// 要么处理,要么重新抛出
throw; // 重新抛出
}
// 原则2:保持原有堆栈信息
try
{
// 代码
}
catch (Exception ex)
{
// ❌ 错误:丢失了堆栈信息
throw ex;
// ✅ 正确:保持堆栈信息
throw;
// ✅ 正确:包装新异常
throw new ApplicationException("操作失败", ex);
}
12.4 抛出异常
12.4.1 使用 throw 抛出异常
csharp
class BankAccount
{
private double balance;
public void Withdraw(double amount)
{
if (amount <= 0)
{
// 抛出参数异常
throw new ArgumentException("取款金额必须大于0", nameof(amount));
}
if (amount > balance)
{
// 抛出自定义业务异常
throw new InsufficientFundsException($"余额不足。可用余额:{balance:C}");
}
balance -= amount;
}
public void Deposit(double amount)
{
if (amount <= 0)
{
throw new ArgumentException("存款金额必须大于0", nameof(amount));
}
balance += amount;
}
}
// 自定义异常类
class InsufficientFundsException : Exception
{
public InsufficientFundsException() { }
public InsufficientFundsException(string message) : base(message) { }
public InsufficientFundsException(string message, Exception inner)
: base(message, inner) { }
}
12.4.2 重新抛出异常
csharp
class DataAccessLayer
{
public string GetData(int id)
{
try
{
// 数据库操作
return QueryDatabase(id);
}
catch (SqlException ex)
{
// 记录日志后重新抛出
LogError("数据库错误", ex);
throw; // 保持原始堆栈信息
}
}
private void LogError(string message, Exception ex)
{
// 写入日志文件
Console.WriteLine($"[ERROR] {message}: {ex.Message}");
}
private string QueryDatabase(int id)
{
// 模拟数据库错误
throw new SqlException();
}
}
class SqlException : Exception { }
12.4.3 异常过滤器(when 子句)
csharp
// 根据条件过滤异常
try
{
ProcessData();
}
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
{
Console.WriteLine("输入格式错误");
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
Console.WriteLine("操作超时");
}
catch (Exception ex) when (LogAndThrow(ex))
{
// 当 LogAndThrow 返回 true 时才进入
}
bool LogAndThrow(Exception ex)
{
Console.WriteLine($"记录异常:{ex.Message}");
return false; // 返回 false 表示不进入这个 catch
}
12.5 自定义异常
12.5.1 创建自定义异常
csharp
// 自定义异常的最佳实践
[Serializable]
public class ValidationException : Exception
{
// 无参构造函数
public ValidationException() { }
// 带消息的构造函数
public ValidationException(string message) : base(message) { }
// 带消息和内部异常的构造函数
public ValidationException(string message, Exception inner)
: base(message, inner) { }
// 用于序列化的构造函数
protected ValidationException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
: base(info, context) { }
// 可以添加自定义属性
public string PropertyName { get; set; }
public object InvalidValue { get; set; }
public ValidationException(string message, string propertyName, object invalidValue)
: base(message)
{
PropertyName = propertyName;
InvalidValue = invalidValue;
}
}
// 使用自定义异常
class UserValidator
{
public static void ValidateEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
throw new ValidationException("邮箱不能为空", "Email", email);
}
if (!email.Contains("@"))
{
throw new ValidationException("邮箱格式无效", "Email", email);
}
}
public static void ValidateAge(int age)
{
if (age < 0 || age > 150)
{
throw new ValidationException("年龄必须在0-150之间", "Age", age);
}
}
}
// 使用
try
{
UserValidator.ValidateEmail("invalid");
UserValidator.ValidateAge(-5);
}
catch (ValidationException ex)
{
Console.WriteLine($"验证失败:{ex.Message}");
Console.WriteLine($"属性:{ex.PropertyName}");
Console.WriteLine($"无效值:{ex.InvalidValue}");
}
12.5.2 业务异常示例
csharp
// 订单系统异常
class OrderException : Exception
{
public int OrderId { get; set; }
public string Operation { get; set; }
public OrderException() { }
public OrderException(string message, int orderId, string operation)
: base(message)
{
OrderId = orderId;
Operation = operation;
}
}
class InsufficientStockException : OrderException
{
public int ProductId { get; set; }
public int RequestedQuantity { get; set; }
public int AvailableStock { get; set; }
public InsufficientStockException(int productId, int requested, int available)
: base($"商品 {productId} 库存不足,请求 {requested},可用 {available}", 0, "下单")
{
ProductId = productId;
RequestedQuantity = requested;
AvailableStock = available;
}
}
class OrderAlreadyShippedException : OrderException
{
public OrderAlreadyShippedException(int orderId)
: base($"订单 {orderId} 已发货,无法取消", orderId, "取消订单")
{
}
}
12.6 异常处理的最佳实践
12.6.1 使用 using 语句自动释放资源
csharp
// 不用 using:需要手动在 finally 中释放
StreamReader reader = null;
try
{
reader = new StreamReader("file.txt");
string content = reader.ReadToEnd();
}
finally
{
if (reader != null) reader.Dispose();
}
// 使用 using:自动释放
using (StreamReader reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
} // 自动调用 Dispose()
// using 可以嵌套
using var conn = new SqlConnection(connectionString);
using var cmd = new SqlCommand(query, conn);
// 退出作用域时自动释放
12.6.2 何时捕获异常
csharp
// 原则:在能正确处理异常的地方捕获
// ✅ 在用户界面层捕获并显示错误
class UI
{
public void Button_Click()
{
try
{
var result = BusinessLayer.ProcessData();
ShowResult(result);
}
catch (ValidationException ex)
{
ShowError($"输入错误:{ex.Message}");
}
catch (BusinessException ex)
{
ShowError($"业务错误:{ex.Message}");
LogError(ex);
}
catch (Exception ex)
{
ShowError("系统错误,请联系管理员");
LogError(ex);
}
}
}
// ✅ 在边界层捕获并转换异常
class ServiceLayer
{
public UserDto GetUser(int id)
{
try
{
var user = Repository.GetUser(id);
return MapToDto(user);
}
catch (SqlException ex)
{
// 将技术异常转换为业务异常
throw new DataAccessException("获取用户信息失败", ex);
}
}
}
12.6.3 避免的空 catch
csharp
// ❌ 不要这样:吞噬异常
try
{
riskyOperation();
}
catch (Exception)
{
// 什么都不做,问题被隐藏了
}
// ✅ 至少记录日志
try
{
riskyOperation();
}
catch (Exception ex)
{
LogError(ex);
// 或者重新抛出
throw;
}
12.6.4 抛出异常 vs 返回错误码
csharp
// 方式1:返回错误码(老式C风格)
class Calculator
{
// 返回 true 表示成功,false 表示失败
public bool Divide(int a, int b, out int result)
{
if (b == 0)
{
result = 0;
return false; // 错误码
}
result = a / b;
return true; // 成功
}
}
// 使用
if (!calculator.Divide(10, 0, out int result))
{
Console.WriteLine("计算失败");
}
// 方式2:抛出异常(C# 推荐方式)
class CalculatorV2
{
public int Divide(int a, int b)
{
if (b == 0)
{
throw new DivideByZeroException("除数不能为零");
}
return a / b;
}
}
// 使用
try
{
int result = calculator.Divide(10, 0);
}
catch (DivideByZeroException ex)
{
Console.WriteLine(ex.Message);
}
12.7 调试技巧
12.7.1 设置断点(Breakpoint)
csharp
// 在代码行左侧单击设置断点(红点)
int x = 10;
int y = 0;
int result = x / y; // ← 在这里设置断点
// 常用快捷键
// F5:开始调试/继续
// F9:设置/取消断点
// F10:逐过程(不进入方法内部)
// F11:逐语句(进入方法内部)
// Shift+F11:跳出当前方法
// F5:继续执行到下一个断点
12.7.2 监视变量
csharp
// 调试时查看变量值
string name = "张三";
int age = 25;
double salary = 5000.50;
// 几种查看方式:
// 1. 鼠标悬停:查看变量值
// 2. 监视窗口:手动添加变量
// 3. 即时窗口:执行表达式
// 4. 自动窗口:显示当前和之前的变量
// 调试时添加监视
// - 选中变量,右键 -> 添加监视
// - 在监视窗口输入表达式,如 age.ToString()
12.7.3 条件断点
csharp
// 只有在满足条件时才中断
for (int i = 0; i < 100; i++)
{
// 设置条件断点:i == 50
ProcessData(i);
}
// 设置方法:
// 1. 右键断点 -> 条件
// 2. 输入条件表达式:i == 50
// 3. 可选:命中次数、筛选器
12.7.4 跟踪点(Tracepoint)
csharp
// 在断点处输出信息而不停止程序
for (int i = 0; i < 10; i++)
{
// 设置跟踪点,输出 i 的值
Console.WriteLine($"循环次数:{i}");
}
// 设置方法:
// 1. 右键断点 -> 操作
// 2. 勾选"将消息记录到输出窗口"
// 3. 使用关键字:$ADDRESS、$CALLER、$FUNCTION 等
12.7.5 诊断输出
csharp
using System.Diagnostics;
class DebugExample
{
static void Main()
{
// Debug:仅在 Debug 模式下执行
Debug.WriteLine("调试信息");
Debug.Assert(x > 0, "x 应该大于 0");
// Trace:在 Debug 和 Release 模式下都执行
Trace.WriteLine("跟踪信息");
Trace.TraceInformation("信息消息");
Trace.TraceWarning("警告消息");
Trace.TraceError("错误消息");
// 条件输出
Debug.WriteLineIf(x < 0, "x 是负数");
// 监听器
TextWriterTraceListener listener = new TextWriterTraceListener("log.txt");
Trace.Listeners.Add(listener);
Trace.AutoFlush = true;
}
}
12.7.6 日志记录
csharp
using System.IO;
class Logger
{
private static string logFile = "app.log";
public static void Info(string message)
{
Log("INFO", message);
}
public static void Warning(string message)
{
Log("WARN", message);
}
public static void Error(string message, Exception ex = null)
{
string fullMessage = message;
if (ex != null)
{
fullMessage += $"\n 异常:{ex.Message}\n 堆栈:{ex.StackTrace}";
}
Log("ERROR", fullMessage);
}
private static void Log(string level, string message)
{
string logLine = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}";
Console.WriteLine(logLine);
// 写入文件
File.AppendAllText(logFile, logLine + Environment.NewLine);
}
}
// 使用
class Program
{
static void Main()
{
Logger.Info("程序启动");
try
{
// 业务逻辑
ProcessData();
}
catch (Exception ex)
{
Logger.Error("处理数据失败", ex);
}
Logger.Info("程序结束");
}
}
12.8 综合示例
示例1:健壮的用户输入处理
csharp
using System;
using System.Collections.Generic;
class SafeInput
{
// 安全的整数输入
public static int GetInt(string prompt, int? min = null, int? max = null)
{
while (true)
{
try
{
Console.Write(prompt);
string input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("输入不能为空");
continue;
}
int result = int.Parse(input);
if (min.HasValue && result < min.Value)
{
Console.WriteLine($"输入不能小于 {min.Value}");
continue;
}
if (max.HasValue && result > max.Value)
{
Console.WriteLine($"输入不能大于 {max.Value}");
continue;
}
return result;
}
catch (FormatException)
{
Console.WriteLine("请输入有效的整数");
}
catch (OverflowException)
{
Console.WriteLine($"数字超出范围({int.MinValue} ~ {int.MaxValue})");
}
}
}
// 安全的双精度浮点数输入
public static double GetDouble(string prompt, double? min = null, double? max = null)
{
while (true)
{
try
{
Console.Write(prompt);
string input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("输入不能为空");
continue;
}
double result = double.Parse(input);
if (min.HasValue && result < min.Value)
{
Console.WriteLine($"输入不能小于 {min.Value}");
continue;
}
if (max.HasValue && result > max.Value)
{
Console.WriteLine($"输入不能大于 {max.Value}");
continue;
}
return result;
}
catch (FormatException)
{
Console.WriteLine("请输入有效的数字");
}
}
}
// 安全的日期输入
public static DateTime GetDateTime(string prompt, DateTime? min = null, DateTime? max = null)
{
while (true)
{
try
{
Console.Write(prompt);
string input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("输入不能为空");
continue;
}
DateTime result = DateTime.Parse(input);
if (min.HasValue && result < min.Value)
{
Console.WriteLine($"日期不能早于 {min.Value:yyyy-MM-dd}");
continue;
}
if (max.HasValue && result > max.Value)
{
Console.WriteLine($"日期不能晚于 {max.Value:yyyy-MM-dd}");
continue;
}
return result;
}
catch (FormatException)
{
Console.WriteLine("请输入有效的日期(如 2024-01-01)");
}
}
}
// 安全的枚举选择
public static T GetEnum<T>(string prompt) where T : struct, Enum
{
while (true)
{
try
{
Console.Write(prompt);
string input = Console.ReadLine();
if (Enum.TryParse<T>(input, true, out T result) && Enum.IsDefined(typeof(T), result))
{
return result;
}
Console.WriteLine($"无效选项。有效值:{string.Join(", ", Enum.GetNames(typeof(T)))}");
}
catch (Exception ex)
{
Console.WriteLine($"错误:{ex.Message}");
}
}
}
}
class Program
{
enum MenuOption
{
添加 = 1,
删除 = 2,
修改 = 3,
查询 = 4,
退出 = 5
}
static void Main()
{
Console.WriteLine("=== 学生成绩管理系统 ===");
while (true)
{
Console.WriteLine("\n请选择操作:");
Console.WriteLine("1. 添加学生");
Console.WriteLine("2. 删除学生");
Console.WriteLine("3. 修改成绩");
Console.WriteLine("4. 查询成绩");
Console.WriteLine("5. 退出");
MenuOption choice = SafeInput.GetEnum<MenuOption>("请输入选项:");
if (choice == MenuOption.退出)
{
Console.WriteLine("再见!");
break;
}
switch (choice)
{
case MenuOption.添加:
AddStudent();
break;
case MenuOption.删除:
DeleteStudent();
break;
case MenuOption.修改:
UpdateScore();
break;
case MenuOption.查询:
QueryScore();
break;
}
}
}
static void AddStudent()
{
Console.WriteLine("\n=== 添加学生 ===");
string name = GetNonEmptyString("请输入姓名:");
int age = SafeInput.GetInt("请输入年龄:", 1, 150);
double score = SafeInput.GetDouble("请输入成绩:", 0, 100);
Console.WriteLine($"已添加学生:{name},年龄 {age},成绩 {score}");
}
static void DeleteStudent()
{
Console.WriteLine("\n=== 删除学生 ===");
int id = SafeInput.GetInt("请输入学生ID:", 1);
Console.WriteLine($"已删除学生 ID:{id}");
}
static void UpdateScore()
{
Console.WriteLine("\n=== 修改成绩 ===");
int id = SafeInput.GetInt("请输入学生ID:", 1);
double newScore = SafeInput.GetDouble("请输入新成绩:", 0, 100);
Console.WriteLine($"已将学生 {id} 成绩改为 {newScore}");
}
static void QueryScore()
{
Console.WriteLine("\n=== 查询成绩 ===");
Console.WriteLine("1. 按学号查询");
Console.WriteLine("2. 按姓名查询");
int choice = SafeInput.GetInt("请选择:", 1, 2);
if (choice == 1)
{
int id = SafeInput.GetInt("请输入学号:", 1);
Console.WriteLine($"查询学号 {id}...");
}
else
{
string name = GetNonEmptyString("请输入姓名:");
Console.WriteLine($"查询姓名 {name}...");
}
}
static string GetNonEmptyString(string prompt)
{
while (true)
{
Console.Write(prompt);
string input = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(input))
return input;
Console.WriteLine("输入不能为空");
}
}
}
示例2:文件处理与异常
csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
// 自定义异常
class ConfigException : Exception
{
public string ConfigFile { get; set; }
public ConfigException(string message, string configFile) : base(message)
{
ConfigFile = configFile;
}
public ConfigException(string message, string configFile, Exception inner)
: base(message, inner)
{
ConfigFile = configFile;
}
}
// 配置数据类
class AppConfig
{
public string DatabaseConnection { get; set; }
public int TimeoutSeconds { get; set; }
public bool EnableLogging { get; set; }
public List<string> AdminUsers { get; set; }
}
// 配置管理器
class ConfigManager
{
private const string ConfigFile = "appsettings.json";
private const string BackupFile = "appsettings.backup.json";
public AppConfig LoadConfig()
{
try
{
if (!File.Exists(ConfigFile))
{
throw new ConfigException($"配置文件 {ConfigFile} 不存在", ConfigFile);
}
string json = File.ReadAllText(ConfigFile);
var config = JsonSerializer.Deserialize<AppConfig>(json);
if (config == null)
{
throw new ConfigException("配置文件反序列化失败", ConfigFile);
}
// 验证配置
ValidateConfig(config);
return config;
}
catch (JsonException ex)
{
throw new ConfigException("配置文件格式错误", ConfigFile, ex);
}
catch (IOException ex)
{
throw new ConfigException("读取配置文件失败", ConfigFile, ex);
}
}
public void SaveConfig(AppConfig config)
{
try
{
// 先备份原文件
if (File.Exists(ConfigFile))
{
File.Copy(ConfigFile, BackupFile, true);
Console.WriteLine("已创建配置文件备份");
}
var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(config, options);
File.WriteAllText(ConfigFile, json, Encoding.UTF8);
Console.WriteLine("配置文件保存成功");
}
catch (UnauthorizedAccessException ex)
{
throw new ConfigException("没有写入配置文件的权限", ConfigFile, ex);
}
catch (IOException ex)
{
throw new ConfigException("保存配置文件失败", ConfigFile, ex);
}
}
private void ValidateConfig(AppConfig config)
{
if (string.IsNullOrWhiteSpace(config.DatabaseConnection))
{
throw new ConfigException("数据库连接字符串不能为空", ConfigFile);
}
if (config.TimeoutSeconds <= 0 || config.TimeoutSeconds > 300)
{
throw new ConfigException("超时时间必须在1-300秒之间", ConfigFile);
}
}
public void RestoreBackup()
{
if (File.Exists(BackupFile))
{
File.Copy(BackupFile, ConfigFile, true);
Console.WriteLine("已从备份恢复配置文件");
}
else
{
Console.WriteLine("没有找到备份文件");
}
}
}
// 日志管理器
class LogManager
{
private const string LogDirectory = "logs";
static LogManager()
{
if (!Directory.Exists(LogDirectory))
{
Directory.CreateDirectory(LogDirectory);
}
}
public static void WriteLog(string level, string message, Exception ex = null)
{
try
{
string logFile = Path.Combine(LogDirectory, $"{DateTime.Now:yyyy-MM-dd}.log");
string logLine = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}";
if (ex != null)
{
logLine += $"\n 异常:{ex.Message}\n 堆栈:{ex.StackTrace}";
}
File.AppendAllText(logFile, logLine + Environment.NewLine);
}
catch (Exception logEx)
{
// 日志失败时的后备方案
Console.WriteLine($"日志写入失败:{logEx.Message}");
}
}
public static void Info(string message) => WriteLog("INFO", message);
public static void Warning(string message) => WriteLog("WARN", message);
public static void Error(string message, Exception ex = null) => WriteLog("ERROR", message, ex);
}
// 主程序
class Program
{
static void Main()
{
Console.WriteLine("=== 应用程序启动 ===");
LogManager.Info("程序启动");
ConfigManager configManager = new ConfigManager();
try
{
// 加载配置
AppConfig config = configManager.LoadConfig();
LogManager.Info($"配置加载成功。超时时间:{config.TimeoutSeconds}秒");
// 模拟使用配置
RunApplication(config);
}
catch (ConfigException ex)
{
LogManager.Error("配置加载失败", ex);
Console.WriteLine($"配置错误:{ex.Message}");
// 尝试恢复
Console.Write("是否从备份恢复?(y/n):");
if (Console.ReadLine()?.ToLower() == "y")
{
try
{
configManager.RestoreBackup();
var config = configManager.LoadConfig();
RunApplication(config);
}
catch (Exception restoreEx)
{
LogManager.Error("恢复失败", restoreEx);
Console.WriteLine($"恢复失败:{restoreEx.Message}");
}
}
}
catch (Exception ex)
{
LogManager.Error("未处理的异常", ex);
Console.WriteLine($"系统错误:{ex.Message}");
}
finally
{
LogManager.Info("程序结束");
Console.WriteLine("\n程序结束,按任意键退出...");
Console.ReadKey();
}
}
static void RunApplication(AppConfig config)
{
Console.WriteLine($"\n应用程序运行中...");
Console.WriteLine($"数据库连接:{config.DatabaseConnection}");
Console.WriteLine($"超时时间:{config.TimeoutSeconds}秒");
Console.WriteLine($"日志记录:{(config.EnableLogging ? "启用" : "禁用")}");
// 模拟业务逻辑
Console.WriteLine("\n按任意键模拟业务操作...");
Console.ReadKey();
// 可能产生异常的业务逻辑
Random random = new Random();
if (random.Next(1, 5) == 1) // 20% 概率失败
{
throw new InvalidOperationException("业务操作失败");
}
Console.WriteLine("业务操作成功完成!");
}
}
示例3:调试技巧演示
csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
class DebugDemo
{
static void Main()
{
Console.WriteLine("=== 调试技巧演示 ===\n");
// 1. 条件断点演示
DemonstrateConditionalBreakpoint();
// 2. 断言演示
DemonstrateAssert();
// 3. 诊断输出演示
DemonstrateDiagnosticOutput();
// 4. 性能分析演示
DemonstratePerformance();
// 5. 内存调试演示
DemonstrateMemoryDebug();
}
// 条件断点:只在特定条件下中断
static void DemonstrateConditionalBreakpoint()
{
Console.WriteLine("1. 条件断点演示");
Console.WriteLine(" (在循环中设置条件断点 i == 50)");
for (int i = 0; i < 100; i++)
{
// 在这里设置条件断点:i == 50
ProcessItem(i);
if (i % 10 == 0)
Console.Write($".");
}
Console.WriteLine("\n");
}
// 断言:调试时验证条件
static void DemonstrateAssert()
{
Console.WriteLine("2. 断言演示");
int score = -5;
// Debug 模式下会中断
Debug.Assert(score >= 0, "成绩不能为负数");
// 带消息的断言
Debug.Assert(score >= 0 && score <= 100, $"成绩 {score} 超出范围 0-100");
// Trace.Assert 在 Release 模式下也会执行
Trace.Assert(score >= 0, "成绩不能为负数");
Console.WriteLine($"当前成绩:{score}\n");
}
// 诊断输出
static void DemonstrateDiagnosticOutput()
{
Console.WriteLine("3. 诊断输出演示");
// Debug 输出(仅 Debug 模式)
Debug.WriteLine("Debug: 这行只在调试模式下输出");
// Trace 输出(所有模式)
Trace.WriteLine("Trace: 这行在所有模式下输出");
// 条件输出
int count = 5;
Debug.WriteLineIf(count > 10, "Debug: count 大于 10");
Trace.WriteLineIf(count > 10, "Trace: count 大于 10");
// 跟踪级别
Trace.TraceInformation("信息消息");
Trace.TraceWarning("警告消息");
Trace.TraceError("错误消息");
// 输出到控制台
Console.WriteLine(" 查看输出窗口(Ctrl+Alt+O)查看调试输出\n");
}
// 性能分析
static void DemonstratePerformance()
{
Console.WriteLine("4. 性能分析演示");
Stopwatch stopwatch = new Stopwatch();
// 方法1:使用 Stopwatch
stopwatch.Start();
Thread.Sleep(100); // 模拟工作
stopwatch.Stop();
Console.WriteLine($" Stopwatch: {stopwatch.ElapsedMilliseconds}ms");
// 方法2:使用 DateTime(精度较低)
var start = DateTime.Now;
Thread.Sleep(100);
var end = DateTime.Now;
Console.WriteLine($" DateTime: {(end - start).TotalMilliseconds}ms");
// 方法3:使用诊断监听器
Trace.WriteLine($" 执行时间: {stopwatch.ElapsedMilliseconds}ms");
}
// 内存调试
static void DemonstrateMemoryDebug()
{
Console.WriteLine("5. 内存调试演示");
List<byte[]> memoryHog = new List<byte[]>();
for (int i = 0; i < 10; i++)
{
// 分配 1MB 内存
memoryHog.Add(new byte[1024 * 1024]);
// 在调试器中可以查看内存使用情况
Console.WriteLine($" 分配了 {i + 1} MB 内存");
Thread.Sleep(100);
}
memoryHog.Clear();
// 强制垃圾回收(调试用)
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(" 内存已释放\n");
}
static void ProcessItem(int i)
{
// 模拟处理
Thread.Sleep(1);
}
}
// 附加调试技巧示例
class DebuggerAttributes
{
// 使用 DebuggerDisplay 自定义调试显示
[DebuggerDisplay("Person: {Name}, Age: {Age}")]
class Person
{
public string Name { get; set; }
public int Age { get; set; }
// 调试时不会显示这个方法
[DebuggerNonUserCode]
public void InternalMethod()
{
// 这个方法在调试时会跳过
}
// 调试时步进此方法
[DebuggerStepThrough]
public void SimpleMethod()
{
// 单步调试时不会进入这个方法
}
// 隐藏属性
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string secret = "hidden";
}
static void DemonstrateAttributes()
{
var person = new Person { Name = "张三", Age = 25 };
Console.WriteLine("查看调试器中的 Person 显示效果");
}
}
12.9 常见错误与陷阱
错误1:吞噬异常
csharp
// ❌ 错误:吞噬异常,隐藏问题
try
{
riskyOperation();
}
catch (Exception)
{
// 什么都不做
}
// ✅ 正确:至少记录日志
try
{
riskyOperation();
}
catch (Exception ex)
{
LogError(ex);
throw; // 或重新抛出
}
错误2:使用 throw ex 丢失堆栈
csharp
// ❌ 错误:丢失原始堆栈信息
try
{
throw new Exception("原始异常");
}
catch (Exception ex)
{
throw ex; // 堆栈信息从这里开始
}
// ✅ 正确:保持堆栈
try
{
throw new Exception("原始异常");
}
catch (Exception)
{
throw; // 保持原始堆栈
}
错误3:在 finally 中抛出异常
csharp
// ❌ 错误:finally 中抛出会覆盖原始异常
try
{
throw new Exception("原始错误");
}
catch (Exception ex)
{
Console.WriteLine($"捕获:{ex.Message}");
}
finally
{
throw new Exception("finally 中的错误"); // 覆盖了原始异常
}
// ✅ 正确:记录日志但不抛出
try
{
throw new Exception("原始错误");
}
catch (Exception ex)
{
Console.WriteLine($"捕获:{ex.Message}");
}
finally
{
try
{
Cleanup(); // 可能抛异常的操作
}
catch (Exception cleanupEx)
{
LogError(cleanupEx); // 记录但不抛出
}
}
错误4:空的 catch
csharp
// ❌ 错误:捕获所有异常但不处理
try
{
riskyOperation();
}
catch
{
// 空的 catch 会捕获所有异常,包括 StackOverflowException 等
}
12.10 本章总结
核心知识点导图
text
异常处理与调试
├── 异常处理
│ ├── try-catch-finally
│ ├── 异常类型体系
│ ├── 抛出异常(throw)
│ └── 自定义异常
│
├── 最佳实践
│ ├── 只捕获能处理的异常
│ ├── 使用 using 释放资源
│ ├── 避免吞噬异常
│ └── 保持堆栈信息
│
└── 调试技巧
├── 断点(条件、跟踪点)
├── 监视变量
├── 诊断输出
├── 断言
├── 性能分析
└── 日志记录
异常处理决策树
text
发生异常
│
▼
这个异常能处理吗?
┌─────────┴─────────┐
│ │
否 是
│ │
▼ ▼
重新抛出 处理后继续
(throw / throw ex)
│ │
▼ ▼
上层捕获 正常流程
12.11 练习题
基础题
-
编写一个程序,处理用户输入的数字,捕获
FormatException和OverflowException。 -
创建一个自定义异常
InvalidAgeException,在年龄不在 0-150 范围时抛出。 -
使用
try-catch-finally模拟文件读写,确保文件总是被关闭。
应用题
-
实现一个带重试机制的操作方法:
-
最多重试 3 次
-
每次失败后等待 1 秒再重试
-
记录每次失败的原因
-
-
实现一个全局异常处理器,捕获未处理的异常并记录到日志文件。
挑战题
-
实现一个简单的断言框架:
-
Assert.IsTrue() -
Assert.AreEqual() -
Assert.IsNotNull() -
断言失败时抛出
AssertionException
-
-
实现一个性能分析器:
-
MeasureTime方法,测量代码执行时间 -
支持多次运行取平均值
-
输出详细的性能报告
-