ООП в F#
Привет, Хабр!
Объектно-ориентированное программирование представляет собой подход к разработке, где основой являются объекты — экземпляры классов, объединяющие в себе и данные, и поведение. В 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# — это класс, который не может быть инстанцирован сам по себе и служит основой для других классов. Абстрактные классы могут содержать абстрактные методы (без реализации) и методы с реализацией:
[]
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.