首先,我们来观察一个简易示例(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();
}
}
}
C#运用垃圾收集机制自动化内存管理,这减轻了开发者手动处理内存分配与释放的负担,有效降低了内存泄露和未初始化指针等错误的发生率。不过,垃圾收集器仅负责托管内存的回收工作,对于非托管资源则无能为力。此外,垃圾收集器的执行时间点是随机的,可能会在资源不再被需要很长时间后才启动。因此,有必要引入一种手段来主动释放非托管资源,这就是Dispose设计的目的之一。
在C#编程中,我们频繁地使用多种资源,例如文件、数据库连接等。这些资源一旦使用完毕即应迅速释放,否则将占用系统资源,影响应用程序的性能。Dispose方法旨在释放此类资源。当某对象不再被需求时,主动或被动地调用Dispose方法,即可将资源交还给系统,防止资源泄露。
简言之,Dispose就是一个“使用后立即清理”的协议。它可以便捷地与using关键字搭配使用。下面再看几个实例。
// 主入口
using (Defer df1 = new(() => Console.WriteLine("Run")))
Console.WriteLine("Hello, World!1"); // 或者用 { ... } 包围代码
Console.WriteLine("Hello, World!2");
// 控制台输出:
// Hello, World!1
// Run
// Hello, World!2
// 主入口
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
public class A_Async:IAsyncDisposable {async ValueTask IAsyncDisposable.DisposeAsync() => await Task.CompletedTask;}
static async void Main(string[] args)
{
await using A_Async a = new();
}
在C#中实现接口时,Visual Studio常建议通过释放模式来完成。那么,究竟何谓释放模式呢?
释放机制是Dispose模式与析构函数(finalizer)共同运用的结果,旨在确保资源能有效释放,不论是通过显式调用Dispose方法,还是在对象被垃圾回收器(GC)处理时触发析构函数。此模式称作“Dispose模式”,它是管理托管与非托管资源的一种最佳实践。
无标题
例如,我们有一个对象,包含了一些非托管资源,以及一些托管资源。示例代码如下:
class SampleObject:IDisposable
{
private ManagedObject _mo; //托管
private UnmanagedObject _umo; //非托管
public void Dispose() //资源释放
{
_mo.Dispose(); //释放托管
_umo.Dispose(); //释放非托管
}
}
3.1 防止重复调用Dispose()
通常情况下我们的代码没有问题。但如果ManagedObject和UnmanagedObject并非我们编写,则需考虑重复调用Dispose可能导致的问题。因此,需在SampleObject内部添加一个标记,防止资源被多次释放,此时代码变为:
class SampleObject:IDisposable
{
private ManagedObject _mo;
private UnmanagedObject _umo;
private bool disposedValue = false; // 添加: 标记变量
public void Dispose()
{
if (!disposedValue) // 添加: 检查标记值,避免重复调用
{
_mo.Dispose();
_umo.Dispose();
disposedValue = true;
}
}
}
3.2 防止忽略调用Dispose()
对于拥有非托管资源的对象,若忘记调用Dispose(),轻微情况会导致内存泄露,严重则可能引发灾难。为了保证对象能够调用Dispose(),我们考虑添加析构函数,以便在程序被GC回收时自动释放资源,示例代码如下:
class SampleObject:IDisposable
{
private ManagedObject _mo;
private UnmanagedObject _umo;
private bool disposedValue = false;
public void Dispose()
{
DisposeFinal(); // 实施资源释放
// 添加: 若手动调用了Dispose(),指示终结器不再执行析构函数
// 即避免重复调用DisposeFinal()方法
GC.SuppressFinalize(this);
}
public void DisposeFinal() //重命名,从Dispose方法中分离
{
if (!disposedValue)
{
_mo.Dispose();
_umo.Dispose();
disposedValue = true;
}
}
// 添加: 析构函数,忘记调用Dispose()时由终结器执行Dispose()
~SampleObject()
{
DisposeFinal();
}
}
3.3 托管资源的早期回收
即使3.2中的对象未调用Dispose(),触发析构函数仍可执行Dispose()。尽管看似一切妥当,但这里依然存在重复调用Dispose()的风险。因为终结器的执行顺序不确定,当SampleObject对象被终结器触发析构函数时,其他对象(如_mo)也可能已触发析构函数。这意味着在SampleObject执行Dispose时,_mo的Dispose()可能被调用两次(一次自身,一次外部调用),导致意外结果。
我们来看一个实例。
3.3.1 定义一个存在缺陷的托管资源类
该类未处理重复释放的情况。
// 我们定义一个存在缺陷的托管资源类
class ManagedData:IDisposable
{
// 模拟托管资源,使用大数组尽可能延长GC保留时间,增加测试结果的多样性
private MemoryStream data= new MemoryStream(new byte[100_000000]);
private bool _finalized = false;
int id;
public ManagedData(int id) //记录当前对象id
{
this.id = id;
}
~ManagedData()
{
_finalized = true; // 由析构函数释放
Console.WriteLine($"{id}:ManagedData 已终结.");
}
public void Dispose()
{
if (_finalized)throw new ObjectDisposedException($"{id}:无法访问已终结的ManagedData.");
data.Dispose();
Console.WriteLine($"{id}:ManagedData 正常释放.");
_finalized = true; // 由dispose释放
}
3.3.2 定义一个继承IDisposable接口的类
再定义一个实现IDisposable接口的SampleObject来使用。在这里我们采用标准的释放模式(Dispose Pattern)编写,但故意将托管资源的释放放在disposing判断之外。
class SampleObject : IDisposable
{
private ManagedData _mo;
int id;
public SampleObject(int id) // 记录当前对象id
{
this.id = id;
_mo = new ManagedData(id);
}
private bool disposedValue;
// 标准的释放模式写法
protected virtual void Dispose(bool disposing)
{
if (!disposedValue) // 如果已执行dispose,则以下代码跳过
{
// 判定来源
// 如果是手动Dispose()调用的,disposing为true释放托管资源
// 如果是由终结器在析构函数中被动调用的,disposing为false此时不应释放托管资源
if (disposing)
{
// 本应在此处编写托管资源的释放
}
try
{
_mo.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,等待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 - 无法访问已处置的对象.
// 对象名称: '1:无法访问已终结的ManagedData.'
// 2:ManagedData 正常释放.
// 2:ManagedData 已终结.
// 3:ManagedData 正常释放.
// 3:ManagedData 已终结.
// 0:异常: ObjectDisposedException - 无法访问已处置的对象.
// 对象名称: '0:无法访问已终结的ManagedData.'
扫码加好友,拉您进群



收藏
