Constructores y eventos parciales en C# 14
C# 14 te permite declarar constructores de instancia y eventos como miembros parciales, dividiendo las definiciones entre archivos para una generación de código más limpia y una mejor separación de responsabilidades.
C# 14 introduce una nueva capacidad para declarar constructores de instancia y eventos como miembros partial. Esto significa que puedes dividir la definición de un constructor o evento en dos partes de una clase parcial, de forma similar a como C# permite desde hace tiempo los métodos parciales y las propiedades parciales. Una parte de la clase parcial puede declarar un constructor o evento, y otra parte puede implementarlo. Esto es especialmente útil en escenarios como la generación de código, donde un archivo puede ser autogenerado y otro mantenido manualmente por un desarrollador.
Constructores parciales en C# 14
Los constructores parciales te permiten dividir el constructor de una clase en dos declaraciones dentro de una clase partial. Una declaración aporta solo la firma (una declaración definitoria) y otra aporta el cuerpo real (una declaración implementadora). El compilador las fusiona para que se comporten como un único constructor en tiempo de ejecución.
Reglas clave para los constructores parciales:
- Exactamente una declaración definitoria y una implementadora: Debes tener una parte de la clase parcial que declare el constructor (terminada en punto y coma sin cuerpo) y otra parte que aporte la implementación (con el bloque de código del constructor). Las firmas (tipos de parámetros, nombres, etc.) deben coincidir exactamente entre ambas. Si falta la parte definitoria o la implementadora (o si hay duplicados), el código no compilará.
- Solo en clases parciales: Ambas declaraciones deben estar en la misma clase, que a su vez debe estar marcada como
partial. No puedes tener un constructor parcial en una clase que no sea parcial. - El inicializador del constructor solo en la implementación: Si el constructor necesita llamar a otro constructor o al constructor de la clase base mediante
: this(...)o: base(...), ese inicializador solo puede aparecer en la declaración implementadora. La parte definitoria (solo firma) no puede incluir ningún inicializador de constructor. En la práctica, cualquier llamadathis(...)obase(...)se escribe en la cabecera del constructor implementador. - Sintaxis del constructor primario en un solo archivo: C# 14 permite usar constructores primarios (parámetros del constructor en la declaración de la clase) con clases parciales, pero solo un archivo parcial puede incluir la lista de parámetros. En otras palabras, si decides usar la forma concisa de constructor primario en una clase parcial, debes ponerla en una única declaración parcial de la clase; las demás declaraciones parciales de la misma clase no deben repetir ni declarar su propia lista de parámetros.
A continuación hay un ejemplo de un constructor parcial en acción. Imagina una clase repartida en dos archivos: una parte autogenerada declara un constructor y otra parte lo implementa:
// File: Car.AutoGenerated.cs (auto-generated partial class)
public partial class Car
{
// Defining partial constructor (signature only, no body)
public partial Car(string model);
}
// File: Car.cs (developer-maintained partial class)
public partial class Car
{
// Implementing partial constructor (actual body)
public partial Car(string model)
{
// (Optional) Can call another constructor or base constructor here
// : base() or : this(...) would be placed after the parameter list if needed.
if (string.IsNullOrEmpty(model))
throw new ArgumentException("Model must be provided", nameof(model));
Console.WriteLine($"Car model set to {model}");
}
}
En este ejemplo, el constructor de la clase Car está dividido: la primera parte declara que existe Car(string model) y la segunda parte aporta la implementación que valida el argumento e imprime un mensaje. La declaración definitoria no puede incluir código; es solo la firma terminada en ;. La declaración implementadora tiene el cuerpo y también podría incluir una llamada a la clase base (si Car heredase de otra clase) o llamar a otro constructor mediante this(). En tiempo de compilación, el compilador se asegura de que haya exactamente una de cada parte y luego trata el constructor como un único constructor unificado.
Eventos parciales en C# 14
Los eventos parciales permiten una división similar en la definición de eventos dentro de una clase parcial. Una parte de la clase define el evento (sin lógica de add/remove, igual que una declaración de evento automática normal) y otra parte aporta los accesores add/remove (el código que se ejecuta cuando los suscriptores adjuntan o eliminan manejadores de eventos).
Reglas clave para los eventos parciales:
- Exactamente una declaración definitoria y una implementadora: Igual que con los constructores, un evento marcado como
partialdebe declararse exactamente dos veces en la clase parcial: una definición sin cuerpo y una implementación con los cuerpos de los accesores. Deben tener el mismo tipo y nombre de evento, y aparecer en la misma clase parcial. No puedes tener piezas adicionales de evento parcial más allá de esas dos. - La declaración definitoria es similar a un campo: La parte definitoria de un evento parcial se escribe como declararías normalmente un evento respaldado por un campo, por ejemplo,
public partial event EventHandler SomethingHappened;(con punto y coma y sin bloque de accesores). Sin embargo, al ser parcial, el compilador no autogenerará ningún campo de respaldo ni lógica add/remove por defecto para este evento. Es esencialmente un marcador que indica “habrá un evento con este nombre y tipo”. - La declaración implementadora debe tener accesores: La parte implementadora del evento parcial debe aportar tanto el accesor
addcomo elremoveen un bloque de evento, igual que un evento implementado manualmente. Aquí escribes lo que ocurre cuando los oyentes se suscriben o se dan de baja. Como la parte definitoria no tiene un campo de respaldo implícito, tu implementación normalmente necesita gestionar el almacenamiento de los suscriptores (por ejemplo, mediante un campo privado, una lista u otro mecanismo) o reenviar a otra fuente de eventos. - Solo en clases parciales (y no como abstractos ni implementaciones de interfaces): Igual que con los constructores parciales, los eventos parciales solo se pueden usar dentro de una clase marcada como
partial. Tampoco puedes marcar un evento parcial comoabstract, ni puedes usarpartialpara implementar el evento de una interfaz: la característica está pensada para dividir dentro de una clase, no a través de fronteras de interfaces.
Aquí tienes un ejemplo que demuestra un evento parcial. Un archivo declara el evento y otro archivo implementa la lógica personalizada de add/remove:
// File: Sensor.AutoGenerated.cs (auto-generated partial class)
public partial class Sensor
{
// Defining partial event (no body, just declaration)
public partial event EventHandler DataReceived;
}
// File: Sensor.cs (developer-maintained partial class)
public partial class Sensor
{
// Implementing partial event (with custom add/remove logic)
private EventHandler _dataReceivedHandlers; // backing storage for event handlers
public partial event EventHandler DataReceived
{
add
{
Console.WriteLine("Subscriber added to DataReceived");
_dataReceivedHandlers += value;
}
remove
{
Console.WriteLine("Subscriber removed from DataReceived");
_dataReceivedHandlers -= value;
}
}
// Optional: a method to raise the event safely from this class
protected void OnDataReceived(EventArgs e)
{
_dataReceivedHandlers?.Invoke(this, e);
}
}
En este ejemplo, la clase Sensor tiene un evento DataReceived repartido entre dos archivos. La parte autogenerada de la clase declara que existe un evento DataReceived de tipo EventHandler. La parte del desarrollador aporta la implementación real: define un campo privado para guardar los suscriptores e implementa los bloques add y remove para registrar un mensaje y actualizar ese campo privado. Cuando otro código hace sensor.DataReceived += Handler;, se ejecuta la lógica personalizada de add. De igual forma, darse de baja activa la lógica personalizada de remove. La clase puede usar _dataReceivedHandlers para disparar el evento (por ejemplo, en el método OnDataReceived). Sin la parte implementadora, la declaración definitoria por sí sola no compilaría, ya que el evento no tiene respaldo por sí mismo: C# requiere que la parte implementadora complete la funcionalidad del evento.
Comparación con versiones anteriores de C#
Antes de C# 14, no podías declarar constructores ni eventos como parciales. En C# 13 y versiones anteriores, la palabra clave partial solo era válida en clases, métodos y (desde C# 13) propiedades/indexadores. Si intentabas marcar un constructor o un evento como parcial, el compilador producía un error. Esta limitación significaba que no había forma de dividir la implementación de un constructor o un evento entre distintos archivos.
Los desarrolladores solían recurrir a soluciones alternativas para lograr una flexibilidad similar. Por ejemplo, considera un escenario en el que el código autogenerado necesita llamar a alguna lógica personalizada durante la construcción del objeto. Sin constructores parciales, un patrón común era usar un método parcial dentro del constructor. La parte autogenerada de la clase podía llamar a este método, mientras que la implementación del método estaba en otro archivo. Por ejemplo, en código C# antiguo podrías ver:
// Before C# 14: using a partial method to extend constructor logic
public partial class Car
{
public Car()
{
InitializeComponents(); // auto-generated initialization
OnConstructed(); // call a partial method hook
}
// This partial method can be implemented in another file to add custom behavior
partial void OnConstructed();
}
En el segundo archivo, el desarrollador podía implementar el método parcial OnConstructed para ejecutar código adicional tras InitializeComponents(). Este patrón permitía inyectar código personalizado en el momento de la construcción, pero era indirecto y un poco torpe. De forma similar, para los eventos no había una solución alternativa sencilla: había que heredar y sobrescribir la funcionalidad o modificar el código generado, ya que no se podía dividir la lógica add/remove de un evento entre archivos.
Con los constructores y eventos parciales de C# 14, estas soluciones alternativas ya no son necesarias. Puedes declarar directamente un constructor o evento como parcial y proporcionar la implementación por separado, lo que hace el código más directo. El nuevo enfoque es más claro y elimina la necesidad de métodos parciales ficticios u otros trucos.
Casos de uso y beneficios
Los principales beneficiarios de los constructores y eventos parciales son los escenarios de generación de código y herramientas. Muchos frameworks y herramientas generan código C# (por ejemplo, diseñadores de UI, ORMs o herramientas de andamiaje de interfaces) y a menudo marcan las clases como parciales para que los desarrolladores puedan extender las clases generadas sin editar el código autogenerado. Con C# 14:
- Los generadores de código pueden definir constructores por ti: por ejemplo, un generador de código fuente podría crear una firma de constructor parcial que el desarrollador debe implementar con lógica de inicialización personalizada. A la inversa, un generador podría implementar un constructor parcial que el desarrollador declaró, quizá para inyectar código de configuración específico del framework entre bambalinas. Esto facilita mezclar inicialización generada con inicialización escrita por el usuario de forma segura.
- Dividir lógica de construcción compleja: en proyectos grandes, podrías querer organizar la lógica del constructor entre varios archivos (por ejemplo, separando el cableado de dependencias de la lógica de negocio). Los constructores parciales ofrecen una forma estructurada de hacerlo si es necesario.
- Patrones de eventos personalizados: los eventos parciales habilitan escenarios avanzados de manejo de eventos. Un ejemplo destacado es el patrón de evento débil (para evitar fugas de memoria con eventos). Un desarrollador o un generador de código fuente puede declarar un evento parcial en un archivo (quizás anotado con algo como
[WeakEvent]) e implementarlo en otro archivo de forma queadd/removeusen referencias débiles a los manejadores. Esto significa que los objetos que se suscriben no impiden la recolección de basura si se olvidan de darse de baja. Antes de los eventos parciales, implementar un patrón de evento débil podía requerir mucho código repetitivo o bibliotecas externas. Ahora un autor de bibliotecas podría proporcionar un generador que aporte automáticamente la lógica compleja de add/remove en una implementación parcial separada, mientras el código del usuario permanece limpio. De forma similar, los eventos parciales podrían usarse para reenviar suscripciones de eventos a sistemas subyacentes (por ejemplo, envolviendo un evento de una API de bajo nivel manejando add/remove y conectándolos a la fuente real). - Código de plataforma e interop: en escenarios de interoperabilidad (como Xamarin o el interop de .NET con bibliotecas nativas), las clases generadas a menudo necesitan llamar a código nativo en los constructores o gestionar enganches de eventos nativos. Los constructores parciales permiten que la configuración de la llamada nativa viva en el código generado, mientras que el resto del constructor lo define el usuario (o viceversa). Los eventos parciales pueden permitir que una clase exponga un evento .NET respaldado en realidad por algún mecanismo de eventos específico de la plataforma, implementado en otro archivo parcial.
En todos estos casos, los constructores y eventos parciales hacen que el código sea más fácil de mantener. El código autogenerado puede declarar lo que debe existir, y el código escrito a mano puede limitarse a lo que el desarrollador realmente quiere implementar. Tampoco hay coste de rendimiento en tiempo de ejecución por usar miembros parciales: la separación existe únicamente en tiempo de compilación. El programa resultante se comporta como si hubieras escrito el constructor o el evento completos en un solo lugar.
Al permitir que los constructores de instancia y los eventos sean partial, C# 14 redondea el conjunto de características de clases parciales para cubrir casi todos los tipos de miembros. Esta mejora aumenta la capacidad del lenguaje para soportar una separación limpia entre código autogenerado y código personalizado. Los desarrolladores obtienen flexibilidad para organizar y extender el comportamiento de las clases sin trucos, y los generadores de código fuente ganan una nueva herramienta poderosa para inyectar funcionalidad de forma modular. Tanto si trabajas en frameworks grandes como si simplemente divides el código por claridad, los constructores y eventos parciales ofrecen un enfoque más expresivo y conveniente en C# 14.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.