Дизайн и эволюция свойств в C# (часть 4)
C# 3.0 - все пропало
Релиз версии 3.0 языка C# был самым ярким релизом языка, во многом задавшим направление и темп дальнейшего развития C#. Новые языковые средства, позаимствованные из функциональных языков (простой вывод типов, лямбда-выражения, from
-выражения) сделали язык гораздо более мультипарадигменным и явно выигрывающим в выразительности у Java - императивным ОО-языком того же класса.
Версия 3.0 языка адресовала проблемы выразительности доступа к данным, предоставив нам удобные функции высшего порядка, цитирование кода через деревья выражений, анонимные типы и методы-расширения, упрощеное конструирование объектов с помощью object initializers. Но самым важным для нас сейчас является новый способ объявления тривиальных свойств - авто-свойства.
Интересно, что на момент версии 2.0, в языке уже существовал механизм, крайне похожий на авто-свойства C# 3.0 - field-like события (я затрудняюсь нормально перевести название этого языкового средства). Выглядели они действительно похожими на поля, вот только имели специальный модификатор event
:
class C {
public event EventHandler SmthHappened;
}
Точно так же, как и поля в C#, field-like события являлись мульти-объявлением (кроме объявлений событий с явными add
/remove
-аксессорами) и поддерживали синтаксис инициализаторов:
class C {
public event EventHandler E1, E2, E3 = delegate { };
}
Доступ к field-like событиям (за исключением операторов +=
и -=
) внутри владеющего им класса превращался в доступ к генерируемому компилятором полю (для хранения делегатов подписчиков на событие) с именем, эквивалентным имени события. Это поле было доступно даже для аннотации атрибутом, просто указав field:
в начале секции атрибутов:
class C {
[field: FieldAttribute]
[EventAttribute]
public event EventHandler AutoPropertyDesigned;
private void OnAutoPropertyDesigned() {
AutoPropertyDesigned(this, EventArgs.Empty); // direct field access
}
}
Несмотря на некоторые ошибки дизайна (например, подписки на такие события в многопоточной среде, защита пользователей от неожиданных NullReferenceException и интересные пересечения с полиморфизмом), дизайн field-like событий можно считать успешным. Даже терминология подчеркивала функциональное и синтаксическое сходство с полями C#, и наличие сгенерированного за событием поля с делегатом.
История одного авто-свойства
В итоге, в версии 3.0 языка C# мы получили новое языковое средство - автоматически реализованное свойство (automatically-implemented property) или просто авто-свойство. От обычного объявления свойства, авто-свойство отличает только объвления аксессоров без блоков тел:
class C {
public string Text { get; set; }
}
Мне кажется, что это языковое средство разрабатывал человек, совершенно не оглядывающийся на спецификацию языка 2.0. Как вы наверное догадываетесь, ничего общего с field-like событиями у этой конструкции просто нет.
- Авто-свойство не поддерживает синтаксис инициализаторов;
- Авто-свойство не является мульти-объявлением;
- Синтаксис авто-свойства не похож на объявление поля из-за уродливого блока с деларациями аксессоров;
- У пользователя нет доступа к генерируемому полю авто-свойства, даже для аннотации его атрибутом (что гораздо важнее для свойств, как частых участников разнообразных сериализаций);
- Терминология “automatically-implemented property” не намекает на связь с field-like с событиями, не намекает на наличие состояние у такого объявления свойства. Я часто не могу вразумительно объяснить людям вне .NET-стека почему мы называем тривиальные члены класса с состоянием с приставкой “авто”, так как никому в голову не приходит, что тривиальное хранение состояния можно обозвать “автоматической реализацией”.
Можно долго рассуждать почему дизайн был столько недоработанным, делался ли релиз в спешке, кто занимался дизайном авто-свойств и чем он думал… Можно даже представить себе C# с field-like свойствами и придумать решения для очевидных проблем (типа пересечение с имеющимися в языке модификаторами доступа на аксессорах, которых в предложенном ниже синтаксисе просто нет):
class C {
public property int Id;
public property string Text = string.Empty;
}
Однако все решения дизайна авто-свойств были приняты и зарелизены так давно, что теперь гораздо продуктивнее рассуждать как нам дальше с этим жить и какие из проблем реально починить в следующих версиях языка.
Неизменяемость? Нет, не слышал
Еще одним большим расстройством, привнесенным дизайном авто-свойств стало требование определять оба аксессора, тем самым не имея возможности определить свойство только для чтения. Неизменяемость данных очень глубоко связана с функциональным программированием, так повлиявшим на C# 3.0, однако авто-свойства просто игнорируют наличие в языке readonly
-полей и свойств только с get
-аксессором. Можно лишь занизить уровень доступа до минимального и не модифицировать авто-свойство внутри класса:
class C {
public string Text { get; private set; }
}
При честном обращении с такими свойствами, большинство пользователей достаточно и подхода с приватным set
-аксессором, однако требование иметь оба аксессора имеет и другие последствия. Например, невозможность переопределить авто-свойством полиморфное get
-свойство (C# пытается переопределить оба аксессора из объявления авто-свойства) или явно реализовать get
-свойство из интерфейса (явная реализация требует точное соответствие количества и типов аксессоров свойства):
interface I {
int Id { get; }
}
class A {
public abstract string Text { get; }
}
class C : A, I {
public override string Text { get; set; } // error
int I.Id { get; set; } // error
}
Пересечение авто-свойств со структурами
Сокрытие от пользователя полей авто-свойств может рассматриваться как позитивное отличие от дизайна field-like событий, однако имеющее негативное пересечение с анализом инициализации в объявлениях конструкторов структур C#. Дело в том, что в коде конструкторов структур выполняется проверка инициализации всех полей структуры до использования this
структуры. К сожалению, вызов set
-аксессора авто-свойства как раз является использованием this
(если свойство не статическое), а поля авто-свойства невозможно инициализировать напрямую, так как их невозможно упомянуть в коде:
struct Rect {
public int Width { get; private set; }
public int Height { get; private set; }
public Rect(int width, int height) {
this.Width = width; // error
this.Height = height; // error
}
}
Эта проблема обходится простым вызовом конструктора по-умолчанию перед текущим конструктором (IDE-инструментарию приходится всегда помнить об этом и иногда вставлять этот вызов), но таким образом мы грубо избавляемся и от самого анализа инициализации (от него все же бывает польза):
struct Rect {
public Rect(int width, int height) : this() {
this.Width = width; // OK
...
}
}
Проблема не особо существенная, так как пользовательские структуры относительно редки и если в них потребовалась надобность из-за различных оптимизациях, то синтаксический сахар только мешает, скрывая детали реализации. Однако чтобы фундаментально решить проблему инициализации, нужно предоставить возможность иницализировать поле авто-свойства напрямую, что кажется не представляющимся возможным с текущим дизайном авто-свойств.
Проблема отсутствия выбора: инкапсуляция
Помните обсуждаемую ранее проблему: инкапсулировать ли доступ к свойству при использовании внутри объявления класса? Из-за того, что в C# мы иногда вынуждены использовать обычные свойства вместо авто-свойств…
class C : B {
private int cantBeAuto;
public override int CantBeAuto { get { return this.cantBeAuto; } }
public int ExtraProperty { get; set; }
}
…использования данных внутри класса в любом случае будут выглядеть по-разному, в зависимости от вашего стиля именования полей и свойств:
public C(int canBe, int extra) {
this.cantBeAuto = canBe;
this.ExtraProperty = extra;
}
Разработчики, предпочитавшие всегда использовать внутри класса поля (в обход инкапсулирующих их свойств), начинают испытывать боль от отсутствия поля и иногда просто перестают использовать авто-свойства! Отчасти поэтому в ReSharper, мы не предлагаем настойчиво конверсию в авто-свойство в виде suggestion’а, если backing-поле имеет использования вне конструктора - это некая эвристика, позволяющая меньше “мешать” пользователям, не привыкшим инкапсулировать использования внутри класса.
Популярность object initializers
TODO: популярность object initializers привела к обилию изменяемых классов TODO: очень тяжко отрефакторить в иммутабельный класс
Свойства-расширения?
TODO: просили для WPF, но никто не понимает зачем TODO: есть в F#, но особо не нужны (откуда данным появится “снаружи” типа?)