.NET Core/.NET 5.0 析构函数依然有效?
前言
最近看到小伙伴在.NET Core中用到了析构函数,不禁打一疑问,大部分情况下,即使在.NET Framework中都不会怎么用到析构函数,我想在.NET Core中是否还依然有效呢?随着时间推移,迭代版本更新,有些当初我们脑海里认定的东西可能在当前并不再适用,这也就需要我们同步知识更新,如今我们所认为可能并不再是往昔我们所认为
.NET Core/.NET 5.0 析构函数
下面首先来看在.NET Framework中一个很标准的资源释放例子,这里我以4.7.2版本为例(其他版本一样)。创建基于当前应用程序域的指定程序集的指定实例
public class CurrentDomainSandbox : IDisposable { private AppDomain _domain = AppDomain.CreateDomain( "CurrentDomainSandbox", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile }); ~CurrentDomainSandbox() { Dispose(false); } public T CreateInstance<T>(params object[] args) => (T)CreateInstance(typeof(T), args); private object CreateInstance(Type type, params object[] args) { HandleDisposed(); return _domain.CreateInstanceAndUnwrap( type.Assembly.FullName, type.FullName, ignoreCase: false, bindingAttr: 0, binder: null, args: args, culture: null, activationAttributes: null); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing && (_domain != null)) { AppDomain.Unload(_domain); _domain = null; } } private void HandleDisposed() { if (_domain == null) { throw new ObjectDisposedException(null); } } }
通过如上定义创建指定名称的应用程序域沙箱盒子,这样我们则可在此沙箱中创建对应程序集和实例,如此则可以其他域完全隔离且独立,然后在控制台进行如下调用
var sanBox = new CurrentDomainSandbox(); var instance = sanBox.CreateInstance<Program>();
还未完毕,直接运行将抛出如下异常
若用于远程传输,我们直接将主类继承自MarshalByRefObject就好,否则将此类通过Serializable特性标记,至于二者区别不详细展开。通过上述比较标准的例子我们则可以创建和释放未被使用的对应实例,我们看到用到了析构函数,但是我们发现最终调用Dispose方法,并未做任何处理,其实不然,问题出在对析构函数概念的理解
析构函数:在应用程序终止之前,将调用尚未被垃圾回收的所有对象的析构函数。析构函数本质是终结器,如果对象已被释放,在合适时机将自动调用Finalize方法,除非我们手动通过GC来抑制调用终结器(GC.SuppressFinalize),但不建议手动调用Finalize方法
通过资源释放标准例子,想必我们已经知道了析构函数的基本原理,接下来我们还是基于上述.NET Framework 4.7.2版本来演示析构函数
public class ExampleDestructor { public ExampleDestructor() { Console.WriteLine("初始化对象"); } public void InvokeExampleMethod() { } ~ExampleDestructor() { Console.WriteLine("终结对象"); } }
既然析构函数是在应用程序终止前进行调用,那么我们在调用上述示例中方法时,如下调用:
var exampleDestructor = new ExampleDestructor(); exampleDestructor.InvokeExampleMethod();
在.NET Framework中如我们所期望,在应用程序卸载时,此时会调用析构函数并进行相关打印。接下来到.NET Core,此时将断点放在析构函数中,将不会再调用,打印如下:
好了,以上只是我个人猜测,接下来我们直接看官方文档进行论证,官网对于析构函数链接
析构函数规范
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors
在.NET Framework应用程序中会尽一切合理努力在程序退出时调用析构函数进行清理(调用终结器方法),除非进行手动抑制,但在.NET Core并不能完全保证此行为。通过调用Collect来强制进行垃圾回收,但是在大多数情况下,应避免此调用,因为这可能会导致性能问题。为何出现如此差异呢?更详细分析请参看链接:
.NET Core析构函数理解分析
https://github.com/dotnet/runtime/issues/16028
根据此链接表述,可以这样理解:在.NET Core中不会在应用程序终止时运行终结器(针对可到达或不可到达的对象),根据建议,并不能保证所有可终结对象在关闭之前都将被终结。由于上述链接原因存在,所以在ECMA的C#5.0规范削弱了这一要求,因此.Net Core并不会违反此版本规范
总结
💡 在应用程序关闭前,.NET Framework会尽一切合理努力调用析构函数即终结器进行资源清理,但在.NET Core中并不能保证此行为,所以在ECMA 语言规范中削弱了这一要求
💡 基于上述,在.NET Core中使用析构函数并没有实质性意义