Dela via


Deklarera primära konstruktorer för klasser och structs

C# 12 introducerar primära konstruktorer, som ger en koncis syntax för att deklarera konstruktorer vars parametrar är tillgängliga var som helst i brödtexten av typen.

Den här artikeln beskriver hur du deklarerar en primär konstruktor för din typ och identifierar var du ska lagra primära konstruktorparametrar. Du kan anropa primära konstruktorer från andra konstruktorer och använda primära konstruktorparametrar i medlemmar av typen.

Förutsättningar

Förstå regler för primära konstruktorer

Du kan lägga till parametrar i en struct- eller class-deklaration för att skapa en primär konstruktor. Primära konstruktorparametrar finns i omfånget i hela klassdefinitionen. Det är viktigt att se primära konstruktorparametrar som parametrar även om de är tillgängliga i hela klassens definition.

Flera regler klargör att dessa konstruktorer är parametrar:

  • Primära konstruktorparametrar kanske inte lagras om de inte behövs.
  • Primära konstruktorparametrar är inte medlemmar i klassen. En primär konstruktorparameter med namnet param kan till exempel inte nås som this.param.
  • Primära konstruktorparametrar kan tilldelas.
  • Primära konstruktorparametrar blir inte egenskaper, förutom i post typer.

Dessa regler är samma regler som redan har definierats för parametrar för alla metoder, inklusive andra konstruktordeklarationer.

Här är de vanligaste användningsområdena för en primär konstruktorparameter:

  • Skicka som ett argument till ett base() konstruktorsanrop
  • Initiera ett medlemsfält eller en egenskap
  • Referera till konstruktorparametern i en instansmedlem

Alla andra konstruktorer för en klass måste anropa den primära konstruktorn, direkt eller indirekt, via en this() konstruktoranrop. Den här regeln säkerställer att primära konstruktorparametrar tilldelas överallt i typens brödtext.

Initiera oföränderliga egenskaper eller fält

Följande kod initierar två skrivskyddade (oföränderliga) egenskaper som beräknas från primära konstruktorparametrar:

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

I det här exemplet används en primärkonstruktor för att initiera beräknade, endast läsbara egenskaper. Fältinitierarna för egenskaperna Magnitude och Direction använder de primära konstruktorparametrarna. De primära konstruktorparametrarna används inte någon annanstans i structen. Koden skapar en struct som om den skrevs på följande sätt:

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

Den här funktionen gör det enklare att använda fältinitierare när du behöver argument för att initiera ett fält eller en egenskap.

Skapa föränderligt tillstånd

I föregående exempel används primära konstruktorparametrar för att initiera skrivskyddade egenskaper. Du kan också använda primära konstruktorer för egenskaper som inte är skrivskyddade.

Överväg följande kod:

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

I det här exemplet ändrar metoden Translate komponenterna dx och dy, vilket kräver att egenskaperna Magnitude och Direction beräknas när de används. Operatorn större än eller lika med (=>) anger en uttrycksfyllig get-accessor, medan operatorn lika med (=) anger en initialiserare.

Den här versionen av koden lägger till en parameterlös konstruktor i structen. Den parameterlösa konstruktorn måste anropa den primära konstruktorn, vilket säkerställer att alla primära konstruktorparametrar initieras. De primära konstruktoregenskaperna används i en metod och kompilatorn skapar dolda fält som representerar varje parameter.

Följande kod visar en uppskattning av vad kompilatorn genererar. De faktiska fältnamnen är giltiga CIL-identifierare (Common Intermediate Language), men inte giltiga C#-identifierare.

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

Kompilatorskapad lagring

För det första exemplet i det här avsnittet behövde kompilatorn inte skapa ett fält för att lagra värdet för de primära konstruktorparametrarna. I det andra exemplet används dock den primära konstruktorparametern i en metod, så kompilatorn måste skapa lagring för parametrarna.

Kompilatorn skapar lagring endast för primära konstruktorer när parametern används i brödtexten för en medlem av din typ. Annars lagras inte de primära konstruktorparametrarna i objektet.

Använda beroendeinmatning

En annan vanlig användning för primära konstruktorer är att ange parametrar för beroendeinmatning. Följande kod skapar en enkel kontrollant som kräver ett tjänstgränssnitt för dess användning:

public interface IService
{
    Distance GetDistance();
}

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

Den primära konstruktorn anger tydligt de parametrar som behövs i klassen. Du använder de primära konstruktorparametrarna på samma sätt som andra variabler i klassen.

Initiera basklass

Du kan anropa den primära konstruktorn för en basklass från den primära konstruktorn för den härledda klassen. Den här metoden är det enklaste sättet att skriva en härledd klass som måste anropa en primär konstruktor i basklassen. Överväg en hierarki med klasser som representerar olika kontotyper som en bank. Följande kod visar hur basklassen kan se ut:

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

Alla bankkonton, oavsett typ, har egenskaper för kontonummer och ägare. I det färdiga programmet kan du lägga till andra vanliga funktioner i basklassen.

Många typer kräver mer specifik validering av konstruktorparametrar. Klassen BankAccount har till exempel specifika krav för parametrarna owner och accountID. Parametern owner får inte vara null eller blanksteg, och parametern accountID måste vara en sträng som innehåller 10 siffror. Du kan lägga till den här valideringen när du tilldelar motsvarande egenskaper:

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

Det här exemplet visar hur du verifierar konstruktorparametrarna innan du tilldelar dem till egenskaperna. Du kan använda inbyggda metoder som String.IsNullOrWhiteSpace(String) eller din egen valideringsmetod, till exempel ValidAccountNumber. I exemplet genereras eventuella undantag från konstruktorn när den anropar initierarna. Om en konstruktorparameter inte används för att tilldela ett fält utlöses eventuella undantag när konstruktorparametern först används.

En härledd klass kan representera ett kontrollkonto:

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

Den härledda CheckingAccount-klassen har en primär konstruktor som tar alla parametrar som behövs i basklassen och en annan parameter med ett standardvärde. Den primära konstruktorn anropar baskonstruktorn med syntaxen : BankAccount(accountID, owner). Det här uttrycket anger både typen för basklassen och argumenten för den primära konstruktorn.

Din härledda klass behöver inte använda en primär konstruktor. Du kan skapa en konstruktor i den härledda klassen som anropar den primära konstruktorn för basklassen, enligt följande exempel:

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

Det finns ett potentiellt problem med klasshierarkier och primära konstruktorer. Det går att skapa flera kopior av en primär konstruktorparameter eftersom parametern används i både härledda klasser och basklasser. Följande kod skapar två kopior var och en av parametrarna owner och 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}";
}

Den markerade raden i det här exemplet visar att metoden ToString använder primära konstruktorparametrar (owner och accountID) i stället för basklassegenskaper (Owner och AccountID). Resultatet är att den härledda klassen, SavingsAccount, skapar lagring för parameterkopior. Kopian i den härledda klassen skiljer sig från egenskapen i basklassen. Om basklassegenskapen kan ändras ser inte instansen av den härledda klassen ändringen. Kompilatorn utfärdar en varning för primära konstruktorparametrar som används i en härledd klass och skickas till en basklasskonstruktor. I det här fallet är korrigeringen att använda egenskaperna för basklassen.