.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,此時將斷點放在析構函數中,將不會再調用,打印如下:
好了,以上只是我個人猜測,接下來我們直接看官方文檔進行論證,官網對於析構函數鏈接
析構函數規範
//docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors
在.NET Framework應用程序中會盡一切合理努力在程序退出時調用析構函數進行清理(調用終結器方法),除非進行手動抑制,但在.NET Core並不能完全保證此行為。通過調用Collect來強制進行垃圾回收,但是在大多數情況下,應避免此調用,因為這可能會導致性能問題。為何出現如此差異呢?更詳細分析請參看鏈接:
.NET Core析構函數理解分析
//github.com/dotnet/runtime/issues/16028
根據此鏈接表述,可以這樣理解:在.NET Core中不會在應用程序終止時運行終結器(針對可到達或不可到達的對象),根據建議,並不能保證所有可終結對象在關閉之前都將被終結。由於上述鏈接原因存在,所以在ECMA的C#5.0規範削弱了這一要求,因此.Net Core並不會違反此版本規範
總結
💡 在應用程序關閉前,.NET Framework會盡一切合理努力調用析構函數即終結器進行資源清理,但在.NET Core中並不能保證此行為,所以在ECMA 語言規範中削弱了這一要求
💡 基於上述,在.NET Core中使用析構函數並沒有實質性意義