Partilhar via


Tutorial: Explore construtores primários

C# 12 introduz 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, irá aprender:

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

Pré-requisitos

Você precisa configurar sua máquina 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 struct declaração ou class para criar um construtor primário. Os parâmetros primários do construtor estão no escopo em toda a definição de classe. É importante visualizar os parâmetros primários do construtor como parâmetros , mesmo que eles estejam no escopo em toda a definição de classe. Várias regras esclarecem que são parâmetros:

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

Essas regras são as mesmas que parâmetros para 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 argumento para uma base() invocação do construtor.
  2. Para inicializar um campo ou propriedade de membro.
  3. Referenciando o parâmetro do construtor em um membro da instância.

Qualquer outro construtor para uma classe deve chamar o construtor primário, direta ou indiretamente, através de uma this() invocação do construtor. Essa regra garante que os parâmetros primários do construtor sejam atribuídos em qualquer lugar no corpo do tipo.

Propriedade Initialize

O código a seguir inicializa duas propriedades somente leitura que são calculadas a partir de parâmetros primários do construtor:

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 primários do construtor. Os parâmetros primários do construtor não são usados em nenhum outro lugar na estrutura. A 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 para inicializar um campo ou propriedade.

Criar estado mutável

Os exemplos anteriores usam parâmetros primários do construtor para inicializar propriedades somente leitura. Você também pode usar construtores primários quando as propriedades não são 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 Translate método altera os dx componentes e dy . Isso requer que as Magnitude propriedades e Direction sejam computadas quando acessadas. O => operador designa um acessador com get corpo de 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 primários do construtor. O segundo exemplo usou o parâmetro do construtor primário dentro de um método e, portanto, exigiu que o compilador criasse armazenamento para eles. O compilador cria armazenamento para quaisquer construtores primários somente quando esse parâmetro é acessado no corpo de um membro do seu tipo. Caso contrário, os parâmetros primários do construtor não serão armazenados no objeto.

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. Você usa os parâmetros primários do construtor 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 representam 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 para o número da conta e um proprietário. No aplicativo concluído, outras funcionalidades comuns seriam adicionadas à classe base.

Muitos tipos requerem validação mais específica nos parâmetros do construtor. Por exemplo, o BankAccount tem requisitos específicos para os owner parâmetros e accountID : O owner não deve ser null ou espaço em branco, e o accountID 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));
}

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 próprio método de validação, como ValidAccountNumber. No exemplo anterior, quaisquer exceções são lançadas do construtor, quando ele invoca os inicializadores. Se um parâmetro do construtor não for usado para atribuir um campo, quaisquer exceções serão lançadas 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 : BankAccount(accountID, owner) sintaxe. Esta 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 da 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 à medida que ele é usado em classes derivadas e base. O exemplo de código a seguir cria duas cópias cada um dos owner campos and 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 ToString método usa os parâmetros primários do construtor (owner e accountID) em vez das propriedades da classe base (Owner e AccountID). O resultado é que a classe derivada, SavingsAccount cria armazenamento para essas cópias. A cópia na classe derivada é diferente da propriedade na classe base. Se a propriedade da classe base puder ser modificada, a instância da classe derivada não verá essa modificação. O compilador emite um aviso para parâmetros primários do construtor que são 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 conforme melhor se adapte ao seu projeto. Para classes e structs, parâmetros primários do construtor são 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 imutáveis ou mutáveis. Você pode usá-los em métodos. Eles são parâmetros, e você os usa da maneira que melhor se adapta ao seu design. Você pode aprender mais sobre construtores primários no artigo do guia de programação em C# sobre construtores de instância e a especificação de construtor primário proposta.