1. Введение
Раньше можно было добавлять классам только Extension-методы. А вот добавить свойство (property) как расширение класса было нельзя! То есть, если вы хотите, чтобы у любого DateTime появилось свойство IsWeekend, приходилось делать это через метод, что не всегда удобно — особенно если хочется синтаксис date.IsWeekend вместо date.IsWeekend().
С выходом C# 14 мечта сбылась: теперь можно писать свойства-расширения немного похожим на методы способом. Они позволяют добавить новое свойство к существующему типу, не изменяя его исходного кода, и использовать их как будто это обычные свойства!
Где это реально полезно?
- Для стандартных типов .NET и сторонних библиотек, код которых изменить нельзя.
- Для удобных «виртуальных» свойств в view-моделях, UI, вычисляемых представлениях.
- Там, где хочется красивого синтаксиса вида .SomeCalculatedProperty без лишних скобок.
2. Новый синтаксис в C# 14
В C# 14 решили отказаться от ключевого слова this для расширений. Вместо этого ввели специальный оператор extension(Type this). И уже с его помощью можно добавлять в класс новые виртуальные сущности.
Старый синтаксис
public static class StringExtensions
{
public static bool IsNullOrWhiteSpace(this string str)
=> string.IsNullOrWhiteSpace(str);
}
Новый синтаксис
public static class Enumerable
{
extension(TSource source)
{
// Виртуальные члены для объекта
}
extension(TSource)
{
// Виртуальные статические члены
}
}
3. Синтаксис свойств-расширений
Как это выглядит?
Допустим, вы хотите добавить к типу DateTime свойство, которое будет возвращать, является ли дата выходным днём:
public static class DateTimeExtensions
{
public static bool IsWeekend(this DateTime date) =>
date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;
}
Но это — старый вариант Extention Method, и вызывается как: myDate.IsWeekend()
Новый стиль — Extension Property
public static class DateTimeExtensions
{
extension(DateTime source)
{
public bool IsWeekend // свойство-расширение!
{
get => source.DayOfWeek == DayOfWeek.Saturday || source.DayOfWeek == DayOfWeek.Sunday;
}
}
}
Теперь вы можете писать так:
DateTime today = DateTime.Now;
if (today.IsWeekend)
{
Console.WriteLine("Можно спать дольше!");
}
4. Какие бывают extension properties
Можно добавить не только read-only свойства, но и read-write:
public static class DateTimeExtensions
{
extension(DateTime source)
{
public int YearFrom1900 // свойство-расширение!
{
get => source.Year - 1900;
set => source.Year = value + 1900;
}
}
}
5. Статические свойства
Благодаря новому синтаксису, виртуальные свойства можно добавлять не только объекту, но и классу! Да, вы все правильно поняли, теперь мы можем добавлять виртуальные статические свойства к классу. Синтаксис почти такой же:
public static class DateTimeExtensions
{
// Новый синтаксис: extension block для DateTime (C# 14)
extension(DateTime)
{
// Статическое свойство для хранения текущей временной зоны пользователя
private static TimeZoneInfo _currentTimeZone = TimeZoneInfo.Local;
// Свойство-расширение для доступа к текущей временной зоне
public static TimeZoneInfo CurrentTimeZone
{
get => _currentTimeZone;
set => _currentTimeZone = value ?? TimeZoneInfo.Local;
}
// Возвращает время в пользовательской временной зоне
public DateTime InCurrentTimeZone
{
get => TimeZoneInfo.ConvertTime(this, CurrentTimeZone);
}
}
}
6. Примеры: расширим класс Dog!
Давайте продолжим развивать наше приложение с собаками, которое мы бережно тянем из лекции в лекцию.
Допустим, теперь у нас есть не только собаки, но и список их прививок (List<DateTime>), но класс Dog из сторонней библиотеки, и мы не можем менять его код. Мы хотим добавить свойство: Сделаны ли все необходимые прививки в этом году?
Старый способ (метод-расширение):
public static class DogExtensions
{
public static bool AllVaccinatedThisYear(this Dog dog)
{
return dog.Vaccinations.Any(v => v.Year == DateTime.Now.Year);
}
}
// Использование:
if (myDog.AllVaccinatedThisYear())
Console.WriteLine("Собака здорова!");
Новый способ (свойство-расширение):
public static class DogExtensions
{
extension(Dog)
{
public bool AllVaccinatedThisYear
{
get => Vaccinations.Any(v => v.Year == DateTime.Now.Year);
}
}
}
// Использование:
if (myDog.AllVaccinatedThisYear)
Console.WriteLine("Собака здорова!");
Полный пример
// Класс Dog — например, из сторонней библиотеки:
public class Dog
{
public string Name { get; set; }
public List<DateTime> Vaccinations { get; set; } = new List<DateTime>();
}
// Класс расширений с extension block:
public static class DogExtensions
{
extension(Dog)
{
public bool AllVaccinatedThisYear
{
get => Vaccinations.Any(v => v.Year == DateTime.Now.Year);
}
}
}
// В программе:
var myDog = new Dog { Name = "Дружок" };
myDog.Vaccinations.Add(new DateTime(DateTime.Now.Year, 2, 16)); // прививка в этом году
Console.WriteLine($"{myDog.Name}: вакцинирована? {(myDog.AllVaccinatedThisYear ? "Да" : "Нет")}");
7. Таблица: Методы-расширения vs Свойства-расширения
| Метод-расширение | Свойство-расширение | |
|---|---|---|
| Синтаксис вызова | obj.Method() | obj.Property |
| Передаёт параметры | Да | Нет (только this obj) |
| Можно записывать | Не применимо | Только если реализовано |
| Удобно для View/UI | Иногда | Да (двусторонние биндинги, лямбда) |
| Видимость в рефлексии | Нет | Нет |
8. Потенциальные ловушки и типичные ошибки
Когда extension-свойства не помогут
- Они не могут заменять полностью настоящие поля: у них нет доступа к приватным членам объекта.
- Нельзя добавить автоматическое изменение состояния объекта (например, не получится учитывать изменение значения в исходном классе напрямую).
- Нельзя использовать extension property для реализации интерфейса или абстрактного класса.
Ошибка с именами
Если вдруг в оригинальном классе появится свойство с таким же именем, как ваше, будет использоваться "родное" свойство, а не ваше расширение. Поэтому будьте аккуратнее с выбором имен, особенно когда работаете с публичными типами из .NET.
Проблемы с сериализацией/рефлексией
Extension property — это не "реальный" член типа, а просто удобный синтаксический сахар. Поэтому, если где-то используется рефлексия или сериализация, свойство-расширение может быть не видно для таких инструментов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ