C# 2.0 - благие намерения

Первое обновление языка C# не только принесло всем нам радость параметрическому полиморфизму (generics), но и адресовало некоторые из проблем дизайна свойств в C# 1.0.

Модификаторы доступа на аксессорах

Одна из проблем трактования аксессоров свойств как единого целого заключается в ограничении гибкости. Самым острым последствием того, что различные модификаторы могла иметь только объявления свойства (но не объявления аксессоров) оказалось невозможность независимо контролировать уровень доступа аксессоров, например, чтобы наделить позволить изменять свойство только владеющему им классу или его наследникам.

class C {
  public int Value {
    get { return ...; }
    private set { ... }
  }
}

Модификатор доступа может быть только у одного аксессора объявления свойства (за исключений объявлений свойств в интерфейсах) и этот уровень доступа должен быть более ограничивающим, чем уровня доступа самого свойства. То есть для public-свойства можно сделать аксессор private, protected, internal и protected internal. Для internal и protected-свойств можно снижать уровень доступа аксессора только до private (так как между уровнями доступа protected и internal нет отношения порядка), а в private-свойствах модификаторами доступа аксессоров не имеют смысла.

С одной стороны, строгость дизайна очень к месту, нельзя объявить какое-нибудь private-свойство в публичным аксессором, а поддержка свойств в IDE не слишком усложняется (все равно при большинстве рефакторингов уже приходилось проверять, что нужный аксессор вообще определен). С другой стороны, мы получили продолжение истории с set-only свойствами (без комментариев):

class C {
  public int Value {
    private get { return ...; }
    set { ...; }
  }
}

Модификаторов доступа и полиморфизм

Чем больше в средствах языка программирования разных параметров/осей/измерений, тем больше вероятность их пересечения в самых неожиданных местах. Чем более ортогональными кажутся средства языка, тем больше вероятность что они все же пересекаются и разработчик компилятора/IDE не обработает эти пересечения должным образом.

Кажется, как раз такая ситуация произошла с полиморфизмом свойств, помноженным на модификаторы доступа аксессоров:

abstract class B {
  public abstract int Value { get; }
}

class C : B {
  public override int Value {
    get { return ...; }
    private set { ...; } // CS0546
  }
}

Компилятор радостно сообщает нам о невозможности переопределить set-аксессор базового свойства, игнорируя модификатор доступа, не совместимый с виртуальностью (в C# private члены класса не могут быть полиморфными):

Error CS0546: ‘C.Value.set’: cannot override because ‘B.Value’ does not have an overridable set accessor

Generic-свойства?

С приходом параметрического полиморфизма в C# 2.0, пользователи языка получили возможность определять собственные обобщенные типы и обобщенные методы через объявления типов-параметров на типах и методах соответственно. Почему же свойства нельзя сделать generic?

С точки зрения формата метаданных CLI, нет ограничений на определение свойств с аксессорами из generic-методов, однако API System.Reflection не поддерживает полноценную работу с такими свойствами (похожая история с generic-атрибутами). С точки зрения синтаксиса C#, выражения доступа к свойствам/полям как раз может включать типы-аргументы: var x = object.Property<T>;.

Единственное возможное применение generic-свойств в C#, с которым я сталкивался - необходимость в аналоге понятия “type function” из языка F# - обобщенная функция без формальный параметров, которую можно вызывать без (), часто указывая типы-аргументы явно. С помощью type function F# избавляется от массы разновидностей выражений, параметризованных типами, просто заменяя их на type functions из стандартной библиотеки:

typeof(List<T>)            =>   typeof<List<T>>
typeof(Action<,,>)         =>   typedefof<Action<_,_,_>>
default(decimal)           =>   Unchecked.defaultof<decimal>
sizeof(int)                =>   sizeof<int>

В C# уже налепили разных видов выражений, обратной дороги нет, но со временем появились и другие источники значений, параметризованных типами. Вы могли сталкиваться со статическими generic-фабриками для некоторых из коллекций:

var xs = Enumerable.Empty<string>();
var ys = ImmutableStack.Empty<int>().Push(1).Push(2);

Пока для возвращения чистого значения в C# требуется написать вызов generic-метода, в F# доступ к type function выглядит как доступ к свойству (еще и типы-аргументы выводит по использованию):

let xs : string seq = Seq.empty
let ys : int list   = 1 :: 2 :: List.empty

Я склоняюсь к тому, что отсутствие generic-свойств пошло C# только на пользу, не смотря на некоторое нарушение симметрии (однако, отсутствие generic-свойств можно считать “симметричным” отсутствию обычных формальных параметров у свойств в C#) - все же большинства программистов идея generic-свойств вызывает недоумение.

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