首页 / C# 入门教程 / 垃圾回收与 IDisposable 模式

C# 入门教程

垃圾回收与 IDisposable 模式

本教程共 100 篇 · 第 93 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

C#C# 入门教程编程语言垃圾回收IDisposableusing

93. 垃圾回收与 IDisposable 模式

本节目标:学完能说清托管内存为何不用手动释放,以及如何用 IDisposable 和 using 释放文件、连接等非托管资源。

很多新手从 C/C++ 转来,总担心「内存会不会漏」。在 C# 里,普通对象的内存由垃圾回收器(Garbage Collector,GC)托管,你基本不用管。但有一类资源例外,必须手动释放。

一、托管资源 vs 非托管资源

托管资源(managed resource)是指 .NET 自己分配、自己能收回的对象,比如普通的 List<string>Person 实例。它们的内存由 GC 负责。

非托管资源(unmanaged resource)是指绕过 .NET 的「操作系统级把手」:打开的文件、数据库连接、网络套接字、图形句柄等。GC 看不见它们,不会替你关。不关掉,句柄就一直占着,最终耗尽系统资源。

Note

关键区别:GC 能回收「内存」,但回收不了「操作系统句柄」。凡是实现了 IDisposable 的类型,几乎都握着某种非托管资源,用完就该 Dispose

二、GC 是怎么工作的

GC 在后台自动运行,当内存压力到一定程度才触发。它分代(0/1/2 代)回收:新对象在 0 代,活得久的升到 1、2 代。绝大多数对象朝生夕死,所以 0 代回收最频繁、最快。

重点:GC 何时回收是不确定的。你不能假设「方法一结束对象就被销毁」。这也是为什么不能依赖「等 GC 来收」去关文件——那可能要等很久。

Tip

如果你想主动帮 GC 一把,可以调 GC.Collect(),但日常几乎不需要。GC 比你想的更会算账,手动干预反而可能打乱它的节奏,让性能更差。

Note

分代回收的核心假设是「大多数对象活不过第一轮」。0 代回收又快又频繁,1、2 代很少触发但代价更大。所以 GC 倾向于先扫「最可能有垃圾」的新生代,整体效率远高于「每次全扫一遍」。理解这点,你就明白为什么不该手动 GC.Collect() 去打断它的节奏——那往往让本该慢慢清的东西提前被拖进重量级回收。

三、IDisposable 与 Dispose

凡是需要释放资源的类型都实现 IDisposable 接口,它只有一个方法 Dispose()。你要做的是:用完就调用它。

namespace CSharpDemo;

class ResourceHolder : IDisposable
{
    private bool _disposed;

    public void DoWork() => Console.WriteLine("正在使用资源...");

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        Console.WriteLine("已释放非托管资源");
    }
}

using var r = new ResourceHolder();
r.DoWork();
// 离开作用域时自动 Dispose,无需手写

当你自己写的类内部持有别人的 IDisposable 对象(比如封装了一个 Stream 或数据库连接),它自己也应该实现 IDisposable,并在 Dispose 里把内部的资源一并释放。这样「释放」会像多米诺骨牌一样层层传递:调用方只要 using 最外层,里层资源也全部被照顾到。这是组合复用时的标准做法,能避免「内层句柄忘了关」这种隐蔽泄漏。

四、using 声明:最省心的写法

手写 Dispose 容易忘,而且出异常时还可能跳过。C# 8 引入的 using 声明(using declaration)最稳妥:变量离开作用域时自动 Dispose,哪怕中途抛异常。

namespace CSharpDemo;

using var reader = new StreamReader("data.txt");
string? firstLine = reader.ReadLine();
Console.WriteLine(firstLine);
// 到这里 reader 自动 Dispose,文件句柄被释放
Tip

也可以用经典的大括号 using 语句块。两者等价,C# 14 首选 using 声明,代码更平、缩进更浅。读文件、开数据库连、建 HttpClient handler 时都该这么写。

Tip

判断一个对象要不要 using,最快的办法是看它名字或文档里有没有「Stream / Connection / Reader / Writer / Handle」这类字眼,或者类型是否实现了 IDisposable。拿不准时,把它当作需要释放的资源处理,总比漏放安全。养成「见到 IDisposable 就 using」的条件反射,能挡掉大部分资源泄漏。

五、标准 Dispose 模式(简述)

当你自己写的类直接握有非托管句柄(比如用 IntPtr 调系统 API),才需要完整的 Dispose 模式:实现 IDisposable,并在真正握有非托管资源时配一个终结器(finalizer,写法 ~ClassName())作兜底。

namespace CSharpDemo;

class NativeWrapper : IDisposable
{
    private IntPtr _handle;      // 非托管句柄
    private bool _disposed;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // 已手动释放,无需终结器再跑
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        // 这里释放 _handle 指向的非托管资源
        _disposed = true;
    }

    ~NativeWrapper() => Dispose(false); // 兜底:忘了 Dispose 时 GC 调用
}

using var w = new NativeWrapper(); // 正常路径自动 Dispose
Note

对绝大多数应用代码,你并不需要写终结器。只有「类内部直接持有非托管指针」时才用。日常用别人封装好的 FileStreamSqlConnection 等,只要 using 就够了。

Note

终结器的代价很高:被它标记的对象,GC 不能在第一轮直接回收,得先排队跑一遍终结器,下一轮才真正释放,等于多活一代、还拖慢整体回收节奏。所以只在「直接握有非托管句柄、且忘了 Dispose 就得兜底」时才写终结器,千万别拿它当普通清理工具,否则反而拖累性能。

六、新手踩坑

  1. 打开文件后忘了 Dispose/using,文件被长时间占用,别的代码删不掉也改不了。
  2. 以为「等 GC 回收就会自动关资源」。GC 只管内存,且时机不定;非托管资源必须主动释放。
  3. using 块外还想用那个对象,结果对象已被释放,抛 ObjectDisposedException
  4. 滥用析构函数(终结器)做清理。终结器运行昂贵、时机不可控,普通托管对象根本不需要它。
Warning

千万不要在 using 声明的变量还没离开作用域时,就把它存进一个外部集合或返回出去。等作用域结束,它会被自动释放,你手里那个引用就成了「已释放」的空壳,下次用会抛 ObjectDisposedException。需要长期持有,就别用 using 声明,而是自己掌控释放时机。

七、为什么这样设计

为什么 C# 不像 C++ 那样让你手动 delete 一切?因为手动管理内存是 bug 大户:忘了释放会泄漏,释放早了会变野指针,重复释放会崩溃。GC 把「内存回收」这件事自动化,开发者只管创建对象,回收交给运行时,极大降低了出错概率。

但 GC 有个天生短板:它只理解「托管内存」,不理解「操作系统句柄」。一个文件句柄在 GC 眼里只是一小块内存,它不知道背后还占着磁盘资源。于是 C# 用 IDisposable 这个约定来补位:凡是握有外部资源的类型,都实现 Dispose,由你在代码里精确决定「什么时候释放」。using 声明再把这件事变成「离开作用域自动做」,把人为遗忘的可能降到最低。

八、using 声明 vs using 语句块 vs 手写 Dispose

方式写法何时用
using 声明using var x = ...;C# 14 首选,作用域结束自动释放,代码最平
using 语句块using (var x = ...) { ... }想严格限制资源存活范围在一对花括号内时
手写 Disposex.Dispose();不推荐,易忘且异常时会跳过

一句话:优先 using var,把释放交给编译器。

九、本节小结

普通内存交给 GC,安心写业务。凡是带 IDisposable 的资源类型(文件、连接、流),养成「用完即 using」的肌肉记忆。完整的终结器兜底只留给直接操作非托管句柄的底层封装。下章我们动手操作文件和目录。