17370845950

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

构造函数怎么写才不会出错

构造函数不是普通方法,它没有返回类型(连 void 都不能写),名字必须和类名完全一致,且不能被显式调用。常见错误是加了 void 或拼错类名,编译器会直接报错:CS0501: 'X.X()' must declare a body because it is not marked abstract, extern, or partial

建议做法:

  • 多个构造函数之间用 this(...) 链式调用,避免重复初始化逻辑
  • 如果类有字段需要非空校验,优先在构造函数里检查并抛出 ArgumentNullException
  • 不要在构造函数里调用虚方法(virtualabstract),子类可能尚未完成初始化,容易引发未定义行为
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 存进静态集合),它会被提升到更高代,析构延迟更久甚至永不执行

所以别靠打印日志验证析构函数是否工作,更别在里面写业务逻辑。它的存在意义只有一个:防止非托管资源泄漏的最后防线,仅此而已。