Compartilhar via


Declarar construtores primários para classes e structs

O C# 12 apresenta construtores primários, que fornecem uma sintaxe concisa para declarar construtores cujos parâmetros estão disponíveis em qualquer lugar no corpo do tipo.

Este artigo descreve como declarar um construtor primário em seu tipo e reconhecer onde armazenar parâmetros de construtor primário. Você pode chamar construtores primários de outros construtores e usar parâmetros de construtor primários em membros do tipo.

Pré-requisitos

Entender regras para construtores primários

Você pode adicionar parâmetros a uma struct ou class declaração para criar um construtor primário. Os parâmetros do construtor primário estão no escopo em toda a definição de classe. É importante exibir parâmetros de construtor primários como parâmetros , mesmo que estejam no escopo em toda a definição de classe.

Várias regras esclarecem que esses construtores são parâmetros:

  • Os parâmetros do construtor primário podem não ser armazenados se não forem necessários.
  • Os parâmetros do construtor primário não são membros da classe. Por exemplo, um parâmetro de construtor primário nomeado param não pode ser acessado como this.param.
  • Parâmetros de construtor primários podem ser atribuídos.
  • Parâmetros de construtor primário não se tornam propriedades, exceto em tipos de registro .

Essas regras são as mesmas regras já definidas para parâmetros para qualquer método, incluindo outras declarações de construtor.

Aqui estão os usos mais comuns para um parâmetro de construtor primário:

  • Passar como um argumento para uma base() invocação de construtor
  • Inicializar um campo de membro ou propriedade
  • Referenciar o parâmetro de construtor em um membro de instância

Todos os outros construtores de uma classe devem chamar o construtor primário, direta ou indiretamente, por meio de uma this() invocação de construtor. Essa regra garante que os parâmetros do construtor primário sejam atribuídos em todos os lugares no corpo do tipo.

Inicializar propriedades ou campos imutáveis

O código a seguir inicializa duas propriedades readonly (imutáveis) computadas a partir de parâmetros de construtor primário:

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

Este exemplo usa um construtor primário para inicializar propriedades de leitura calculadas. Os inicializadores de campo para as propriedades e Direction o Magnitude campo usam os parâmetros do construtor primário. Os parâmetros do construtor primário não são usados em nenhum outro lugar no struct. O código cria um struct como se tivesse sido escrito da seguinte maneira:

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

Esse recurso facilita o uso de inicializadores de campo quando você precisa de argumentos para inicializar um campo ou propriedade.

Criar estado mutável

Os exemplos anteriores usam parâmetros de construtor primário para inicializar propriedades somente leitura. Você também pode usar construtores primários para propriedades que não são lidas somente leitura.

Considere o seguinte código:

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

Neste exemplo, o Translate método altera os componentes e dy os dx componentes, que exigem que as propriedades e Direction as Magnitude propriedades sejam computadas quando acessadas. O operador lambda (=>) designa um acessador com corpo get de expressão, enquanto o operador igual a (=) designa um inicializador.

Esta versão do código adiciona um construtor sem parâmetros ao struct. O construtor sem parâmetros deve invocar o construtor primário, o que garante que todos os parâmetros do construtor primário sejam inicializados. As propriedades do construtor primário são acessadas em um método e o compilador cria campos ocultos para representar cada parâmetro.

O código a seguir demonstra uma aproximação do que o compilador gera. Os nomes de campo reais são identificadores CIL (Common Intermediate Language) válidos, mas não identificadores C# válidos.

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

Armazenamento criado pelo compilador

Para o primeiro exemplo nesta seção, o compilador não precisava criar um campo para armazenar o valor dos parâmetros do construtor primário. No entanto, no segundo exemplo, o parâmetro do construtor primário é usado dentro de um método, portanto, o compilador deve criar armazenamento para os parâmetros.

O compilador cria armazenamento para quaisquer construtores primários somente quando o parâmetro é acessado no corpo de um membro do seu tipo. Caso contrário, os parâmetros do construtor primário não serão armazenados no objeto.

Usar injeção de dependência

Outro uso comum para construtores primários é especificar parâmetros para injeção de dependência. O código a seguir cria um controlador simples que requer uma interface de serviço para seu uso:

public interface IService
{
    Distance GetDistance();
}

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

O construtor primário indica claramente os parâmetros necessários na classe. Use os parâmetros do construtor primário como faria com qualquer outra variável na classe.

Inicializar classe base

Você pode invocar o construtor primário para uma classe base do construtor primário da classe derivada. Essa abordagem é a maneira mais fácil de escrever uma classe derivada que deve invocar um construtor primário na classe base. Considere uma hierarquia de classes que representam diferentes tipos de conta como um banco. O código a seguir mostra a aparência da classe base:

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

Todas as contas bancárias, independentemente do tipo, têm propriedades para o número da conta e o proprietário. No aplicativo concluído, você pode adicionar outra funcionalidade comum à classe base.

Muitos tipos exigem uma validação mais específica em parâmetros de construtor. Por exemplo, a BankAccount classe tem requisitos específicos para os parâmetros e accountID os owner parâmetros. O owner parâmetro não deve ser null ou espaço em branco e o accountID parâmetro deve ser uma cadeia de caracteres contendo 10 dígitos. Você pode adicionar essa validação ao atribuir as propriedades correspondentes:

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

Este exemplo mostra como validar os parâmetros do construtor antes de atribuí-los às propriedades. Você pode usar métodos internos como String.IsNullOrWhiteSpace(String) ou seu próprio método de validação, como ValidAccountNumber. No exemplo, todas as exceções são geradas do construtor quando ele invoca os inicializadores. Se um parâmetro de construtor não for usado para atribuir um campo, quaisquer exceções serão geradas quando o parâmetro do construtor for acessado pela primeira vez.

Uma classe derivada pode representar uma conta corrente:

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 classe derivada CheckingAccount tem um construtor primário que usa todos os parâmetros necessários na classe base e outro parâmetro com um valor padrão. O construtor primário chama o construtor base com a : BankAccount(accountID, owner) sintaxe. Essa expressão especifica o tipo para a classe base e os argumentos para o construtor primário.

Sua classe derivada não é necessária para usar um construtor primário. Você pode criar um construtor na classe derivada que invoca o construtor primário para a classe base, conforme mostrado no exemplo a seguir:

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

Há uma preocupação potencial com hierarquias de classe e construtores primários. É possível criar várias cópias de um parâmetro de construtor primário porque o parâmetro é usado em classes derivadas e base. O código a seguir cria duas cópias cada um dos owner parâmetros e 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}";
}

A linha realçada neste exemplo mostra que o ToString método usa os parâmetros do construtor primário (owner e accountID) em vez das propriedades da classe base (Owner e AccountID). O resultado é que a classe SavingsAccountderivada cria armazenamento para as cópias de parâmetro. A cópia na classe derivada é diferente da propriedade na classe base. Se a propriedade de classe base puder ser modificada, a instância da classe derivada não verá a modificação. O compilador emite um aviso para parâmetros de construtor primário usados em uma classe derivada e passados para um construtor de classe base. Nesse caso, a correção é usar as propriedades da classe base.