异常
当一个程序遇到各种各样的问题而无法正常运行时,我们需要知道导致问题的原因,
并根据原因来解决问题。
一种常见的方式是程序给出一串错误码,然后开发人员查找对应的错误信息。
而在c#中,我们可以在程序内部就处理异常,而不需要用字符串来显示错误信息。
我们只需要使用异常类来创建和抛出异常对象就行了。
异常类
异常是一个表示程序运行时出现的错误或者意外情况的对象。异常对象包含了一些有关错误的信息,
例如错误的类型,错误的消息,错误发生的位置,错误的原因等。
异常对象都继承或间接继承自Exception
类。作为所有异常基类的Exception
,
它提供了一些通用的属性和方法来获取和处理异常对象。
Message
属性:获取描述异常的消息。Source
属性:获取或设置导致异常的应用程序或对象的名称。StackTrace
属性:获取调用堆栈上的即时框架。InnerException
属性:获取导致当前异常的Exception实例。ToString
方法:创建并返回当前异常的字符串表示形式。GetBaseException
方法:返回一个Exception,它是一个或多个并发的异常的根本原因。
自定义异常
如果我们想要创建一个自己的异常类,我们需要继承或间接继承Exception
类,
并且提供以下几个构造器:
- 一个无参构造器,调用基类的无参构造器。
- 一个带有字符串参数的构造器,调用基类的带有字符串参数的构造器,并将字符串作为异常消息传递给基类。
- 一个带有字符串参数和
Exception
参数的构造器,调用基类的带有字符串参数和Exception参数的构造器,并将字符串作为异常消息,Exception
作为内部异常传递给基类。 - 一个带有
SerializationInfo
参数和StreamingContext
参数的构造器,调用基类的带有SerializationInfo
参数和StreamingContext
参数的构造器,并将序列化信息和流上下文传递给基类。
例如,我们可以创建一个分数异常类来表示分数异常:
using System;
using System.Runtime.Serialization;
[Serializable]
class ScoreException : Exception
{
public ScoreException() : base() { }
public ScoreException(string message) : base(message) { }
public ScoreException(string message, Exception inner) : base(message, inner) { }
protected ScoreException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
注意我们需要给自定义异常类加上
[Serializable]
特性,表示这个类可以被序列化,
否则在序列化的过程中遇到这个异常,会被认为在程序中出现了异常。
抛出异常
有时候,我们需要主动地抛出一个异常对象,来表示程序中发生了某种错误或者不符合预期的情况。
这样可以让上层代码知道问题的原因,并且可以选择合适的方式来处理异常。
抛出异常使用throw
关键字加上一个异常对象来实现。
可以用new
创建一个新的异常对象,也可以使用已经存在的变量。
不使用抛出异常时,异常就是一个普通的值。
class Student
{
int score;
public int Score
{
get => score;
set
{
if (value < 0)
throw new ScoreException("分数不能是负数");
score = value;
}
}
}
try
{
Student st = new Student();
st.Score = -3;
}
catch (ScoreException e)
{
Console.WriteLine(e.Message);
}
代码终止
抛出异常语句会终止当前方法的执行,并将异常对象传递给上层方法。
如果上层代码块没有处理这个异常,它也会终止执行并继续传递异常对象。
直到Main
方法终止导致程序结束,或者有代码块处理了这个异常为止。
抛出异常语句也具有不可达代码检测的效果。抛出异常语句后面的代码不会被执行。
在有返回值的方法中使用抛出异常来终止方法,不需要返回值。
抛出异常语句可以放在三元运算符或switch
表达式中当作一个值使用。
public int Score
{
get => score;
set => score = value < 0 ? throw new ScoreException("分数不能是负数") : value;
}
如果抛出null
的话,那么你会得到一个空引用异常System.NullReferenceException
。
好的,我按照你的要求,把文章细分为四个小节,并且修改了示例代码。请看下面的内容:
捕获异常
有时候,我们需要捕获并处理一个或多个异常对象,来避免程序崩溃。
我们也可以根据异常对象中包含的信息来进行调试和排错。
对于可能引发异常的代码,我们可以使用try-catch
语句块来捕获异常。
try-catch
语句块不能省略大括号,并且大括号也有作用域限制。
所以通常来说,需要在try
之前声明变量,才能在try
和catch
中都能访问到。
在try
中,如果触发了异常,那么try
块会终止运行,转而运行catch
块。
每个catch
块后的括号内,是要捕获的异常类型。
每个catch
会和触发的异常进行一次类型判断比较,
如果触发的异常是指定类型或由它派生,则匹配成功。
try
{
int x = 10;
int y = 0;
int z = x / y; //触发除零异常
}
catch (ArithmeticException) //只捕获算术异常
{
throw; //在catch块中抛出异常可以不指示实例,这会把捕获到的异常抛出
}
处理异常
如果我们想处理捕获到的异常对象,我们可以在每个catch
块后的括号内声明一个变量。
如果捕获成功,那么这个变量就会赋值为这个异常对象。
然后我们就可以根据这个变量的属性和方法来处理异常。
try
{
int x = 10;
int y = 0;
int z = x / y; //触发除零异常
}
catch (Exception e) //声明变量e,并捕获所有类型的异常
{
Console.WriteLine("发生了一个异常:");
Console.WriteLine("类型:" + e.GetType());
Console.WriteLine("消息:" + e.Message);
Console.WriteLine("源:" + e.Source);
Console.WriteLine("堆栈跟踪:" + e.StackTrace);
}
多级判断
有时候,我们需要根据不同类型的异常来执行不同的处理逻辑。
这时候,我们可以使用多级判断来精确捕获异常。
多级判断是指在一个try
块后跟多个catch
块,并且每个catch
块指定不同类型的异常。
它们像if-else if-else
一样。上一个catch
没有捕获到的异常会由下一个catch
块尝试捕获和处理。
直到有一个catch
块捕获到,或所有的catch
块都没有捕获到。
最后一个catch
块可以不指定任何类型,这表示捕获所有类型的异常,就像没有if
的else
一样。
这个catch
块通常用来处理未知或意外的异常。
try
{
//可能引发多种异常的代码
}
catch (FileNotFoundException e)
{
//处理文件不存在的情况
}
catch (IOException e)
{
//处理其他输入输出异常的情况
}
catch
{
//处理其他未知异常的情况
}
次要判断
有时候,我们需要根据不同条件的异常来执行不同的处理逻辑。
这时候,我们可以使用次要判断来精确捕获异常。
次要判断是指在一个catch
块中,根据异常对象的属性或方法来进行进一步的判断。
这样就可以根据触发的异常对象的具体信息来执行更细致的处理逻辑。
在C#中,我们可以使用when
关键字来添加一个布尔表达式作为次要判断的条件。
只有当这个条件为真时,才会进入这个catch
块。
try
{
//可能引发多种异常的代码
}
catch (Exception e) when (e.Message.Contains("文件"))
{
//处理与文件相关的异常
}
catch (Exception e) when (e.Message.Contains("网络"))
{
//处理与网络相关的异常
}
catch (Exception e)
{
//处理其他未知异常
}
finally块
finally
块是用于执行一些必须或总是要执行的操作,无论是否发生异常。这些操作通常是一些收尾工作,例如关闭一些打开的文件或网络连接,释放一些占用的资源,恢复一些设置等。
finally
块需要配合try
块使用。
try
块中的代码可能会因为异常而中断不运行,导致一些操作没有完成。
catch
块中的代码可能因为没有或没匹配到异常而不运行,导致一些操作没有执行。
为了保证一些必经操作能够执行,我们可以使用finally
块来进行收尾。
无论是正常运行完毕try块,还是由catch块
捕获异常,都会执行finally
块。
finally
块可以直接try
块后面,不是一定需要catch
块。
使用finally收尾
在finally块中,我们可以写一些必须执行的操作,无论是否发生异常。
例如,我们可以在读取文件后,无论是否发生异常,都要关闭文件流。
这样可以避免文件被锁住,无法被其他程序访问。
FileStream? fs = null; //因为作用域限制,必须在try之前声明才能让他们都能访问
try
{
fs = new FileStream("test.txt", FileMode.Open);
//读取文件内容的代码
}
catch (FileNotFoundException e)
{
Console.WriteLine("文件不存在:" + e.Message);
}
catch (IOException e)
{
Console.WriteLine("输入输出异常:" + e.Message);
}
finally
{
fs?.Close(); //关闭文件流
}
finally块一定执行
finally
块一定会被执行,即使在try
或catch
中有return
语句,或是在catch
中也触发了异常。
Console.WriteLine(ReadFile("test.txt"));
string ReadFile(string path)
{
FileStream? fs = null;
try
{
fs = new FileStream(path, FileMode.Open);
//读取文件内容的代码
return "文件读取成功";
}
catch (FileNotFoundException e)
{
Console.WriteLine("文件不存在:" + e.Message);
return "文件读取失败";
}
catch (IOException e)
{
Console.WriteLine("输入输出异常:" + e.Message);
return "文件读取失败";
}
finally
{
fs?.Close(); //关闭文件流
Console.WriteLine("finally还是会执行");
}
}
在这个代码中,无论是try还是catch中,都有return语句来提前结束方法。
但是无论哪种情况,都会执行finally块中的关闭文件流的操作。
这样就可以避免文件被锁住,无法被其他程序访问。
好的,我按照你的要求,修改了using语句的内容。请看下面的内容:
using语句
有些类实现了IDisposable
接口,表示它们可以被释放或关闭。例如文件流或网络流等。
这些类通常有一个Dispose
方法来释放占用的资源。
但是如果我们没有调用这些方法,那么资源就会被浪费或占用过久。
为了避免这种情况,我们可以使用using语句来自动调用Dispose
方法。
使用using语句必须在声明变量时进行,声明的变量必须实现IDisposable
接口。
并且必须是单独的声明语句,
不能是模式匹配的声明模式,析构元组,捕获异常,out参数声明赋值的变量。
使用using语句的好处是可以简化代码,避免忘记调用Dispose
方法,
并且可以保证即使发生异常也能正确释放资源。
using System.Net;
using var wc = new WebClient();
//访问微软文档的官网(中文)
var html = wc.DownloadString("https://docs.microsoft.com/zh-cn/");
Console.WriteLine(html);
这段代码等效于:
var wc = new WebClient();
try
{
//访问微软文档的官网(中文)
var html = wc.DownloadString("https://docs.microsoft.com/zh-cn/");
Console.WriteLine(html);
}
finally
{
wc.Dispose(); //在离开作用域时调用Dispose方法
}
在这个代码中,我们使用using语句来声明和初始化一个WebClient对象。
在using语句的作用域内,我们可以正常使用wc对象。
当离开using语句的作用域时,wc对象会自动调用Dispose方法来关闭网络连接。
非托管资源
.NET会对一些资源进行分配,监管,回收,释放的机制,这种资源称为托管资源。
但是对于非托管资源(即由操作系统或其他程序分配和管理的资源),
.NET就无能为力了。例如,文件句柄,数据库连接等。
如果我们不及时释放非托管资源,那么就会造成资源泄漏和浪费。
程序在关闭时,操作系统会尝试释放这些资源。
但这些资源可能有释放的顺序要求,如果不按要求就会出错。
释放模式
释放模式是一种实现IDisposable
接口的推荐方式。
它可以有效地处理托管资源和非托管资源的释放,并且可以支持继承和多态。
释放模式的一般化格式如下:
using System;
using System.Runtime.InteropServices;
class BaseClass : IDisposable
{
// 标志:Dispose方法是否已经被调用过
protected bool disposed = false;
// 实例化一个SafeHandle类型的对象
SafeHandle handle = new SafeFileHandle(IntPtr.Zero, true);
// 公开的Dispose方法,供用户手动调用
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// 受保护的虚拟Dispose方法,用于实现释放逻辑
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
// 在这里释放其他托管资源
handle.Dispose();
}
// 在这里释放非托管资源
disposed = true;
}
// 终结器,用于在垃圾回收器回收对象时调用
~BaseClass()
{
Dispose(false);
}
}
释放模式要做的事情有以下几点:
- 声明一个
bool
字段,用于标记对象是否已经被释放过。 - 声明一个
SafeHandle
类型的字段,用于封装非托管资源的句柄。 - 实现
IDisposable
接口的Dispose
方法,用于供用户手动调用。在这个方法中,调用一个受保护的虚拟方法Dispose(bool)
,并传入true
作为参数,表示是由用户显式调用的。然后调用GC.SuppressFinalize
方法,告诉垃圾回收器不要再调用终结器了。 - 实现一个受保护的虚拟方法
Dispose(bool)
,用于实现所有资源释放的逻辑。在这个方法中,首先检查对象是否已经被释放过,如果是则直接返回。然后根据传入的参数判断是由用户显式调用还是由垃圾回收器隐式调用。如果是由用户显式调用,则释放所有托管资源和非托管资源。如果是由垃圾回收器隐式调用,则只释放非托管资源。最后将标记字段设为true
,表示对象已经被释放过了。 - 实现一个终结器,用于在垃圾回收器回收对象时调用。在这个方法中,调用受保护的虚拟方法
Dispose(bool)
,并传入false
作为参数,表示是由垃圾回收器隐式调用的。