MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的异常处理机制与try-catch语句

2023-10-116.2k 阅读

C# 异常处理机制概述

在程序运行过程中,难免会出现各种意外情况,比如用户输入不合法的数据、文件不存在、网络连接中断等。这些情况如果不加以妥善处理,可能会导致程序崩溃,给用户带来不好的体验。C# 提供了一套完善的异常处理机制,帮助开发者优雅地应对这些意外情况,确保程序的稳定性和健壮性。

异常是指在程序执行过程中出现的错误或意外情况。当异常发生时,正常的程序执行流程会被中断,系统会寻找相应的异常处理代码来处理这个异常。如果没有找到合适的异常处理代码,程序就会终止,并向用户显示错误信息。

C# 的异常处理机制基于面向对象的思想,所有的异常都继承自 System.Exception 类。这个类提供了一些基本的属性和方法,用于获取异常的相关信息,如异常消息、堆栈跟踪等。开发者可以根据具体的需求,自定义继承自 System.Exception 的异常类,以便更好地处理特定类型的异常。

try - catch 语句基础

try 块

try 块是异常处理的核心部分,它包含了可能会抛出异常的代码。语法如下:

try
{
    // 可能抛出异常的代码
}

try 块内的代码执行时,如果没有发生异常,程序会正常执行完 try 块内的所有语句,然后跳过 catch 块(如果有的话),继续执行后续的代码。例如:

try
{
    int result = 10 / 2;
    Console.WriteLine("计算结果: " + result);
}

在这个简单的例子中,10 / 2 不会抛出异常,所以程序会顺利输出 “计算结果: 5”,并继续执行 try 块之后的代码(如果有的话)。

catch 块

catch 块用于捕获并处理 try 块中抛出的异常。它必须紧跟在 try 块之后,语法有几种不同的形式:

  1. 捕获所有异常
try
{
    // 可能抛出异常的代码
}
catch
{
    // 处理异常的代码
}

这种形式的 catch 块会捕获任何类型的异常。例如:

try
{
    int[] numbers = { 1, 2, 3 };
    Console.WriteLine(numbers[3]); // 访问越界,会抛出异常
}
catch
{
    Console.WriteLine("发生了异常");
}

这里,由于访问数组越界,会抛出 IndexOutOfRangeException 异常,catch 块捕获到该异常并输出 “发生了异常”。

  1. 捕获特定类型的异常
try
{
    // 可能抛出异常的代码
}
catch (SpecificExceptionType ex)
{
    // 处理 SpecificExceptionType 类型异常的代码
    Console.WriteLine("异常信息: " + ex.Message);
}

这种形式只捕获指定类型的异常,例如 DivideByZeroException

try
{
    int result = 10 / 0; // 除以零,会抛出 DivideByZeroException
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("捕获到除以零异常: " + ex.Message);
}

try 块中抛出 DivideByZeroException 异常时,该 catch 块会捕获到并输出异常信息 “试图除以零”。

  1. 多个 catch 块: 可以有多个 catch 块,每个 catch 块捕获不同类型的异常。例如:
try
{
    int[] numbers = { 1, 2, 3 };
    int result = 10 / 0;
    Console.WriteLine(numbers[3]);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("捕获到除以零异常: " + ex.Message);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine("捕获到索引越界异常: " + ex.Message);
}

在这个例子中,try 块中有两个可能抛出异常的操作。如果先发生 DivideByZeroException,第一个 catch 块会捕获并处理;如果没有发生 DivideByZeroException,而发生了 IndexOutOfRangeException,则第二个 catch 块会捕获并处理。需要注意的是,多个 catch 块的顺序很重要,子类异常的 catch 块应该放在父类异常的 catch 块之前,否则会出现编译错误,因为子类异常会被父类异常的 catch 块捕获,导致子类异常的 catch 块永远不会被执行。例如:

try
{
    // 代码
}
catch (FormatException ex) // 子类异常
{
    // 处理代码
}
catch (Exception ex) // 父类异常
{
    // 处理代码
}

这样的顺序是正确的。如果反过来:

try
{
    // 代码
}
catch (Exception ex) // 父类异常
{
    // 处理代码
}
catch (FormatException ex) // 子类异常
{
    // 处理代码
}

就会出现编译错误,提示 “无法访问的代码”,因为 FormatException 作为 Exception 的子类,会被第一个 catch 块捕获。

异常类的属性和方法

System.Exception 类提供了一些重要的属性和方法,帮助开发者获取关于异常的详细信息,以便更好地处理异常。

Message 属性

Message 属性返回一个描述异常的字符串,这个字符串通常包含了异常发生的原因。例如:

try
{
    int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("异常消息: " + ex.Message);
}

输出结果为 “异常消息: 试图除以零”,通过这个消息,开发者可以很清楚地知道异常发生的原因。

StackTrace 属性

StackTrace 属性返回一个字符串,包含了异常发生时的堆栈跟踪信息。这个信息对于调试非常有帮助,可以让开发者了解异常发生在程序的哪个位置,以及方法调用的顺序。例如:

try
{
    Method1();
}
catch (Exception ex)
{
    Console.WriteLine("堆栈跟踪: " + ex.StackTrace);
}

static void Method1()
{
    Method2();
}

static void Method2()
{
    int result = 10 / 0;
}

输出的堆栈跟踪信息类似:

在 ConsoleApp1.Program.Method2() 位置 C:\Users\...\Program.cs:行号 15
在 ConsoleApp1.Program.Method1() 位置 C:\Users\...\Program.cs:行号 12
在 ConsoleApp1.Program.Main() 位置 C:\Users\...\Program.cs:行号 7

从这个堆栈跟踪信息可以清晰地看到,异常发生在 Method2 方法中,通过 Method1 方法调用,最终在 Main 方法中被捕获。

InnerException 属性

当一个异常是由另一个异常引起时,InnerException 属性可以用来获取内部异常。例如,在自定义异常中,可能会因为其他异常而抛出自定义异常,这时就可以通过 InnerException 属性来保留原始异常信息。

try
{
    try
    {
        int result = 10 / 0;
    }
    catch (DivideByZeroException ex)
    {
        throw new CustomOperationException("自定义操作异常", ex);
    }
}
catch (CustomOperationException ex)
{
    Console.WriteLine("自定义异常消息: " + ex.Message);
    Console.WriteLine("内部异常消息: " + ex.InnerException.Message);
}

class CustomOperationException : Exception
{
    public CustomOperationException(string message, Exception innerException) : base(message, innerException)
    {
    }
}

在这个例子中,先捕获 DivideByZeroException,然后抛出一个自定义的 CustomOperationException,并将 DivideByZeroException 作为内部异常传递进去。在最外层的 catch 块中,可以通过 InnerException 属性获取到内部的 DivideByZeroException 的消息。

GetBaseException 方法

GetBaseException 方法用于获取异常链中的根异常。在异常处理过程中,可能会发生多个异常嵌套的情况,通过这个方法可以找到最初引发异常的那个根异常。例如:

try
{
    try
    {
        try
        {
            int result = 10 / 0;
        }
        catch (DivideByZeroException ex)
        {
            throw new FormatException("格式异常", ex);
        }
    }
    catch (FormatException ex)
    {
        throw new CustomOperationException("自定义操作异常", ex);
    }
}
catch (CustomOperationException ex)
{
    Exception baseException = ex.GetBaseException();
    Console.WriteLine("根异常消息: " + baseException.Message);
}

class CustomOperationException : Exception
{
    public CustomOperationException(string message, Exception innerException) : base(message, innerException)
    {
    }
}

在这个复杂的异常嵌套例子中,通过 GetBaseException 方法可以获取到最开始的 DivideByZeroException,输出 “根异常消息: 试图除以零”。

finally 块

finally 块是异常处理机制中的另一个重要部分,它可以和 try - catch 语句一起使用。finally 块中的代码无论 try 块中是否发生异常,也无论 catch 块是否捕获到异常,都会被执行。语法如下:

try
{
    // 可能抛出异常的代码
}
catch
{
    // 处理异常的代码
}
finally
{
    // 一定会执行的代码
}

例如:

try
{
    int result = 10 / 2;
    Console.WriteLine("计算结果: " + result);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("捕获到除以零异常: " + ex.Message);
}
finally
{
    Console.WriteLine("finally 块被执行");
}

在这个例子中,由于没有发生除以零的异常,catch 块不会执行,但 finally 块依然会执行,输出 “finally 块被执行”。即使发生异常,finally 块也会执行,例如:

try
{
    int result = 10 / 0;
    Console.WriteLine("计算结果: " + result);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("捕获到除以零异常: " + ex.Message);
}
finally
{
    Console.WriteLine("finally 块被执行");
}

这里发生了除以零的异常,catch 块捕获并处理异常后,finally 块还是会执行,输出 “finally 块被执行”。

finally 块常用于释放资源,比如关闭文件、数据库连接等。例如,在使用文件流时:

StreamReader reader = null;
try
{
    reader = new StreamReader("test.txt");
    string content = reader.ReadToEnd();
    Console.WriteLine("文件内容: " + content);
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("文件未找到异常: " + ex.Message);
}
finally
{
    if (reader != null)
    {
        reader.Close();
    }
}

在这个例子中,try 块中打开一个文件并读取内容。如果文件不存在,会抛出 FileNotFoundExceptioncatch 块捕获并处理该异常。无论是否发生异常,finally 块都会关闭文件流,确保资源被正确释放。

自定义异常

在实际开发中,System.Exception 类及其派生的标准异常类可能无法满足所有的需求。这时,开发者可以自定义异常类,以便更好地处理特定业务逻辑中的异常情况。

自定义异常类需要继承自 System.Exception 类或它的某个派生类。通常,自定义异常类会添加一些特定的属性和方法,以提供更详细的异常信息。例如,定义一个表示用户注册失败的自定义异常类:

public class UserRegistrationException : Exception
{
    public string UserName { get; set; }
    public UserRegistrationException(string message, string userName) : base(message)
    {
        UserName = userName;
    }
}

在这个自定义异常类中,添加了一个 UserName 属性,用于表示注册失败的用户名。构造函数接受一个异常消息和用户名作为参数,并调用基类的构造函数来设置异常消息。

使用自定义异常的示例:

try
{
    string userName = "testUser";
    if (userName.Length < 3)
    {
        throw new UserRegistrationException("用户名长度不能小于 3", userName);
    }
    Console.WriteLine("用户注册成功: " + userName);
}
catch (UserRegistrationException ex)
{
    Console.WriteLine("用户注册失败: " + ex.Message);
    Console.WriteLine("用户名: " + ex.UserName);
}

在这个例子中,如果用户名长度小于 3,就抛出 UserRegistrationException 异常。catch 块捕获到该异常后,可以获取到详细的异常信息和用户名,以便进行相应的处理。

自定义异常类还可以提供一些特定的方法,例如:

public class FileProcessingException : Exception
{
    public string FilePath { get; set; }
    public FileProcessingException(string message, string filePath) : base(message)
    {
        FilePath = filePath;
    }

    public void LogException()
    {
        Console.WriteLine($"文件处理异常: {Message},文件路径: {FilePath}");
    }
}

这里的 FileProcessingException 类添加了一个 LogException 方法,用于记录异常信息和文件路径。使用示例如下:

try
{
    string filePath = "nonexistentFile.txt";
    // 假设这里有处理文件的代码,可能会抛出异常
    if (!File.Exists(filePath))
    {
        throw new FileProcessingException("文件不存在", filePath);
    }
}
catch (FileProcessingException ex)
{
    ex.LogException();
}

通过自定义异常类及其特定的属性和方法,开发者可以更精确地处理业务逻辑中的异常情况,提高程序的可读性和可维护性。

异常处理的最佳实践

捕获特定异常

尽量捕获特定类型的异常,而不是使用通用的 catch 块捕获所有异常。捕获特定异常可以让开发者更准确地了解异常的类型,并采取针对性的处理措施。例如:

try
{
    int result = int.Parse("abc");
}
catch (FormatException ex)
{
    Console.WriteLine("输入格式错误: " + ex.Message);
}

这样可以清晰地处理输入格式错误的情况,而不是捕获所有异常导致无法区分异常类型。

合理使用 finally 块释放资源

如前面提到的,finally 块常用于释放资源,如文件流、数据库连接等。确保在使用资源的代码周围使用 try - finallytry - catch - finally 结构,以保证资源无论是否发生异常都能被正确释放。例如:

SqlConnection connection = null;
try
{
    connection = new SqlConnection("connectionString");
    connection.Open();
    // 执行数据库操作
}
catch (SqlException ex)
{
    Console.WriteLine("数据库异常: " + ex.Message);
}
finally
{
    if (connection != null)
    {
        connection.Close();
    }
}

避免过度捕获异常

不要在不必要的地方捕获异常,以免隐藏真正的问题。例如,在一个方法内部,如果该方法本身没有处理异常的逻辑,只是简单地捕获异常然后重新抛出,这种做法可能会掩盖异常发生的真正位置和原因。正确的做法是让异常自然传播,直到有合适的地方进行处理。例如:

// 不好的做法
public void Method1()
{
    try
    {
        Method2();
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

public void Method2()
{
    int result = 10 / 0;
}

// 好的做法
public void Method1()
{
    Method2();
}

public void Method2()
{
    int result = 10 / 0;
}

在好的做法中,异常会直接从 Method2 传播到调用 Method1 的地方,这样更容易定位异常发生的位置。

提供详细的异常信息

在抛出异常时,尽量提供详细的异常信息,包括异常发生的原因、相关的数据等。这样在处理异常时,开发者可以更好地了解问题所在,从而更快地解决问题。例如:

public void ValidateUserAge(int age)
{
    if (age < 18)
    {
        throw new ArgumentException("用户年龄必须大于等于 18", nameof(age));
    }
}

这里通过 ArgumentException 的构造函数提供了详细的异常消息和参数名称,方便调用者了解异常原因。

异常处理与性能

虽然异常处理机制对于程序的健壮性很重要,但频繁地使用异常处理可能会对性能产生一定的影响。异常处理涉及到额外的堆栈操作和对象创建等开销。因此,在性能敏感的代码中,应该尽量避免使用异常来控制正常的程序流程,而应该使用条件判断等方式。例如,在读取文件时,先判断文件是否存在再进行读取操作,而不是依赖异常处理来捕获文件不存在的情况。

// 好的做法
string filePath = "test.txt";
if (File.Exists(filePath))
{
    using (StreamReader reader = new StreamReader(filePath))
    {
        string content = reader.ReadToEnd();
        Console.WriteLine("文件内容: " + content);
    }
}
else
{
    Console.WriteLine("文件不存在");
}

// 不好的做法
try
{
    using (StreamReader reader = new StreamReader("nonexistentFile.txt"))
    {
        string content = reader.ReadToEnd();
        Console.WriteLine("文件内容: " + content);
    }
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("文件不存在异常: " + ex.Message);
}

在好的做法中,通过条件判断避免了不必要的异常处理,提高了性能。

异常的传播

当一个方法中发生异常,而该方法没有处理这个异常时,异常会向上传播到调用该方法的方法。这个过程会一直持续,直到找到一个能够处理该异常的 catch 块,或者异常传播到程序的入口点(如 Main 方法)仍未被处理,这时程序就会终止,并显示异常信息。

例如:

class Program
{
    static void Main()
    {
        try
        {
            Method1();
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("在 Main 方法中捕获到异常: " + ex.Message);
        }
    }

    static void Method1()
    {
        Method2();
    }

    static void Method2()
    {
        int result = 10 / 0;
    }
}

在这个例子中,Method2 中发生了 DivideByZeroException 异常,由于 Method2 没有处理这个异常,异常会传播到 Method1Method1 也没有处理,继续传播到 Main 方法。在 Main 方法中,catch 块捕获到了这个异常并进行处理,输出 “在 Main 方法中捕获到异常: 试图除以零”。

异常传播的好处是可以将异常处理的逻辑集中在合适的地方,而不是在每个可能抛出异常的方法中都进行处理。这样可以提高代码的可读性和维护性。例如,在一个大型的应用程序中,底层的数据访问层方法可能会抛出各种数据库相关的异常,这些异常可以向上传播到业务逻辑层或表示层,在那里进行统一的处理,根据不同的异常类型向用户显示友好的错误信息。

异步编程中的异常处理

在 C# 的异步编程中,异常处理有一些特殊之处。当在异步方法中抛出异常时,异常不会立即被捕获,而是被封装在 Task 对象中。当等待这个 Task 时,异常会被重新抛出。

例如:

class Program
{
    static async Task Main()
    {
        try
        {
            await Method1();
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("捕获到异常: " + ex.Message);
        }
    }

    static async Task Method1()
    {
        await Method2();
    }

    static async Task Method2()
    {
        int result = 10 / 0;
    }
}

在这个异步代码示例中,Method2 中抛出 DivideByZeroException 异常,该异常被封装在 Method2 返回的 Task 中。当 Method1 等待 Method2 返回的 Task 时,异常继续传播,直到 Main 方法中等待 Method1 返回的 Task 时,异常被重新抛出并被 catch 块捕获,输出 “捕获到异常: 试图除以零”。

如果异步方法返回的 Task 没有被等待,异常不会被立即处理,可能会导致程序在后台抛出未处理的异常,这可能会使程序出现难以调试的问题。为了避免这种情况,可以使用 ConfigureAwait(false) 来优化异步操作,并在适当的地方捕获异常。例如:

class Program
{
    static async Task Main()
    {
        try
        {
            await Method1().ConfigureAwait(false);
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("捕获到异常: " + ex.Message);
        }
    }

    static async Task Method1()
    {
        await Method2().ConfigureAwait(false);
    }

    static async Task Method2()
    {
        int result = 10 / 0;
    }
}

ConfigureAwait(false) 可以避免将上下文切换回原始的同步上下文,从而提高性能,同时也能确保异常在合适的地方被捕获。

另外,在处理多个异步任务时,例如使用 Task.WhenAllTask.WhenAny,异常处理也有不同的方式。对于 Task.WhenAll,如果任何一个任务抛出异常,Task.WhenAll 返回的任务也会失败,并抛出第一个发生的异常。例如:

class Program
{
    static async Task Main()
    {
        try
        {
            await Task.WhenAll(Method1(), Method2());
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("捕获到异常: " + ex.Message);
        }
    }

    static async Task Method1()
    {
        await Task.Delay(1000);
        int result = 10 / 0;
    }

    static async Task Method2()
    {
        await Task.Delay(2000);
        Console.WriteLine("Method2 完成");
    }
}

在这个例子中,Method1 抛出 DivideByZeroException 异常,Task.WhenAll 返回的任务失败并抛出该异常,被 catch 块捕获。

对于 Task.WhenAny,当任何一个任务完成或失败时,Task.WhenAny 返回的任务就会完成。如果完成的任务抛出异常,需要手动检查并处理异常。例如:

class Program
{
    static async Task Main()
    {
        Task[] tasks = { Method1(), Method2() };
        Task completedTask = await Task.WhenAny(tasks);
        try
        {
            await completedTask;
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("捕获到异常: " + ex.Message);
        }
    }

    static async Task Method1()
    {
        await Task.Delay(1000);
        int result = 10 / 0;
    }

    static async Task Method2()
    {
        await Task.Delay(2000);
        Console.WriteLine("Method2 完成");
    }
}

在这个例子中,Task.WhenAny 返回最先完成的任务(这里是 Method1 抛出异常的任务),然后通过 await completedTask 来检查并处理异常。

通过正确处理异步编程中的异常,可以确保异步应用程序的稳定性和健壮性,避免未处理的异常导致程序崩溃。

异常处理与单元测试

在进行单元测试时,异常处理也是一个重要的方面。单元测试应该验证方法在正常情况下的行为,同时也要验证在异常情况下的行为是否符合预期。

例如,对于一个可能抛出异常的方法 DivideNumbers

public class MathOperations
{
    public int DivideNumbers(int a, int b)
    {
        if (b == 0)
        {
            throw new DivideByZeroException();
        }
        return a / b;
    }
}

在单元测试中,可以使用 Assert.Throws 方法来验证当传入特定参数时是否会抛出预期的异常。例如,使用 MSTest 框架:

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class MathOperationsTests
{
    [TestMethod]
    public void DivideNumbers_WhenDivisorIsZero_ShouldThrowDivideByZeroException()
    {
        MathOperations mathOperations = new MathOperations();
        Assert.ThrowsException<DivideByZeroException>(() => mathOperations.DivideNumbers(10, 0));
    }
}

在这个单元测试中,Assert.ThrowsException 方法验证 DivideNumbers 方法在传入除数为 0 时是否会抛出 DivideByZeroException。如果方法没有抛出预期的异常,单元测试将会失败。

另外,对于方法正常执行时的情况,也应该进行测试,例如:

[TestMethod]
public void DivideNumbers_WhenDivisorIsNotZero_ShouldReturnCorrectResult()
{
    MathOperations mathOperations = new MathOperations();
    int result = mathOperations.DivideNumbers(10, 2);
    Assert.AreEqual(5, result);
}

通过这样的单元测试,可以确保方法在正常和异常情况下的行为都符合预期,从而提高代码的质量和可靠性。

在单元测试中处理自定义异常时,同样可以使用 Assert.ThrowsException 方法。例如,对于前面定义的 UserRegistrationException

public class UserService
{
    public void RegisterUser(string userName)
    {
        if (userName.Length < 3)
        {
            throw new UserRegistrationException("用户名长度不能小于 3", userName);
        }
        // 注册用户的逻辑
    }
}

[TestClass]
public class UserServiceTests
{
    [TestMethod]
    public void RegisterUser_WhenUserNameIsTooShort_ShouldThrowUserRegistrationException()
    {
        UserService userService = new UserService();
        Assert.ThrowsException<UserRegistrationException>(() => userService.RegisterUser("ab"));
    }
}

这样可以验证自定义异常在特定条件下是否会被正确抛出,保证业务逻辑的正确性。

通过合理的单元测试,可以有效地发现异常处理代码中的问题,确保程序在面对各种异常情况时都能正确运行。

异常处理在不同应用场景中的应用

控制台应用程序

在控制台应用程序中,异常处理主要用于捕获可能发生的异常,并向用户提供友好的错误信息。例如,在一个简单的控制台计算器程序中:

class Program
{
    static void Main()
    {
        try
        {
            Console.WriteLine("请输入第一个数字:");
            double num1 = double.Parse(Console.ReadLine());
            Console.WriteLine("请输入运算符:");
            string op = Console.ReadLine();
            Console.WriteLine("请输入第二个数字:");
            double num2 = double.Parse(Console.ReadLine());

            double result;
            switch (op)
            {
                case "+":
                    result = num1 + num2;
                    break;
                case "-":
                    result = num1 - num2;
                    break;
                case "*":
                    result = num1 * num2;
                    break;
                case "/":
                    result = num1 / num2;
                    break;
                default:
                    throw new ArgumentException("无效的运算符");
            }

            Console.WriteLine("计算结果: " + result);
        }
        catch (FormatException ex)
        {
            Console.WriteLine("输入格式错误: " + ex.Message);
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("不能除以零: " + ex.Message);
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine("参数错误: " + ex.Message);
        }
    }
}

在这个控制台应用程序中,通过 try - catch 语句捕获用户输入可能导致的 FormatException(输入不是有效的数字格式)、DivideByZeroException(除数为零)以及自定义的 ArgumentException(无效的运算符),并向用户显示相应的错误信息,提高程序的健壮性和用户体验。

Windows 桌面应用程序

在 Windows 桌面应用程序(如 WinForms 或 WPF 应用程序)中,异常处理同样重要。通常,异常处理会与用户界面的交互相结合。例如,在一个 WinForms 应用程序中,有一个按钮用于读取文件内容并显示在文本框中:

using System;
using System.IO;
using System.Windows.Forms;

namespace WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                using (StreamReader reader = new StreamReader("test.txt"))
                {
                    textBox1.Text = reader.ReadToEnd();
                }
            }
            catch (FileNotFoundException ex)
            {
                MessageBox.Show("文件未找到: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (IOException ex)
            {
                MessageBox.Show("读取文件时发生错误: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}

在这个例子中,当用户点击按钮时,程序尝试读取文件内容。如果文件未找到,FileNotFoundException 异常被捕获,并通过 MessageBox 向用户显示错误信息。如果在读取文件过程中发生其他 I/O 错误,IOException 异常被捕获并同样显示错误信息。这样可以确保用户在使用应用程序时遇到问题时能得到明确的提示,而不是程序崩溃。

Web 应用程序

在 Web 应用程序(如 ASP.NET Core 应用程序)中,异常处理需要考虑到 HTTP 响应和用户体验。ASP.NET Core 提供了全局异常处理机制,可以统一处理应用程序中发生的异常,并返回合适的 HTTP 响应。

例如,在 ASP.NET Core 应用程序中,可以通过注册全局异常处理中间件来处理异常:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Net;
using System.Text.Json;

public class GlobalExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public GlobalExceptionHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            var errorResponse = new
            {
                StatusCode = context.Response.StatusCode,
                Message = "发生了未处理的异常",
                Details = ex.Message
            };

            await context.Response.WriteAsync(JsonSerializer.Serialize(errorResponse));
        }
    }
}

public static class GlobalExceptionHandlerMiddlewareExtensions
{
    public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<GlobalExceptionHandlerMiddleware>();
    }
}

然后在 Startup.cs 中注册该中间件:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace WebApplication
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            // 配置服务
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseGlobalExceptionHandler();
            // 其他中间件配置
        }
    }
}

这样,当应用程序中发生未处理的异常时,全局异常处理中间件会捕获异常,并返回一个包含错误信息的 JSON 格式的 HTTP 响应,状态码为 500(Internal Server Error)。同时,还可以根据不同类型的异常返回不同的 HTTP 状态码和错误信息,以提供更友好的用户体验和更好的调试信息。例如,对于 ArgumentException 可以返回 400(Bad Request)状态码,并提供更具体的错误描述。

在 Web 应用程序中,还可以在控制器级别处理异常。例如,在 ASP.NET Core 的控制器中:

using Microsoft.AspNetCore.Mvc;

public class HomeController : Controller
{
    public IActionResult Index()
    {
        try
        {
            // 可能抛出异常的代码
            int result = 10 / 0;
            return View();
        }
        catch (DivideByZeroException ex)
        {
            return BadRequest($"发生错误: {ex.Message}");
        }
    }
}

在这个控制器方法中,try - catch 语句捕获 DivideByZeroException 异常,并返回一个 HTTP 400(Bad Request)的响应,向客户端传达错误信息。

通过合理的异常处理,Web 应用程序可以提供稳定的服务,避免因异常导致的页面崩溃或无响应情况,提高用户满意度。

在不同的应用场景中,根据其特点和需求,合理地运用异常处理机制,可以确保程序的可靠性、稳定性和用户体验。无论是控制台应用程序、桌面应用程序还是 Web 应用程序,都需要认真对待异常处理,以构建高质量的软件。

通过以上对 C# 中异常处理机制与 try - catch 语句的详细介绍,开发者可以更好地掌握如何在 C# 程序中有效地处理异常,提高程序的健壮性和可靠性,从而开发出更稳定、更优秀的软件应用。从异常的基本概念、try - catch 语句的使用,到异常类的属性方法、自定义异常、异常处理的最佳实践以及在不同应用场景中的应用等方面,全面深入地了解了 C# 异常处理的各个环节,有助于在实际开发中更好地应对各种异常情况。