El estilo clásico de declarar una clase con campos privados, un constructor que los asigne y propiedades que los expongan produce mucho código repetitivo. Las versiones recientes del lenguaje incorporan varias funciones que reducen ese ruido sin renunciar a ningún principio de encapsulación. Los primary constructors y el modificador required son dos piezas clave en ese objetivo.
Primary constructors en clases
Un primary constructor es una lista de parámetros declarada junto al nombre de la clase, entre paréntesis. Esos parámetros están disponibles en todo el cuerpo de la clase como si fueran miembros privados, pero sin requerir una declaración explícita de campos.
public class CalculadoraPrecios(decimal iva, decimal descuentoBase)
{
public decimal Aplicar(decimal importe)
{
var conIva = importe * (1 + iva);
return conIva - descuentoBase;
}
}
La clase no tiene constructor visible, ni campos para iva ni descuentoBase. El compilador captura los parámetros automáticamente en el estado de la instancia. Cada llamada a Aplicar los puede leer sin indirecciones. La creación de instancias es la habitual.
var calculadora = new CalculadoraPrecios(0.21m, 5);
decimal total = calculadora.Aplicar(100);
Este estilo es especialmente cómodo para servicios con dependencias. Una clase que recibe varios colaboradores por inyección reduce su tamaño de forma notable.
public class NotificadorPedidos(
IRepositorioPedidos repositorio,
IEnviadorCorreo correo,
ILogger<NotificadorPedidos> logger)
{
public async Task EnviarConfirmacionAsync(int pedidoId)
{
var pedido = await repositorio.BuscarAsync(pedidoId);
await correo.EnviarAsync(pedido.Cliente, $"Pedido {pedidoId} confirmado");
logger.LogInformation("Confirmacion enviada al pedido {PedidoId}", pedidoId);
}
}
La sintaxis compacta evita escribir tres campos privados, un constructor con tres parámetros y tres asignaciones. El propósito de la clase queda más visible.
El primary constructor es obligatorio si se declara. No se puede crear una instancia sin pasar todos sus parámetros, aunque sí se pueden definir constructores adicionales que encadenen al primario con la sintaxis
: this(...).
Capturar parámetros como propiedades públicas
Los parámetros de un primary constructor no son públicos por defecto. Son visibles solo dentro de la clase. Para exponerlos, basta con declarar una propiedad que los referencie.
public class Libro(string titulo, string autor, int paginas)
{
public string Titulo { get; } = titulo;
public string Autor { get; } = autor;
public int Paginas { get; } = paginas;
public bool EsLargo => paginas > 300;
}
Las propiedades con inicializador reutilizan el parámetro en la construcción. Una vez construido el objeto, las propiedades quedan disponibles para el exterior mientras la clase sigue viendo también el parámetro original.
var libro = new Libro("Fundacion", "Asimov", 420);
Console.WriteLine(libro.Titulo);
Console.WriteLine(libro.EsLargo);
Este patrón cubre la mayoría de modelos de dominio donde el estado se fija al construir y no cambia después. La inmutabilidad es implícita, ya que no hay setters, y el código cabe en unas pocas líneas.
Propiedades init-only
Una propiedad init-only se declara con el accesor init en lugar de set. Permite asignar el valor solo durante la construcción del objeto o en un inicializador de objeto, pero no en un momento posterior.
public class Usuario
{
public string Nombre { get; init; }
public string Correo { get; init; }
public DateOnly FechaAlta { get; init; }
}
El uso típico combina un inicializador de objeto con una sintaxis declarativa cercana a la de los records.
var usuario = new Usuario
{
Nombre = "Ana",
Correo = "ana@ejemplo.com",
FechaAlta = DateOnly.FromDateTime(DateTime.Today)
};
usuario.Nombre = "Cambio prohibido";
La última línea produce un error de compilación. El accesor init garantiza que tras la construcción nadie podrá modificar el valor, algo que antes requería un constructor con parámetros y muchas líneas repetidas.
required para forzar inicialización
El modificador required se aplica a propiedades o campos para indicar que deben ser inicializados cada vez que se cree una instancia. El compilador exige al consumidor asignar esas propiedades en el inicializador, o recibirá un error.
public class Producto
{
public required string Nombre { get; init; }
public required decimal Precio { get; init; }
public string Descripcion { get; init; } = string.Empty;
}
La creación debe incluir obligatoriamente Nombre y Precio. Descripcion queda opcional por tener un valor por defecto.
var valido = new Producto
{
Nombre = "Teclado",
Precio = 49.90m
};
var invalido = new Producto
{
Nombre = "Raton"
};
El segundo caso no compila, porque Precio es required y falta. El error se produce antes de ejecutar, lo cual hace que la clase sea casi imposible de mal usar desde fuera.
requiredes perfecto para modelos donde un campo vacío no tiene sentido, como identificadores, referencias de pedido, correos o rutas de archivo. Es una alternativa más expresiva que lanzarArgumentExceptiondesde el setter.
Combinar primary constructor y required
En escenarios reales, una clase suele tener parámetros obligatorios recibidos desde fuera y otros campos que se inicializan desde un DTO o desde una configuración. Combinar primary constructors con miembros required cubre ambas necesidades.
public class PedidoCreacion(DateTime fecha, string canalOrigen)
{
public DateTime Fecha { get; } = fecha;
public string CanalOrigen { get; } = canalOrigen;
public required string ClienteId { get; init; }
public required IReadOnlyList<int> Productos { get; init; }
public string Nota { get; init; } = string.Empty;
}
La clase exige la fecha y el canal en el constructor, mientras que ClienteId y Productos se pasan por inicializador y son obligatorios. El compilador impone el contrato sin necesidad de validaciones manuales.
var pedido = new PedidoCreacion(DateTime.UtcNow, "web")
{
ClienteId = "C-1024",
Productos = new[] { 101, 102 }
};
flowchart LR
A[Creación] --> B[Primary ctor]
B --> C[Campos posicionales fijados]
A --> D[Object initializer]
D --> E{Propiedades required?}
E -->|si, completas| F[Instancia lista]
E -->|falta alguna| G[Error compilación]
Herencia con primary constructors
Una clase con primary constructor puede heredar de otra y pasar argumentos al constructor base mediante la lista de base después del tipo. El parámetro del primary constructor está disponible tanto en la expresión base como dentro del cuerpo.
public abstract class EventoDominio(DateTime ocurridoEn)
{
public DateTime OcurridoEn { get; } = ocurridoEn;
}
public class PedidoCreado(int pedidoId, DateTime ocurridoEn)
: EventoDominio(ocurridoEn)
{
public int PedidoId { get; } = pedidoId;
}
La clase hija reutiliza ocurridoEn para inicializar la propiedad pública heredada y mantiene su propio parámetro pedidoId. Esta forma produce jerarquías compactas con muy poca ceremonia de paso de argumentos.
Uso con structs y records
Los primary constructors existen también para struct y record. En struct proporcionan la misma reducción de código, con especial utilidad en tipos pequeños del dominio.
public readonly struct Coordenada(double latitud, double longitud)
{
public double Latitud { get; } = latitud;
public double Longitud { get; } = longitud;
public double DistanciaA(Coordenada otra) =>
Math.Sqrt(Math.Pow(Latitud - otra.Latitud, 2)
+ Math.Pow(Longitud - otra.Longitud, 2));
}
El modificador readonly en el struct garantiza que ningún método modifica el estado, una garantía habitual en tipos de valor usados como coordenadas, cantidades o identificadores fuertes.
En record, el primary constructor existe desde su introducción y define directamente las propiedades posicionales. La combinación con required para propiedades adicionales funciona igual que en las clases.
public record Factura(int Id, DateTime Emision)
{
public required string Serie { get; init; }
public decimal ImporteTotal { get; init; }
}
Elegir entre
class,structorecorddepende de la identidad del tipo. Las clases representan entidades con ciclo de vida, los structs son valores pequeños sin identidad y los records son datos inmutables con igualdad estructural.
Buenas prácticas y matices
Tres reglas prácticas ayudan a no abusar de estas características. La primera es preferir inmutabilidad siempre que la lógica lo permita. Las propiedades init con required cubren esa necesidad sin escribir clases enteras a mano.
La segunda es no mezclar primary constructor con campos privados que dupliquen los parámetros. Si un parámetro va a ser usado en varios métodos, es suficiente con referenciarlo directamente. El compilador se encarga de capturarlo.
La tercera es documentar los parámetros del primary constructor con comentarios <param> cuando la clase sea parte de una API pública. La experiencia del consumidor mejora porque los IDEs muestran la documentación en cada instancia.
/// <param name="descuentoMaximo">Descuento máximo permitido en tanto por uno.</param>
/// <param name="reloj">Proveedor de la fecha actual para ser testeable.</param>
public class GestorDescuentos(decimal descuentoMaximo, IReloj reloj)
{
public bool Valida(decimal descuento) =>
descuento >= 0 && descuento <= descuentoMaximo && reloj.Ahora.DayOfWeek != DayOfWeek.Sunday;
}
La combinación de primary constructors, propiedades init, modificador required y records cubre casi todos los patrones de declaración de tipos de datos. En proyectos nuevos esta es la forma recomendada de empezar antes de plantearse construcciones más elaboradas.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, C# es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de C#
Explora más contenido relacionado con C# y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Aplicar primary constructors en clases y structs, combinarlos con propiedades init-only e instrucciones de campo, y usar required para obligar la inicialización de miembros sin escribir constructores completos.