Kurz: Prozkoumání primárních konstruktorů
C# 12 zavádí primární konstruktory, stručnou syntaxi pro deklaraci konstruktorů, jejichž parametry jsou k dispozici kdekoli v těle typu.
V tomto kurzu se dozvíte:
- Kdy deklarovat primární konstruktor ve vašem typu
- Jak volat primární konstruktory z jiných konstruktorů
- Jak používat parametry primárního konstruktoru ve členech typu
- Kde jsou uloženy parametry primárního konstruktoru
Požadavky
Musíte nastavit počítač tak, aby běžel .NET 8 nebo novější, včetně kompilátoru C# 12 nebo novějšího. Kompilátor C# 12 je k dispozici od sady Visual Studio 2022 verze 17.7 nebo sady .NET 8 SDK.
Primární konstruktory
K vytvoření primárního konstruktoru struct
můžete přidat parametry nebo class
deklaraci. Parametry primárního konstruktoru jsou v oboru v rámci definice třídy. Parametry primárního konstruktoru je důležité zobrazit jako parametry , i když jsou v oboru v rámci definice třídy. Několik pravidel objasňuje, že jsou parametry:
- Parametry primárního konstruktoru nemusí být uloženy, pokud nejsou potřeba.
- Parametry primárního konstruktoru nejsou členy třídy. Například primární parametr konstruktoru s názvem
param
nemůže být přístupný jakothis.param
. - Primární parametry konstruktoru lze přiřadit.
- Parametry primárního konstruktoru se nestanou vlastnostmi, s výjimkou
record
typů.
Tato pravidla jsou stejná jako parametry jakékoli metody, včetně jiných deklarací konstruktoru.
Nejběžnějšími způsoby použití primárního parametru konstruktoru jsou:
- Jako argument vyvolání konstruktoru
base()
. - Inicializace pole nebo vlastnosti člena
- Odkazování na parametr konstruktoru v členu instance.
Každý druhý konstruktor třídy musí volat primární konstruktor přímo nebo nepřímo prostřednictvím vyvolání konstruktoru this()
. Toto pravidlo zajišťuje, aby byly primární parametry konstruktoru přiřazeny kdekoli v těle typu.
Inicializace vlastnosti
Následující kód inicializuje dvě vlastnosti jen pro čtení vypočítané z parametrů primárního konstruktoru:
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);
}
Předchozí kód ukazuje primární konstruktor použitý k inicializaci počítaných vlastností jen pro čtení. Inicializátory polí pro Magnitude
a Direction
používají primární parametry konstruktoru. Parametry primárního konstruktoru se ve struktuře nepoužívají nikde jinde. Předchozí struktura je, jako byste napsali následující kód:
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);
}
}
Nová funkce usnadňuje použití inicializátorů polí, když potřebujete argumenty pro inicializaci pole nebo vlastnosti.
Vytvoření proměnlivých stavů
Předchozí příklady používají parametry primárního konstruktoru k inicializaci vlastností jen pro čtení. Primární konstruktory můžete použít také v případech, kdy nejsou vlastnosti jen pro čtení. Uvažujte následující kód:
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) { }
}
V předchozím příkladu Translate
metoda změní a dx
dy
komponenty. To vyžaduje, aby se při Magnitude
přístupu počítaly vlastnosti a Direction
vlastnosti. Operátor =>
určuje přístupový objekt s bodyied get
výrazu, zatímco =
operátor určuje inicializátor. Tato verze přidá konstruktor bez parametrů do struktury. Konstruktor bez parametrů musí vyvolat primární konstruktor, aby byly inicializovány všechny parametry primárního konstruktoru.
V předchozím příkladu jsou vlastnosti primárního konstruktoru přístupné v metodě. Kompilátor proto vytvoří skrytá pole, která představují každý parametr. Následující kód ukazuje přibližně to, co kompilátor generuje. Skutečné názvy polí jsou platné identifikátory CIL, ale nejsou platné identifikátory jazyka 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) { }
}
Je důležité si uvědomit, že první příklad nepožadoval, aby kompilátor vytvořil pole pro uložení hodnoty parametrů primárního konstruktoru. Druhý příklad použil parametr primárního konstruktoru uvnitř metody, a proto vyžadoval kompilátor, aby pro ně vytvořil úložiště. Kompilátor vytvoří úložiště pro všechny primární konstruktory pouze v případě, že je tento parametr přístupný v těle člena vašeho typu. V opačném případě nejsou parametry primárního konstruktoru uloženy v objektu.
Injektáž závislostí
Dalším běžným použitím primárních konstruktorů je zadání parametrů pro injektáž závislostí. Následující kód vytvoří jednoduchý kontroler, který vyžaduje rozhraní služby pro jeho použití:
public interface IService
{
Distance GetDistance();
}
public class ExampleController(IService service) : ControllerBase
{
[HttpGet]
public ActionResult<Distance> Get()
{
return service.GetDistance();
}
}
Primární konstruktor jasně označuje parametry potřebné ve třídě. Parametry primárního konstruktoru použijete stejně jako jakoukoli jinou proměnnou ve třídě.
Inicializace základní třídy
Primární konstruktor základní třídy můžete vyvolat z primárního konstruktoru odvozené třídy. Je to nejjednodušší způsob, jak napsat odvozenou třídu, která musí vyvolat primární konstruktor v základní třídě. Představte si například hierarchii tříd, které představují různé typy účtů jako banku. Základní třída by vypadala přibližně takto:
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}";
}
Všechny bankovní účty bez ohledu na typ mají vlastnosti čísla účtu a vlastníka. V dokončené aplikaci by se do základní třídy přidaly další běžné funkce.
Mnoho typů vyžaduje konkrétnější ověřování u parametrů konstruktoru. Například BankAccount
má specifické požadavky na parametry owner
: accountID
owner
Nesmí být null
nebo prázdné znaky a accountID
musí to být řetězec obsahující 10 číslic. Toto ověření můžete přidat při přiřazování odpovídajících vlastností:
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));
}
Předchozí příklad ukazuje, jak můžete ověřit parametry konstruktoru před jejich přiřazením k vlastnostem. Můžete použít předdefinované metody, například String.IsNullOrWhiteSpace(String), nebo vlastní metodu ověřování, například ValidAccountNumber
. V předchozím příkladu jsou všechny výjimky vyvolány z konstruktoru, když vyvolá inicializátory. Pokud se k přiřazení pole nepoužívá parametr konstruktoru, při prvním přístupu k parametru konstruktoru se vyvolá všechny výjimky.
Jedna odvozená třída by představila kontrolní účet:
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}";
}
Odvozená CheckingAccount
třída má primární konstruktor, který přebírá všechny parametry potřebné v základní třídě a jiný parametr s výchozí hodnotou. Primární konstruktor volá základní konstruktor pomocí : BankAccount(accountID, owner)
syntaxe. Tento výraz určuje typ základní třídy i argumenty primárního konstruktoru.
Odvozená třída není nutná k použití primárního konstruktoru. V odvozené třídě můžete vytvořit konstruktor, který vyvolá primární konstruktor základní třídy, jak je znázorněno v následujícím příkladu:
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}";
}
Existuje jeden potenciální problém s hierarchiemi tříd a primárními konstruktory: je možné vytvořit více kopií parametru primárního konstruktoru, protože se používá v odvozených i základních třídách. Následující příklad kódu vytvoří dvě kopie každého pole owner
a accountID
pole:
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}";
}
Zvýrazněný řádek ukazuje, že ToString
metoda používá primární parametry konstruktoru (owner
a accountID
) místo vlastností základní třídy (Owner
a AccountID
). Výsledkem je, SavingsAccount
že odvozená třída vytvoří úložiště pro tyto kopie. Kopie v odvozené třídě se liší od vlastnosti v základní třídě. Pokud lze vlastnost základní třídy upravit, instance odvozené třídy neuvidí danou úpravu. Kompilátor vydává upozornění pro primární parametry konstruktoru, které se používají v odvozené třídě a předané konstruktoru základní třídy. V tomto případě je oprava použití vlastností základní třídy.
Shrnutí
Primární konstruktory můžete použít tak, aby nejlépe vyhovovaly vašemu návrhu. U tříd a struktur jsou parametry primárního konstruktoru parametry konstruktoru, který musí být vyvolán. Můžete je použít k inicializaci vlastností. Pole můžete inicializovat. Tyto vlastnosti nebo pole můžou být neměnné nebo proměnlivé. Můžete je použít v metodách. Jsou to parametry a vy je použijete způsobem, který nejlépe vyhovuje vašemu designu. Další informace o primárních konstruktorech najdete v článku průvodce programováním v jazyce C# o konstruktorech instancí a navrhované specifikaci primárního konstruktoru.