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#, но особо не нужны (откуда данным появится “снаружи” типа?)

Продолжение следует…