Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В C# 12 представлены первичных конструкторов, которые предоставляют краткий синтаксис для объявления конструкторов, параметры которых доступны в любом месте текста типа.
В этой статье описывается, как объявить главный конструктор на вашем типе и определить, где хранить параметры главного конструктора. Вы можете вызывать первичные конструкторы из других конструкторов и использовать параметры первичного конструктора в членах типа.
Предпосылки
- Последняя версия .NET SDK
- Visual Studio Code редактор
- C# DevKit
Общие сведения о правилах для основных конструкторов
Можно добавить параметры в объявление struct
или class
, чтобы создать основной конструктор. Основные параметры конструктора находятся в области определения класса. Важно рассматривать основные параметры конструктора как параметры, даже если они доступны во всей области определения класса.
Несколько правил определяют, что эти конструкторы являются параметрами:
- Параметры первичного конструктора могут не храниться, если они не нужны.
- Основные параметры конструктора не являются членами класса. Например, к основному параметру конструктора с именем
param
не удается получить доступ как кthis.param
. - Параметрам основного конструктора можно присваивать значения.
- В основных конструкторах параметры не становятся свойствами, за исключением типов записей .
Эти правила являются теми же правилами, которые уже определены для параметров любого метода, включая другие объявления конструктора.
Ниже приведены наиболее распространенные способы использования для основного параметра конструктора:
- Передать в качестве аргумента для вызова конструктора
base()
- Инициализация поля или свойства элемента
- Ссылка на параметр конструктора в элементе экземпляра
Каждый другой конструктор для класса должен вызывать основной конструктор напрямую или косвенно через вызов конструктора this()
. Это правило гарантирует, что основные параметры конструктора назначаются везде в тексте типа.
Инициализация неизменяемых свойств или полей
Следующий код инициализирует два свойства, доступные только для чтения (неизменяемые), которые вычисляются из основных параметров конструктора.
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);
}
В этом примере используется основной конструктор для инициализации вычисляемых свойств только для чтения. Инициализаторы полей для свойств Magnitude
и Direction
используют основные параметры конструктора. Параметры первичного конструктора не используются нигде в структуре. Код создает структуру, как если бы она была написана следующим образом:
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);
}
}
Эта функция упрощает использование инициализаторов полей при необходимости аргументов для инициализации поля или свойства.
Создание изменяемого состояния
В предыдущих примерах используются параметры первичного конструктора для инициализации свойств, доступных только для чтения. Вы также можете использовать первичные конструкторы для свойств, которые не читаются.
Рассмотрим следующий код:
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) { }
}
В этом примере метод Translate
изменяет компоненты dx
и dy
, что требует вычисления свойств Magnitude
и Direction
при их доступе. Оператор больше или равно (=>
) обозначает аксессор с телом-выражением get
, тогда как оператор равный (=
) обозначает инициализатор.
Эта версия кода добавляет в структуру конструктор без параметров. Конструктор без параметров должен вызывать первичный конструктор, который гарантирует инициализацию всех параметров первичного конструктора. К основным свойствам конструктора обращаются в методе, и компилятор создает скрытые поля для представления каждого параметра.
Следующий код демонстрирует приближение того, что создает компилятор. Фактические имена полей являются допустимыми идентификаторами CIL, но недопустимыми идентификаторами 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) { }
}
Созданное компилятором хранилище
В первом примере этого раздела компилятору не нужно создавать поле для хранения значения параметров первичного конструктора. Однако во втором примере основной параметр конструктора используется внутри метода, поэтому компилятор должен создать хранилище для параметров.
Компилятор создает хранилище для любых основных конструкторов только тогда, когда параметр используется в теле члена вашего типа. В противном случае параметры первичного конструктора не хранятся в объекте.
Используйте внедрение зависимостей
Другим распространенным способом использования первичных конструкторов является указание параметров внедрения зависимостей. Следующий код создает простой контроллер, требующий интерфейса службы для его использования:
public interface IService
{
Distance GetDistance();
}
public class ExampleController(IService service) : ControllerBase
{
[HttpGet]
public ActionResult<Distance> Get()
{
return service.GetDistance();
}
}
Основной конструктор четко указывает параметры, необходимые для класса. Вы используете параметры первичного конструктора, как и любую другую переменную в классе.
Инициализация базового класса
Вы можете вызвать основной конструктор базового класса из основного конструктора производного класса. Этот подход является самым простым способом написания производного класса, который должен вызывать первичный конструктор в базовом классе. Рассмотрим иерархию классов, представляющих различные типы счетов в качестве банка. В следующем коде показано, что может выглядеть базовый класс:
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}";
}
Все банковские счета, независимо от типа, имеют свойства для номера счета и владельца. В завершенном приложении можно добавить другие распространенные функциональные возможности в базовый класс.
Для многих типов требуется более конкретная проверка параметров конструктора. Например, класс BankAccount
имеет определенные требования к параметрам owner
и accountID
. Параметр owner
не должен быть null
или пробелами, а параметр accountID
должен быть строкой, содержащей 10 цифр. Эту проверку можно добавить при назначении соответствующих свойств:
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));
}
В этом примере показано, как проверить параметры конструктора перед их назначением свойствам. Вы можете использовать встроенные методы, такие как String.IsNullOrWhiteSpace(String) или собственный метод проверки, например ValidAccountNumber
. В этом примере все исключения выбрасываются из конструктора при вызове инициализаторов. Если для назначения поля не используется параметр конструктора, при первом доступе к параметру конструктора возникают исключения.
Один производный класс может представлять расчетный счет:
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}";
}
Производный CheckingAccount
класс имеет основной конструктор, который принимает все параметры, необходимые в базовом классе, и другой параметр со значением по умолчанию. Основной конструктор вызывает базовый конструктор с синтаксисом : BankAccount(accountID, owner)
. Это выражение указывает тип базового класса и аргументы основного конструктора.
Ваш производный класс не обязуется использовать основной конструктор. Конструктор можно создать в производном классе, который вызывает основной конструктор базового класса, как показано в следующем примере:
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}";
}
Существует одна потенциальная проблема с иерархиями классов и основными конструкторами. Можно создать несколько копий первичного параметра конструктора, так как этот параметр используется как в производных, так и в базовых классах. Следующий код создает две копии каждого из параметров owner
и 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}";
}
Выделенная строка в этом примере показывает, что метод ToString
использует параметры основного конструктора (owner
и accountID
) вместо свойств базового класса (Owner
и AccountID
). Результатом является то, что производный класс, SavingsAccount
, создает хранилище для копирования параметров. Копия в производном классе отличается от свойства в базовом классе. Если свойство базового класса можно изменить, экземпляр производного класса не видит изменения. Компилятор выдает предупреждение для основных параметров конструктора, которые используются в производном классе и передаются конструктору базового класса. В этом случае исправление заключается в использовании свойств базового класса.