Sdílet prostřednictvím


Deklarace primárních konstruktorů pro třídy a struktury

C# 12 zavádí primární konstruktory, které poskytují stručnou syntaxi pro deklaraci konstruktorů, jejichž parametry jsou k dispozici kdekoli v těle typu.

Tento článek popisuje, jak deklarovat primární konstruktor ve vašem typu a rozpoznat, kam se mají ukládat parametry primárního konstruktoru. Primární konstruktory můžete volat z jiných konstruktorů a parametry primárního konstruktoru použít ve vlastnostech typu.

Požadavky

Porozumět pravidlům pro primární konstruktory

Do deklarace struct nebo class můžete přidat parametry pro vytvoření primárního konstruktoru. Parametry primárního konstruktoru jsou dostupné v průběhu celé definice třídy. Parametry primárního konstruktoru je důležité chápat jako parametry , i když jsou v platnosti v rámci definice třídy.

Několik pravidel objasňuje, že tyto konstruktory 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 k primárnímu parametru konstruktoru s názvem param není možné přistupovat jako this.param.
  • Primární parametry konstruktoru lze přiřadit.
  • Parametry primárního konstruktoru se nestanou vlastnostmi, s výjimkou záznamových typů .

Tato pravidla jsou stejná pravidla, která jsou již definována pro parametry jakékoli metody, včetně jiných deklarací konstruktoru.

Tady jsou nejběžnější použití parametru primárního konstruktoru:

  • Předání argumentu volání konstruktoru base()
  • Inicializovat členské pole nebo vlastnost
  • Odkazovat na parametr konstruktoru v členu instance

Každý další konstruktor třídy musí volat primární konstruktor, a to přímo nebo nepřímo, prostřednictvím vyvolání konstruktoru this(). Toto pravidlo zajišťuje, aby se parametry primárního konstruktoru přiřadily všude v těle typu.

Inicializace neměnných vlastností nebo polí

Následující kód inicializuje dvě vlastnosti jen pro čtení (neměnné), které se počítají 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);
}

Tento příklad používá primární konstruktor k inicializaci výpočtových vlastností readonly. Inicializátory polí pro vlastnosti Magnitude a Direction používají parametry primárního konstruktoru. Parametry primárního konstruktoru se ve struktuře nepoužívají nikde jinde. Kód vytvoří strukturu, jako by byla napsána následujícím způsobem:

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);
    }
}

Tato funkce usnadňuje použití inicializátorů polí v případě, že potřebujete argumenty k 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é pro vlastnosti, které nejsou jen pro čtení.

Vezměte v úvahu 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 tomto příkladu metoda Translate změní komponenty dx a dy, které vyžadují, aby se při přístupu počítaly vlastnosti Magnitude a Direction. Operátor lambda (=>) určuje přístupový objekt s body výrazu get , zatímco operátor equal-to (=) označuje inicializátor.

Tato verze kódu přidá konstruktor bez parametrů do struktury. Konstruktor bez parametrů musí vyvolat primární konstruktor, který zajišťuje inicializaci všech parametrů primárního konstruktoru. Vlastnosti primárního konstruktoru jsou přístupné v metodě a kompilátor vytvoří skrytá pole představující každý parametr.

Následující kód ukazuje aproximaci toho, co kompilátor generuje. Skutečné názvy polí jsou platné identifikátory CIL (Common Intermediate Language), 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) { }
}

Úložiště vytvořené kompilátorem

V prvním příkladu v této části kompilátor nemusel vytvořit pole pro uložení hodnoty parametrů primárního konstruktoru. V druhém příkladu se však primární parametr konstruktoru používá uvnitř metody, takže kompilátor musí vytvořit úložiště pro parametry.

Kompilátor vytvoří úložiště pro všechny primární konstruktory pouze v případě, že je 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.

Injektování 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ě uvádí parametry potřebné pro třídu. 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. Tento přístup je nejjednodušší způsob, jak napsat odvozenou třídu, která musí vyvolat primární konstruktor v základní třídě. Představte si hierarchii tříd, které představují různé typy účtů jako banku. Následující kód ukazuje, jak může základní třída vypadat:

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 pro číslo účtu a vlastníka. V dokončené aplikaci můžete do základní třídy přidat další běžné funkce.

Mnoho typů vyžaduje konkrétnější ověřování u parametrů konstruktoru. Například třída BankAccount má specifické požadavky na parametry owner a accountID. Parametr owner nesmí být null ani prázdné znaky a parametr accountID musí 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));
}

Tento příklad ukazuje, jak ověřit parametry konstruktoru před jejich přiřazením k vlastnostem. Můžete použít předdefinované metody, jako je String.IsNullOrWhiteSpace(String) nebo vlastní metoda ověřování, například ValidAccountNumber. V příkladu jsou jakékoli výjimky vyvolány z konstruktoru, když ten zavolá 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 může představovat 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 další parametr s výchozí hodnotou. Primární konstruktor volá základní konstruktor se syntaxí : BankAccount(accountID, owner). Tento výraz určuje typ základní třídy i argumenty primárního konstruktoru.

Vaše odvozená třída nemusí použít primární konstruktor. 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 parametr se používá v odvozených i základních třídách. Následující kód vytvoří dvě kopie každého z owner a accountID parametrů:

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 v tomto příkladu ukazuje, že metoda ToString používá primární konstruktor parametry (owner a accountID) místo vlastností základní třídy (Owner a AccountID). Výsledkem je, že odvozená třída, SavingsAccount, vytvoří úložiště pro kopie parametrů. Kopie v odvozené třídě se liší od vlastnosti v základní třídě. Pokud lze upravit vlastnost základní třídy, instance odvozené třídy nevidí změnu. 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 řešením využití vlastností základní třídy.