Un constructor es el código que se ejecuta cuando creas un objeto con new. Tiene el mismo nombre que la clase, no tiene tipo de retorno, y su trabajo es poner el objeto nuevo en un estado válido antes de que nadie lo use.
Salida:
Mug: 8.50
Rejected: price
Como el constructor rechaza un precio negativo, no puede existir ningún Product con precio negativo. Esa garantía es la razón principal para escribir constructores en lugar de asignar los campos desde fuera.
El constructor por defecto, y cuándo desaparece
Si una clase no declara ningún constructor, el compilador proporciona uno público sin parámetros que no hace nada más que ejecutar los inicializadores de los campos. Por eso new BankAccount() funciona en una clase que solo tiene campos.
En el momento en que declaras cualquier constructor, ese implícito deja de generarse:
class Product
{
public string Name;
public Product(string name) { Name = name; }
}
var p = new Product(); // error CS7036: There is no argument given that corresponds to the required parameter 'name'
Esto sorprende a la gente cuando un serializador, un ORM o una restricción genérica new() necesita un constructor sin parámetros. Si quieres los dos, declara tú el constructor sin parámetros:
public Product() { } // or
public Product() : this("Unnamed") { } // delegate to the other one (next section)
Un constructor puede ser public, internal, protected o private. Su acceso decide quién puede crear objetos con él.
Sobrecarga y encadenamiento con this(...)
Una clase puede tener varios constructores con listas de parámetros distintas. Para no copiar las mismas asignaciones en cada uno, encadénalos: : this(...) llama a otro constructor de la misma clase antes de que se ejecute el cuerpo actual.
Salida:
new Pizza():
full constructor: medium, classic, 0
size-only constructor body
parameterless constructor body
Result: medium classic with 0 toppings
La cadena ejecuta primero el constructor más completo y vuelve hasta el que llamaste. Deja el trabajo real (validación, asignaciones) en un constructor y haz que los demás solo aporten valores por defecto.
Los parámetros opcionales son una alternativa: public Pizza(string size = "medium", string crust = "classic", int toppings = 0) da un solo constructor que cubre las tres llamadas. El encadenamiento sigue siendo mejor cuando los constructores cortos necesitan calcular algo, o cuando cambiar un valor por defecto no debe obligar a recompilar a quien llama (los valores por defecto de los parámetros opcionales se copian en el código que llama en tiempo de compilación).
Llamar a un constructor de la clase base con base(...)
Una clase derivada no hereda los constructores. Cada constructor de la clase derivada debe ejecutar antes un constructor de la clase base. Si no escribes nada, el compilador inserta una llamada al constructor sin parámetros de la base; si la base no tiene ninguno, tienes que elegir uno con : base(...).
Merece la pena ver una vez el orden en que se ejecuta todo:
Salida:
Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles
Primero se ejecutan los inicializadores de los campos, los de la clase derivada antes que los de la base, y después los cuerpos de los constructores, desde la base hacia abajo. Así que cuando se ejecuta el cuerpo de Truck, Plate ya está asignado. La única trampa de este orden: si el constructor base llama a un método virtual que Truck sobrescribe, la sobrescritura se ejecuta antes del cuerpo del constructor de Truck, y ve Axles todavía a 0. Evita llamar a métodos virtuales desde los constructores.
Constructores static
Un constructor static inicializa el tipo en lugar de un objeto. No tiene parámetros ni modificador de acceso, y el runtime lo llama exactamente una vez, justo antes de que el tipo se use por primera vez.
Salida:
Program started
Loading tax table (runs once)
123.00
119.00
El runtime hace que el constructor static sea seguro para hilos: aunque varios hilos toquen el tipo a la vez, se ejecuta una sola vez. Si lanza una excepción, cada uso posterior del tipo lanza TypeInitializationException durante toda la vida del proceso, así que mantenlo sencillo y sin E/S que pueda fallar.
Constructor frente a inicializador de objetos
Un inicializador de objetos, new Pizza("large") { Toppings = 3 }, no es un segundo constructor. El compilador lo convierte en "ejecuta el constructor y después asigna estos miembros". Usa cada uno para lo que se le da bien:
- Parámetros del constructor para los valores sin los que el objeto no puede ser válido. Todos los que llaman deben pasarlos, y el constructor puede comprobarlos.
- Inicializador para los ajustes opcionales con valores por defecto razonables.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };
Desde C# 11, una propiedad marcada como required debe asignarse en el inicializador, lo que da a la sintaxis de inicializador parte de las garantías del constructor. Eso se explica en la página de propiedades.
Constructores privados
Un constructor private significa que solo la propia clase puede crear instancias. Lo usan dos patrones: un método de fábrica static, en el que la clase controla la creación y puede devolver un objeto en caché o null, y un singleton:
Salida:
21
100
El error de compilación de new Temperature(5m) fuera de la clase es CS0122, el mismo que producen los modificadores de acceso con cualquier miembro privado. Los métodos de fábrica con nombre resuelven una limitación real: dos constructores no pueden recibir los dos un solo decimal, pero FromCelsius y FromFahrenheit sí pueden. Una clase que solo tiene miembros static (una clase de utilidades) debería declararse static en lugar de esconder su constructor.
Constructores con cuerpo de expresión
Un constructor cuyo cuerpo es una sola sentencia puede usar =>:
public Product(string name) => Name = name;
Con tuplas puedes incluso asignar varios campos en una línea: public Point(int x, int y) => (X, Y) = (x, y);.
Constructores primarios (C# 12)
C# 12 permite que una clase o un struct declare los parámetros de su constructor en el propio tipo. Los parámetros están en ámbito en todos los miembros:
public class Customer(string name, int id)
{
public string Name { get; } = name; // copy into a property
public string Label => $"#{id} {Name}"; // or read a parameter directly
public Customer(string name) : this(name, 0) { } // other constructors must chain to it
}
Dos diferencias con los records sorprenden a la gente. Los parámetros del constructor primario de una clase no se convierten en propiedades públicas; los expones tú. Y un parámetro usado dentro de un miembro se captura en un campo oculto que sigue siendo mutable, así que name = "x"; dentro de un método compila. Los constructores primarios encajan sobre todo con la inyección de dependencias, donde los parámetros son servicios a los que la clase solo llama:
public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
public Order Get(int id) => repo.Find(id);
}
Errores comunes
- Perder el constructor sin parámetros. Añadir un constructor con parámetros elimina el implícito (CS7036 en cada
new X()). - Olvidar
: base(...)cuando la clase base no tiene constructor sin parámetros. El compilador no puede insertar la llamada implícita e informa de que no hay argumento para el parámetro obligatorio del constructor base. - Llamar a métodos virtuales en un constructor. La sobrescritura se ejecuta antes del cuerpo del constructor derivado.
- Trabajo pesado en un constructor. Las llamadas de red, las lecturas de archivos o cualquier cosa lenta hacen que los objetos sean caros y las excepciones difíciles de manejar. Usa un método de fábrica static, o un método de inicialización async, para ese trabajo.
- Escribir un tipo de retorno.
public void Product()es un método normal llamadoProduct, y el compilador lo rechaza porque un miembro no puede llamarse igual que el tipo que lo contiene (CS0542).
Preguntas frecuentes
¿Qué es un constructor en C#?
Un constructor es un método especial que se ejecuta cuando se crea un objeto con new. Tiene el mismo nombre que la clase y no tiene tipo de retorno: public Product(string name) { Name = name; }. Su trabajo es dejar el objeto nuevo en un estado válido, normalmente asignando campos a partir de sus parámetros y rechazando la entrada incorrecta.
¿Crea C# un constructor por defecto automáticamente?
Solo cuando la clase no declara ningún constructor. En ese caso el compilador añade uno público sin parámetros que deja cada campo con su inicializador o su valor por defecto. En cuanto escribes cualquier constructor, por ejemplo uno que recibe un nombre, el implícito desaparece y new Product() deja de compilar (error CS7036). Declara tú public Product() { } si todavía lo necesitas.
¿Cómo llamo a un constructor desde otro en C#?
Usa : this(...) después de la lista de parámetros del constructor: public Product(string name) : this(name, 0m) { }. Primero se ejecuta el constructor de destino, y después el cuerpo del que llamaste. Para llamar a un constructor de la clase base, usa : base(...) de la misma forma.
¿Qué es un constructor static en C#?
Un constructor marcado como static, sin parámetros ni modificador de acceso, que se ejecuta una vez por tipo antes de crear la primera instancia o de usar el primer miembro static. Se usa para inicializar campos static que necesitan más que un inicializador de una línea. No puedes llamarlo tú, y si lanza una excepción, el tipo queda inutilizable durante el resto del programa (TypeInitializationException).
¿Debo usar un constructor o un inicializador de objetos?
Usa parámetros del constructor para los valores sin los que el objeto no puede ser válido, porque el compilador obliga a todos los que llaman a pasarlos y el constructor puede comprobarlos. Usa un inicializador de objetos (new Product("Mug") { Color = "blue" }) para los ajustes opcionales. El inicializador se ejecuta después de que termine el constructor.
¿Qué son los constructores primarios de C# 12?
Parámetros escritos en la propia declaración de la clase, class Customer(string name, int id) { ... }, disponibles en todos los miembros de la clase. A diferencia de los parámetros posicionales de un record, no se convierten en propiedades públicas: exponlos de forma explícita con public string Name => name; o public string Name { get; } = name;.