一、理解托管资源与非托管资源
在C#编程中,资源管理是开发者必须掌握的核心技能。C#中的对象主要分为两类:托管对象由.NET垃圾回收器(GC)自动管理,无需手动干预;而非托管对象则需要开发者显式释放,因为GC无法自动回收。常见的非托管资源包括文件句柄、窗口句柄、网络连接和数据库连接等。如果使用后不及时清理,就会导致内存泄漏,影响程序性能。

二、区分托管对象与非托管对象
为了更清晰地理解这两类对象的区别,请参考以下对比表格:
| 对比维度 | 托管对象 | 非托管对象 |
常见类型 | C#中通过 |
|
| 内存管理方式 | 由 .NET 垃圾回收器(GC)自动管理,无需手动干预 | 必须由开发者手动分配和释放 |
| 典型示例 | FileStream、SqlConnection、Person、List 等 | 文件句柄、GDI 绘图对象、Marshal.AllocHGlobal 分配的内存等 |
三、Dispose 模式
3.1 Dispose 模式概述
为此,微软提供了一套标准方案——释放模式(Dispose Pattern),旨在规范非托管资源的释放流程,降低开发者的心智负担。该模式遵循以下两个核心准则:
1. 幂等性:Dispose 方法应支持多次调用,后续调用不会产生任何副作用。
2. 层级调用:如果一个类包含实现了 IDisposable 的成员,那么该类本身也应实现 IDisposable,并在其 Dispose 方法中调用这些成员的 Dispose 方法。
3.2 IDisposable 接口
该模式的核心接口是 IDisposable,它仅定义了一个无参方法 Dispose:
namespace System
{
[ComVisible(true)]
public interface IDisposable
{
void Dispose();
}
}
3.3 无参 Dispose 方法
以下是其标准实现:
public void Dispose()
{
// 释放非托管资源
Dispose(true);
// 抑制终结器,避免重复释放
GC.SuppressFinalize(this);
}
3.4 GC.SuppressFinalize 的作用
SuppressFinalize 的主要作用是性能优化:它通知 GC 该对象已被手动释放,无需再执行终结流程,从而让 GC 可以提前回收该对象的内存。此方法会阻止终结器(析构函数)的执行。如果类中没有定义终结器,则调用 GC.SuppressFinalize 不会产生任何效果。
3.5 有参 Dispose 方法
实际执行清理任务的是以下重载方法。参数 disposing 为 true 表示由无参 Dispose() 方法主动调用,为 false 则表示由终结器调用:
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// 释放托管资源(托管对象)
// ...
}
// 释放非托管资源
// ...
_disposed = true;
}
该方法之所以同时处理两种场景,是为了避免代码重复并确保安全性:
- 确定性释放(
disposing = true):当主动调用Dispose()时触发,此时托管和非托管资源都可以安全释放。 - 非确定性释放(
disposing = false):由 GC 在终结器中触发,此时托管资源可能已被回收,因此只应释放非托管资源。
3.6 使用实现 IDisposable 的对象
如果通过 new 创建的对象实现了 IDisposable(例如 StreamReader),则使用后必须调用其 Dispose 方法。常见做法有两种:
方法一:try-finally 结构
StreamReader sr;
try
{
sr = new StreamReader(filename);
txt = sr.ReadToEnd();
}
finally
{
if (sr != null) sr.Dispose();
}
方法二:using 语句
当控制离开 using 语句块时,会自动调用 Dispose 方法。实际上,using 是语法糖,编译后等价于上面的 try-finally 结构。
using (StreamReader sr = new StreamReader(filename))
{
txt = sr.ReadToEnd();
}
此外,还可以使用不带括号的 using 声明。在这种写法下,Dispose 会在当前方法结束时自动触发:
using StreamReader sr = new StreamReader(filename); txt = sr.ReadToEnd();
四、终结器(Finalizer)
终结器(历史上称为析构函数)会在 GC 回收对象内存之前由 GC 调用,相当于一个兜底方案:当开发者忘记手动调用 Dispose 时,终结器可以防止内存泄漏。但它的代价是增加 GC 的性能开销,因此不推荐作为常规做法。
- 终结器应仅用于非托管对象。
- C# 编译器不允许直接重写
Finalize方法,但可以通过在类中定义析构函数来提供终结器。 - 避免编写空的终结器,因为空终结器只会无谓地消耗性能。
例如,以下 Car 类的终结器声明:
class Car
{
~Car() // 终结器
{
// 清理语句...
}
}
编译器会将终结器隐式转换为以下代码:
protected override void Finalize()
{
try
{
// 清理语句...
}
finally
{
base.Finalize();
}
}