El estilo clasico de declarar una clase con campos privados, un constructor que los asigne y propiedades que los expongan produce mucho codigo repetitivo. Las versiones recientes del lenguaje incorporan varias funciones que reducen ese ruido sin renunciar a ningun principio de encapsulacion. 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 parametros declarada junto al nombre de la clase, entre parentesis. Esos parametros estan disponibles en todo el cuerpo de la clase como si fueran miembros privados, pero sin requerir una declaracion explicita 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 parametros automaticamente en el estado de la instancia. Cada llamada a Aplicar los puede leer sin indirecciones. La creacion de instancias es la habitual.
var calculadora = new CalculadoraPrecios(0.21m, 5);
decimal total = calculadora.Aplicar(100);
Este estilo es especialmente comodo para servicios con dependencias. Una clase que recibe varios colaboradores por inyeccion reduce su tamano 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 parametros y tres asignaciones. El proposito de la clase queda mas visible.
El primary constructor es obligatorio si se declara. No se puede crear una instancia sin pasar todos sus parametros, aunque si se pueden definir constructores adicionales que encadenen al primario con la sintaxis
: this(...).
Capturar parametros como propiedades publicas
Los parametros de un primary constructor no son publicos 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 parametro en la construccion. Una vez construido el objeto, las propiedades quedan disponibles para el exterior mientras la clase sigue viendo tambien el parametro original.
var libro = new Libro("Fundacion", "Asimov", 420);
Console.WriteLine(libro.Titulo);
Console.WriteLine(libro.EsLargo);
Este patron cubre la mayoria de modelos de dominio donde el estado se fija al construir y no cambia despues. La inmutabilidad es implicita, ya que no hay setters, y el codigo cabe en unas pocas lineas.
Propiedades init-only
Una propiedad init-only se declara con el accesor init en lugar de set. Permite asignar el valor solo durante la construccion 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 tipico 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 ultima linea produce un error de compilacion. El accesor init garantiza que tras la construccion nadie podra modificar el valor, algo que antes requeria un constructor con parametros y muchas lineas repetidas.
required para forzar inicializacion
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 recibira 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 creacion 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 vacio no tiene sentido, como identificadores, referencias de pedido, correos o rutas de archivo. Es una alternativa mas expresiva que lanzarArgumentExceptiondesde el setter.
Combinar primary constructor y required
En escenarios reales, una clase suele tener parametros obligatorios recibidos desde fuera y otros campos que se inicializan desde un DTO o desde una configuracion. 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[Creacion] --> 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 compilacion]
Herencia con primary constructors
Una clase con primary constructor puede heredar de otra y pasar argumentos al constructor base mediante la lista de base despues del tipo. El parametro del primary constructor esta disponible tanto en la expresion 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 publica heredada y mantiene su propio parametro pedidoId. Esta forma produce jerarquias compactas con muy poca ceremonia de paso de argumentos.
Uso con structs y records
Los primary constructors existen tambien para struct y record. En struct proporcionan la misma reduccion de codigo, con especial utilidad en tipos pequenos 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 ningun metodo modifica el estado, una garantia habitual en tipos de valor usados como coordenadas, cantidades o identificadores fuertes.
En record, el primary constructor existe desde su introduccion y define directamente las propiedades posicionales. La combinacion 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 pequenos sin identidad y los records son datos inmutables con igualdad estructural.
Buenas practicas y matices
Tres reglas practicas ayudan a no abusar de estas caracteristicas. La primera es preferir inmutabilidad siempre que la logica 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 parametros. Si un parametro va a ser usado en varios metodos, es suficiente con referenciarlo directamente. El compilador se encarga de capturarlo.
La tercera es documentar los parametros del primary constructor con comentarios <param> cuando la clase sea parte de una API publica. La experiencia del consumidor mejora porque los IDEs muestran la documentacion en cada instancia.
/// <param name="descuentoMaximo">Descuento maximo 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 combinacion de primary constructors, propiedades init, modificador required y records cubre casi todos los patrones de declaracion de tipos de datos. En proyectos nuevos esta es la forma recomendada de empezar antes de plantearse construcciones mas 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 inicializacion de miembros sin escribir constructores completos.