Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
C# 12 introduz 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 primários do construtor. Você pode chamar construtores primários a partir de outros construtores e usar parâmetros de construtores primários nos membros do tipo.
Pré-requisitos
- O mais recente SDK do .NET
- Editor de código do Visual Studio
- O Kit de Desenvolvimento C#
Entenda as regras para 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 âmbito ao longo de toda a definição da classe. É importante considerar 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 esses construtores são parâmetros:
- Os parâmetros primários do construtor podem não ser armazenados se não forem necessários.
- 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 comothis.param
. - Os parâmetros primários do construtor podem ser atribuídos.
- Os parâmetros primários do construtor não se tornam propriedades, exceto em tipos de registo .
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 invocação do construtor
base()
- Inicializar um campo ou propriedade de membro
- Referenciar o parâmetro do construtor em um membro da instância
Todo outro construtor de uma classe deve chamar o construtor primário, direta ou indiretamente, através da invocação do construtor this()
. Esta regra garante que os parâmetros primários do construtor 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 somente leitura (imutáveis) 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);
}
Este exemplo usa um construtor primário para inicializar propriedades somente leitura calculadas. Os inicializadores de campo para as propriedades 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. O código cria uma struct como se fosse escrita 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 primários do construtor para inicializar propriedades de somente leitura. Você também pode usar construtores primários para propriedades que 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) { }
}
Neste exemplo, o método Translate
altera os componentes dx
e dy
, o que requer que as propriedades Magnitude
e Direction
sejam calculadas quando acessadas. O operador maior ou igual a (=>
) designa um acessador com corpo de expressão get
, 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 primárias do construtor 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 primários do construtor. 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 primários do construtor não serão armazenados no objeto.
Usar injeção de dependência
Um uso comum dos construtores primários é a especificação de parâmetros para a injeção de dependências. 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. 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 num banco. O código a seguir mostra como a classe base pode parecer:
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 de conta e o proprietário. No aplicativo concluído, você pode adicionar outras funcionalidades comuns à classe base.
Muitos tipos requerem validação mais específica nos parâmetros do construtor. Por exemplo, a classe BankAccount
tem requisitos específicos para os parâmetros owner
e accountID
. O parâmetro owner
não deve ser null
ou espaço em branco, e o parâmetro 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));
}
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, quaisquer exceções são lançadas pelo 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 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 CheckingAccount
derivada 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 sintaxe : BankAccount(accountID, owner)
. 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 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 de cada um dos parâmetros 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 neste exemplo mostra que o método ToString
usa os parâmetros do construtor primário (owner
e accountID
) em vez das propriedades de classe base (Owner
e AccountID
). O resultado é que a classe derivada, SavingsAccount
, cria armazenamento para as cópias de parâmetro. 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á a 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.