Руководство. Изучение основных конструкторов

В C# 12 представлены основные конструкторы, краткий синтаксис для объявления конструкторов, параметры которых доступны в любом месте текста типа.

Из этого руководства вы узнаете:

  • Объявление основного конструктора в типе
  • Как вызывать первичные конструкторы из других конструкторов
  • Использование параметров первичного конструктора в членах типа
  • Где хранятся основные параметры конструктора

Необходимые компоненты

Необходимо настроить компьютер для запуска .NET 8 или более поздней версии, включая компилятор C# 12 или более поздней версии. Компилятор C# 12 доступен начиная с Visual Studio 2022 версии 17.7 или пакета SDK для .NET 8.

Основные конструкторы

Можно добавить параметры в struct или class объявление для создания первичного конструктора. Основные параметры конструктора находятся в область по определению класса. Важно просматривать параметры первичного конструктора как параметры, даже если они находятся в область в определении класса. Несколько правил поясняют, что они параметры:

  1. Параметры первичного конструктора могут не храниться, если они не нужны.
  2. Основные параметры конструктора не являются членами класса. Например, к основному параметру конструктора с именем param не удается получить доступ this.param.
  3. Основным параметрам конструктора можно назначить.
  4. Основные параметры конструктора не становятся свойствами, за исключением record типов.

Эти правила совпадают с параметрами любого метода, включая другие объявления конструктора.

Наиболее часто используются для основного параметра конструктора:

  1. В качестве аргумента вызова конструктора base() .
  2. Чтобы инициализировать поле или свойство элемента.
  3. Ссылка на параметр конструктора в элементе экземпляра.

Каждый другой конструктор для класса должен вызывать основной конструктор напрямую или косвенно через вызов конструктора 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 не должно быть или пробелы, и accountID должна быть null строка, содержащая 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. В предыдущем примере все исключения создаются из конструктора при вызове инициализаторов. Если для назначения поля не используется параметр конструктора, при первом доступе к параметру конструктора возникают исключения.

Один производный класс представляет учетную запись проверка ing:

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и) вместо свойств базового класса (OwnerиaccountIDAccountID). Результатом является то, что производный класс SavingsAccount создает хранилище для этих копий. Копия в производном классе отличается от свойства в базовом классе. Если свойство базового класса может быть изменено, экземпляр производного класса не увидит этого изменения. Компилятор выдает предупреждение для основных параметров конструктора, которые используются в производном классе и передаются конструктору базового класса. В этом экземпляре исправление — использовать свойства базового класса.

Итоги

Вы можете использовать основные конструкторы в соответствии с вашим проектом. Для классов и структур первичные параметры конструктора — это параметры конструктора, который должен вызываться. Их можно использовать для инициализации свойств. Можно инициализировать поля. Эти свойства или поля могут быть неизменяемыми или изменяемыми. Их можно использовать в методах. Они параметры, и вы используете их таким образом, как подходит ваш дизайн лучше всего. Дополнительные сведения о первичных конструкторах см. в статье руководства по программированию на C# по конструкторам экземпляров и предлагаемой спецификации основного конструктора.