让我们先从一个简单的例子开始(Net 8)。创建一个实现Dispose方法的基本对象Defer。接下来,在控制台中执行以下代码。
// 定义Defer类型
ref struct Defer(Action action) { public void Dispose() => action?.Invoke(); }
// 主入口
static void Main(string[] args)
{
using var df = new Defer(() => Console.WriteLine("Run"));
Console.WriteLine("Hello, World!");
}
// 控制台输出:
// Hello, World!
// Run
从输出结果可以看出,“Hello, World!”和“Run”的顺序被颠倒了。这个Defer结构体能够模拟Golang中的Defer关键字,提供延迟执行的功能。使用using语句可以更精确地控制Dispose()方法的调用时机。
对于ref struct,上面的代码等价于:
{
Defer df = new Defer(() => Console.WriteLine("Run"));
try
{
Console.WriteLine("Hello, World!");
}
finally
{
df.Dispose();
}
}
这里,try块内保护的是df对象生命周期内的代码。对于异步DisposeAsync(),using的等效形式为:
{
ResourceType resource = ?expression?;
try
{
?statement?;
}
finally
{
IAsyncDisposable d = (IAsyncDisposable)resource;
if (d != null)
{
await d.DisposeAsync();
}
}
}
为什么需要设计Dispose方法?
C#利用垃圾回收机制自动管理内存,这减轻了程序员手动管理内存分配与释放的负担,有效减少了内存泄漏和野指针等错误的发生。不过,垃圾回收器主要针对托管内存,对于非托管资源,它无法自动管理。此外,垃圾回收器的运行时间不固定,可能在资源不再需要很长时间后才启动。因此,需要一种机制来主动释放非托管资源,这也是引入Dispose方法的原因之一。
在C#编程中,我们常处理各种资源,例如文件、数据库连接等。使用完这些资源后,应及时释放,以避免占用系统资源,影响程序性能。通过调用Dispose方法,可以将资源归还给系统,防止资源泄露。简而言之,Dispose是一种“使用完毕即清理”的协议,便于与using关键字结合使用。下面是一些具体示例。
示例1:使用using在特定代码执行后触发Dispose
// 主入口
using (Defer df1 = new(() => Console.WriteLine("Run")))
Console.WriteLine("Hello, World!1"); // 或者通过 { ... } 包围代码
Console.WriteLine("Hello, World!2");
// 控制台输出:
// Hello, World!1
// Run
// Hello, World!2
示例2:通过多重using按变量定义的逆序触发(出栈顺序)
// 主入口
using Defer df1 = new(() => Console.WriteLine("Run1")),
df2 = new(() => Console.WriteLine("Run2")),
df3 = new(() => Console.WriteLine("Run3"));
Console.WriteLine("Hello, World!");
// 控制台输出:
// Hello, World!
// Run3
// Run2
// Run1
示例3:异步IAsyncDisposable,调用await using:
public class A_Async : IAsyncDisposable
{
async ValueTask IAsyncDisposable.DisposeAsync() => await Task.CompletedTask;
}
static async void Main(string[] args)
{
await using A_Async a = new();
}
为何要采用释放模式(Dispose Pattern)?
在C#中实现接口时,Visual Studio常常建议通过释放模式来完成。那么,释放模式究竟是什么呢?
Dispose模式与析构函数的组合使用旨在确保资源无论是在显式调用Dispose方法时,还是在对象被垃圾回收器(GC)回收时,都能得到正确的释放。这种模式通常称为“Dispose模式”,是处理托管和非托管资源的一种最佳实践。
示例说明
设想一个对象,其中既包含非托管资源也包含托管资源。以下是一个示例代码:
class SampleResource: IDisposable
{
private ManagedResource _mr; // 托管资源
private UnmanagedResource _ur; // 非托管资源
public void Dispose() // 资源释放
{
_mr.Dispose(); // 释放托管资源
_ur.Dispose(); // 释放非托管资源
}
}
防止Dispose方法的重复调用
通常情况下,我们的代码逻辑是合理的。然而,考虑到某些资源(如托管资源和非托管资源)可能不是由我们自己编写的,因此重复调用Dispose可能会导致问题。为了解决这个问题,可以在SampleResource类中添加一个标志位,以避免资源的重复释放。更新后的代码如下:
class SampleResource: IDisposable
{
private ManagedResource _mr;
private UnmanagedResource _ur;
private bool isDisposed = false; // 添加: 标志变量
public void Dispose()
{
if (!isDisposed) // 添加: 检查标志变量,避免重复调用
{
_mr.Dispose();
_ur.Dispose();
isDisposed = true;
}
}
}
避免遗漏Dispose方法的调用
对于包含非托管资源的对象,如果忽略了Dispose()的调用,轻则可能导致内存泄漏,重则可能引发严重问题。为了确保即使在忘记调用Dispose()的情况下也能释放资源,可以通过添加析构函数来实现。这样,当对象被GC回收时,资源将自动释放。以下是更新后的示例代码:
class SampleResource: IDisposable
{
private ManagedResource _mr;
private UnmanagedResource _ur;
private bool isDisposed = false;
public void Dispose()
{
ReleaseResources(); // 执行资源释放
GC.SuppressFinalize(this); // 如果手动调用了Dispose(),告知终结器不再执行析构函数
}
protected virtual void ReleaseResources() // 从Dispose方法中分离出的资源释放逻辑
{
if (!isDisposed)
{
_mr.Dispose();
_ur.Dispose();
isDisposed = true;
}
}
~SampleResource() // 新增: 析构函数,当忘记调用Dispose()时由终结器执行
{
ReleaseResources();
}
}
托管资源的提前回收
即使对象忘记了调用Dispose(),触发析构函数也可以执行资源的释放。然而,由于终结器的执行顺序不确定,当SampleResource对象被终结器触发析构函数时,其他对象(如_mr)也可能已经触发了析构函数。这意味着当SampleResource执行Dispose时,可能存在_mr的Dispose()方法被调用两次的情况,这可能会导致意外的结果。
存在缺陷的托管资源类定义
下面是一个没有处理重复释放情况的托管资源类示例:
class FaultyManagedResource: IDisposable
{
private MemoryStream _data = new MemoryStream(new byte[100_000_000]); // 模拟托管资源
private bool _isFinalized = false;
int _id;
public FaultyManagedResource(int id) // 记录当前对象ID
{
_id = id;
}
~FaultyManagedResource()
{
_isFinalized = true; // 由析构函数释放
Console.WriteLine($"{_id}: FaultyManagedResource 已终结.");
}
public void Dispose()
{
if (_isFinalized)
{
// 处理重复释放的问题
}
}
}3.3.2 定义一个继承IDisposable接口的类
接下来,我们将定义一个实现了IDisposable接口的SampleObject类。这里采用标准的释放模式(Dispose Pattern),但会故意将托管资源的释放置于disposing判断之外。
class SampleObject : IDisposable
{
private ManagedData _managedData;
private int _id;
public SampleObject(int id)
{
_id = id;
_managedData = new ManagedData(id);
}
private bool _disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// 这里通常应处理托管资源的释放
}
try
{
_managedData.Dispose(); // 为了测试目的,将托管资源的释放置于外部
}
catch (Exception ex)
{
Console.WriteLine($"{_id}: 异常: {ex.GetType().Name} - {ex.Message}");
}
_disposedValue = true;
}
}
~SampleObject()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
3.3.3 测试对象的创建与释放
我们将通过一个循环创建这些对象,随后调用垃圾回收器(GC),并等待其完成资源的释放过程。
for (int i = 0; i < 5; i++)
{
new SampleObject(i); // 对象立即变为垃圾
}
Console.WriteLine("对象创建完毕,启动GC...");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC已完成");
Console.ReadLine(); // 暂停程序,以便查看最终的输出结果
控制台输出示例:
创建完成,开始GC...
0:ManagedData 已终结.
1:ManagedData 已终结.
1:异常: ObjectDisposedException - Cannot access a disposed object.
对象名称: '1:无法访问已终结的ManagedData.'.
2:ManagedData 正常释放.
2:ManagedData 已终结.
3:ManagedData 正常释放.
3:ManagedData 已终结.
0:异常: ObjectDisposedException - Cannot access a disposed object.
对象名称: '0:无法访问已终结的ManagedData.'.


雷达卡


京公网安备 11010802022788号







