C# 析构函数全详解:原理+实战+避坑

C# 析构函数全详解:原理+实战+避坑

很多学过 C++ 的同学,初学 C# 都会踩一个大坑:试图用 C++ 的析构思维写 C# 代码

在 C++ 中,析构函数是确定性执行的,对象销毁立刻触发;但在 C# 中,析构函数完全是另一套逻辑,乱用会导致内存泄漏、程序卡顿、资源残留等问题。

今天这篇博文,一次性讲透C# 析构函数:是什么、什么时候执行、怎么写、怎么用、以及90%人都会犯的错误,附带全套可运行代码。


一、什么是 C# 析构函数?

1. 核心定义

析构函数(终结器)是类的特殊方法,由CLR 垃圾回收器(GC)自动调用,作用是:兜底释放非托管资源

简单记住两句话:

  • 托管资源 GC 自动管,不用你操心

  • 非托管资源 GC 管不了,靠析构函数兜底

2. 标准语法规则

语法特征极强,一眼就能识别:

  • ~波浪号开头 + 类名

  • 无返回值、无参数、无访问修饰符(不能加 public/private)

  • 一个类只能有一个析构函数,不支持重载

3. 最简入门代码示例

using System; namespace DestructorDemo { public class Student { // 构造函数:创建对象时执行 public Student() { Console.WriteLine("对象创建:构造函数执行"); } // 析构函数:GC回收对象前自动执行 ~Student() { Console.WriteLine("对象销毁:析构函数执行"); } } class Program { static void Main(string[] args) { // 创建对象 Student stu = new Student(); // 主动置空,让GC可以回收 stu = null; // 手动触发垃圾回收(仅测试用!项目中禁止写) GC.Collect(); GC.WaitForPendingFinalizers(); } } }
运行结果:

对象创建:构造函数执行

对象销毁:析构函数执行

💡 注意:日常开发绝对不要手动写 GC.Collect()!此处仅为测试,让大家直观看到析构函数效果。


二、析构函数 VS 构造函数(图文对比逻辑)

为了方便大家理解,我用最直白的生命周期逻辑展示:

生命周期流程图解

【代码执行 new】→ 构造函数(初始化资源) ↓ 【对象使用中】→ 执行业务逻辑 ↓ 【对象无引用、闲置】→ GC扫描标记 ↓ 【垃圾回收】→ 析构函数(清理资源) ↓ 【对象彻底销毁】

核心区别对照表

对比维度

构造函数

析构函数

执行时机

new 对象时,确定性执行

GC回收时,不确定时机

调用者

开发者代码主动调用

CLR 垃圾回收器自动调用

作用

初始化字段、开辟资源

兜底释放非托管资源

数量

可多个、可重载

只能1个、不可重载

手动调用

支持

完全不支持


三、重中之重:托管资源 & 非托管资源

能不能用好析构函数,核心就在于分清这两种资源!

1. 托管资源(GC 自动回收)

由 .NET 框架管理的资源,GC 会自动清理,不需要析构函数

例:string、List、数组、自定义类对象 等

2. 非托管资源(GC 管不了,必须兜底)

操作系统原生资源,.NET 无法自动识别回收,必须手动释放/析构兜底

例:文件流、数据库连接、网络套接字、IntPtr 非托管内存、窗口句柄

核心结论: 没有非托管资源的类,坚决不写析构函数,只会增加 GC 性能开销!


四、企业级标准写法:Dispose 释放模式

纯析构函数有致命问题:执行时机太晚、不确定。 所以 .NET 官方规范:IDisposable + 析构函数 双保险模式

  • 用户主动 using/Dispose:立刻释放所有资源(高效、及时)

  • 用户忘记释放:析构函数兜底,防止内存泄漏

完整可运行实战代码

using System; namespace DestructorDemo { // 实现释放接口 public class FileResource : IDisposable { private IntPtr _unmanagedHandle; // 模拟非托管资源句柄 private bool _isDisposed = false; // 构造函数:占用非托管资源 public FileResource() { Console.WriteLine("获取非托管资源句柄"); _unmanagedHandle = (IntPtr)10086; // 模拟资源赋值 } // 公共释放方法:给开发者手动调用 public void Dispose() { Dispose(true); // 告诉GC:资源已释放,无需执行析构函数 GC.SuppressFinalize(this); Console.WriteLine("手动释放资源完成,跳过析构执行"); } // 核心释放逻辑 protected virtual void Dispose(bool isManualDispose) { if (_isDisposed) return; // 主动释放时:可清理托管资源 if (isManualDispose) { Console.WriteLine("清理托管资源"); } // 无论手动/GC回收:必须清理非托管资源 if (_unmanagedHandle != IntPtr.Zero) { Console.WriteLine("释放非托管资源句柄"); _unmanagedHandle = IntPtr.Zero; } _isDisposed = true; } // 析构函数:GC兜底执行 ~FileResource() { Dispose(false); Console.WriteLine("析构函数兜底释放完成"); } } class Program { static void Main(string[] args) { // using 语法糖:出作用域自动调用Dispose using (var file = new FileResource()) { Console.WriteLine("正在使用资源..."); } Console.WriteLine("程序执行结束"); } } }

运行结果

获取非托管资源句柄 正在使用资源... 清理托管资源 释放非托管资源句柄 手动释放资源完成,跳过析构执行 程序执行结束

可以看到:手动 Dispose 成功后,析构函数不会执行,性能最优!


五、析构函数 4 大高频误区(避坑必看)

误区1:C# 析构 = C++ 析构

错误:C++ 析构确定性执行,C# 析构完全随机,程序退出都可能不执行!

正确:需要及时释放的资源,一律用using + Dispose

误区2:析构函数中操作托管对象

错误:GC 回收顺序不可控,其他托管对象可能已经被回收,直接报空引用异常

正确:析构中只处理非托管资源

误区3:所有类都写析构函数

错误:无非托管资源时,写析构会让对象进入终结队列,降低GC回收效率

正确:无原生资源,不写析构函数

误区4:手动调用析构函数

错误:C# 禁止手动调用 ~类名() 和 Finalize()

正确:资源释放统一用 Dispose


六、总结(博文核心干货)

  1. 析构函数是GC 自动兜底的非托管资源清理方法,执行时机不确定

  2. 语法:~类名(),无参无返回值,唯一不可重载

  3. 托管资源不用管,非托管资源必须靠Dispose+析构双保险

  4. 业务开发优先使用using 语法糖,杜绝手动 GC 操作

  5. 无原生非托管资源,坚决不写析构函数,避免性能损耗


码字不易,欢迎点赞收藏!后续持续更新 C# 底层原理、.NET 性能优化、实战避坑干货