Benvido á primeira entrega de Pílulas de C#, onde explicaremos eses pequenos cambios e características máis sucosas de C# coa paixón dun chef explicando a súa receita secreta.
Hoxe imos falar dunha característica que vive connosco dende C# 12 (que se corresponde coa versión .Net 8) e que, aínda que non faga tanto ruído como outras características, está cambiando silenciosamente a forma na que escribimos as nosas clases. Preséntoche os construtores primarios: eses pequenos heroes que nos permiten escribir menos código e ser máis produtivos.
TL;DR (Para os impacientes)
- Os construtores primarios chegaron con C# 12 para clases e structs
- Permiten declarar parámetros directamente na definición do tipo
- Os parámetros están dispoñibles en todo o corpo da clase
- Se non se usan, o compilador descártalos automaticamente
- Se se usan en métodos, captúranse como campos privados
- Todos os demais construtores deben chamar ao primario con
this(...)
- Son perfectos para inxección de dependencias e inicialización simple
Que son os construtores primarios?
Imaxina que puideras declarar os parámetros do teu construtor favorito directamente na cabeceira da túa clase, como se fose a sinatura dun método. Iso é exactamente o que fan os construtores primarios.
Antes tiñas que facer isto:
public class CustomerService
{
private readonly IRepository _repository;
private readonly ILogger _logger;
public CustomerService(IRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
public Customer GetCustomer(int id)
{
_logger.LogInformation($"Getting customer {id}");
return _repository.GetById(id);
}
}
Agora podes simplificalo así:
public class CustomerService(IRepository repository, ILogger logger)
{
public Customer GetCustomer(int id)
{
logger.LogInformation($"Getting customer {id}");
return repository.GetById(id);
}
}
Ves a diferenza? O código é máis limpo, máis conciso e vai directo ao gran. Os parámetros repository
e logger
están dispoñibles en toda a clase como se fosen variables máxicas.
A maxia detrás do telón
Cando o compilador ve que usas un parámetro do construtor primario dentro dun método (como no exemplo anterior), automaticamente crea un campo privado para almacenalo. É como ter un asistente invisible que se encarga do traballo sucio por ti.
// O que escribes:
public class BankAccount(string accountNumber, decimal initialBalance)
{
public void Deposit(decimal amount)
{
initialBalance += amount; // Usa o parámetro
Console.WriteLine($"Account {accountNumber}: New balance = {initialBalance}");
}
}
// O que fai o compilador (aproximadamente):
public class BankAccount
{
private readonly string __accountNumber; // Campo xerado
private decimal __initialBalance; // Campo xerado
public BankAccount(string accountNumber, decimal initialBalance)
{
__accountNumber = accountNumber;
__initialBalance = initialBalance;
}
public void Deposit(decimal amount)
{
__initialBalance += amount;
Console.WriteLine($"Account {__accountNumber}: New balance = {__initialBalance}");
}
}
Pero se só usas os parámetros para inicialización, o compilador é o suficientemente intelixente para non crear campos innecesarios:
public class Circle(double radius)
{
public double Area { get; } = Math.PI * radius * radius; // Só inicialización
public double Circumference { get; } = 2 * Math.PI * radius; // Só inicialización
}
Neste caso, radius
non se captura nun campo porque só se usa durante a inicialización.
As regras do xogo
Os construtores primarios teñen algunhas regras que debes coñecer para usalos como un mestre:
Regra #1: Todos os camiños levan ao primario
Se tes un construtor primario, calquera outro construtor que engadas debe chamalo usando this(...)
:
public class Product(string name, decimal price)
{
public string Name { get; } = name;
public decimal Price { get; } = price;
// Construtor adicional que DEBE chamar ao primario
public Product(string name) : this(name, 0.0m) { }
// Este construtor causaría erro de compilación:
// public Product() { } // ❌ Non chama ao construtor primario
}
Regra #2: Os parámetros son parámetros, non propiedades
Os parámetros do construtor primario non son membros da clase. Non podes acceder a eles con this.parametro
:
public class Person(string name)
{
public void PrintName()
{
Console.WriteLine(name); // ✅ Correcto
Console.WriteLine(this.name); // ❌ Erro de compilación
}
}
Regra #3: Ámbito intelixente
Os parámetros teñen diferentes comportamentos segundo onde os uses:
public class SmartClass(int value, string name) : BaseClass(value)
{
// En inicializadores de campos/propiedades, os parámetros teñen prioridade
public string Name { get; } = name; // Usa o parámetro 'name'
// No corpo de métodos, os membros da clase teñen prioridade
public void DoSomething()
{
Console.WriteLine(Name); // Usa a propiedade 'Name', non o parámetro 'name'
Console.WriteLine(name); // Usa o parámetro 'name' (captúrase automaticamente)
}
}
Casos de uso perfectos
Inxección de dependencias
Os construtores primarios brillan especialmente en escenarios de inxección de dependencias:
// Controlador API limpo e conciso
public class WeatherController(IWeatherService weatherService, ILogger<WeatherController> logger) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<Weather>> Get(string city)
{
logger.LogInformation($"Getting weather for {city}");
var weather = await weatherService.GetWeatherAsync(city);
return Ok(weather);
}
}
Validación con estilo
Podes engadir validación directamente nos inicializadores de propiedades:
public class User(string email, int age)
{
public string Email { get; } = IsValidEmail(email)
? email
: throw new ArgumentException("Invalid email", nameof(email));
public int Age { get; } = age >= 0
? age
: throw new ArgumentException("Age cannot be negative", nameof(age));
private static bool IsValidEmail(string email)
=> email.Contains("@"); // Validación simple para o exemplo
}
Herdanza elegante
Os construtores primarios lévanse moi ben coa herdanza:
public class Vehicle(string brand, int year)
{
public string Brand { get; } = brand;
public int Year { get; } = year;
}
public class Car(string brand, int year, int doors) : Vehicle(brand, year)
{
public int Doors { get; } = doors;
public void ShowInfo()
{
Console.WriteLine($"Car: {Brand} {Year} with {Doors} doors");
// Nota: Brand e Year son propiedades da clase base
// doors captúrase automaticamente porque se usa aquí
Console.WriteLine($"Original doors parameter: {doors}");
}
}
Perigos e advertencias
O perigo do dobre almacenamento
Coidado con capturar parámetros que xa usaches para inicializar propiedades:
public class Person(string name)
{
public string Name { get; set; } = name; // Almacénase na propiedade
public override string ToString() => name; // Captúrase TAMÉN como campo!
}
Neste caso, name
almacénase dúas veces: unha na propiedade Name
e outra nun campo privado xerado. Pero non te preocupes, o compilador advertirache sobre isto.
Records vs Clases normais
Nos records, os parámetros do construtor primario convértense automaticamente en propiedades públicas:
// Nun record, 'Name' convértese nunha propiedade pública
public record PersonRecord(string Name);
// Nunha clase normal, 'name' só é un parámetro
public class PersonClass(string name)
{
// Necesitas crear a propiedade manualmente se a queres
public string Name { get; } = name;
}
Atributos en construtores primarios
Podes aplicar atributos ao construtor primario usando o target method
:
[method: JsonConstructor] // O atributo vai ao construtor xerado
public class ApiResponse(int statusCode, string message)
{
public int StatusCode { get; } = statusCode;
public string Message { get; } = message;
}
Cando NON usar construtores primarios?
Os construtores primarios son xeniais, pero non sempre son a mellor opción:
- Cando necesitas lóxica complexa de inicialización: Os construtores primarios non permiten código adicional. Se necesitas validacións complexas ou configuración especial, un construtor tradicional podería ser mellor.
- Cando tes moitos parámetros opcionais: Os construtores primarios non soportan parámetros opcionais de forma elegante.
- Cando a visibilidade do construtor debe ser diferente á da clase: Non podes facer un construtor primario privado mentres a clase é pública.
Conclusión: A revolución silenciosa
Os construtores primarios non van cambiar o mundo, pero definitivamente van facer que o teu código sexa máis limpo, menos "verboso" e expresivo. Son esa mellora pequena pero significativa que te fai sorrir cada vez que escribes unha nova clase.
Son especialmente útiles para:
- Servizos con inxección de dependencias
- DTOs e obxectos de transferencia de datos
- Clases que principalmente almacenan e expoñen datos
- Calquera escenario onde queiras menos ceremonia e máis funcionalidade
A próxima vez que vaias crear unha clase nova, pregúntate: "Podería usar un construtor primario aquí?". Probablemente a resposta sexa si, e o teu código agradeceríacho.
Espero que esta pequena pílula che fose de utilidade. Vémolo na próxima pílula de C#, onde seguiremos explorando as características que fan deste linguaxe unha delicia para desenvolver!
Referencias útiles: