Share via


Oktatóanyag: Az elsődleges konstruktorok felfedezése

A C# 12 bemutatja az elsődleges konstruktorokat, amelyek tömör szintaxissal deklarálják azokat a konstruktorokat, amelyek paraméterei a típus törzsében bárhol elérhetők.

Az oktatóanyag során a következőket fogja elsajátítani:

  • Mikor kell elsődleges konstruktort deklarálni a típuson?
  • Elsődleges konstruktorok meghívása más konstruktoroktól
  • Elsődleges konstruktorparaméterek használata a típus tagjaiban
  • Elsődleges konstruktorparaméterek tárolása

Előfeltételek

Be kell állítania a gépet a .NET 8 vagy újabb verziójának futtatására, beleértve a C# 12 vagy újabb fordítót is. A C# 12 fordító a Visual Studio 2022 17.7-es verziójától vagy a .NET 8 SDK-tól kezdve érhető el.

Elsődleges konstruktorok

Az elsődleges konstruktor létrehozásához paramétereket adhat hozzá egy struct vagy class egy deklarációhoz. Az elsődleges konstruktorparaméterek hatókörben vannak az osztálydefinícióban. Fontos, hogy az elsődleges konstruktorparamétereket paraméterekként tekintsük meg, annak ellenére, hogy az osztálydefiníció hatókörében vannak. Több szabály is tisztázza, hogy ezek paraméterek:

  1. Előfordulhat, hogy az elsődleges konstruktorparaméterek nem tárolhatók, ha nincs rájuk szükség.
  2. Az elsődleges konstruktorparaméterek nem tagjai az osztálynak. A névvel ellátott param elsődleges konstruktorparaméter például nem érhető el this.param.
  3. Elsődleges konstruktorparaméterek rendelhetők hozzá.
  4. Az elsődleges konstruktorparaméterek nem lesznek tulajdonságok, kivéve a típusokat record .

Ezek a szabályok ugyanazok, mint bármely metódus paraméterei, beleértve az egyéb konstruktor-deklarációkat is.

Az elsődleges konstruktorparaméter leggyakoribb felhasználási módjai a következők:

  1. Konstruktor-meghívás argumentumaként base() .
  2. Tagmező vagy tulajdonság inicializálása.
  3. Hivatkozás a konstruktorparaméterre egy példánytagban.

Az osztály minden más konstruktorának közvetlenül vagy közvetve, konstruktor-meghíváson keresztül kell meghívnia az elsődleges konstruktort this() . Ez a szabály biztosítja, hogy az elsődleges konstruktorparaméterek a típus törzsében bárhol legyenek hozzárendelve.

Tulajdonság inicializálása

A következő kód inicializál két egyszerű tulajdonságot, amelyek az elsődleges konstruktorparaméterekből vannak kiszámítva:

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

Az előző kód egy elsődleges konstruktort mutat be, amely a számított írásvédett tulajdonságok inicializálására szolgál. A mező inicializálói az MagnitudeDirection elsődleges konstruktorparamétereket használják és használják. Az elsődleges konstruktorparamétereket a rendszer sehol máshol nem használja a szerkezetben. Az előző szerkezet olyan, mintha a következő kódot írta volna:

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

Az új funkció megkönnyíti a mező inicializálóinak használatát, ha argumentumokra van szüksége egy mező vagy tulajdonság inicializálásához.

Mutable állapot létrehozása

Az előző példák elsődleges konstruktorparamétereket használnak az olvasható tulajdonságok inicializálásához. Akkor is használhat elsődleges konstruktorokat, ha a tulajdonságok nem olvashatók. Tekintse meg az alábbi kódot:

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

Az előző példában a Translate metódus módosítja az összetevőket és dy az dx összetevőket. Ehhez a hozzáféréskor ki kell számítani a tulajdonságokat és Direction a Magnitude tulajdonságokat. Az => operátor kifejezési testű get tartozékot jelöl ki, míg az = operátor inicializálót jelöl ki. Ez a verzió egy paraméter nélküli konstruktort ad hozzá a szerkezethez. A paraméter nélküli konstruktornak meg kell hívnia az elsődleges konstruktort, hogy az összes elsődleges konstruktorparaméter inicializálva legyen.

Az előző példában az elsődleges konstruktor tulajdonságai egy metódusban érhetők el. Ezért a fordító rejtett mezőket hoz létre az egyes paraméterek megjelenítéséhez. Az alábbi kód nagyjából azt mutatja be, hogy a fordító mit hoz létre. A tényleges mezőnevek érvényes CIL-azonosítók, de nem érvényes C# azonosítók.

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

Fontos tisztában lenni azzal, hogy az első példa nem követelte meg, hogy a fordító hozzon létre egy mezőt az elsődleges konstruktorparaméterek értékének tárolásához. A második példa az elsődleges konstruktorparamétert használta egy metóduson belül, ezért a fordítónak kellett létrehoznia a tárterületet. A fordító csak akkor hoz létre tárolót az elsődleges konstruktorok számára, ha a paraméter az Ön típusának egy tagja törzsében érhető el. Ellenkező esetben az elsődleges konstruktorparaméterek nincsenek tárolva az objektumban.

Függőséginjektálás

Az elsődleges konstruktorok másik gyakori használata a függőséginjektálás paramétereinek megadása. Az alábbi kód egy egyszerű vezérlőt hoz létre, amely használatához szolgáltatásfelületre van szükség:

public interface IService
{
    Distance GetDistance();
}

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

Az elsődleges konstruktor egyértelműen jelzi az osztályban szükséges paramétereket. Az elsődleges konstruktorparamétereket ugyanúgy használja, mint az osztály bármely más változója.

Alaposztály inicializálása

Az alaposztály elsődleges konstruktorát a származtatott osztály elsődleges konstruktorából hívhatja meg. Ez a legegyszerűbb módja annak, hogy olyan származtatott osztályt írjon, amely meg kell hívnia egy elsődleges konstruktort az alaposztályban. Vegyük például bankként a különböző számlatípusokat képviselő osztályok hierarchiáját. Az alaposztály a következő kódhoz hasonló:

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

Minden bankszámla típustól függetlenül rendelkezik a számlaszám és a tulajdonos tulajdonságaival. A befejezett alkalmazásban más általános funkciók is hozzáadva lesznek az alaposztályhoz.

Számos típushoz pontosabb ellenőrzés szükséges a konstruktorparamétereken. A paraméterekre és a BankAccount paraméterekre vonatkozó konkrét követelmények például a következő: A owner nem lehet null vagy üres, a accountID sztringnek pedig 10 számjegyet tartalmazó sztringnek kell lennie.owneraccountID Ezt az ellenőrzést a megfelelő tulajdonságok hozzárendelésekor veheti fel:

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

Az előző példa bemutatja, hogyan ellenőrizheti a konstruktorparamétereket, mielőtt hozzárendeli őket a tulajdonságokhoz. Használhat beépített metódusokat, például String.IsNullOrWhiteSpace(String), vagy saját érvényesítési módszert, például ValidAccountNumber. Az előző példában a konstruktor kivételeket ad ki, amikor meghívja az inicializálókat. Ha egy konstruktorparaméter nem használható mező hozzárendelésére, a rendszer kivételeket alkalmaz a konstruktorparaméter első elérésekor.

Egy származtatott osztály egy ellenőrző fiókot mutatna be:

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

A származtatott CheckingAccount osztály egy elsődleges konstruktorsal rendelkezik, amely az alaposztályban szükséges összes paramétert, egy másik, alapértelmezett értékkel rendelkező paramétert használ. Az elsődleges konstruktor a szintaxis használatával hívja meg az alapkonstruktort : BankAccount(accountID, owner) . Ez a kifejezés az alaposztály típusát és az elsődleges konstruktor argumentumait is meghatározza.

A származtatott osztály nem szükséges elsődleges konstruktor használatához. Létrehozhat egy konstruktort a származtatott osztályban, amely meghívja az alaposztály elsődleges konstruktorát az alábbi példában látható módon:

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

Az osztályhierarchiákkal és az elsődleges konstruktorokkal kapcsolatban egyetlen lehetséges probléma merülhet fel: az elsődleges konstruktorparaméterek több példánya is létrehozható, mivel a származtatott és az alaposztályokban is használható. Az alábbi példakód két példányt hoz létre a mező és accountID a owner mező mindegyikéből:

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

A kiemelt vonal azt mutatja, hogy a metódus az ToString alaposztály tulajdonságai (owner és ) helyett az elsődleges konstruktorparamétereket (Owner és accountIDAccountID) használja. Az eredmény az, hogy a származtatott osztály SavingsAccount tárolót hoz létre ezekhez a másolatokhoz. A származtatott osztály másolata eltér az alaposztályban lévő tulajdonságtól. Ha az alaposztály tulajdonsága módosítható, a származtatott osztály példánya nem fogja látni ezt a módosítást. A fordító figyelmeztetést ad ki a származtatott osztályban használt és egy alaposztály-konstruktornak átadott elsődleges konstruktorparaméterekre vonatkozóan. Ebben az esetben a javítás az alaposztály tulajdonságainak használata.

Összegzés

Az elsődleges konstruktorok a legjobban illeszkednek a tervezéshez. Osztályok és szerkezetek esetén az elsődleges konstruktorparaméterek olyan konstruktor paraméterei, amelyeket meg kell hívni. A tulajdonságok inicializálásához használhatja őket. A mezők inicializálhatók. Ezek a tulajdonságok vagy mezők nem módosíthatók vagy módosíthatók. Metódusokban is használhatja őket. Ezek paraméterek, és ön a legjobban megfelelő módon használja őket. Az elsődleges konstruktorokról a C#-programozási útmutató példánykonstruktorokról és a javasolt elsődleges konstruktor-specifikációról szóló cikkében tudhat meg többet.