Pull to refresh
952.85
OTUS
Цифровые навыки от ведущих экспертов

ООП в F#

Level of difficultyEasy
Reading time5 min
Views3.3K

Привет, Хабр!

Объектно-ориентированное программирование представляет собой подход к разработке, где основой являются объекты — экземпляры классов, объединяющие в себе и данные, и поведение. В F#, который вырос на функциональных концепциях, ООП представлен как дополнение к уже существующему функционалу. F# позволяет использовать классы, наследование и интерфейсы, при этом так, что ООП элементы просто идеально вписываются в функциональный контекст, не создавая ощущения чужеродности.

Основы ООП в F#

Классы и поля

Классы в F# — это структуры, которые позволяют объединить поля и методы, определяющие состояние и поведение объекта соответственно. Создание класса в F# начинается с ключевого слова type, за которым следует имя класса и его конструктор:

type MyClass(param1 : int, param2 : string) =
    // поля
    let mutable field1 = param1
    let field2 = param2

    // свойство
    member this.Property1 = field1
    member this.Property2 with get() = field2 and set(value) = field1 <- value

    // метод
    member this.Method1() =
        printfn "Method1 called with field1: %d and field2: %s" field1 field2

MyClass с двумя полями: field1 и field2, они инициализируются через параметры конструктора. Также присутствуют два свойства Property1 и Property2, где Property1 является только для чтения, а Property2 — для чтения и записи, что демонстрирует использование свойств в F# для контроля доступа к данным класса. Метод Method1 печатает значения полей.

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

do привязки выполняют код инициализации при создании экземпляра класса.

type MyClass(x: int, y: int) =
    let sum = x + y
    do printfn "Сумма x и y равна %d" sum
    member this.Sum = sum

Конструктор инициализирует поля и выполняет дополнительное действие

Можно использовать самоидентификаторы для обращения к текущему экземпляру класса:

type MyClass(x: int, y: int) as self =
    member self.Sum = x + y
    member self.Multiply = x * y

as self позволяет обратиться к текущему экземпляру класса внутри его методов.

Интерфейсы

Интерфейсы определяются с использованием ключевого слова type с указанием ключевого слова interface и перечислением методов и свойств без их реализации. Каждый метод или свойство в интерфейсе является абстрактным, определяя форму без конкретной реализации:

type IExampleInterface =
    abstract member ExampleMethod : string -> string
    abstract member ExampleProperty : int with get

IExampleInterface определяет интерфейс с одним методом ExampleMethod, принимающим строку и возвращающим строку, и свойством ExampleProperty только для чтения типа int.

Реализация интерфейса указывается в теле класса с использованием ключевого слова interface с последующим ключевым словом with, за которым следуют реализации методов и свойств интерфейса:

type ExampleClass() =
    interface IExampleInterface with
        member this.ExampleMethod(input) = "Processed: " + input
        member this.ExampleProperty with get() = 42

ExampleClass реализует IExampleInterface, предоставляя конкретные реализации для ExampleMethod и ExampleProperty. Это позволяет объектам ExampleClass быть использованными там, где ожидается IExampleInterface.

Наследование и абстрактные классы

Наследование позволяет создать новый класс на основе существующего, наследуя его свойства и методы:

try
    let result = 10 / 0
with
| :? System.DivideByZeroException -> printfn "Деление на ноль."
| ex -> printfn "Произошло исключение: %s" (ex.Message)

DerivedClass наследует BaseClass, добавляя новый параметр param2 и сохраняя параметр param1 от базового класса.

Абстрактный класс в F# — это класс, который не может быть инстанцирован сам по себе и служит основой для других классов. Абстрактные классы могут содержать абстрактные методы (без реализации) и методы с реализацией:

[<AbstractClass>]
type Shape(x0: float, y0: float) =
    let mutable x, y = x0, y0
    abstract member Area: float
    abstract member Perimeter: float
    member this.Move(dx: float, dy: float) =
        x <- x + dx
        y <- y + dy

Shape определяет общие характеристики для фигур, такие как площадь и периметр, но не предоставляет их конкретные реализации, делая класс абстрактным.

Пример наследования от абстрактного класса:

type Circle(x: float, y: float, radius: float) =
    inherit Shape(x, y)
    override this.Area =
        Math.PI * radius * radius
    override this.Perimeter =
        2.0 * Math.PI * radius

Circle наследует абстрактный класс Shape, предоставляя конкретные реализации для Area и Perimeter.

Управление исключениями и ошибками

Блок try...with используется для перехвата и обработки исключений. Код внутри блока try выполняется, и если в процессе его выполнения возникает исключение, исполнение передается в блок with, где исключение может быть обработано:

try
    let result = 10 / 0
with
| :? System.DivideByZeroException -> printfn "Деление на ноль."
| ex -> printfn "Произошло исключение: %s" (ex.Message)

Исключение перехватывается в блоке with, и программа выводит соответствующее сообщение, вместо того чтобы аварийно завершиться

Блок try...finally используется для гарантии выполнения определенного кода после блока try, независимо от того, произошло исключение или нет:

try
    let result = 10 / 2
finally
    // код в этом блоке выполнится в любом случае
    printfn "Этот код выполнится независимо от исключений."

Можно создавать собственные типы исключений для обработки специфических ошибочных ситуаций. Это делается путем наследования от класса System.Exception или любого другого встроенного исключения:

type MyCustomException(message: string) =
    inherit System.Exception(message)

MyCustomException - это пользовательское исключение, которое принимает сообщение об ошибке в качестве параметра и передает его конструктору базового класса System.Exception:

let riskyOperation x =
    if x < 0 then
        raise (MyCustomException("Число не должно быть отрицательным"))
    else
        printfn "%d - это положительное число" x

riskyOperation генерирует MyCustomException, если ей передано отрицательное число, что позволяет точно указать тип ошибки и облегчить ее обработку.

Так же можно подключить логирование ошибок с помощью Serilog. Добавляем зависимость Serilog к проекту через NuGet:

open Serilog

Log.Logger <- LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File("logs/myapp.txt", rollingInterval = RollingInterval.Day)
    .CreateLogger()

Log.Information("Запуск приложения")

try
    let result = 10 / 0
with
| ex ->
    Log.Error(ex, "Произошло исключение при выполнении операции")

В блоке try...with исключение перехватывается и логируется с использованием Serilog, предоставляя подробную информацию об ошибке.

Оптимизация

Инкапсуляция — это ООП-принцип, заключающийся в ограничении прямого доступа к некоторым компонентам объекта и управлении доступом к этим данным через методы:

type BankAccount() =
    let mutable balance = 0.0

    member this.Deposit(amount: float) =
        if amount > 0.0 then
            balance <- balance + amount
        else
            raise (ArgumentException("Сумма должна быть больше нуля"))

    member this.Withdraw(amount: float) =
        if amount <= balance && amount > 0.0 then
            balance <- balance - amount
        else
            raise (InvalidOperationException("Недостаточно средств или сумма меньше нуля"))

    member this.GetBalance() = balance

Здесь юзаем инкапсуляцию для контроля изменений баланса банковского счета через методы Deposit и Withdraw, предотвращая прямой доступ к полю balance

Наследование позволяет создать новый класс на основе существующего, переиспользуя его код и расширяя функциональность:

type Vehicle() =
    abstract member Move: unit -> string

type Car() =
    inherit Vehicle()
    override this.Move() = "Едет на четырех колесах"

type Bicycle() =
    inherit Vehicle()
    override this.Move() = "Едет на двух колесах"

Car и Bicycle наследуют от Vehicle и реализуют абстрактный метод Move

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

type IDrawable =
    abstract member Draw: unit -> unit

type Circle() =
    interface IDrawable with
        member this.Draw() = printfn "Рисуется круг"

type Square() =
    interface IDrawable with
        member this.Draw() = printfn "Рисуется квадрат"

let drawShapes shapes =
    shapes |> List.iter (fun shape -> shape.Draw())

Circle и Square реализуют интерфейс IDrawable, и так можно обрабатывать их одинаково в функции drawShapes.


ООП позволяет создавать структурированные и легко поддерживаемые системы. А больше про ООП и ЯП в целом можно узнать в рамках экспертных курсов по программированию от моих друзей из OTUS.

Tags:
Hubs:
Total votes 15: ↑12 and ↓3+9
Comments11

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS