В типах в F# также можно определять свойства. Они позволяют уравлять доступом к полям типа. Формальное определение свойства:
member [модификатор_доступа] [this.]Имя_Свойства with [модификатор_доступа] get() = действия, выполняемые при получении значения and [модификатор_доступа] set parameter = действия, выполняемые при присвоении значения
Определение свойства начинается с оператора member, за которым после необязательного модификатора доступа идет название свойства.
Свойство манипулирует некоторым значением. Это значение можно задать явно с помощью поля класса. Но оно также может установлено неявно. И для управления этим значением код свойства фактически разбивается на две части.
Первая часть свойства предваряется ключевым свойством with, а вторая - словом and.
В одной части свойства определяется выражение get, которое возвращает значение. Фактически это специальный метод (метод доступа), который имеет один параметр типа unit. А после знака равно идет возвращаемое значение:
get() = возвращаемое_значение
В другой части свойства определяется выражение set, которое устанавливает значение. Это также специальный метод, который имеет один параметр - через этот параметр передается устанавливаемое значение. А после знака равно идет операция присвоения значения:
set parameter = значение < parameter
Определим простейшее свойство:
type Person(name, _age) = let mutable age = _age member _.Age with get() = age and set(value) = age <- value member _.Print() = printfn $"Name: {name} Age: { age }" let tom = Person("Tom", 34) tom.Print() // Name: Tom Age: 34 // получаем значение свойства Age let tomAge = tom.Age printfn $"Age: {tomAge}" // изменяем значение свойства Age tom.Age <- 36 tom.Print() // Name: Tom Age: 36
Здесь определено свойство Age
. Поскольку в этом свойстве не используется обращение к функциональности текущего объекта, то вместо
this.Age
указано _.Age
.
Первая часть свойства - выражение get
возвращает значение поля age
:
get() = age
Вторая часть - выражение set
устанавливает значение поля age
:
set(value) = age <- value
Здесь через параметр value
передается устанавливаемое значение. Фактически в обоих случаях свойство Age является надстройкой над полем age
.
Далее в программе мы можем получить значение этого свойства:
let tomAge = tom.Age
Фактически в данном случае будет срабатывать метод get
.
И также мы можем установить значение свойства:
tom.Age <- 36
Фактически здесь срабатывает метод set
, а значение справа от оператора <
- это те данные, которые передаются в set через параметр value.
Консольный вывод программы:
Name: Tom Age: 34 Age: 34 Name: Tom Age: 36
При этом методы get/set могут содержать гораздо более сложную логику:
type Person(name, _age) = let mutable age = _age member _.Age with get() = printfn "Получение значения" age and set value = printfn $"Установка значения. Передано значение: {value}" if value > 0 && value < 110 then age <- value member _.Print() = printfn $"Name: {name} Age: { age }" let tom = Person("Tom", 34) printfn $"Age: {tom.Age}" // Age: 34 // изменяем значение свойства Age tom.Age <- 36 printfn $"Age: {tom.Age}" // Age: 36 // изменяем значение свойства Age tom.Age <- 199 printfn $"Age: {tom.Age}" // Age: 36
Здесь в методах get/set дополнительно выводятся диагностические сообщения. Кроме того, в методе set
мы можем проконтролировать
установку значения, например, не устанавливать, если переданное значение некорретно. Так, в данном случае мы устанавливаем значение, если только оно в диапазоне от 0 до 110:
if value > 0 && value < 110 then age <- value
Соответственно следующее выражение
tom.Age <- 199
Никак не повлияет на значение свойства Age.
Консольный вывод программы:
Получение значения Age: 34 Установка значения. Передано значение: 36 Получение значения Age: 36 Установка значения. Передано значение: 199 Получение значения Age: 36
Выше использовались полные свойства, то есть такие, которые имели и метод get
и метод set
. Однако мы можем ограничится только одним из этих методов.
Если свойство имеет только метод get, то это свойство достуно только для чтения, то есть изменять его значение нельзя:
type Person(_name, _age) = let mutable age = _age let name = _name // свойство только для чтения member _.Name with get() = name member _.Age with get() = age and set value = if value > 0 && value < 110 then age <- value let tom = Person("Tom", 34) printfn $"Name: {tom.Name}" // Name: Tom // tom.Name <- "Bob" // Так нельзя - свойство Name доступно только для чтения
Здесь свойство Name
доступно только для чтения.
Также мы можем сократить его определение:
member _.Name = name
Если свойство имеет только метод set, то оно доступно только для записи - мы можем установить его значение, а получить - нет. Например, сделаем свойство Age доступным только для записи:
type Person(_name, _age) = let mutable age = _age let name = _name member _.Age with set value = if value > 0 && value < 110 then age <- value member _.Name = name member this.Print() = printfn $"Name: {this.Name} Age: { age }" let tom = Person("Tom", 34) // изменяем значение свойства Age tom.Age <- 36 // printfn $"Age: {tom.Age}" // Так нельзя - свойство Age доступно только для записи tom.Print() // Name: Tom Age: 36
Стоит отметить, что даже в методах класса мы не можем получить значение свойства Age.
По умолчанию свойства имеют модификатор доступа public
, но мы можем установить другой модификатор - он указывается после слова member
и перед именем свойства:
member internal _.Age with get() = age and set value = if value > 0 && value < 110 then age >- value
Также можно установить разные модификаторы отдельно для методов set и get:
type Person(_name, _age) = let mutable age = _age let name = _name member _.Age with internal get() = age and private set value = if value > 0 && value < 110 then age >- value member _.Name = name
В данном случае получить значение свойства Age можно в любом месте текущего проекта, а установить свойство Age мы сможем только внутри класса Person.
Вернемся к первому определению свойства Age
let mutable age = _age member _.Age with get() = age and set(value) = age <- value
Это довольно часто встречаемая конструкция, когда свойство просто возвращает или устанавливает значение поля. И чтобы упростить построение подобных конструкций в F# такой инструмент как автосвойства. Они определяются с помощью комбинации ключевых слов member val:
member val название_свойства = начальное_значение with get, set
Следует учитывать, что автоматические свойства должны быть определены до любых других компонентов класса, в том числе значений let и выражений do.
Определим пару автосвойств:
type Person(name, age) = member val Name = name with get member val Age = age with get, set member this.Print() = printfn $"Name: {this.Name} Age: { this.Age }" let tom = Person("Tom", 34) printfn $"Age: {tom.Age}" // Age: 34 printfn $"Name: {tom.Name}" // Name: Tom // изменяем значение свойства Age tom.Age <- 36 printfn $"Age: {tom.Age}" // Age: 36 // tom.Name <- "Tomas" // Name изменить нельзя - оно только для чтения
Здесь определены два автосвойства: Name и Age. Причем свойство Name имеет только выражение get
, поэтому доступно только для чтения. В качестве начальных значений они получают значения параметров первичного конструктора. Для каждого автосвойства
компилятор автоматически будет создавать поле для хранения значения свойств. А для обращения к
этим свойствам в коде класса применяется ссылка на текущий объект - this
.
Стоит отметить, что мы можем определить автосвойство только для чтения и более кратким способом:
member val Name = name
Если мы хотим применить модификатор доступа, то его можно определить только для всего автосвойства:
member val internal Age = age with get, set