Классы F#. Конструкторы

Все исходники / Язык программирования F# / OS Windows / Desktop / Исходники приложений / Классы F#. Конструкторы
Оглавление:
  1. Конструкторы классов
  2. Первичные конструкторы классов F#
  3. Дополнительные конструкторы классов F#
  4. Статические конструкторы классов F#
  5. Конструкторы с необязательными параметрами
  6. Инициализация свойств класса в конструкторе
  7. Вызов членов-методов из конструктора класса
  8. Исходник тестирования конструкторов классов F#

Конструкторы классов

Театр начинается с гардеробной, а классы с конструкторов. Конструктор должен выполнить определенный программный код и подготовить объект класса к использованию. Классы языка программирования F# имеют в своём составе различные виды конструкторов, из которых один, называемый первичным, является обязательным. Все дополнительные конструкторы строго должны вызывать первичный конструктор.

// Определение класса
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] =
[ class ]
[ inherit base-type-name(base-constructor-args) ]

// --- Код первичного конструктора ---
[ let-bindings ]
[ do-bindings ]
// --- /Код первичного конструктора ---

member-list
...
end

Первичные конструкторы классов F#

Первичный конструктор класса F# состоит из входных параметров и комплексного программного кода. Код конструктора образуют let и/или do привязки. Данные привязки определяются в начале класса и исполняются в порядке их объявления. Количество let и do привязок может быть любым. Локальные поля, образуемые привязками доступны во всём определении класса, но не доступны из статических методов и свойств. Статические члены класса могут получать доступ только к полям, объявленным как статические.

Параметры конструктора объявляются внутри круглых скобок. Конструктор без параметров обозначается двумя пустыми круглыми скобками. Код первичного конструктора находится вначале кода класса, сразу после определения родительских классов. После кода первичного компьютера определяются члены класса: дополнительные конструкторы, методы, свойства. В отличие от других языков .NET классы F# имеют составной первичный конструктор, из параметров, кода формирования полей из let-привязок и исполняющего кода do-привязок.

Пример кода первичного конструктора без параметров. Тело первичного конструктора состоит только из одной let-привязки - функции построения таблицы умножения.
// Таблица умножения 
type MultiplicationTable() = 
    class
        // --- Код первичного конструктора ---
        let res = 
            for n = 1 to 9 do
                for m = 1 to 9 do
                    printf "%dx%d=%d	" m n (m*n)
                // Переход на следующую строку.
                printfn""
            // Пустая строка после таблицы умножения.
            printfn""
        // --- /Код первичного конструктора ---

        // Открытый метод класса после кода первичного конструктора.
        member this.Exponentiation x y = x ** y
    end

// Код запуска консольного приложения.
[<EntryPoint>]
let main argv =
    // Создание экземпляра класса.
    let pc = MultiplicationTable()

    0 // return an integer exit code
Вывод в консоль исполнение кода первичного конструктора экземпляра класса MultiplicationTable:
1x1=1   2x1=2   3x1=3   4x1=4   5x1=5   6x1=6   7x1=7   8x1=8   9x1=9
1x2=2   2x2=4   3x2=6   4x2=8   5x2=10  6x2=12  7x2=14  8x2=16  9x2=18
1x3=3   2x3=6   3x3=9   4x3=12  5x3=15  6x3=18  7x3=21  8x3=24  9x3=27
1x4=4   2x4=8   3x4=12  4x4=16  5x4=20  6x4=24  7x4=28  8x4=32  9x4=36
1x5=5   2x5=10  3x5=15  4x5=20  5x5=25  6x5=30  7x5=35  8x5=40  9x5=45
1x6=6   2x6=12  3x6=18  4x6=24  5x6=30  6x6=36  7x6=42  8x6=48  9x6=54
1x7=7   2x7=14  3x7=21  4x7=28  5x7=35  6x7=42  7x7=49  8x7=56  9x7=63
1x8=8   2x8=16  3x8=24  4x8=32  5x8=40  6x8=48  7x8=56  8x8=64  9x8=72
1x9=9   2x9=18  3x9=27  4x9=36  5x9=45  6x9=54  7x9=63  8x9=72  9x9=81
Пример кода первичного конструктора с параметрами num, maxmultiplier. Первичный конструктор включает код нескольких let и do привязок. Обратите внимание на использование статических полей в членах класса.
// Умножение данного числа на данный диапазон чисел
type MultiplicationRange(num: int, maxmultiplier: int) as self =
    // -- Статический конструктор ---
    static let mutable staticlet = 230 
    // -- /Статический конструктор ---

    // --- Код первичного конструктора ---
    let check = 
        if maxmultiplier > 100 
        then 100 
        else maxmultiplier
    do
        for m = 1 to check do
            printfn $"%d{num}x%d{m}=%d{(m*num)}"
    do
        self.Message
        self.ViewStaticLet
    // --- /Код первичного конструктора ---

    member this.Message = 
        printfn "Максимальный множитель = %d" check
    // Использование статического поля в методе экземпляра
    member this.ViewStaticLet = 
        printfn "Значение статического поля staticlet= %d" staticlet

    static member StaticProperty
            with get () = staticlet
            and set (value) = staticlet <- value

    static member StaticFunction = 45 + staticlet

[<EntryPoint>]
let main argv =
    let pc = MultiplicationRange(9,10)

    0 // return an integer exit code
Вывод в консоль после создания экземпляра класса MultiplicationRange:
9x1=9
9x2=18
9x3=27
9x4=36
9x5=45
9x6=54
9x7=63
9x8=72
9x9=81
9x10=90
Максимальный множитель = 10
Значение статического поля staticlet= 230

Дополнительные конструкторы классов F#

Для повышения универсальности при применении объектов, в классах F# можно создавать вторичные или дополнительные конструкторы. Вторичные конструкторы "внешне" не отличаются от первичных, т. е. могут быть с параметрами и без параметров. Необходимым условием создания дополнительного конструктора это вызов первичного конструктора вначале программного кода дополнительного конструктора.

Программный код самого дополнительного конструктора определяется после ключевого слова then, следующего за вызовом основного конструктора. Доступ к членам класса из кода вторичного конструктора можно получить только с помощью идентификатора именно для этого дополнительного конструктора.

Пример кода дополнительных конструкторов
type AddConstructor(p1: int, p2: int) as this = 
    // --- Тело первичного конструктора ---
    let pc = printfn $"
Я первичный конструктор! Параметры: p1=%d{p1}; p2=%d{p2}"  
    do
        printfn "do-привязка первичного конструктора"

    // --- Тело первичного конструктора ---

    new(x: int) as this2 = 
        // let-привязки могут участвовать в формировании кода 
        let mult1 = 9 * 7
        let mult2 = 9 * 8
        AddConstructor(mult1, mult2) then
            printfn "Я второй конструктор. Параметр: %d" x
            // У дополнительных конструкторов нет доступа к параметрам первичного.
            // Поскольку параметры первичного формирует дополнительный конструктор.
            // xx let letprimaryconstructor = p2  // так нельзя 

            // Доступ к членам класса только посредством собственного идентификатора
            // для дополнительного конструктора.
            let d = this2.Member
            d |> ignore

    new(str: string) = 
        AddConstructor(3, 4) then
            printfn "Я третий конструктор. Параметр: %s" str

    new(str: string, i: int) = 
        AddConstructor(5, 6) then
            printfn "Я четвёртый конструктор. Параметры: %s, %d" str i

    new() = 
        AddConstructor(7, 8) then
            printfn "Я пятый конструктор. Параметры отсутствуют."

    member d.Member = 89

[<EntryPoint>]
let main argv =
    let addc = AddConstructor(10, 20)
    let addc = AddConstructor(4500)
    let addc = AddConstructor("Погода сегодня отличная!")
    let addc = AddConstructor("Май", 20)
    let addc = AddConstructor()

    0 // return an integer exit code
Вывод в консоль после запуска приложения. Код основного, первичного конструктора всегда вызывается перед исполнением кода в дополнительных конструкторах.
Я первичный конструктор! Параметры: p1=10; p2=20
do-привязка первичного конструктора

Я первичный конструктор! Параметры: p1=63; p2=72
do-привязка первичного конструктора
Я второй конструктор. Параметр: 4500

Я первичный конструктор! Параметры: p1=3; p2=4
do-привязка первичного конструктора
Я третий конструктор. Параметр: Погода сегодня отличная!

Я первичный конструктор! Параметры: p1=5; p2=6
do-привязка первичного конструктора
Я четвёртый конструктор. Параметры: Май, 20

Я первичный конструктор! Параметры: p1=7; p2=8
do-привязка первичного конструктора
Я пятый конструктор. Параметры отсутствуют.

Статические конструкторы классов F#

В F# не применяется модификатор доступа static к классу. Но можно без ограничений создавать в классе let и do статические привязки и статические члены, добавляя классу полноценную статическую функциональность. По аналогии с первичным конструктором, статический конструктор включает в себя статические let и do привязки.

Статические конструкторы выполняются до создания экземпляров класса. Если класс имеет смешанный состав let, do привязок, то при создании нескольких экземпляров таких классов в первую очередь однократно выполняются статические конструкторы, и только затем локальные конструкторы для каждого экземпляра класса.

Если вызывается хотя бы один статический конструктор одного из классов текущего модуля, то выполнятся все статические конструкторы классов текущего модуля, в том числе и в дочерних модулях. При этом явный вызов всех статических классов не требуется, достаточно вызова любого, даже одного, статического конструктора класса.

module StaticConstructors

    type SimpleClass() =
        // Привязка статического конструктора 
        static let staticlet1 = printfn "Статическая let-привязка 1 SimpleClass"
        let locallet = printfn "
Локальная let-привязка SimpleClass"
        static let staticlet2 = printfn "Статическая let-привязка 2 SimpleClass"
        do
            printfn "Локальная do-привязка SimpleClass"
        // Привязка статического конструктора
        static do
            printfn "Статическая do-привязка SimpleClass"

        static member StaticProperty = printfn "StaticProperty SimpleClass"

    type StaticClass() = 
        // Привязка статического конструктора
        static let staticlet = 
            printfn "Статической let-привязка StaticClass"

    type LocalClass() = 
        let locallet = 
            printfn "Локальная let-привязка LocalClass"

    module AddModule1 =
        type SC() =
            static let staticlet = 
                printfn "Статическая let-привязка класса в дочернем модуле AddModule1"

        module AddModule2 =
            type SC() =
                static let staticlet = 
                    printfn "Статической let-привязка класса в дочернем модуле AddModule2"

    [<EntryPoint>]
    let main argv =

        let sc = SimpleClass()
        let sc = SimpleClass()
        let sc = SimpleClass()

        0 // return an integer exit code
Вывод в консоль результата создания экземпляров класса. Наглядно видно, что в первую очередь исполнились статические конструкторы всех классов текущего модуля и только затем были вызваны локальные первичные конструкторы необходимое количество раз. Можно заметить, что исполнились все статические конструкторы классов текущего модуля, даже без использования этих классов.
Статическая let-привязка 1 SimpleClass
Статическая let-привязка 2 SimpleClass
Статическая do-привязка SimpleClass
Статической let-привязка StaticClass
Статическая let-привязка класса в дочернем модуле AddModule1
Статической let-привязка класса в дочернем модуле AddModule2

Локальная let-привязка SimpleClass
Локальная do-привязка SimpleClass

Локальная let-привязка SimpleClass
Локальная do-привязка SimpleClass

Локальная let-привязка SimpleClass
Локальная do-привязка SimpleClass

Конструкторы с необязательными параметрами

Язык F# позволяет определять в конструкторах необязательные параметры, значения для которых можно не указывать. Такая возможность упрощает создание экземпляров классов и позволяет создавать объекты даже при неизвестных инициирующих значениях. В конструкторах класса можно смешивать обязательные и необязательные параметры.

При анонимном инициировании необязательных параметров требуется соблюдать порядок их определения. Вы можете расставлять аргументы в произвольном порядке, передавая значения по именам параметров, например: (color="Синий", width=45).

При создании собственных классов с конструкторами в которых определены необязательные параметры, очень желательно назначить параметрам значения по умолчанию. Default-значения помогут избежать возникновения исключений в случаях частичного или полного отсутствия аргументов для необязательных параметров.

type ClassParameter(?name: string, ?price: float) =
    // В случае отсутствия аргумента присваивается значение по умолчанию.
    let mutable n = defaultArg name "без названия"
    let mutable p= defaultArg price 0.0

    // Метод вывода информации.
    member x.Info = 
        if p > 0 then
            printfn $"Название компонента: %s{n}, стоимость %f{p}"
        else
            printfn "Компонент отсутствует"


type ClassParameters2(?speed: int, ?weight: int, ?color: string) = 
    // Другой способ присвоения значения по умолчанию.
    let s = if speed.IsSome = false then 0 else speed.Value
    let w = defaultArg weight 0
    let c = defaultArg color "нет данных"

    member x.Info = 
        let speed = if s = 0 then "нет данных" else s.ToString()
        let weight =  if w = 0 then "Нет данных" else w.ToString()
        
        printfn "Характеристика автомобиля:"
        printfn "Скорость - %s" speed
        printfn "Вес - %s" weight
        printfn "Цвет - %s" c

[<EntryPoint>]
let main argv =

    let op = ClassParameter()
    op.Info
    printfn ""

    let op = ClassParameter("Транзистор BC547C", 5.7)
    op.Info
    printfn ""

    // Результатом будут только значения по умолчанию.
    let op = ClassParameters2()
    op.Info
    printfn ""

    // Неименованные аргументы должны указываться в порядке определения параметров.
    let op = ClassParameters2(220, 2300, "Красный")
    op.Info
    printfn ""
    
    // Именованные аргументы можно писать в произвольном порядке
    let op = ClassParameters2(color = "Желтый", speed = 245)
    op.Info
    printfn ""


    0 // return an integer exit code
Вывод результатов в консоль:
Компонент отсутствует

Название компонента: Транзистор BC547C, стоимость 5.700000

Характеристика автомобиля:
Скорость - нет данных
Вес - Нет данных
Цвет - нет данных

Характеристика автомобиля:
Скорость - 220
Вес - 2300
Цвет - Красный

Характеристика автомобиля:
Скорость - 245
Вес - Нет данных
Цвет - Желтый

Инициализация свойств класса в конструкторе

Если класс содержит открытые свойства для чтения и записи, то такие свойства можно инициализировать при создании объекта класса. Внешне это напоминает вызов конструктора с именованными параметрами с тем отличием что значения присваиваются свойствам класса.

F# предоставляет гибкую схему инициализации свойств при создании объекта класса одновременно с параметрами конструктора, как обязательными, так и необязательными.

// Конструктор без параметров.
type PropClass() =
    let mutable length = 0
    let mutable width = 0
    let mutable height = 0
    

    member x.Length
        with get() = length
        and set(v) = length <- v

    member x.Width
        with get() = width
        and set(v) = width <- v

    member x.Height 
        with get() = height
        and set(v) = height <- v

    member x.Volume = length * width * height

// Конструктор с одним обязательным параметром.
type PropClassParameter(l: int) =
    let mutable length = l
    let mutable width = 0
    let mutable height = 0
    

    member x.Length
        with get() = length
        and set(v) = length <- v

    member x.Width
        with get() = width
        and set(v) = width <- v

    member x.Height 
        with get() = height
        and set(v) = height <- v

    member x.Volume = length * width * height
    
    
[<EntryPoint>]
let main argv =
    // Инициализация свойств при создании объекта класса 
    // визуально напоминает конструктор с параметрами.
    let pi = PropInit.PropClass(Length = 100, Width = 34, Height = 2)
    // Вывод результата вычисления.
    printfn $"Объем равен %d{pi.Volume}"

    // Инициализация свойств одновременно с обязательным параметром конструктора.
    let pi = PropInit.PropClassParameter(100, Width = 34, Height = 2)
    printfn $"Объем равен %d{pi.Volume}"

    0 // return an integer exit code
Результат вывода в консоль:
Объем равен 6800
Объем равен 6800

Вызов членов-методов из конструктора класса

Члены-методы классов можно вызывать из кода первичных конструкторов. При этом необходимо руководствоваться несложными правилами: для доступа к методу необходимо создать собственный идентификатор класса, вызов метода осуществлять в do-привязке, после которой нет let-привязок. Т. е. код конструктора, при вызове члена-метода, должен быть уже сформирован и экземпляр класса инициализирован.

Если попытаться вызвать метод в let-привязке или в do-привязке после которой есть let-привязки, то в этом случае возникнет исключение: System.InvalidOperationException: "The initialization of an object or value resulted in an object or value being accessed recursively before it was fully initialized." ("Инициализация объекта или значения привела к рекурсивному вызову объекта или значения перед его полной инициализацией.")

При вызове метода класса из тела вторичного конструктора обязательные правила: необходимо создать собственный идентификатор для вторичного конструктора, метод вызывать после вызова первичного конструктора.

Программный код вызов метода класса из первичного и вторичного конструктора:
module MethodsCall
    
type MyClass() as this =
    let a = 90

    // Код исполнить нельзя, поскольку экземпляр класса еще не создан.
    // Выполнение первичного конструктора не закончено.
    //let b = this.Method 7 2

    let c = 78

    // Так можно
    do
        this.Method 55 48
    do
        this.Method 23 67

    // Строка вызовет исключение
    // let lastlet = 10 

    member w.Method x y : unit = 
        let res = x * y
        printfn $"Результат умножения %d{x} на %d{y} равен %d{res}"



type MyClass2() =
    let c = 78
    do
        printfn "Первичный конструктор"

    member w.Method x y : unit = 
        let res = x * y
        printfn $"Результат умножения %d{x} на %d{y} равен %d{res}"

    new(i: int) as this2 = MyClass2() then
            this2.Method 98 12

Исходник тестирования конструкторов классов F#

Проект конструкторов классов F# в Visual Studio 2022

Теоретическую статью подтверждает практический исходник с классами и их конструкторами на языке программирования F#.

Исходник написан в MS Visual Studio 2022 preview 5, протестирован в MS Visual Studio 2019. Проект состоит из нескольких модулей для раздельного тестирования каждого типа конструктора.