Реализация INotifyPropertyChanged: есть ли способ лучше?
Использование атрибута CallerMemberName и вспомогательного метода
Да, есть гораздо более элегантный способ! Начиная с C# 5.0 (.NET 4.5), появился атрибут CallerMemberName, который автоматически подставляет имя вызывающего члена (свойства или метода). Это избавляет от необходимости передавать строку с именем свойства вручную и исключает опечатки.
Вот как можно переписать ваш класс:
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
private int _age;
private string _address;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
public string Address
{
get => _address;
set => SetProperty(ref _address, value);
}
// Универсальный метод для всех свойств
private bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
OnPropertyChanged(propertyName);
return true;
}
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Что изменилось и почему это лучше?
- Убраны строковые литералы: Атрибут
[CallerMemberName]автоматически подставляет имя свойства, из которого вызван метод. Теперь невозможно ошибиться в написании "Name" или "Age". - Один метод для всех свойств:
SetProperty— это обобщенный метод, который принимает ссылку на поле (ref T backingStore), новое значение и автоматически получает имя свойства. Он также возвращаетbool, который показывает, было ли значение изменено — это удобно для дополнительной логики. - Лаконичность: Сеттер каждого свойства теперь состоит из одной строки:
set => SetProperty(ref _name, value);. Никаких повторяющихся проверок и вызовов. - Производительность: Метод
EqualityComparer<T>.Default.Equalsработает оптимально для любого типа и избегает боксинга для значимых типов (какint).
Дополнительные улучшения
Если вы хотите еще больше сократить код, можно использовать кодогенерацию. В современных версиях .NET (например, .NET 6+ и .NET 8) есть source generators, которые могут автоматически создавать реализации INotifyPropertyChanged. Например, популярный пакет PropertyChanged.SourceGenerator из NuGet позволяет писать так:
// После добавления пакета и использования partial class
public partial class Person : INotifyPropertyChanged
{
[NotifyPropertyChanged]
private string _name = "";
[NotifyPropertyChanged]
private int _age;
[NotifyPropertyChanged]
private string _address = "";
}
Генератор сам создаст свойства Name, Age, Address с полной реализацией INotifyPropertyChanged. Это максимально сокращает ручной код.
Но если вы не хотите подключать сторонние библиотеки, описанный выше подход с CallerMemberName и SetProperty является золотым стандартом для WinForms и WPF приложений уже много лет. Он прост, надежен и не требует дополнительных зависимостей.
Альтернатива: Использование класса ObservableObject из MVVM Toolkit
Если вы работаете с .NET Core или .NET 5+ и используете паттерн MVVM, рекомендую обратить внимание на официальный пакет CommunityToolkit.Mvvm (ранее Microsoft.Toolkit.Mvvm). Он предоставляет готовый базовый класс ObservableObject, который уже содержит всю логику INotifyPropertyChanged.
Пример использования:
using CommunityToolkit.Mvvm.ComponentModel;
public partial class Person : ObservableObject
{
[ObservableProperty]
private string _name = "";
[ObservableProperty]
private int _age;
[ObservableProperty]
private string _address = "";
}
Атрибут [ObservableProperty] (доступен в C# 11 и выше с partial классами) автоматически генерирует публичное свойство Name, Age, Address с полной поддержкой уведомлений. Вам не нужно писать ни полей, ни свойств, ни методов — всё делает кодогенератор.
Плюсы: Минимум кода, встроенная поддержка команд, сообщений и других MVVM-функций. Минусы: Зависимость от внешнего пакета и требование .NET 6+ с C# 11. Для старых проектов (.NET Framework) этот подход не подходит.
Ответов пока нет.
Войдите чтобы оставить ответ