簡體   English   中英

在 object 構造期間,構造函數的用例與 init 設置值有什么區別?

[英]What is the difference between use case of constructor vs init to set the value during object construction?

僅 init 設置器僅在 object 構造期間將值分配給屬性或索引器元素。 在 object 構造期間,構造函數的用例與 init 設置值有什么區別?

樣品 1:

public class Person
{
   private string _myName;

   public Person(string myName)
   {
      _myName= myName;
   }

   public string Name => _myName;
}

樣本 2:

public class Person
{
   private string _myName;

   public string Name
     {
         get { _myName; }
         init { _myName= value; }
     }
}

示例 3(忽略此示例,因為它與示例 2 相同):

public class Person
{
   private string _myName;

   public string Name
     {
         get => _myName; 
         init => _myName= value;
     }
}

主要區別和相似之處在此表中進行了描述:(包括我的編輯...)

構造函數參數 init屬性
自從 C# 1.0 C# 9.0
是必需的/有關於在場的硬保證 是的
自我記錄 自 C# 4.0 是的
可以覆蓋readonly字段 是的 是的
適用於 必需值和可選值 可選值
易於反射 只需使用ConstructorInfo 可怕
MEDI提供支持 是的
打破IDisposable 是的
Class 知道初始化順序 是的
子類化時的人體工程學 乏味 體面的

init的缺點主要繼承自 C# 的對象初始化器表達式的缺點,該表達式仍然存在許多問題(在腳注中)。

至於什么時候應該和不應該:

  • 不要將init屬性用於所需值 - 改用構造函數參數。
  • 對非必要、非必需或其他可選值使用init屬性,這些值在通過單個屬性設置時不會使對象的 state 無效。

  • 簡而言之, init屬性使初始化不可變類型中的非必要屬性變得稍微容易一些 - 但是,如果您將init用於必需的成員而不是使用構造函數參數,它們也使您更容易自責,尤其是C# 8.0 可空 -引用類型(因為不能保證會分配一個不可為空的引用類型屬性)。
  • 在指導方面:
    • 如果您的class不是不可變的,或者至少沒有在某些屬性上使用不可變語義,那么您不需要在這些屬性上使用init
    • 如果它是一個struct ,則根本不要使用init屬性,因為struct復制行為中的所有小細節。
    • 在我看來(不是其他人共享的),我建議您考慮一個可選的(也可以是可為空的)構造函數參數或一個完全不同的構造函數重載而不是init屬性,因為我認為它們存在問題並且缺乏任何真正的優勢。

腳注:C# 對象初始化器語法的問題,由init屬性繼承:

  • 中斷調試:即使在 C# 9 中,如果初始化程序的任何行引發異常,則異常的StackTrace將與new語句的行相同,而不是導致異常的子表達式的行。
  • Breaks IDisposable :如果屬性設置器(或初始化表達式)拋出異常並且類型實現IDisposable則新創建的實例將不會被釋放,即使構造函數已完成(並且 object 已完全初始化至關注 CLR)。

最大的不同:

  • 構造函數參數是必需的,並且順序是從 class 本身定義的。
  • 屬性初始化器是可選的,順序由調用者定義。

因此,如果您有強制參數和/或必須設置屬性的特定順序,請使用構造函數,否則進行屬性初始化。

讓我們從剖析init屬性開始。

舉兩個簡單的例子:

public class Foo
{
    public string Name { get; set; }
}

public class Bar
{
    public string Name { get; init; }
}

讓我們看一下兩者之間的IL差異:

.class nested public auto ansi beforefieldinit Foo extends [System.Runtime]System.Object
{
    .method public hidebysig specialname instance string get_Name () cil managed { ... }
    .method public hidebysig specialname instance void set_Name (string 'value') cil managed { ... }

    .property instance string Name()
    {
        .get instance string Foo::get_Name()
        .set instance void Foo::set_Name(string)
    }
}

.class nested public auto ansi beforefieldinit Bar extends [System.Runtime]System.Object
{
    .method public hidebysig specialname instance string get_Name () cil managed { ... }
    .method public hidebysig specialname instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Name (string 'value') cil managed { ... }

    .property instance string Name()
    {
        .get instance string Bar::get_Name()
        .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) Bar::set_Name(string)
    }
}

兩者之間的唯一區別是modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)

FooBar都實現了public getter 和 setter,只是Barpublic setter 上有一個特殊的IsExternalInit屬性。

區別是編譯器控制的。 唯一可以設置init屬性的時間是在構造期間或在object 初始化程序中。

讓我們看一個更復雜的例子:

public class Electricity
{
    private const double _amps = 4.2;
    public double Amps => _amps;
    
    private readonly double _power;
    public double Power => _power;
    
    private double _volts;
    public double Volts
    {
        get => _volts;
        init
        {
            _volts = value * 2;
            _power = value * _amps;
            //_amps = 99.0; // Not allowed! Can only be set by direct assignment - effectively prior to construction!
        }
    }
    
    public Electricity(double volts)
    {
        this.Volts = volts * 5;
        _power = 9.2;
        //_amps = 42.0; // Not allowed! Can only be set by direct assignment - effectively prior to construction!
    }
    
    public void Danger()
    {
        //_amps = 0.0; // Not allowed! Can only be set by direct assignment - effectively prior to construction!
        //_power = 0.0; // Not allowed! Can only be set in constructor or init
        //this.Volts = -1.0; // Not allowed! Can only be set in constructor or init
        _volts = 0.0; // Allowed!
    }
}

有了這個,我可以使用以下代碼創建一個實例:

Electricity e = new Electricity(1.0) { Volts = 2.0 };

現在這有效地調用new Electricity(1.0)來創建 class 的實例,並且由於它是唯一的構造函數,我不得不使用volts的參數來調用它。 請注意,在構造函數內部,我可以調用this.Volts = volts * 5; .

在賦值給e之前,調用初始化塊中的代碼。 它只是將2.0分配給Volts - 它直接等效於e.Volts = 2.0; 如果我們沒有使用init setter 聲明Volts

結果是,當e被賦值時,構造函數和對設置Volts的調用都已完成。

現在讓我們試着讓這個Electricity代碼更加健壯。 假設我希望能夠設置任意兩個屬性並讓代碼計算第三個。

一個天真的和不正確的實現是這樣的:

public class Electricity
{
    public double Volts { get; private set; }
    public double Amps { get; private set; }
    public double Watts => this.Volts * this.Amps;

    public Electricity(double volts, double amps)
    {
        this.Volts = volts;
        this.Amps = amps;
    }

    public Electricity(double volts, double watts)
    {
        this.Volts = volts;
        this.Amps = watts / this.Volts;
    }

    public Electricity(double amps, double watts)
    {
        this.Amps = amps;
        this.Volts = watts / this.Amps;
    }
}

但是,當然,這不會編譯,因為三個構造函數簽名是相同的。

但是我們可以使用init來制作一個 object,無論我設置什么屬性(除了下面示例中的Watts ),它都可以工作。

public class Electricity
{
    private readonly double? _volts = null;
    private readonly double? _amps = null;
    private readonly double? _watts = null;

    public double Volts
    {
        get => _volts ?? 0.0;
        init
        {
            _volts = value;
            if (_amps.HasValue)
            {
                _watts = _volts * _amps;
            }
            else if (_watts.HasValue)
            {
                _amps = _watts / _volts;
            }
        }
    }

    public double Amps
    {
        get => _amps ?? 0.0;
        init
        {
            _amps = value;
            if (_volts.HasValue)
            {
                _watts = _volts * _amps;
            }
            else if (_watts.HasValue)
            {
                _volts = _watts / _amps;
            }
        }
    }

    public double Watts
    {
        get => _watts ?? 0.0;
        init
        {
            _watts = value;
            if (_volts.HasValue)
            {
                _amps = _watts / _volts;
            }
            else if (_amps.HasValue)
            {
                _volts = _watts / _amps;
            }
        }
    }
}

我現在可以這樣做:

var electricities = new[]
{
    new Electricity() { Amps = 2.0, Volts = 3.0 },
    new Electricity() { Watts = 2.0, Volts = 3.0 },
    new Electricity() { Amps = 2.0, Watts = 3.0 },
    new Electricity(),
};

這給了我:

電力

因此,最終結果是構造函數是強制性的,但init屬性是可選的,但必須在構造時使用,然后才能將任何引用傳遞回調用代碼。

這個想法是允許您知道的只讀屬性僅在 object 初始化期間具有寫入值,但在初始化之后無法寫入該值。

如果您定義一個set方法而不是init ,則可以在對象的生命周期內的任何時間寫入該值。

這允許您初始化屬性,而無需將其作為參數直接傳遞給構造函數。

例如:

class A
{
    public int X { get; init;} 
} 

允許這樣做:

A a = new A() 
{
    X = 3,
};

之后嘗試寫入X將無法編譯:

a.X = 5;

給出編譯錯誤:

CS8852  Init-only property or indexer 'A.X' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.

如果您不使用init ,而是使用set ,那么您的 object 可以隨時寫入。

如果您在沒有init方法的情況下定義只讀屬性,您仍然可以在構造函數中初始化該屬性,但是您必須將參數傳遞給構造函數。 所以使用init的決定更多地取決於你的代碼樣式,你在哪里保存初始化字段的參數,以及你是否喜歡將它傳遞給構造函數。

請注意,這與字段訪問無關。 如果init定義為public ,則可以從 class 外部對其進行初始化。 它也可以聲明為privateprotected ,然后可以分別從 class 或派生類訪問它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM