Delen via


Zelfstudie: Primaire constructors verkennen

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

In deze zelfstudie leert u het volgende:

  • Wanneer moet u een primaire constructor declareren voor uw type
  • Primaire constructors aanroepen van andere constructors
  • Primaire constructorparameters gebruiken in leden van het type
  • Waar primaire constructorparameters worden opgeslagen

Vereisten

U moet uw computer instellen om .NET 8 of hoger uit te voeren, inclusief de C# 12- of hoger compiler. De C# 12-compiler is beschikbaar vanaf Visual Studio 2022 versie 17.7 of de .NET 8 SDK.

Primaire constructors

U kunt parameters toevoegen aan een struct of class declaratie om een primaire constructor te maken. Primaire constructorparameters vallen binnen het bereik van de klassedefinitie. Het is belangrijk om primaire constructorparameters weer te geven als parameters , ook al bevinden ze zich binnen het bereik van de klassedefinitie. Verschillende regels verduidelijken dat het parameters zijn:

  1. Primaire constructorparameters worden mogelijk niet opgeslagen als ze niet nodig zijn.
  2. Primaire constructorparameters zijn geen leden van de klasse. Een primaire constructorparameter met de naam param kan bijvoorbeeld niet worden geopend als this.param.
  3. Primaire constructorparameters kunnen worden toegewezen aan.
  4. Primaire constructorparameters worden geen eigenschappen, behalve in record typen.

Deze regels zijn hetzelfde als parameters voor elke methode, inclusief andere constructordeclaraties.

De meest voorkomende toepassingen voor een primaire constructorparameter zijn:

  1. Als argument voor een aanroep van een base() constructor.
  2. Een lidveld of eigenschap initialiseren.
  3. Verwijst naar de constructorparameter in een exemplaarlid.

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

Eigenschap initialiseren

Met de volgende code worden twee alleen-lezen 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);
}

De voorgaande code demonstreert een primaire constructor die wordt gebruikt om berekende alleen-lezen-eigenschappen te initialiseren. De veld-initialisatieprogramma's voor Magnitude en Direction gebruiken de primaire constructorparameters. De primaire constructorparameters worden nergens anders in de struct gebruikt. De voorgaande struct is alsof u de volgende code hebt 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 de nieuwe functie kunt u gemakkelijker veld initialisatiefuncties gebruiken wanneer u argumenten nodig hebt om een veld of eigenschap te initialiseren.

Veranderlijke status maken

In de voorgaande voorbeelden worden primaire constructorparameters gebruikt om alleen-lezen eigenschappen te initialiseren. U kunt ook primaire constructors gebruiken wanneer de eigenschappen niet alleen-lezen zijn. Kijk eens naar 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 het voorgaande voorbeeld wordt de Translate methode gewijzigd en dy worden de dx onderdelen gewijzigd. Hiervoor moeten de Magnitude en Direction eigenschappen worden berekend wanneer ze worden geopend. De => operator wijst een expressiegerichte get toegangsfunctie aan, terwijl de = operator een initialisatiefunctie aanwijst. Deze versie voegt een parameterloze constructor toe aan de struct. De parameterloze constructor moet de primaire constructor aanroepen, zodat alle primaire constructorparameters worden geïnitialiseerd.

In het vorige voorbeeld worden de eigenschappen van de primaire constructor geopend in een methode. Daarom maakt de compiler verborgen velden om elke parameter weer te geven. De volgende code laat ongeveer zien wat de compiler genereert. De werkelijke veldnamen zijn geldige CIL-id's, 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) { }
}

Het is belangrijk om te weten dat het eerste voorbeeld niet vereist dat de compiler een veld maakt om de waarde van de primaire constructorparameters op te slaan. In het tweede voorbeeld is de primaire constructorparameter in een methode gebruikt en daarom is de compiler vereist om opslag voor deze parameters te maken. De compiler maakt alleen opslag voor primaire constructors wanneer deze parameter wordt geopend in de hoofdtekst van een lid van uw type. Anders worden de primaire constructorparameters niet opgeslagen in het object.

Afhankelijkheidsinjectie

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 van een basisklasse aanroepen vanuit de primaire constructor van de afgeleide klasse. Het is de eenvoudigste manier om een afgeleide klasse te schrijven die een primaire constructor in de basisklasse moet aanroepen. Denk bijvoorbeeld aan een hiërarchie van klassen die verschillende accounttypen vertegenwoordigen als bank. De basisklasse ziet er ongeveer als volgt uit:

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 een eigenaar. In de voltooide toepassing wordt andere algemene functionaliteit toegevoegd aan de basisklasse.

Veel typen vereisen specifiekere validatie voor constructorparameters. Het heeft bijvoorbeeld BankAccount specifieke vereisten voor de owner en accountID parameters: de owner mag geen witruimte zijn null of de accountID tekenreeks met 10 cijfers. 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 het vorige voorbeeld ziet u hoe u de constructorparameters kunt valideren voordat u deze toewijst aan de eigenschappen. U kunt ingebouwde methoden gebruiken, zoals , of String.IsNullOrWhiteSpace(String)uw eigen validatiemethode, zoals ValidAccountNumber. In het vorige 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 zou een controleaccount presenteren:

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 behulp van de : BankAccount(accountID, owner) syntaxis. Met deze expressie geeft u 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 van 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 deze wordt gebruikt in zowel afgeleide als basisklassen. In het volgende codevoorbeeld worden twee kopieën gemaakt van elk van de en accountID het owner veld:

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 lijn laat zien dat de ToString methode gebruikmaakt van de primaire constructorparameters (ownerenaccountID) in plaats van de eigenschappen van de basisklasse (Owner en AccountID). Het resultaat is dat met de afgeleide klasse SavingsAccount opslag voor deze kopieën wordt gemaakt. De kopie in de afgeleide klasse verschilt van de eigenschap in de basisklasse. Als de eigenschap van de basisklasse kan worden gewijzigd, ziet het exemplaar van de afgeleide klasse die 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.

Samenvatting

U kunt de primaire constructors het beste gebruiken voor uw ontwerp. Voor klassen en structs zijn primaire constructorparameters parameters voor een constructor die moet worden aangeroepen. U kunt ze gebruiken om eigenschappen te initialiseren. U kunt velden initialiseren. Deze eigenschappen of velden kunnen onveranderbaar of onveranderbaar zijn. U kunt ze gebruiken in methoden. Het zijn parameters en u gebruikt ze op welke manier het beste bij uw ontwerp past. Meer informatie over primaire constructors vindt u in het C#-programmeerhandleidingartikel over exemplaarconstructors en de voorgestelde primaire constructorspecificatie.