Sdílet prostřednictvím


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 structmůž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:

  1. Parametry primárního konstruktoru nemusí být uloženy, pokud nejsou potřeba.
  2. 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ý jako this.param.
  3. Primární parametry konstruktoru lze přiřadit.
  4. 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:

  1. Jako argument vyvolání konstruktoru base() .
  2. Inicializace pole nebo vlastnosti člena
  3. 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 dxdy 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 : accountIDowner 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.