Share via


Tutorial: Explorar construtores primários

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

Neste tutorial, você irá aprender:

  • Quando declarar um construtor primário em seu tipo
  • Como chamar construtores primários de outros construtores
  • Como usar parâmetros do construtor primário em membros do tipo
  • Onde os parâmetros do construtor primário são armazenados

Pré-requisitos

Você precisará configurar seu computador para executar o .NET 8 ou posterior, incluindo o compilador C# 12 ou posterior. O compilador C# 12 está disponível a partir do Visual Studio 2022 versão 17.7 ou do SDK do .NET 8.

Construtores primários

Você pode adicionar parâmetros a uma declaração struct ou class 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 os parâmetros do construtor primário como parâmetros mesmo que estejam no escopo em toda a definição de classe. Várias regras esclarecem que eles são parâmetros:

  1. Pode ser que os parâmetros do construtor primário não sejam armazenados se não forem necessários.
  2. Os parâmetros do construtor primário não são membros da classe. Por exemplo, o parâmetro de construtor primário param não pode ser acessado como this.param.
  3. Parâmetros de construtor primário podem ser atribuídos.
  4. Os parâmetros do construtor primário não se tornam propriedades, exceto em tipos record.

Essas regras são iguais aos parâmetros de qualquer método, incluindo outras declarações de construtor.

Os usos mais comuns para um parâmetro de construtor primário são:

  1. Como um argumento para uma invocação de construtor base().
  2. Para inicializar um campo ou propriedade de membro.
  3. Referenciando 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 invocação de construtor this(). Essa regra garante que os parâmetros do construtor primário sejam atribuídos em qualquer lugar no corpo do tipo.

Inicializar propriedade

O código a seguir inicializa duas propriedades somente leitura computadas a partir dos parâmetros do 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);
}

O código anterior demonstra um construtor primário usado para inicializar propriedades somente leitura calculadas. Os inicializadores de campo para Magnitude e Direction 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 struct anterior é como se você tivesse escrito o seguinte código:

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

O novo recurso facilita o uso de inicializadores de campo quando você precisa de argumentos a fim de inicializar um campo ou propriedade.

Criar estado mutável

Os exemplos anteriores usam os parâmetros do construtor primário para inicializar propriedades somente leitura. Você também pode usar construtores primários quando as propriedades não forem 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) { }
}

No exemplo anterior, o método Translate altera os componentes dx e dy. Isso requer que as propriedades Magnitude e Direction sejam computadas quando acessadas. O operador => designa um acessador get apto para expressão, enquanto o operador = designa um inicializador. Esta versão adiciona um construtor sem parâmetros ao struct. O construtor sem parâmetros deve invocar o construtor primário para que todos os parâmetros do construtor primário sejam inicializados.

No exemplo anterior, as propriedades do construtor primário são acessadas em um método. Portanto, o compilador cria campos ocultos para representar cada parâmetro. O código a seguir mostra aproximadamente o que o compilador gera. Os nomes de campo reais são identificadores CIL 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) { }
}

É importante entender que o primeiro exemplo não exigia que o compilador criasse um campo para armazenar o valor dos parâmetros do construtor primário. O segundo exemplo usou o parâmetro do construtor primário dentro de um método e, portanto, foi necessário que o compilador criasse um armazenamento para eles. O compilador cria o armazenamento para um construtor primário somente quando esse 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.

Injeção de dependência

Outro uso comum dos 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 de uma classe base a partir do construtor primário da classe derivada. É a maneira mais fácil de escrever uma classe derivada que deve invocar um construtor primário na classe base. Por exemplo, considere uma hierarquia de classes que representa diferentes tipos de conta como um banco. A classe base seria semelhante ao seguinte código:

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 como o número da conta e um proprietário. No aplicativo concluído, outras funcionalidades comuns seriam adicionadas à classe base.

Muitos tipos exigem uma validação mais específica em parâmetros de construtor. Por exemplo, o BankAccount tem requisitos específicos para os parâmetros owner e accountID: o owner não deve ser null ou um espaço em branco e accountID deve ser uma cadeia de caracteres que contém 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));
}

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

Uma classe derivada apresentaria 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 usando a sintaxe : BankAccount(accountID, owner). Essa expressão especifica o tipo para a classe base e os argumentos para o construtor primário.

Não é necessário que sua classe derivada use um construtor primário. Você pode criar um construtor na classe derivada que invoca o construtor primário da classe base, como 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, já que ele é usado em classes base e derivadas. O exemplo de código a seguir cria duas cópias de cada um dos campos owner 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 mostra que o método ToString 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 derivada SavingsAccount cria o armazenamento para essas cópias. A cópia na classe derivada é diferente da propriedade na classe base. Se for possível modificar a propriedade da classe base, a instância da classe derivada não verá essa modificação. O compilador emite um aviso para os parâmetros do construtor primário usados em uma classe derivada e passados para um construtor de classe base. Neste caso, a correção é usar as propriedades da classe base.

Resumo

Você pode usar os construtores primários da maneira mais adequada ao seu design. Em classes e structs, os parâmetros do construtor primário são os parâmetros para um construtor que deve ser invocado. Você pode usá-los para inicializar propriedades. Você pode inicializar campos. Essas propriedades ou campos podem ser mutáveis ou imutáveis. Você pode usá-los em métodos. Esses são parâmetros para usar da maneira mais adequada ao seu design. Você pode saber mais sobre construtores primários no artigo do guia de programação em C# sobre construtores de instâncias e a especificação de construtor primário proposta.