Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Język C# 12 wprowadza konstruktory podstawowe, które zapewniają zwięzłą składnię do deklarowania konstruktorów, których parametry są dostępne w dowolnym miejscu w definicji typu.
W tym artykule opisano sposób deklarowania podstawowego konstruktora w typie i rozpoznawania miejsca przechowywania podstawowych parametrów konstruktora. Można wywoływać konstruktory podstawowe z innych konstruktorów i używać parametrów konstruktorów podstawowych w członkach typu.
Wymagania wstępne
Omówienie reguł dla konstruktorów podstawowych
Możesz dodać parametry do deklaracji struct
lub class
, aby utworzyć podstawowego konstruktora. Podstawowe parametry konstruktora są dostępne w całej definicji klasy. Ważne jest, aby wyświetlić podstawowe parametry konstruktora jako parametry , mimo że są one w zakresie w całej definicji klasy.
Kilka reguł wyjaśnia, że te konstruktory są parametrami:
- Podstawowe parametry konstruktora mogą nie być przechowywane, jeśli nie są potrzebne.
- Podstawowe parametry konstruktora nie są elementami członkowskimi klasy. Na przykład podstawowy parametr konstruktora o nazwie
param
nie może być dostępny jakothis.param
. - Do podstawowego konstruktora można przypisać parametry.
- Podstawowe parametry konstruktora nie stają się atrybutami, z wyjątkiem typów rekordów .
Te reguły są tymi samymi regułami, które zostały już zdefiniowane dla parametrów dowolnej metody, w tym innych deklaracji konstruktorów.
Poniżej przedstawiono najczęstsze zastosowania dla podstawowego parametru konstruktora:
- Przekaż jako argument do wywołania konstruktora
base()
- Inicjowanie pola lub właściwości
- Odwołanie do parametru konstruktora w elemencie członkowskim wystąpienia
Każdy inny konstruktor klasy musi wywołać konstruktor podstawowy, bezpośrednio lub pośrednio, poprzez wywołanie konstruktora this()
. Ta reguła gwarantuje, że podstawowe parametry konstruktora są przypisywane wszędzie w treści typu.
Inicjowanie niezmiennych właściwości lub pól
Poniższy kod inicjuje dwie właściwości tylko do odczytu (niezmienne), które są obliczane z parametrów podstawowego konstruktora.
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);
}
W tym przykładzie użyto podstawowego konstruktora, aby zainicjować właściwości obliczane tylko do odczytu. Inicjatory pól dla właściwości Magnitude
i Direction
używają podstawowych parametrów konstruktora. Podstawowe parametry konstruktora nie są używane nigdzie indziej w struktury. Kod tworzy strukturę tak, jakby została napisana w następujący sposób:
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);
}
}
Ta funkcja ułatwia używanie inicjatorów pól, gdy potrzebne są argumenty do inicjowania pola lub właściwości.
Tworzenie stanu modyfikowalnego
Poprzednie przykłady używają podstawowych parametrów konstruktora do inicjowania właściwości tylko do odczytu. Można również użyć konstruktorów podstawowych dla właściwości niemających atrybutu readonly.
Rozważ następujący 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) { }
}
W tym przykładzie metoda Translate
zmienia składniki dx
i dy
, co wymaga obliczenia właściwości Magnitude
i Direction
podczas uzyskiwania dostępu. Operator większy lub równy (=>
) wyznacza akcesor get
z wyrażeniem, natomiast operator równy (=
) wyznacza inicjalizator.
Ta wersja kodu dodaje konstruktor bez parametrów do struktury. Konstruktor bez parametrów musi wywołać podstawowy konstruktor, co gwarantuje zainicjowanie wszystkich podstawowych parametrów konstruktora. Dostęp do podstawowych właściwości konstruktora jest uzyskiwany w metodzie, a kompilator tworzy ukryte pola reprezentujące każdy parametr.
Poniższy kod przedstawia przybliżenie tego, co generuje kompilator. Rzeczywiste nazwy pól są prawidłowymi identyfikatorami języka CIL (Common Intermediate Language), ale nie są prawidłowymi identyfikatorami języka C#.
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) { }
}
Magazyn utworzony przez kompilator
W pierwszym przykładzie w tej sekcji kompilator nie musiał utworzyć pola do przechowywania wartości parametrów konstruktora podstawowego. Jednak w drugim przykładzie podstawowy parametr konstruktora jest używany wewnątrz metody, więc kompilator musi utworzyć magazyn dla parametrów.
Kompilator tworzy miejsce do przechowywania dla konstruktorów pierwotnych tylko wtedy, gdy parametr jest odwoływany w ciele członka typu. W przeciwnym razie podstawowe parametry konstruktora nie są przechowywane w obiekcie.
Użyj wstrzykiwania zależności
Innym typowym zastosowaniem dla konstruktorów podstawowych jest określenie parametrów iniekcji zależności. Poniższy kod tworzy prosty kontroler, który wymaga interfejsu usługi do użycia:
public interface IService
{
Distance GetDistance();
}
public class ExampleController(IService service) : ControllerBase
{
[HttpGet]
public ActionResult<Distance> Get()
{
return service.GetDistance();
}
}
Konstruktor podstawowy wyraźnie wskazuje parametry wymagane w klasie. Podstawowe parametry konstruktora są używane tak, jak każda inna zmienna w klasie.
Inicjowanie klasy bazowej
Można wywołać podstawowy konstruktor dla klasy bazowej z podstawowego konstruktora klasy pochodnej. To podejście jest najprostszym sposobem na napisanie klasy pochodnej, która musi wywołać konstruktor podstawowy w klasie bazowej. Rozważ hierarchię klas reprezentujących różne typy kont jako bank. Poniższy kod pokazuje, jak może wyglądać klasa bazowa:
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}";
}
Wszystkie konta bankowe, niezależnie od typu, mają dane dotyczące numeru konta i właściciela. W ukończonej aplikacji można dodać inne typowe funkcje do klasy bazowej.
Wiele typów wymaga bardziej szczegółowej weryfikacji parametrów konstruktora. Na przykład klasa BankAccount
ma określone wymagania dotyczące parametrów owner
i accountID
. Parametr owner
nie może być null
ani biały znak, a parametr accountID
musi być ciągiem zawierającym 10 cyfr. Tę walidację można dodać podczas przypisywania odpowiednich właściwości:
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));
}
W tym przykładzie pokazano, jak zweryfikować parametry konstruktora przed przypisaniem ich do właściwości. Możesz użyć wbudowanych metod, takich jak String.IsNullOrWhiteSpace(String) lub własnej metody weryfikacji, takich jak ValidAccountNumber
. W tym przykładzie wszystkie wyjątki są zgłaszane z konstruktora, gdy wywołuje inicjatory. Jeśli parametr konstruktora nie jest używany do przypisywania pola, wyjątki są zgłaszane tylko wtedy, gdy po raz pierwszy uzyskuje się dostęp do tego parametru.
Jedna klasa pochodna może reprezentować rachunek bieżący.
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}";
}
Klasa pochodna CheckingAccount
ma podstawowy konstruktor, który przyjmuje wszystkie parametry wymagane w klasie bazowej, a inny parametr z wartością domyślną. Konstruktor główny wywołuje konstruktor bazowy ze składnią : BankAccount(accountID, owner)
. To wyrażenie określa zarówno typ klasy bazowej, jak i argumenty dla konstruktora podstawowego.
Klasa pochodna nie musi używać konstruktora podstawowego. Konstruktor można utworzyć w klasie pochodnej, który wywołuje konstruktor podstawowy dla klasy bazowej, jak pokazano w poniższym przykładzie:
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}";
}
Istnieje jeden potencjalny problem z hierarchiami klas i konstruktorami podstawowymi. Istnieje możliwość utworzenia wielu kopii podstawowego parametru konstruktora, ponieważ parametr jest używany zarówno w klasach pochodnych, jak i podstawowych. Poniższy kod tworzy dwie kopie każdego z parametrów owner
i 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}";
}
Wyróżniony wiersz w tym przykładzie pokazuje, że metoda ToString
używa podstawowych parametrów konstruktora (owner
i accountID
), a nie właściwości klasy bazowej (Owner
i AccountID
). Wynikiem jest to, że klasa pochodna, SavingsAccount
, tworzy magazyn dla kopii parametrów. Kopia w klasie pochodnej różni się od właściwości w klasie bazowej. Jeśli można zmodyfikować właściwość klasy bazowej, wystąpienie klasy pochodnej nie widzi modyfikacji. Kompilator wystawia ostrzeżenie dla podstawowych parametrów konstruktora, które są używane w klasie pochodnej i przekazywane do konstruktora klasy bazowej. W tym przypadku poprawka polega na użyciu właściwości klasy bazowej.