Start Debugging
2025-04-08 Обновлено 2025-04-12 csharp-14csharpdotnetdotnet-10 Edit on GitHub

Partial-конструкторы и события в C# 14

C# 14 позволяет объявлять конструкторы экземпляров и события как partial-члены, разделяя определения между файлами для более чистой генерации кода и разделения ответственности.

В C# 14 появилась новая возможность объявлять конструкторы экземпляров и события как partial-члены. Это означает, что определение конструктора или события можно разнести по двум частям partial-класса — аналогично тому, как C# уже давно позволяет делать это с partial-методами и partial-свойствами. Одна часть partial-класса может объявлять конструктор или событие, а другая — его реализовывать. Это особенно полезно в сценариях вроде кодогенерации, когда один файл генерируется автоматически, а другой ведётся вручную разработчиком.

Partial-конструкторы в C# 14

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-событие. Один файл объявляет событие, а другой реализует пользовательскую логику 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:

Во всех этих случаях 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.

< Назад