Share via


Självstudie: Utforska primära konstruktorer

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

I den här kursen lär du dig:

  • När du ska deklarera en primär konstruktor för din typ
  • Anropa primära konstruktorer från andra konstruktorer
  • Använda primära konstruktorparametrar i medlemmar av typen
  • Där primära konstruktorparametrar lagras

Förutsättningar

Du måste konfigurera datorn för att köra .NET 8 eller senare, inklusive kompilatorn C# 12 eller senare. C# 12-kompilatorn är tillgänglig från och med Visual Studio 2022 version 17.7 eller .NET 8 SDK.

Primära konstruktorer

Du kan lägga till parametrar i en struct eller class deklarationen för att skapa en primär konstruktor. Primära konstruktorparametrar finns i omfånget i hela klassdefinitionen. Det är viktigt att visa primära konstruktorparametrar som parametrar även om de finns i omfånget i hela klassdefinitionen. Flera regler klargör att de är parametrar:

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

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

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

  1. Som ett argument till ett base() konstruktoranrop.
  2. Initiera ett medlemsfält eller en egenskap.
  3. 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 regeln säkerställer att primära konstruktorparametrar tilldelas var som helst i brödtexten av typen.

Initiera egenskapen

Följande kod initierar två skrivskyddade 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);
}

Föregående kod visar en primär konstruktor som används för att initiera beräknade skrivskyddade egenskaper. Fältinitierarna för Magnitude och Direction använder de primära konstruktorparametrarna. De primära konstruktorparametrarna används inte någon annanstans i structen. Föregående struct är som om du hade skrivit följande kod:

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 nya 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 när egenskaperna inte är skrivskyddade. Ta följande kod som exempel:

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 föregående exempel Translate ändrar metoden komponenterna dx och dy . Det kräver att Magnitude egenskaperna och Direction beräknas när de används. Operatorn => anger en uttrycksfyllig get accessor, medan operatorn = anger en initialiserare. Den här versionen lägger till en parameterlös konstruktor i structen. Den parameterlösa konstruktorn måste anropa den primära konstruktorn så att alla primära konstruktorparametrar initieras.

I föregående exempel används de primära konstruktoregenskaperna i en metod. Kompilatorn skapar därför dolda fält som representerar varje parameter. Följande kod visar ungefär vad kompilatorn genererar. De faktiska fältnamnen är giltiga CIL-identifierare, 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) { }
}

Det är viktigt att förstå att det första exemplet inte krävde att kompilatorn skapade ett fält för att lagra värdet för de primära konstruktorparametrarna. Det andra exemplet använde den primära konstruktorparametern i en metod och krävde därför att kompilatorn skulle skapa lagring för dem. Kompilatorn skapar lagring endast för primära konstruktorer när den parametern används i brödtexten för en medlem av din typ. Annars lagras inte de primära konstruktorparametrarna i objektet.

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 en primär konstruktor för basklassen från den härledda klassens primära konstruktor. Det är det enklaste sättet att skriva en härledd klass som måste anropa en primär konstruktor i basklassen. Tänk dig till exempel en hierarki med klasser som representerar olika kontotyper som en bank. Basklassen skulle se ut ungefär så här:

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 kontonumret och en ägare. I det slutförda programmet skulle andra vanliga funktioner läggas till i basklassen.

Många typer kräver mer specifik validering av konstruktorparametrar. Till exempel BankAccount har specifika krav för parametrarna owner och accountID : Får owner inte vara null eller tomt utrymme, och 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));
}

I föregående exempel visas hur du kan verifiera konstruktorparametrarna innan du tilldelar dem till egenskaperna. Du kan använda inbyggda metoder, till exempel String.IsNullOrWhiteSpace(String), eller din egen valideringsmetod, till exempel ValidAccountNumber. I föregående exempel 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 skulle presentera 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 hjälp av 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 krävs inte för att använda en primär konstruktor. Du kan skapa en konstruktor i den härledda klassen som anropar basklassens primära konstruktor, 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 är möjligt att skapa flera kopior av en primär konstruktorparameter eftersom den används i både härledda klasser och basklasser. I följande kodexempel skapas två kopior av vart och ett av fälten 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 visar att ToString metoden använder de primära konstruktorparametrarna (owner och accountID) i stället för basklassegenskaperna (Owner och AccountID). Resultatet är att den härledda klassen SavingsAccount skapar lagring för dessa kopior. Kopian i den härledda klassen skiljer sig från egenskapen i basklassen. Om basklassegenskapen kunde ändras ser inte instansen av den härledda klassen den ä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.

Sammanfattning

Du kan använda de primära konstruktorerna som bäst passar din design. För klasser och structs är primära konstruktorparametrar parametrar till en konstruktor som måste anropas. Du kan använda dem för att initiera egenskaper. Du kan initiera fält. Dessa egenskaper eller fält kan vara oföränderliga eller föränderliga. Du kan använda dem i metoder. De är parametrar och du använder dem på vilket sätt som passar din design bäst. Du kan lära dig mer om primära konstruktorer i programmeringsguiden för C# om instanskonstruktorer och den föreslagna primära konstruktorns specifikation.