Delen via


Primaire constructors declareren voor klassen en structs

C# 12 introduceert primaire constructors, die een beknopte syntaxis bieden om constructors te declareren waarvan de parameters overal in de hoofdtekst van het type beschikbaar zijn.

In dit artikel wordt beschreven hoe u een primaire constructor kunt declareren voor uw type en waar primaire constructorparameters moeten worden opgeslagen. U kunt primaire constructors aanroepen vanuit andere constructors en primaire constructorparameters gebruiken in leden van het type.

Benodigdheden

Begrijp de regels voor primaire constructors

U kunt parameters toevoegen aan een struct of class declaratie om een primaire constructorte maken. Primaire constructorparameters vallen binnen het bereik van de klassedefinitie. Het is belangrijk om primaire constructorparameters weer te geven als parameters, zelfs als ze binnen het bereik van de klassedefinitie vallen.

Verschillende regels verduidelijken dat deze constructors parameters zijn:

  • Primaire constructorparameters worden mogelijk niet opgeslagen als ze niet nodig zijn.
  • Primaire constructorparameters zijn geen leden van de klasse. Een primaire constructorparameter met de naam param kan bijvoorbeeld niet worden geopend als this.param.
  • Primaire constructorparameters kunnen waarden toegewezen krijgen.
  • Primaire constructorparameters vormen geen eigenschappen, behalve in record typen.

Deze regels zijn dezelfde regels die al zijn gedefinieerd voor parameters voor elke methode, inclusief andere constructordeclaraties.

Dit zijn de meest voorkomende toepassingen voor een primaire constructorparameter:

  • Doorgeven als een argument aan een base() constructoraanroep
  • Een lidveld of eigenschap initialiseren
  • Verwijzen naar de constructorparameter in een instantielid

Elke andere constructor voor een klasse moet de primaire constructor rechtstreeks of indirect aanroepen via een this() constructor-aanroep. Deze regel zorgt ervoor dat er overal in de hoofdtekst van het type primaire constructorparameters worden toegewezen.

Onveranderbare eigenschappen of velden initialiseren

Met de volgende code worden twee leesbare (onveranderbare) eigenschappen geïnitialiseerd die worden berekend op basis van primaire constructorparameters:

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

In dit voorbeeld wordt een primaire constructor gebruikt om berekende alleen-lezen eigenschappen te initialiseren. De velditiitiizers voor de eigenschappen Magnitude en Direction maken gebruik van de primaire constructorparameters. De primaire constructorparameters worden nergens anders in de struct gebruikt. De code maakt een struct alsof deze op de volgende manier is geschreven:

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

Met deze functie kunt u gemakkelijker veld initialisatiefuncties gebruiken wanneer u argumenten nodig hebt om een veld of eigenschap te initialiseren.

Veranderlijke status maken

In de vorige voorbeelden worden primaire constructorparameters gebruikt om alleen-lezen eigenschappen te initialiseren. U kunt ook primaire constructors gebruiken voor eigenschappen die niet readonly zijn.

Houd rekening met de volgende code:

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

In dit voorbeeld wordt met de methode Translate de dx- en dy-onderdelen gewijzigd. Hiervoor moeten de Magnitude- en Direction-eigenschappen worden berekend wanneer ze worden geopend. De lambda-operator (=>) wijst een toegangsmiddel met expressies get aan, terwijl de gelijk-aan-operator (=) een initialisatiefunctie aanwijst.

Deze versie van de code voegt een parameterloze constructor toe aan de struct. De parameterloze constructor moet de primaire constructor aanroepen, waardoor alle primaire constructorparameters worden geïnitialiseerd. De eigenschappen van de primaire constructor worden geopend in een methode en de compiler maakt verborgen velden om elke parameter weer te geven.

De volgende code laat een benadering zien van wat de compiler genereert. De werkelijke veldnamen zijn geldige CIL-id's (Common Intermediate Language), maar geen geldige C#-id's.

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

Door compiler gemaakte opslag

Voor het eerste voorbeeld in deze sectie hoeft de compiler geen veld te maken om de waarde van de primaire constructorparameters op te slaan. In het tweede voorbeeld wordt de primaire constructorparameter echter gebruikt in een methode, dus moet de compiler opslag voor de parameters maken.

De compiler maakt alleen opslag voor primaire constructors wanneer de parameter wordt geopend in de hoofdtekst van een lid van uw type. Anders worden de primaire constructorparameters niet opgeslagen in het object.

Afhankelijkheidsinjectie gebruiken

Een ander veelvoorkomend gebruik voor primaire constructors is het opgeven van parameters voor afhankelijkheidsinjectie. Met de volgende code maakt u een eenvoudige controller waarvoor een service-interface is vereist voor het gebruik ervan:

public interface IService
{
    Distance GetDistance();
}

public class ExampleController(IService service) : ControllerBase
{
    [HttpGet]
    public ActionResult<Distance> Get()
    {
        return service.GetDistance();
    }
}

De primaire constructor geeft duidelijk de parameters aan die nodig zijn in de klasse. U gebruikt de primaire constructorparameters zoals elke andere variabele in de klasse.

Basisklasse initialiseren

U kunt de primaire constructor voor een basisklasse aanroepen vanuit de primaire constructor van de afgeleide klasse. Deze methode is de eenvoudigste manier om een afgeleide klasse te schrijven die een primaire constructor in de basisklasse moet aanroepen. Overweeg een hiërarchie van klassen die verschillende accounttypen vertegenwoordigen als bank. De volgende code laat zien hoe de basisklasse eruit kan zien:

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

Alle bankrekeningen, ongeacht het type, hebben eigenschappen voor het rekeningnummer en de eigenaar. In de voltooide toepassing kunt u andere algemene functionaliteit toevoegen aan de basisklasse.

Veel typen vereisen specifiekere validatie voor constructorparameters. De klasse BankAccount heeft bijvoorbeeld specifieke vereisten voor de parameters owner en accountID. De parameter owner mag niet null of witruimte zijn en de parameter accountID moet een tekenreeks met tien cijfers zijn. U kunt deze validatie toevoegen wanneer u de bijbehorende eigenschappen toewijst:

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

In dit voorbeeld ziet u hoe u de constructorparameters valideert voordat u deze aan de eigenschappen toewijst. U kunt ingebouwde methoden gebruiken, zoals String.IsNullOrWhiteSpace(String) of uw eigen validatiemethode, zoals ValidAccountNumber. In het voorbeeld worden eventuele uitzonderingen gegenereerd vanuit de constructor, wanneer deze de initialisatieprogramma's aanroept. Als een constructorparameter niet wordt gebruikt om een veld toe te wijzen, worden er uitzonderingen gegenereerd wanneer de constructorparameter voor het eerst wordt geopend.

Een afgeleide klasse kan een betaalrekening vertegenwoordigen.

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

De afgeleide CheckingAccount-klasse heeft een primaire constructor die alle parameters gebruikt die nodig zijn in de basisklasse en een andere parameter met een standaardwaarde. De primaire constructor roept de basisconstructor aan met de : BankAccount(accountID, owner) syntax. Deze expressie geeft zowel het type voor de basisklasse als de argumenten voor de primaire constructor op.

Uw afgeleide klasse is niet vereist om een primaire constructor te gebruiken. U kunt een constructor maken in de afgeleide klasse die de primaire constructor voor de basisklasse aanroept, zoals wordt weergegeven in het volgende voorbeeld:

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

Er is één potentieel probleem met klassehiërarchieën en primaire constructors. Het is mogelijk om meerdere kopieën van een primaire constructorparameter te maken, omdat de parameter wordt gebruikt in zowel afgeleide als basisklassen. Met de volgende code worden twee kopieën gemaakt van elk van de parameters owner en 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}";
}

De gemarkeerde regel in dit voorbeeld laat zien dat de ToString methode gebruikmaakt van de primaire constructorparameters (owner en accountID) in plaats van de eigenschappen van de basisklasse (Owner en AccountID). Het resultaat is dat de afgeleide klasse, SavingsAccount, opslag maakt voor de parameterkopieën. De kopie in de afgeleide klasse verschilt van de eigenschap in de basisklasse. Als de eigenschap basisklasse kan worden gewijzigd, ziet het exemplaar van de afgeleide klasse de wijziging niet. De compiler geeft een waarschuwing uit voor primaire constructorparameters die worden gebruikt in een afgeleide klasse en worden doorgegeven aan een basisklasseconstructor. In dit geval is de oplossing het gebruik van de eigenschappen van de basisklasse.