Partial-конструкторы и события в C# 14
C# 14 позволяет объявлять конструкторы экземпляров и события как partial-члены, разделяя определения между файлами для более чистой генерации кода и разделения ответственности.
В C# 14 появилась новая возможность объявлять конструкторы экземпляров и события как partial-члены. Это означает, что определение конструктора или события можно разнести по двум частям partial-класса — аналогично тому, как C# уже давно позволяет делать это с partial-методами и partial-свойствами. Одна часть partial-класса может объявлять конструктор или событие, а другая — его реализовывать. Это особенно полезно в сценариях вроде кодогенерации, когда один файл генерируется автоматически, а другой ведётся вручную разработчиком.
Partial-конструкторы в C# 14
Partial-конструкторы позволяют разделить конструктор класса на два объявления внутри partial-класса. Одно объявление содержит только сигнатуру (определяющее объявление), а другое — сам тело (реализующее объявление). Компилятор объединяет их так, что во время выполнения они ведут себя как один конструктор.
Ключевые правила partial-конструкторов:
- Ровно одно определяющее и одно реализующее объявление. В одной части partial-класса нужно объявить конструктор (заканчивая точкой с запятой и без тела), а в другой — предоставить реализацию (с блоком кода конструктора). Сигнатуры (типы параметров, имена и т. д.) должны точно совпадать. Если одна из частей отсутствует (или имеются дубликаты), код не скомпилируется.
- Только в partial-классах. Оба объявления должны находиться в одном и том же классе, который сам должен быть помечен как
partial. В не-partial классе partial-конструктор иметь нельзя. - Инициализатор конструктора — только в реализации. Если конструктору нужно вызвать другой конструктор или конструктор базового класса через
: this(...)или: base(...), такой инициализатор может быть только в реализующем объявлении. Определяющая (сигнатурная) часть не может содержать никаких инициализаторов конструктора. На практике любой вызовthis(...)илиbase(...)пишется в заголовке реализующего конструктора. - Синтаксис primary-конструктора только в одном файле. C# 14 разрешает использовать primary-конструкторы (параметры конструктора в объявлении класса) с partial-классами, но список параметров может содержать только один из partial-файлов. Иначе говоря, если вы используете компактную форму primary-конструктора в partial-классе, она должна находиться в одном partial-объявлении класса; другие partial-объявления того же класса не должны повторять список параметров и не должны объявлять собственный.
Ниже приведён пример partial-конструктора в действии. Представьте класс, разделённый на два файла: одна автогенерируемая часть объявляет конструктор, а другая — его реализует.
// 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}");
}
}
В этом примере конструктор класса Car разделён: первая часть объявляет, что Car(string model) существует, а вторая часть содержит реализацию, которая проверяет аргумент и выводит сообщение. Определяющее объявление не может содержать кода — это только сигнатура, заканчивающаяся на ;. Реализующее объявление содержит тело и также может включать вызов базового класса (если бы Car наследовал от другого класса) или вызов другого конструктора через this(). Во время компиляции компилятор проверяет, что есть ровно одна часть каждого вида, а затем рассматривает конструктор как единый.
Partial-события в C# 14
Partial-события позволяют аналогичным образом разделить определение событий в partial-классе. Одна часть класса определяет событие (без логики add/remove, как обычное автоматическое объявление события), а другая часть содержит акцессоры add/remove (код, выполняемый, когда подписчики добавляют или удаляют обработчики событий).
Ключевые правила partial-событий:
- Ровно одно определяющее и одно реализующее объявление. Как и для конструкторов, событие, помеченное
partial, должно быть объявлено в partial-классе ровно дважды: одно определение без тела и одна реализация с телами акцессоров. Они должны иметь одинаковый тип и имя события и находиться в одном partial-классе. Дополнительных partial-частей события сверх этих двух быть не может. - Определяющее объявление похоже на поле. Определяющая часть partial-события записывается так, как обычно записывают событие, поддерживаемое полем, например:
public partial event EventHandler SomethingHappened;(с точкой с запятой и без блока акцессоров). Однако, поскольку оно partial, компилятор не сгенерирует автоматически ни поле для хранения подписчиков, ни логику add/remove по умолчанию. По сути это маркер, обозначающий: “будет событие с таким именем и типом”. - Реализующее объявление должно содержать акцессоры. Реализующая часть partial-события должна предоставлять и акцессор
add, и акцессорremoveв блоке события, как у вручную реализованного события. Здесь вы пишете, что происходит, когда слушатели подписываются или отписываются. Поскольку у определяющей части нет неявного хранилища, ваша реализация обычно должна сама управлять хранением подписчиков (например, через приватное поле, список или другой механизм) либо пересылать вызовы другому источнику событий. - Только в partial-классах (и не как абстрактные или реализации интерфейсов). Как и partial-конструкторы, partial-события можно использовать только внутри класса, помеченного
partial. Также partial-событие нельзя пометить какabstract, иpartialнельзя использовать для реализации события интерфейса — эта возможность предназначена для разделения внутри одного класса, а не на границе с интерфейсами.
Ниже — пример, демонстрирующий partial-событие. Один файл объявляет событие, а другой реализует пользовательскую логику 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);
}
}
В этом примере у класса Sensor есть событие DataReceived, разделённое между двумя файлами. Автогенерируемая часть класса объявляет, что существует событие DataReceived типа EventHandler. Часть, ведомая разработчиком, содержит фактическую реализацию: она объявляет приватное поле для хранения подписчиков и реализует блоки add и remove, которые записывают сообщение и обновляют это поле. Когда другой код выполняет sensor.DataReceived += Handler;, срабатывает пользовательская логика add. Аналогично отписка запускает пользовательскую логику remove. Класс может использовать _dataReceivedHandlers для возбуждения события (например, в методе OnDataReceived). Без реализующей части определяющее объявление само по себе не скомпилируется, поскольку у события нет собственного хранилища — C# требует, чтобы реализующая часть завершала функциональность события.
Сравнение с предыдущими версиями C#
До C# 14 объявлять конструкторы или события как partial было нельзя. В C# 13 и более ранних версиях ключевое слово partial было допустимо только для классов, методов и (с C# 13) свойств/индексаторов. Попытка пометить конструктор или событие как partial приводила к ошибке компиляции. Это ограничение означало, что разделить реализацию конструктора или события между разными файлами было невозможно.
Разработчики часто прибегали к обходным решениям, чтобы добиться похожей гибкости. Например, представьте сценарий, в котором автогенерируемому коду нужно вызвать пользовательскую логику во время построения объекта. Без partial-конструкторов одним из распространённых паттернов было использование partial-метода внутри конструктора. Автогенерируемая часть класса вызывала этот метод, а его реализация находилась в другом файле. Например, в старом C#-коде можно было встретить такое:
// 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();
}
Во втором файле разработчик мог реализовать partial-метод OnConstructed, чтобы выполнить дополнительный код после InitializeComponents(). Этот паттерн позволял внедрять пользовательский код во время построения, но был непрямым и слегка неуклюжим. Для событий простого обходного пути не было: приходилось наследоваться и переопределять функциональность или менять сгенерированный код, потому что разнести логику add/remove события по разным файлам было нельзя.
С появлением partial-конструкторов и событий в C# 14 эти обходные решения больше не нужны. Конструктор или событие можно напрямую объявить как partial и предоставить реализацию отдельно, что делает код более прямолинейным. Новый подход яснее и устраняет необходимость в “затычках” в виде partial-методов и других ухищрений.
Сценарии использования и преимущества
Главные бенефициары partial-конструкторов и событий — сценарии, связанные с кодогенерацией и инструментами. Многие фреймворки и инструменты генерируют C#-код (например, дизайнеры UI, ORM или средства каркаса интерфейсов) и часто помечают классы как partial, чтобы разработчики могли расширять сгенерированные классы, не редактируя автогенерированный код. С C# 14:
- Кодогенераторы могут определять конструкторы за вас. Например, source generator может создать сигнатуру partial-конструктора, который разработчик должен реализовать с пользовательской логикой инициализации. И наоборот, генератор может реализовать partial-конструктор, объявленный разработчиком, чтобы незаметно внедрить специфический для фреймворка код настройки. Это упрощает безопасное смешивание сгенерированной и пользовательской инициализации.
- Разделение сложной логики построения. В крупных проектах вы можете захотеть распределить логику конструктора между несколькими файлами (например, отделить связывание зависимостей от бизнес-логики). Partial-конструкторы дают для этого структурный способ.
- Пользовательские шаблоны событий. Partial-события открывают продвинутые сценарии обработки событий. Яркий пример — шаблон слабых событий (для предотвращения утечек памяти на событиях). Разработчик или source generator может объявить partial-событие в одном файле (возможно, с аннотацией вроде
[WeakEvent]), а в другом файле реализовать его так, чтобыadd/removeиспользовали слабые ссылки на обработчики. Тогда подписавшиеся объекты не будут мешать сборке мусора, если забудут отписаться. До partial-событий реализация шаблона слабых событий часто требовала много шаблонного кода или внешних библиотек. Теперь автор библиотеки может предоставить генератор, который автоматически добавляет сложную логику add/remove в отдельной partial-реализации, оставляя пользовательский код чистым. Аналогично partial-события можно использовать, чтобы пересылать подписки на события в нижележащие системы (например, оборачивая событие из API более низкого уровня и связывая add/remove с фактическим источником). - Платформенный код и interop. В сценариях interop (например, Xamarin или взаимодействие .NET с нативными библиотеками) сгенерированные классы часто должны вызывать нативный код в конструкторах или управлять подключением нативных событий. Partial-конструкторы позволяют оставить настройку нативного вызова в сгенерированном коде, а остальную часть конструктора отдать пользователю (или наоборот). Partial-события позволяют классу выставлять .NET-событие, которое на самом деле опирается на специфичный для платформы механизм событий, реализованный в другом partial-файле.
Во всех этих случаях partial-конструкторы и события упрощают сопровождение кода. Автогенерируемый код может объявлять, что должно существовать, а написанный вручную код может ограничиваться лишь тем, что разработчик действительно хочет реализовать. При этом нет накладных расходов во время выполнения от использования partial-членов — разделение существует только во время компиляции. Итоговая программа ведёт себя так, как если бы вы написали весь конструктор или событие в одном месте.
Позволяя конструкторам экземпляров и событиям быть partial, C# 14 завершает набор возможностей partial-классов, охватывая практически все типы членов. Это улучшение усиливает способность языка поддерживать чистое разделение автогенерируемого и пользовательского кода. Разработчики получают гибкость для организации и расширения поведения классов без хаков, а source generators получают мощный новый инструмент для модульного внедрения функциональности. Работаете ли вы над крупными фреймворками или просто разделяете код ради ясности — partial-конструкторы и события дают более выразительный и удобный подход в C# 14.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.