Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
C# 12 führt primäre Konstruktoren ein, die eine präzise Syntax zum Deklarieren von Konstruktoren bereitstellen, deren Parameter an einer beliebigen Stelle im Textkörper des Typs verfügbar sind.
In diesem Artikel wird beschrieben, wie Sie einen primären Konstruktor für Ihren Typ deklarieren und erkennen, wo primäre Konstruktorparameter gespeichert werden sollen. Sie können primäre Konstruktoren aus anderen Konstruktoren aufrufen und primäre Konstruktorparameter in Membern des Typs verwenden.
Voraussetzungen
- Das neueste .NET SDK
- Visual Studio Code-Editor
- Das C# DevKit
Grundlegendes zu Regeln für primäre Konstruktoren
Sie können einer oder class
einer struct
Deklaration Parameter hinzufügen, um einen primären Konstruktor zu erstellen. Primäre Konstruktorparameter befinden sich in der gesamten Klassendefinition im Gültigkeitsbereich. Es ist wichtig, primäre Konstruktorparameter als Parameter anzuzeigen, obwohl sie sich in der gesamten Klassendefinition im Gültigkeitsbereich befinden.
Mehrere Regeln verdeutlichen, dass es sich bei diesen Konstruktoren um Parameter handelt:
- Primäre Konstruktorparameter werden möglicherweise nicht gespeichert, wenn sie nicht benötigt werden.
- Primäre Konstruktorparameter sind keine Member der Klasse. Auf einen primären Konstruktorparameter mit dem Namen
param
kann z. B. nicht zugegriffen werden.this.param
- Primäre Konstruktorparameter können zugewiesen werden.
- Primäre Konstruktorparameter werden nicht zu Eigenschaften, außer in Datensatztypen .
Diese Regeln sind die gleichen Regeln, die bereits für Parameter für jede Methode definiert sind, einschließlich anderer Konstruktordeklarationen.
Hier sind die am häufigsten verwendeten Verwendungsmöglichkeiten für einen primären Konstruktorparameter:
- Übergeben als Argument an einen
base()
Konstruktoraufruf - Initialisieren eines Elementfelds oder einer Eigenschaft
- Verweisen auf den Konstruktorparameter in einem Instanzmitglied
Jeder andere Konstruktor für eine Klasse muss den primären Konstruktor direkt oder indirekt über einen this()
Konstruktoraufruf aufrufen. Diese Regel stellt sicher, dass primäre Konstruktorparameter überall im Textkörper des Typs zugewiesen werden.
Initialisieren unveränderlicher Eigenschaften oder Felder
Der folgende Code initialisiert zwei readonly (unveränderliche) Eigenschaften, die aus primären Konstruktorparametern berechnet werden:
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);
}
In diesem Beispiel wird ein primärer Konstruktor verwendet, um berechnete Readonly-Eigenschaften zu initialisieren. Die Feldinitialisierer für die Magnitude
und Direction
Eigenschaften verwenden die primären Konstruktorparameter. Die primären Konstruktorparameter werden an keiner anderen Stelle in der Struktur verwendet. Der Code erstellt eine Struktur so, als ob sie wie folgt geschrieben wurde:
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);
}
}
Dieses Feature erleichtert die Verwendung von Feldinitialisierern, wenn Sie Argumente zum Initialisieren eines Felds oder einer Eigenschaft benötigen.
Erstellen eines veränderbaren Zustands
In den vorherigen Beispielen werden primäre Konstruktorparameter verwendet, um readonly-Eigenschaften zu initialisieren. Sie können auch primäre Konstruktoren für Eigenschaften verwenden, die nicht schreibgeschützt sind.
Beachten Sie den folgenden 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 diesem Beispiel ändert die Translate
Methode die dx
und dy
die Komponenten, für die die Berechnung der Magnitude
Eigenschaften erforderlich Direction
ist, wenn auf sie zugegriffen wird. Der Lambda-Operator (=>
) bezeichnet einen Ausdruckskörper-Accessor get
, während der Gleichheitsoperator (=
) einen Initialisierer bezeichnet.
Diese Version des Codes fügt der Struktur einen parameterlosen Konstruktor hinzu. Der parameterlose Konstruktor muss den primären Konstruktor aufrufen, wodurch sichergestellt wird, dass alle primären Konstruktorparameter initialisiert werden. Auf die primären Konstruktoreigenschaften wird in einer Methode zugegriffen, und der Compiler erstellt ausgeblendete Felder, um jeden Parameter darzustellen.
Der folgende Code veranschaulicht eine Annäherung an das, was der Compiler generiert. Die tatsächlichen Feldnamen sind gültige CIL-IDs (Common Intermediate Language), jedoch keine gültigen C#-Bezeichner.
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) { }
}
Compiler erstellter Speicher
Für das erste Beispiel in diesem Abschnitt musste der Compiler kein Feld erstellen, um den Wert der primären Konstruktorparameter zu speichern. Im zweiten Beispiel wird der primäre Konstruktorparameter jedoch innerhalb einer Methode verwendet, sodass der Compiler Speicher für die Parameter erstellen muss.
Der Compiler erstellt Speicher für alle primären Konstruktoren nur, wenn auf den Parameter im Textkörper eines Elements Ihres Typs zugegriffen wird. Andernfalls werden die primären Konstruktorparameter nicht im Objekt gespeichert.
Abhängigkeitsinjektion verwenden
Eine weitere häufige Verwendung für primäre Konstruktoren besteht darin, Parameter für die Abhängigkeitsinjektion anzugeben. Der folgende Code erstellt einen einfachen Controller, der eine Dienstschnittstelle für die Verwendung benötigt:
public interface IService
{
Distance GetDistance();
}
public class ExampleController(IService service) : ControllerBase
{
[HttpGet]
public ActionResult<Distance> Get()
{
return service.GetDistance();
}
}
Der primäre Konstruktor weist eindeutig auf die in der Klasse erforderlichen Parameter hin. Sie verwenden die primären Konstruktorparameter wie jede andere Variable in der Klasse.
Initialisieren der Basisklasse
Sie können den primären Konstruktor für eine Basisklasse aus dem primären Konstruktor der abgeleiteten Klasse aufrufen. Dieser Ansatz ist die einfachste Möglichkeit, eine abgeleitete Klasse zu schreiben, die einen primären Konstruktor in der Basisklasse aufrufen muss. Betrachten Sie eine Hierarchie von Klassen, die unterschiedliche Kontotypen als Bank darstellen. Der folgende Code zeigt, wie die Basisklasse aussehen kann:
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 Bankkonten verfügen unabhängig vom Typ über Eigenschaften für die Kontonummer und den Besitzer. In der abgeschlossenen Anwendung können Sie der Basisklasse weitere allgemeine Funktionen hinzufügen.
Viele Typen erfordern eine spezifischere Überprüfung für Konstruktorparameter. Die Klasse verfügt beispielsweise BankAccount
über spezifische Anforderungen für die owner
Und accountID
Parameter. Der owner
Parameter darf weder Leerzeichen noch Leerzeichen sein null
, und der accountID
Parameter muss eine Zeichenfolge mit 10 Ziffern sein. Sie können diese Überprüfung hinzufügen, wenn Sie die entsprechenden Eigenschaften zuweisen:
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 diesem Beispiel wird gezeigt, wie Sie die Konstruktorparameter überprüfen, bevor Sie sie den Eigenschaften zuweisen. Sie können integrierte Methoden wie String.IsNullOrWhiteSpace(String) oder Ihre eigene Validierungsmethode verwenden, z ValidAccountNumber
. B. . Im Beispiel werden alle Ausnahmen vom Konstruktor ausgelöst, wenn sie die Initialisierer aufruft. Wenn ein Konstruktorparameter nicht zum Zuweisen eines Felds verwendet wird, werden ausnahmen ausgelöst, wenn der Konstruktorparameter zuerst zugegriffen wird.
Eine abgeleitete Klasse kann ein Prüfkonto darstellen:
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}";
}
Die abgeleitete CheckingAccount
Klasse verfügt über einen primären Konstruktor, der alle in der Basisklasse benötigten Parameter und einen anderen Parameter mit einem Standardwert verwendet. Der primäre Konstruktor ruft den Basiskonstruktor mit der : BankAccount(accountID, owner)
Syntax auf. Dieser Ausdruck gibt sowohl den Typ für die Basisklasse als auch die Argumente für den primären Konstruktor an.
Ihre abgeleitete Klasse ist nicht erforderlich, um einen primären Konstruktor zu verwenden. Sie können einen Konstruktor in der abgeleiteten Klasse erstellen, der den primären Konstruktor für die Basisklasse aufruft, wie im folgenden Beispiel gezeigt:
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}";
}
Es gibt ein potenzielles Problem mit Klassenhierarchien und primären Konstruktoren. Es ist möglich, mehrere Kopien eines primären Konstruktorparameters zu erstellen, da der Parameter sowohl in abgeleiteten als auch in Basisklassen verwendet wird. Der folgende Code erstellt zwei Kopien der owner
einzelnen Und accountID
Parameter:
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}";
}
Die hervorgehobene Zeile in diesem Beispiel zeigt, dass die ToString
Methode die primären Konstruktorparameter (owner
und accountID
) anstelle der Basisklasseneigenschaften (Owner
und AccountID
) verwendet. Das Ergebnis ist, SavingsAccount
dass die abgeleitete Klasse Speicher für die Parameterkopien erstellt. Die Kopie in der abgeleiteten Klasse unterscheidet sich von der Eigenschaft in der Basisklasse. Wenn die Basisklasseneigenschaft geändert werden kann, wird die Änderung in der Instanz der abgeleiteten Klasse nicht angezeigt. Der Compiler gibt eine Warnung für primäre Konstruktorparameter aus, die in einer abgeleiteten Klasse verwendet und an einen Basisklassenkonstruktor übergeben werden. In dieser Instanz besteht der Fix darin, die Eigenschaften der Basisklasse zu verwenden.