从零开始:C#回收魔法—深入浅出揭开Dispose与释放模式的神秘面纱
一、什么是Dispose?
我们先来看一个简单例子(Net 8)。定义一个实现了Dispose方法的简单对象Defer。然后在控制台中我们执行以下代码。
// 定义Defer类型
ref struct Defer(Action action) { public void Dispose() => action?.Invoke();}
// Main入口
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方法,就能把资源归还给系统,避免资源泄露。
简单来说,Dispose就是约定号的一个“用完就收拾”的方法。可以方便的配合using关键词来使用。我们可以再看看几个例子。
案例1 通过using在指定代码完成后触发Dispose:
// Main入口
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多重触发,最终按变量定义的顺序反着执行(出栈顺序):
// Main入口
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模式和析构函数(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; // 新增: flag变量
public void Dispose()
{
if (!disposedValue) // 新增: 判断flag值,避免重复调用
{
_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()方法被执行了2次(自身一次,外部调用一次),从而造成意外后果。
我们可以看一个例子。
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接口的类
再定义一个实现IDisposabled接口的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 - Cannot access a disposed object.
// Object name: '1:无法访问已终结的ManagedData.'.
// 2:ManagedData 正常释放.
// 2:ManagedData 已终结.
// 3:ManagedData 正常释放.
// 3:ManagedData 已终结.
// 0:异常: ObjectDisposedException - Cannot access a disposed object.
// Object name: '0:无法访问已终结的ManagedData.'.
// GC完成
上述的结果是不确定的,有时候所有的都能成功,有时候部分会触发失败。因为终结器触发不确定行,有时引用的子对象已经被回收,但父对象析构函数还在访问它。
总的来说,Dispose释放模式是对潜在的Dispose问题进行了充分考虑的。如果每个方法都按要求写,那么就很安全。
四、最后
总的来说,C#中的Dispoe()是一个很有用的,他不仅在资源释放场景下是一大助力,而且在很多需要延迟执行或需要统一结束的场合也是一个很好的范式(比如Stream既有Close()方法同时还支持Dispose(),二者等效)。很多事务处理的Transaction对象也经常使用using来控制Commit()时机。另外就是图形资源、网络资源、数据库连接、系统句柄、非托管内存管理等等,通过Dispose()实现可以极大提高代码的可读性和可维护性。
感谢您的耐心阅读,希望各位从零开始的新朋友和老朋友有所收获!如果你对这篇文章的内容有任何建议或想法,欢迎随时交流!关注微信公众号‘萤火初芒’

重要关注微信公众号‘萤火初芒’,有问题公众号留言,务必第一时间回复解答~!!!
从零开始:C#回收魔法—深入浅出揭开Dispose与释放模式的神秘面纱的更多相关文章
- 揭开GrowingIO无埋点的神秘面纱
揭开GrowingIO无埋点的神秘面纱 早在研究用户行为分析的时候,就发现国内的GrowingIO在宣传无埋点技术,最近正好抽出时间来研究一下所谓的无埋点到底是什么样的. 我分六部分来分析一下无埋 ...
- 揭开自然拼读法(Phonics)的神秘面纱
揭开自然拼读法(Phonics)的神秘面纱 自然拼读法 (Phonics),是指看到一个单词,就可以根据英文字母在单词里的发音规律把这个单词读出来的一种方法.即从“字母发音-字母组合发音-单词-简单 ...
- 揭开js之constructor属性的神秘面纱
揭开 constructor 在 Javascript 语言中,constructor 属性是专门为 function 而设计的,它存在于每一个 function 的prototype 属性中.这个 ...
- 通过一个生活中的案例场景,揭开并发包底层AQS的神秘面纱
本文导读 生活中案例场景介绍 联想到 AQS 到底是什么 AQS 的设计初衷 揭秘 AQS 底层实现 最后的总结 当你在学习某一个技能的时候,是否曾有过这样的感觉,就是同一个技能点学完了之后,过了一段 ...
- 揭开C++类中虚表的“神秘面纱”
C++类中的虚表结构是C++对象模型中一个重要的知识点,这里咱们就来深入分析下虚表的在内存中的结构. C++一个类中有虚函数的话就会有一个虚表指针,其指向对应的虚表,一般一个类只会有一个虚表,每个虚表 ...
- 毫不留情地揭开 ArrayList 和 LinkedList 之间的神秘面纱
先看再点赞,给自己一点思考的时间,思考过后请毫不犹豫微信搜索[沉默王二],关注这个靠才华苟且的程序员.本文 GitHub github.com/itwanger 已收录,里面还有技术大佬整理的面试题, ...
- 揭开Sass和Compass的神秘面纱
揭开Sass和Compass的神秘面纱 可能之前你像我一样,对Sass和Compass毫无所知,好一点儿的可能知道它们是用来作为CSS预处理的.那么,今天请跟我一起学习下Sass和Compass的一些 ...
- ASP.NET 运行时详解 揭开请求过程神秘面纱
对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...
- 带你揭开ATM的神秘面纱
相信大家都用过ATM取过money吧,但是有多少人真正是了解ATM的呢?相信除了ATM从业者外了解的人寥寥无几吧,鄙人作为一个从事ATM软件开发的伪专业人士就站在我的角度为大家揭开ATM的神秘面纱吧. ...
- 揭开.NET消息循环的神秘面纱(GetMessage()无法取得任何消息,就会进入Idle(空闲)状态,进入睡眠状态(而不是Busy Waiting)。当消息队列不再为空的时候,程序会自动醒过来)
揭开.NET消息循环的神秘面纱(-) http://hi.baidu.com/sakiwer/item/f17dc33274a04df2a9842866 曾经在Win32平台下奋战的程序员们想必记得, ...
随机推荐
- CF1928E Modular Sequence 题解
CF1928E Modular Sequence 考虑合法的答案的构成为一个 \(x,x+y,\dots x+ky\) 的块加上若干个 \(x\bmod y,x\bmod y+y,\dots x\bm ...
- Wordpress设置必须登录才能查看内容
参考文章地址 我是一个不会编程的小白,在网上查了好多篇的文章都没有实现这个功能.都是在改完php的代码后,网站就报废了.后来我还是求助了万能的谷歌,找了这篇文章. 上代码.大概猜测了一下,就是判断你现 ...
- qt 显示中文
参考链接 CSDN Tips 直接使用第三种方法 也可以使用 QString::fromLocal8Bit("打开文档文件") 这种方式
- python print 输出重定向
简介 print 重定向的功能,很实用,记录一下 参考链接 https://www.cnblogs.com/marsggbo/p/10293484.html code import sys impor ...
- 谷云科技RestCloud完成数千万人民币Pre-A轮融资
聚焦企业系统集成及数据融合场景的谷云科技RestCloud iPaaS于近期完成数千万人民币Pre-A轮融资,本轮融资由SIG 海纳亚洲创投基金独家投资. 谷云科技RestCloud是一家专注于大型企 ...
- SciTech-BigDataAIML-Machine Learning Tutorials
Machine Learning Tutorials Machine Learning Tutorials This page lists all of the machine learning tu ...
- SciTech-Pipes-PPR-Lesso联塑: 管壁厚(DN)简称:4分/6分/1寸 + PPR管材规格及外径尺寸 + PP-R给水管规格介绍 - $\large \bm{OD}: \text{Outside Diameter},\ \bm{WT}:\text{Wall Thickness}, \bm{ID}:\text{Inside Diameter}$
管壁厚(DN)简称:4分/6分/一寸 PPR管材规格及外径尺寸 PPR管材的规格通常由系列和尺寸组成PPR管材规格和表示方法. PPR管材规格以S区分系列,公称外径(DN)×公称壁厚(en). 例如: ...
- Rust中的代码组织:package/crate/mod
刚接触Rust遇到一堆新概念,特别是package, crate, mod 这些,特别迷糊,记录一下 一.pakcage与crate 当我们用cargo 创建一个新项目时,默认就创建了一个packag ...
- 国内所说的PACS系统都包含哪些内容-九五小庞
- ACME协议
ACME 协议是一种开放标准,旨在实现数字证书颁发和续订流程的自动化,它彻底改变了证书管理.ACME 的开发旨在简化整个流程,已被许多证书颁发机构 (CA) 广泛采用,并已成为互联网标准 (RFC 8 ...