Интерфейсы представляют набор связанных компонентов, которые должен реализовать класс. Определение интерфейсов имеет следующий синтаксис:
type имя_интерфейса = [ interface ] abstract компонент1 : тип_компонента1 abstract компонент2 : тип_компонента2 abstract компонент3 : тип_компонента3 [ end ]
Объявления интерфейса напоминают объявления классов - интерфейс определяется с помощью оператора type, после которого идет имя интерфейса. Затем идет тело интерфейса между операторами interface и end. Внутри тела интерфейса, как и в классе, можно определять свойства и методы. Но в отличие от классов свойства и методы интерфейсов не имеют реализации. Вместо этого они являются абстрактными, на что указывает ключевое слово abstract.
Если интерфейс не пустой - имеет свойства и методы, то ключевые слова interface и end можно не использовать, а компилятор автоматически выведет, что тип представляет интерфейс.
Например, определим пустой интерфейс:
type IPrintable = interface end
Этот интерфейс называется IPrintable (обычно названия интерфейсов в .NET начинаются с буквы I, однако это не более чем необязательная условность). Этот интерфейс пустой, он не определяет никаких свойств и методов.
Определим в интерфейсе метод:
type IPrintable = abstract member Print: unit -> unit
Здесь определен метод Print, который имеет тип unit -> unit
, то не принимает никаких параметров и ничего не возвращает.
Реализуем интерфейс в классе:
type IPrintable = abstract member Print: unit -> unit type Person(name: string, age: int) = interface IPrintable with member this.Print() = printfn "Name: %s Age: %d" name age let tom: IPrintable = Person("Tom", 39) tom.Print() // Name: Tom Age: 39
Для реализации интерфейса внутри класса применяем выражение
interface имя_интерфейса with member реализация_метода
В данном случае в реализации метода Print выводим на консоль имя и возраст пользователя.
Далее мы определяем переменную типа IPrintable, то есть типа интерфейса, а для создания объекта используется конструктор класса Person.
let tom: IPrintable = Person("Tom", 39) tom.Print()
Главное ограничение интерфейсов в F# заключается в том, что методы интерфейса можно вызывать только через объект этого интерфейса. Поэтому в примере выше значение tom представлял объект интерфейса IPrintable, а не просто Person, несмотря на то, что класс Person реализует этот интерфейс. То есть мы НЕ можем написать следующим образом:
let tom = Person("Tom", 39) tom.Print()
В этом случае компилятор нам сгенерирует ошибку, потому что в классе Person нет метода Print.
В качестве альтернативы мы можем выполнить преобразование к типу интерфейса с помощью оператора :> или оператора восходящего преобразования:
type IPrintable = abstract member Print: unit -> unit type Person(name: string, age: int) = interface IPrintable with member this.Print() = printfn "Name: %s Age: %d" name age // значение tom имеет тип Person let tom = Person("Tom", 39) // преобразуем значение tom к типу IPrintable (tom :> IPrintable).Print()
В качестве альтернативы можно объявить в классе одноименный метод и вызывать в нем реализацию интерфейса:
type IPrintable = abstract member Print: unit -> unit type Person(name: string, age: int) = // метод Print вызывает реализацию метода интерфейса member this.Print() = (this :> IPrintable).Print() interface IPrintable with member this.Print() = printfn "Name: %s Age: %d" name age // значение tom имеет тип Person let tom = Person("Tom", 39) // преобразование к типу IPrintable не нужно tom.Print()
Аналогичным образом можно применять несколько интерфейсов:
type IPrintable = abstract member Print: unit -> unit type ISendable = abstract member Send: unit -> unit type Message(text:string) = interface IPrintable with member this.Print() = printfn "Text: %s" text interface ISendable with member this.Send() = printfn "Sending message: %s" text let mes = Message("Hello World") (mes :> IPrintable).Print() (mes :> ISendable).Send()
В данном случае класс Message реализует два интерфейса - IPrintable и ISendable.
Суть интерфейса заключается в том, что он определяет контракт, что определенный тип реализует определенные методы. И когда мы используем объект интерфейса, мы уверены, что у этого объекта есть определенный набор методов, который мы можем использовать. Например:
type IPrintable = abstract member Print: unit -> unit type Person(name: string, age: int) = interface IPrintable with member this.Print() = printfn "Name: %s Age: %d" name age type Message(text:string) = interface IPrintable with member this.Print() = printfn "Text: %s" text let print (obj:IPrintable) = obj.Print() let tom = Person("Tom", 39) let mes = Message("Hello World") print tom print mes
Здесь функция print в качестве параметра принимает объект IPrintable. Мы не знаем, что это будет за объект, какой именно тип он будет представлять (в примере выше это может быть объект Person или объект Message). Но мы знаем, что он реализует интерфейс IPrintable, и поэтому у него есть метод Print, который можно вызвать.
Выражения объектов (object expression) предоставляют краткий способ реализации некоторого функционала, например, интерфейса. Выражения объектов применяются, когда надо избежать дополнительного кода, необходимого для создания нового именованного типа. Выражения объектов позволяют минимизировать код и количество типов.
Выражение объектов на основе интерфейса имеет следующий синтаксис:
{ new имя_интерфейса with компоненты_объекта }
Все выражение объекта заключается в фигурные скобки и начинается со слова new. Затем идет имя интерфейса, на основе которого создается объект. После оператора with указываются реализации методов интерфейса.
Объекты на основе интерфейсов могут быть полезны, когда нет смысла создавать отдельный класс, который реализует интерфейс, а просто нужен объект, который имеет методы интерфейса без какой-то дополнительной функциональности. Другой вариант - если у нас есть класс, который не реализует интерфейс, и мы хотим, чтобы для этих объектов выполнить методы интефейса. Например:
type IPrintable = abstract member Print: unit -> unit // класс, который не реализует интерфейс IPrintable type Person(name: string, age: int) = member this.Name = name member this.Age = age // выражение объекта let makePrintable (person: Person) = { new IPrintable with member this.Print() = printfn "Name: %s Age: %d" person.Name person.Age } // создаем объекты с помощью выражения объектов let tom = makePrintable (Person("Tom", 39)) let bob = makePrintable (Person("Bob", 43)) tom.Print() bob.Print()
Здесь класс Person не реализует интерфейс IPrintable, однако мы хотим, чтобы для объектов Person был реализован метод Print. Возможно, в каких-то ситуациях лучше реализовать интерфейс в классе. Но, допуcтим, класс используется довольно широко в проекте, а возможность вывода обекта Person нам нужна в каком-то единичном месте. Не говоря уже о том, что мы можем использовать внешний класс, код которого нам не доступен. В этом случае можно было бы использовать выражения объектов.
В данном случае определяется функция makePrintable, которая использует выражение объектов и возвращает объект на основе интерфейса IPrintable:
let makePrintable (person: Person) = { new IPrintable with member this.Print() = printfn "Name: %s Age: %d" person.Name person.Age }
Здесь определяется метод Print, который выводит значения свойств Name и Age из параметра person.
Далее мы можем вызвать функцию makePrintable и создать несколько объектов, которые будут иметь метод Print:
let tom = makePrintable (Person("Tom", 39)) let bob = makePrintable (Person("Bob", 43))
Консольный вывод:
Name: Tom Age: 39 Name: Bob Age: 43