Что нового в F# 3.0 - part 1: автосвойства
Сегодня я хочу рассказать о нововведениях мультипарадигменного (но functional-first) языка программирования общего назначения F# версии 3.0. Последняя версия F# на данный момент доступна только в составе с Visual Studio “11” Beta (качать тут), что немного грустно. Самих нововведений совсем не много, но некоторые из них достаточно значимые. Сегодня речь пойдёт об автосвойствах.
В F# достаточно запутанный синтаксис определения обычных классов, который я постоянно забываю, хотя после того как осознаёшь все нюансы, он может начинать казаться достаточно лаконичным. До версии 3.0, для того чтобы определить класс с парой get
-only свойств, приходилось писать достаточно немного:
type Person(name, age) =
member this.Name = name : string
member this.Age = age : int
Пример выше - особая короткая форма записи get
-only свойств, более полная запись выглядит следующим образом (обратите внимание, что в примере выше я уточнил тип не самим свойствам, а их выражениям. В примере ниже аннотации типов относятся непосредственно к свойствам, что удобно, когда выражение свойства не является тривиальным):
type Person(name, age) =
member this.Name : string with get() = name
member this.Age : int with get() = age
Причём набор необходимых полей компилятор определяет сам - в тривиальном случае он просто превращает в поля все параметры primary-конструктора, которые имеют как минимум одно использование в определении членов класса. В более сложных случаях, в поля превращаются let
-биндинги уровня типа. Например, у класса ниже будет два поля - параметр name
используется только в инициализации let
-биндинга и ни разу не используется в членах класса, поэтому поле для него создано не будет, а для самого let
-биндинга mrName
- будет:
type MrPerson(name, age) =
let mrName = "Mr. " + name
member this.Name = mrName : string
member this.Age = age : int
Так как F# является полноправным жителем объектно-ориентированного мутабельного мира .NET, то язык, конечно же, предоставляет возможность определять изменяемые, get
/set
-свойства. Однако параметры primary-конструктора являются неизменяемыми (как и вообще любые другие параметры функций и методов F#), то их не выйдет использовать для хранения изменяемых полей. На помощь приходят изменяемые let
-биндинги уровня типа, однако запись класса значительно разбухает:
type MutablePerson(name, age) =
let mutable name = name
let mutable age = age
member this.Name with get() = name : string
and set x = name <- x
member this.Age with get() = age : int
and set x = age <- x
Или даже вот так:
type HugePerson(name, age) =
let mutable name = name
let mutable age = age
member this.Name with get() = name : string
member this.Name with set x = name <- x
member this.Age with get() = age : int
member this.Age with set x = age <- x
Обратите внимание, что параметры методов-аксессоров пишутся явно и можно объявлять “индексированные свойства”, такие же как в VB.NET, но это достаточно редко используемая возможность. Однако в F# существует ещё более громоздкий синтаксис для классов без primary-конструктора (то есть без скобочек с опциональными параметрами класса после имени типа) и let
-биндингов, дающий пользователю возможность задать набор полей явно. При такой записи конструкторы должны содержать либо вызовы других конструкторов, либо инициализировать все явно определённые поля объекта (кроме помеченных атрибутом [<DefaultValue>]
) и при необходимости вызывать конструктор базового класса. Определение всего этого безобразия, соответственно, распухает практически до уровня C# 2.0:
type ExplicitMutablePerson =
new (name, age) = { name = name; age = age }
val mutable name : string
val mutable age : int
member this.Name with get() = this.name
and set x = this.name <- x
member this.Age with get() = this.age
and set x = this.age <- x
Обратите внимание, что в отличии от параметров primary-конструктора и let
-биндингов уровня типа, к явным полям приходится обращаться точно так же, как и к другим членам класса - явно через квалификатор this
(смотря как вы его назвали в том или ином члене класса). Поля по-умолчанию являются изменяемыми, поэтому требуют модификатора mutable
. Неизменяемый вариант класса с явными полями принимает следующий вид:
type ExplicitImmutablePerson =
new (name, age) = { name = name; age = age }
val name : string
val age : int
member this.Name = this.name
member this.Age = this.age
Кстати, при такой записи возможно выполнить какой-либо дополнительный код после инициализации объекта:
type InstantiateMe =
new () = { } // инициализация объекта
then // side-effects тут:
printfn "thank you!"
Теперь мы наконец подошли к новому синтаксису автосвойств, которые представляет нашему вниманию F# 3.0, неизменяемые свойства принимают вид:
type Person(name, age) =
member val Name = name : string
member val Age = age : int
Код практически идентичен самому первому, однако есть отличия. При использовании ключевого слова member val
становится не нужно давать имя this
-параметру, поле для значения генерируется компилятором в любом случае, а обязательное выражение после символа =
является не телом get
-аксессора свойства, а выражением инициализации автосвойства при инстанциировании экземпляра класса. То есть конкатенация строк в примере ниже происходит только один раз, при создании экземпляра MrPerson
:
type MrPerson(name, age) =
member val Name = "Mr. " + name
member val Age = age : int
При этом так же, как в C#, вы не имеете доступа к backing-полям таких свойств. Теперь самое приятное - чтобы сделать такие свойства изменяемыми, достаточно после выражения инициализации дописать with get, set
:
type MutablePerson(name, age) =
member val Name = name : string with get, set
member val Age = age : int with get, set
Получаемый синтаксис практически аналогичен автосвойствам C#, но выглядит немного более громоздким. Однако аннотации типов в F# могут быть выведены из использования в других членах класса, а синтаксис инициализации всё равно гораздо компактнее явных присваиваний в конструкторе, используемых в мире C#:
class MutablePerson {
public MutablePerson(string name, int age) {
Name = name;
Age = age;
}
public string Name { get; set; }
public int Age { get; set; }
}
Автосвойства могут быть статическими, как и обычные свойства, достаточно использовать для определения модификатор static
. Однако у автосвойств F# есть ограничения - например, их можно использовать только в типах с primary-конструктором, точно так же, как и let
-биндинги уровня типа (это связано с особенностями процесса инициализации объектов в F#, о которых я постараюсь рассказать в других постах). Ещё одним важным и просто замечательным ограничением является то, что автосвойства в F# не могут быть виртуальными - это решает потенциальную проблему о “забытом” состоянии в базовом классе (к аналогичным проблемам могут приводить виртуальные field-like события в C#).
Ещё одним нюансом является то, что события в F# представляются в виде свойств, которые компилятор можно заставить компилироваться в обычные CLR-события путём аннотации свойства атрибутом [<CLIEvent>]
. Этот атрибут работает и для автосвойств:
type WithEvent() =
let doneEvent = new Event<EventHandler, _>()
[<CLIEvent>] // автосвойство-событие
member val Done = doneEvent.Publish
member this.Complete() =
doneEvent.Trigger(this, EventArgs.Empty)
Однако такой класс содержит два поля (поля для свойства Done
и let
-привязки doneEvent
), вместо одного действительно нужного (doneEvent
). Замена свойства на обычное устраняет избыточное поле. К сожалениею, в отличие от автосвойств C#, аннотировать аттрибутами методы-аксессоры ни обычных свойст, ни автосвойств - невозможно (точно так же, как в F# 2.0). Однако backing-поле автосвойства аннотировать всё же можно:
type Person(name: string, age: int) =
[<field:NonSerialized>]
member val FullName = name + string age with get
Автосвойства F# 3.0 производят приятное впечатление и существенно снижают уровень синтаксического шума при определении изменяемых свойств, которые достаточно частно нужны для интероперабельности с .NET-библиотеками. Однако становится важно отличать автосвойства от обычных свойств и понимать отличие выражения инициализации автосвойства от выражений значений обычных свойств, что ещё немного запутывает и без того нелёгкие правила деклараций типов F#. В то же время определять обычные классы в F# приходится гораздо реже, чем record-типы и union-типы.