Перейти к основному содержимому

ООП в F# для взаимодействия с .NET

Разработчику Архитектору

Когда нужен объектный стиль

Внутри F#-проекта домен чаще описывают записями (type Person = { Name: string }) и размеченными объединениями (type Payment = Cash of decimal | Card of string). Это даёт match, исчерпывающие ветки и неизменяемые данные (186).

Классы, интерфейсы и перечисления CLI подключают, когда:

  • сборку вызывают из C# или VB;
  • нужно наследовать тип из .NET Framework или библиотеки;
  • DI-контейнер ASP.NET Core ждёт IService, IHostedService и т.п.;
  • оборачивают компонент с событиями и IDisposable (UI, сокеты, HttpClient в старом стиле).

Цель статьи — минимальный набор ООП для interop, без полного курса C#.

Предварительно: F# в экосистеме .NET · Сопоставление с образцом.


Как F# попадает в объектную модель .NET

Любой тип F# компилируется в тип CLR (как C#). Записи и DU — тоже классы с особыми атрибутами. Снаружи solution на C# видит обычные типы с методами и свойствами, если вы их явно экспортируете (public, member).

Термины:

ТерминСмысл
CLRСреда выполнения .NET
memberМетод или свойство типа, видимое снаружи
Конструктор ()Создание экземпляра класса
InteropСовместная работа F# и других .NET-языков в одном solution

Класс с состоянием

type Counter() =
let mutable value = 0
member _.Increment() = value <- value + 1
member _.Current = value

Разбор:

  • type Counter() = — класс с параметрless-конструктором ().
  • let mutable value = 0 в теле класса — приватное поле экземпляра (снаружи недоступно).
  • member _.Increment() — метод; _ вместо имени this (можно писать this, если нужно).
  • member _.Current — свойство только для чтения (вычисляется при каждом обращении).

Использование:

let c = Counter()
c.Increment()
printfn "%d" c.Current // 1

Из C#:

var c = new Counter();
c.Increment();
Console.WriteLine(c.Current);

Имена и сигнатуры совпадают с правилами .NET.


Свойства и параметры конструктора

type Person(name: string, age: int) =
member val Name = name with get, set
member val Age = age with get, set
  • name и ageпараметры конструктора, доступны в теле класса.
  • member val ... with get, set — автосвойство: компилятор создаёт поле и аксессоры.

Для DTO, которые сериализуют в JSON, часто достаточно записи с [<CLIMutable>] — проще и привычнее для REST API (Справочник).


Структуры (struct)

[<Struct>]
type Point =
struct
val X: float
val Y: float
new(x, y) = { X = x; Y = y }
end

Структура в .NET — тип значения: копируется при присваивании, может уменьшить давление на сборщик мусора в горячих циклах. Записи F# по умолчанию — ссылочные типы; для мелких неизменяемых значений иногда ставят [<Struct>] на record (зависит от версии языка и сценария).

ВыборКогда
Record / DUдомен, match, неизменяемость
classнаследование, изменяемое состояние, события
structмалые значения, производительность

Интерфейсы

type IRepository =
abstract member Get: int -> string option

type InMemoryRepository() =
interface IRepository with
member _.Get id =
if id = 1 then Some "alpha" else None
  • В интерфейсе методы объявляют через abstract member.
  • Реализация — блок interface IRepository with ... внутри класса.
  • Несколько интерфейсов — несколько блоков interface ... with.

C#-код может принять IRepository и не знать, что реализация написана на F#.

Явная реализация интерфейса (как в C# «скрыть метод от публичного API») поддерживается, если нужно развести имена.


Наследование

type Animal() =
abstract member Speak: unit -> string
default _.Speak() = "..."

type Dog() =
inherit Animal()
override _.Speak() = "woof"
Ключевое словоРоль
abstract memberКонтракт для наследников
defaultРеализация по умолчанию в базовом типе
inherit Animal()Вызов конструктора базового класса
overrideЗамена реализации в наследнике

Глубокие иерархии в F# редки; композиция функций и DU обычно короче и безопаснее для match.


Перечисление CLI и DU

Enum для контракта, совместимого с C#:

type Status =
| Draft = 0
| Published = 1
| Archived = 2

Числовые значения фиксируют совместимость с enum в C#.

DU — когда у каждого случая свои данные:

type DocumentState =
| Draft of title: string
| Published of publishedAt: System.DateTime

Обработка — через match (186). Enum — когда внешний API уже зафиксировал набор констант без полезной нагрузки.


Статические члены и модули

type MathConstants =
static member Pi = 3.14159265
static member CircleArea r = MathConstants.Pi * r * r

Аналог static в C#. Набор чистых функций без состояния часто оформляют модулем верхнего уровня:

module Geometry =
let circleArea r = System.Math.PI * r * r

Модуль не требует new — удобнее для F#-клиентов; класс со static — когда так ожидает C#.


obj и переопределение Object

Все типы наследуют System.Object (obj в F#). Для классов можно переопределить ToString(), Equals(), GetHashCode(). Записи и DU получают разумные реализации от компилятора. При вызове API, ожидающего obj, приведение обычно не требуется.


Рекомендации для общих solution

СитуацияПодход
Публичный API библиотеки для C#Понятные имена типов, [<CLIMutable>] на DTO, [<RequireQualifiedAccess>] на модулях при конфликтах имён
Скрыть деталиprivate DU внутри, наружу — интерфейс или функции модуля
WPF / WinFormsКлассы с событиями допустимы; бизнес-логику — в чистых функциях
ASP.NET CoreТонкие классы-обработчики + функциональное ядро в F# library

Пример границы: в App.Core (F#) — type Order = ... и функции validate, price; в App.Web (C#) — Controller, вызывающий OrderService из DLL.


Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).