IDisposable 与 using
一、IDisposable:显式释放资源的契约
1. 为什么要使用IDisposable
先看一个问题:C# 会自动清理垃圾,为什么还需要手动释放?
C# 的内存(比如你 new 出来的对象)确实由垃圾回收器(GC)自动管理,你不用管。但是程序里还有一些不属于内存的东西,比如:
- 文件(你打开了一个文件,就占用了操作系统的文件句柄)
- 数据库连接(你连上了数据库,数据库服务器那边有资源)
- 网络端口(你发 HTTP 请求,系统开了端口)
- 绘图画笔(你画图时用的 GDI 对象)
这些东西,GC 不认识,也不会帮你去关。如果你不主动释放,它们会一直存在,导致:
- 文件被你的程序一直占着,别人打不开
- 数据库连接数爆满,新用户连不上
- 程序越跑越慢,最后崩溃
所以,C# 给了你一个约定:如果你用了这种特殊资源,你用完必须亲自归还。
这个约定就是 IDisposable。
2. IDisposable 就像一张"归还资源的标签"
C# 源码里面的IDisposable 是一个接口,里面只有一句话:
cs
void Dispose();
任何类(比如 FileStream、SqlConnection)只要实现了这个接口,就等于贴了个标签:"我占用了特殊资源,用完请调用我的 Dispose() 方法归还"。
cs
FileStream fs = new FileStream("a.txt", FileMode.Open);
// ... 使用文件
fs.Dispose(); // 归还文件
但是这样写有个隐患:如果中间代码抛出异常,Dispose() 可能永远不会执行,资源照样泄露。你还得写 try-finally:
cs
FileStream fs = null;
try{
fs = new FileStream("a.txt", FileMode.Open);// 使用文件
}finally{
if (fs != null) fs.Dispose();
}
太麻烦,而且容易忘。于是 C# 提供了一个语法糖using 。
3. 什么时候自己实现 IDisposable
如果你的类里面包含了需要释放的东西,那么你的类也应该实现 IDisposable。
CS
class Logger : IDisposable{
private FileStream logFile; // 这个需要释放!
public Logger(string path){
logFile = new FileStream(path, FileMode.Append);
}
public void Write(string msg) {
// 写日志
}
public void Dispose(){
logFile?.Dispose(); // 释放掉内部的资源
}
}
然后别人用你的 Logger 时:
cs
using (var logger = new Logger("app.log")){
logger.Write("程序启动");
}
// 自动释放文件
原则: 如果你的类里有任何 IDisposable 类型的成员(比如 FileStream、SqlConnection),你就应该实现 IDisposable,并在 Dispose() 里调用它们的 Dispose()。
二、using 语句:优雅且安全的自动释放
1. using 就是帮你自动写 try-finally 的偷懒工具
using 语句的作用: 出了这个大括号,自动调用 Dispose(),不管有没有异常。
基本用法:
cs
using (FileStream fs = new FileStream("a.txt", FileMode.Open)){
// 使用 fs 读文件
}
// 到这里,自动 fs.Dispose()
-----------------------------------------
//而上面的using基本用法和下面这段代码实现是一样的
FileStream fs = new FileStream("a.txt", FileMode.Open);
try{
// 使用 fs
}finally{
if (fs != null) fs.Dispose();
}
所以 using 的本质 = 自动 + 安全的资源归还
2. 如果用 using 包多个资源
等价于嵌套的 try-finally,保证两个都被释放。
cs
using (FileStream input = File.OpenRead("1.txt"))
using (FileStream output = File.OpenWrite("2.txt"))
{
// 复制文件内容
}
// 先自动释放 output,再释放 input
3. C# 8 之后更简单的写法:using 声明
cs
using var fs = new FileStream("a.txt", FileMode.Open);
// 使用 fs
// 方法结束时会自动 Dispose()
效果: fs 的生命周期持续到方法结束,而不是大括号结束。适合那些"在整个方法里都要用,最后一起释放"的场景。
cs
void ReadFile(){
using var fs = new FileStream("a.txt", FileMode.Open);
// 在这里使用 fs
// 方法是这里结束,fs 自动释放
}
4. 延迟释放与手动控制生命周期
有些场景你不想在代码块结束立刻释放,比如把对象返回给调用方使用。这时就不能在方法内 using。而在创建资源的责任交给调用方,调用方负责 using。
cs
// 不负责释放
public Stream GetStream(){
return new FileStream("data.bin", FileMode.Open);
}
// 调用方:
using (var stream = GetStream()){
// 调用方负责释放
}
四、Dispose 模式
如果你写一个类,内部只包含托管资源(例如另一个 IDisposable 对象),只需要:
cs
public class MyClass : IDisposable{
private SqlConnection conn;
public void Dispose() => conn?.Dispose();
}
这完全正确,对于大多数日常业务代码已经足够。但是,当你面对以下情况时,简单的 Dispose 就不够了:
- 类直接持有非托管资源(比如通过 IntPtr 获得的文件句柄、GDI 对象、原生内存)
- 类可能被继承,派生类也可能有自己的资源需要释放
- 用户可能忘记调用 Dispose,导致非托管资源泄漏
为了同时解决这三个问题,.NET 设计了一个经典的 Dispose 模式(也叫"可释放模式"),它包含:
- Dispose() 公共方法
- protected virtual void Dispose(bool disposing) 方法
- 终结器(析构函数)
- GC.SuppressFinalize(this)
Dispose 模式 其实在实际开发并不常见,这里详情就懒得写了
Dispose 模式 = 一个 Dispose() 方法 + 一个受保护的 Dispose(bool) 虚方法 + 一个终结器(仅当有非托管资源时)。
本文参考
阿里云开发者社区 https://developer.aliyun.com/article/805187
C# 官方文档 https://learn.microsoft.com/zh-cn/dotnet/standard/garbage-collection/using-objects
博客园 https://www.cnblogs.com/chenlight/p/16558148.html