构造函数需无返回类型、名同类名、不可显式调用;推荐 this 链式调用、字段非空校验、避免虚方法调用;析构函数非资源清理首选,仅作非托管资源兜底;Dispose 模式须配合 GC.SuppressFinalize 实现两阶段清理。

构造函数怎么写才不会出错
构造函数不是普通方法,它没有返回类型(连 void 都不能写),名字必须和类名完全一致,且不能被显式调用。常见错误是加了 void 或拼错类名,编译器会直接报错:CS0501: 'X.X()' must declare a body because it is not marked abstract, extern, or partial。
建议做法:
- 多个构造函数之间用
this(……)链式调用,避免重复初始化逻辑 - 如果类有字段需要非空校验,优先在构造函数里检查并抛出
ArgumentNullException - 不要在构造函数里调用虚方法(
virtual或abstract),子类可能尚未完成初始化,容易引发未定义行为
public class Person {public string Name { get;} public int Age {get;} public Person(string name) : this(name, 0) {} public Person(string name, int age) {Name = name ?? throw new ArgumentNullException(nameof(name)); Age = Math.Max(0, age); }
}
析构函数不是“资源清理首选”
C# 的析构函数(即 ~ClassName())本质是编译器生成的 Finalize() 重写,它由 GC 在不确定时间调用,** 不能保证执行时机,也不能保证一定执行 **。它不适用于释放文件句柄、数据库连接、网络流等需要及时释放的资源。
真正该用的是 IDisposable 接口 + using 语句。析构函数只应在极少数场景下作为“安全网”:当用户忘记调用 Dispose(),且你持有非托管资源(如通过 Marshal.AllocHGlobal 分配的内存)时,才在析构函数里做兜底释放。
注意点:
- 析构函数不能带访问修饰符(不能写
public ~MyClass()) - 一个类最多只能有一个析构函数,且不能被继承或重载
- 如果实现了
IDisposable,应在Dispose(bool)中调用GC.SuppressFinalize(this),防止析构函数被重复执行
Dispose 模式怎么配合析构函数写
标准 Dispose 模式不是可选项,而是处理混合资源(托管 + 非托管)的强制约定。核心是两阶段清理:用户调用 Dispose() 时走快速路径;GC 调用析构函数时走慢速兜底路径。
关键结构:
-
Dispose()方法只负责标记已释放,并调用Dispose(true) -
Dispose(bool disposing)中,disposing == true表示可安全访问托管对象;false表示只能操作非托管资源 - 析构函数里只调用
Dispose(false),绝不调用任何托管对象的方法或属性
public class FileReader : IDisposable {private bool _disposed = false; private IntPtr _fileHandle = IntPtr.Zero; ~FileReader() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) {if (_disposed) return; if (disposing) {// 可以安全释放托管资源,比如关闭 StreamReader} // 总是释放非托管资源 if (_fileHandle != IntPtr.Zero) {Marshal.FreeHGlobal(_fileHandle); _fileHandle = IntPtr.Zero; } _disposed = true; }
}
为什么 new 之后没调用析构函数
这是最常被误解的一点:析构函数不是“对象销毁时自动触发”,而是“GC 决定回收该对象内存前,可能调用 Finalizer 队列里的方法”。这意味着:
- 程序运行中几乎看不到析构函数执行——GC 可能一直不触发,尤其内存充足时
- 即使调用了
GC.Collect(),析构函数也不会立刻执行,而是被放入终结队列,由专用线程异步调用 - 如果对象在 Finalizer 运行前又被其他地方引用(例如在析构函数里把
this存进静态集合),它会被提升到更高代,析构延迟更久甚至永不执行
所以别靠打印日志验证析构函数是否工作,更别在里面写业务逻辑。它的存在意义只有一个:防止非托管资源泄漏的最后防线,仅此而已。