Для хранения состояния объекта в типах F# могут использоваться поля. Есть несколько способов определения полей.
Как и в целом в модулях, в F# внутри классов с помощью оператора let можно определять значения, которые будут хранить некоторые данные. Но стоит учитывать, что такие значения доступны только внутри своего класса:
type Person (name, surname, age) = let fullName = $"{name} {surname}" // приватное поле member _.Print() = printfn $"Name: {fullName} Age: { age }" let tom = Person("Tom", "Smith", 37) tom.Print() // Name: Tom Smith Age: 37
Здесь определено приватное поле fullName
, которое хранит полное имя пользователя на основе его имени и фамилии. Вне класса к этому полю мы обратиться не можем.
При этом подобные поля также могут изменять свои данные при определении с оператором mutable:
type Person (name, surname, _age) = let fullName = $"{name} {surname}" let mutable age = _age // изменяемое поле age member _.Print() = printfn $"Name: {fullName} Age: { age }" member _.Grow() = age <- age + 1 let tom = Person("Tom", "Smith", 37) tom.Print() // Name: Tom Smith Age: 37 tom.Grow() tom.Print() // Name: Tom Smith Age: 38
Здесь определено изменяемое поле age
, начальное значение которого равно значению параметра конструктора _age
. Далее
мы можем изменять значение этого поля на единицу с помощью метода Grow()
. Консольный вывод:
Name: Tom Smith Age: 37 Name: Tom Smith Age: 38
Другой способ определения полей в классе представляет оператор val. Формальный синтаксис его применения:
val [ mutable ] [ модификатор_доступа ] имя_поля : тип_данных
В отличие от let-полей val-поля могут иметь модификатор доступа, соответственно поля с модификаторами public/internal
могут быть доступны вне
класса. По умолчанию val-поля без модификаторов доступны вне класса.
Может показаться, что val-поля гораздо проще и удобнее использовать, чем let-поля, но на самом деле их применение обставлено рядом ограничений. Например, определим пару таких полей:
type Person(name, surname, _age) = [<DefaultValue>] val mutable fullName: string [<DefaultValue>] val mutable age: int member this.Print() = printfn $"Name: {this.fullName} Age: { this.age }" member this.Grow() = this.age <- this.age + 1 let tom = Person("Tom", "Smith", 37) tom.Print() // Name: Age: 0 tom.Grow() tom.Print() // Name: Age: 1 tom.age <- tom.age + 4 tom.Print() // Name: Age: 5
Здесь определены два поля: fullName
и age
. При определении полей мы НЕ можем им тут же присвоить некоторое значение, например:
val mutable fullName: string = $"{name} {surname}" // это работать не будет val mutable age: int = 6 // это работать не будет
Для инициализации полей нужно использовать другие способы.
Следующее ограничение: если класс имеет первичный конструктор (как в случае выше), то val-поля должны иметь атрибут [<DefaultValue>]. Этот
атрибут устанавливает для таких полей значение по умолчанию. Так, для числовых данных это число 0, а для типа string
(как и классов) это специальное значение null
,
которое указывает на отсутствие значения.
Еще одно ограничение - в этом случае val-поля должны быть изменяемыми, то есть определены с оператором mutable.
В итоге при создании объекта Person его поле fullName
фактически не будет иметь никакого значения (при выводе на консоль мы увидим пустую строку),
а поле age
будет иметь значение 0. Консольный вывод программы:
Name: Age: 0 Name: Age: 1 Name: Age: 5
Но что если мы хотим все-таки присвоить этим полям некоторые значения, например, значения первичного конструктора? В этом случае мы можем определить специальный метод, который будет выполять подобную работу:
type Person(name, surname, _age) = [<DefaultValue>] val mutable fullName: string [<DefaultValue>] val mutable age: int member this.SetValues() = this.fullName <- $"{name} {surname}" this.age <- _age member this.Print() = printfn $"Name: {this.fullName} Age: { this.age }" member this.Grow() = this.age <- this.age + 1 let tom = Person("Tom", "Smith", 37) tom.SetValues() tom.Print() // Name: Tom Smith Age: 37 tom.Grow() tom.Print() // Name: Tom Smith Age: 38 tom.age <- tom.age + 4 tom.Print() // Name: Tom Smith Age: 42
В данном случае определен метод SetValues
, который передает val-полям значения параметров первичного конструктора. При этом значения для полей также можно передавать и через
параметры метода или брать их из let-полей (при наличии таковых). Однако кроме определения нам надо еще вызывать этот метод в программе. Консольный вывод:
Name: Tom Smith Age: 37 Name: Tom Smith Age: 38 Name: Tom Smith Age: 42
В классах без первичного конструктора val-поля могут быть неизменяемыми, для них не надо определять атрибут [<DefaultValue>]
,
однако нам надо дополнительно с помощью конструктора определить логику их инициализации:
type Person = val fullName: string val mutable age: int new (name, surname, _age) = { fullName = $"{name} {surname}"; age = _age;} member this.Print() = printfn $"Name: {this.fullName} Age: { this.age }" member this.Grow() = this.age <- this.age + 1 let tom = Person("Tom", "Smith", 37) tom.Print() // Name: Tom Smith Age: 37 tom.Grow() tom.Print() // Name: Tom Smith Age: 38 tom.age <- tom.age + 5 tom.Print() // Name: Tom Smith Age: 43
Здесь изменяемым полем является только поле age
, а для инициализации полей определен специальный конструктор, который называется конструктором типа:
new (name, surname, _age) = { fullName = $"{name} {surname}"; age = _age;}
Обратите внимание, как выглядит инициализация - тело конструктора обернуто в фигурные скобки, а присвоение значений полям отделяется точкой с запятой.
Если надо установить модификатор доступа, то он указывается перед именем поля и после операторов let/mutable
:
val internal fullName: string val mutable private age: int