.NET Core/.NET 5.0 解構函式依然有效?

Jeffcky發表於2020-12-06

前言

最近看到小夥伴在.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中使用解構函式並沒有實質性意義

相關文章