C# 12 引進主要建 構函式,提供簡潔的語法來宣告函式,其參數可在類型主體中的任何位置使用。
本文說明如何在您的類型上宣告主要建構函式,並辨識儲存主要建構函式參數的位置。 您可以從其他建構函式呼叫主要建構函式,並在型別的成員中使用主要建構函式參數。
先決條件
- 最新 .NET SDK
- Visual Studio Code 編輯器
- C# 開發套件
瞭解主要建構函式的規則
您可以將參數新增至 struct 或 class 宣告,以建立 主要建構函式。 主要建構函式參數在類別定義的範圍內。 請務必將主要建構函式的參數視為 參數,,儘管它們在整個類別定義中都在範圍內。
數個規則會釐清這些建構函式是參數:
- 如果不需要主要建構函式參數,則可能不會儲存這些參數。
- 主要建構函式參數不是 類別的成員。 例如,名為
param的主要建構函式參數無法被this.param存取。 - 主要建構函式參數可以指派給 。
- 除了 記錄 類型之外,主要建構函式參數不會變成屬性。
這些規則與已針對任何方法的參數所定義的規則相同,包括其他建構函式宣告。
以下是主要建構函式參數最常見的用法:
- 將參數傳遞給
base()建構函式呼叫 - 初始化成員欄位或屬性
- 參考實例成員中的建構函式參數
類別的所有其他建構函式 呼叫主要建構函式。 此規則可確保主要建構函式參數會在類型主體的任何地方指派。
初始化不可變的屬性或欄位
下列程式代碼會初始化從主要建構函式參數計算的兩個唯讀 (不可變) 屬性:
public readonly struct Distance(double dx, double dy)
{
public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction { get; } = Math.Atan2(dy, dx);
}
這個範例會使用主要建構函式來初始化計算的唯讀屬性。
Magnitude 和 Direction 屬性的欄位初始化運算式會使用主要建構函式參數。 結構中的其他位置都不會使用主要建構函式參數。 程式碼會建立一個結構,彷彿按照以下方式撰寫:
public readonly struct Distance
{
public readonly double Magnitude { get; }
public readonly double Direction { get; }
public Distance(double dx, double dy)
{
Magnitude = Math.Sqrt(dx * dx + dy * dy);
Direction = Math.Atan2(dy, dx);
}
}
當您需要自變數來初始化欄位或屬性時,這項功能可讓您更輕鬆地使用欄位初始化表達式。
建立可變狀態
上述範例會使用主要建構函式參數來初始化只讀屬性。 您也可以針對非唯讀屬性使用主建構函式。
請考慮下列程式碼:
public struct Distance(double dx, double dy)
{
public readonly double Magnitude => Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction => Math.Atan2(dy, dx);
public void Translate(double deltaX, double deltaY)
{
dx += deltaX;
dy += deltaY;
}
public Distance() : this(0,0) { }
}
在此範例中,方法 Translate 會變更 dx 和 dy 元件,這需要在存取 Magnitude 和 Direction 屬性時進行計算。 Lambda 運算子(=>)會指定為表示式主體的 get 存取子,而等於運算子(=)則指定初始化表達式。
此版本的程式代碼會將無參數建構函式新增至 結構。 無參數建構函式必須叫用主要建構函式,以確保所有主要建構函式參數都會初始化。 主要建構函式屬性是在 方法中存取,編譯程式會建立隱藏欄位來代表每個參數。
下列程式代碼示範編譯程式所產生的近似值。 實際的欄位名稱是有效的通用中繼語言(CIL)識別碼,但不是有效的 C# 識別碼。
public struct Distance
{
private double __unspeakable_dx;
private double __unspeakable_dy;
public readonly double Magnitude => Math.Sqrt(__unspeakable_dx * __unspeakable_dx + __unspeakable_dy * __unspeakable_dy);
public readonly double Direction => Math.Atan2(__unspeakable_dy, __unspeakable_dx);
public void Translate(double deltaX, double deltaY)
{
__unspeakable_dx += deltaX;
__unspeakable_dy += deltaY;
}
public Distance(double dx, double dy)
{
__unspeakable_dx = dx;
__unspeakable_dy = dy;
}
public Distance() : this(0, 0) { }
}
編譯程式建立的記憶體
針對本節中的第一個範例,編譯程式不需要建立字段來儲存主要建構函式參數的值。 不過,在第二個範例中,主要建構函式參數會在方法內使用,因此編譯程式必須建立參數的記憶體。
編譯器僅在存取類別成員的程式碼區塊中的參數時,為任何基本建構函數創建儲存空間。 否則,主要建構函式參數不會儲存在物件中。
使用依賴注入
主要建構函式的另一個常見用途是指定相依性插入的參數。 下列程式代碼會建立需要服務介面的簡單控制器,以供其使用:
public interface IService
{
Distance GetDistance();
}
public class ExampleController(IService service) : ControllerBase
{
[HttpGet]
public ActionResult<Distance> Get()
{
return service.GetDistance();
}
}
主要建構函式清楚指出 類別中所需的參數。 您可以使用主要建構函式參數,如同 類別中的其他任何變數一樣。
初始化基類
您可以從衍生類別的主建構函式調用基類的主建構函式。 此方法是撰寫衍生類別的最簡單方式,該類別必須在基類中叫用主要建構函式。 請考慮一個代表不同銀行帳戶類型的類別階層。 下列程式碼顯示基類可能的外觀:
public class BankAccount(string accountID, string owner)
{
public string AccountID { get; } = accountID;
public string Owner { get; } = owner;
public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}
不論類型為何,所有銀行帳戶都有帳戶號碼和擁有者的屬性。 在已完成的應用程式中,您可以將其他一般功能新增至基類。
許多類型需要對建構函式參數進行更具體的驗證。 例如,BankAccount類別對owner和accountID參數有特定的要求。 參數 owner 不得為 null 或空格符,而且 accountID 參數必須是包含10位數的字串。 當您指派對應的屬性時,您可以新增此驗證:
public class BankAccount(string accountID, string owner)
{
public string AccountID { get; } = ValidAccountNumber(accountID)
? accountID
: throw new ArgumentException("Invalid account number", nameof(accountID));
public string Owner { get; } = string.IsNullOrWhiteSpace(owner)
? throw new ArgumentException("Owner name cannot be empty", nameof(owner))
: owner;
public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
public static bool ValidAccountNumber(string accountID) =>
accountID?.Length == 10 && accountID.All(c => char.IsDigit(c));
}
此範例示範如何在您將建構函式參數指派給屬性之前先驗證建構函式參數。 您可以使用內建方法,例如 String.IsNullOrWhiteSpace(String) 或您自己的驗證方法, 例如 ValidAccountNumber。 在此範例中,當建構函式叫用初始化表達式時,會從建構函式擲回任何例外狀況。 如果未使用建構函式參數來指派欄位,則第一次存取建構函式參數時,會擲回任何例外狀況。
某個衍生類別可能表示支票帳戶:
public class CheckingAccount(string accountID, string owner, decimal overdraftLimit = 0) : BankAccount(accountID, owner)
{
public decimal CurrentBalance { get; private set; } = 0;
public void Deposit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
}
CurrentBalance += amount;
}
public void Withdrawal(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
}
if (CurrentBalance - amount < -overdraftLimit)
{
throw new InvalidOperationException("Insufficient funds for withdrawal");
}
CurrentBalance -= amount;
}
public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}, Balance: {CurrentBalance}";
}
衍生 CheckingAccount 類別具有主要建構函式,其採用基類所需的所有參數,以及具有預設值的另一個參數。 主要建構函式會使用 : BankAccount(accountID, owner) 語法呼叫基底建構函式。 這個表達式同時指定基類的類型和主要建構函式的自變數。
您的衍生類別不需要使用主要建構函式。 您可以在衍生類別中建立建構函式,以叫用基類的主要建構函式,如下列範例所示:
public class LineOfCreditAccount : BankAccount
{
private readonly decimal _creditLimit;
public LineOfCreditAccount(string accountID, string owner, decimal creditLimit) : base(accountID, owner)
{
_creditLimit = creditLimit;
}
public decimal CurrentBalance { get; private set; } = 0;
public void Deposit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
}
CurrentBalance += amount;
}
public void Withdrawal(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
}
if (CurrentBalance - amount < -_creditLimit)
{
throw new InvalidOperationException("Insufficient funds for withdrawal");
}
CurrentBalance -= amount;
}
public override string ToString() => $"{base.ToString()}, Balance: {CurrentBalance}";
}
類別階層和主要建構函式有一個潛在問題。 可以建立主要建構函式參數的多個複本,因為 參數同時用於衍生類別和基類中。 下列程式代碼會為owner參數和accountID參數各建立兩個複本:
public class SavingsAccount(string accountID, string owner, decimal interestRate) : BankAccount(accountID, owner)
{
public SavingsAccount() : this("default", "default", 0.01m) { }
public decimal CurrentBalance { get; private set; } = 0;
public void Deposit(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
}
CurrentBalance += amount;
}
public void Withdrawal(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
}
if (CurrentBalance - amount < 0)
{
throw new InvalidOperationException("Insufficient funds for withdrawal");
}
CurrentBalance -= amount;
}
public void ApplyInterest()
{
CurrentBalance *= 1 + interestRate;
}
public override string ToString() => $"Account ID: {accountID}, Owner: {owner}, Balance: {CurrentBalance}";
}
此範例中反白的行顯示,ToString方法使用的是主要建構函式參數(owner和accountID),而非基類屬性(Owner和AccountID)。 結果是衍生類別 SavingsAccount, 會建立參數複本的記憶體。 衍生類別中的複本與基類中的 屬性不同。 如果可以修改基類屬性,則衍生類別的實例不會看到修改。 編譯程式會針對衍生類別中使用的主要建構函式參數發出警告,並傳遞至基類建構函式。 在此實例中,修補措施是使用基類的屬性。