F# 程式設計/介面
| F# : 介面 |
物件的介面指的是物件向使用者公開的所有公共成員和函式。例如,以下內容
type Monkey(name : string, birthday : DateTime) =
let mutable _birthday = birthday
let mutable _lastEaten = DateTime.Now
let mutable _foodsEaten = [] : string list
member this.Speak() = printfn "Ook ook!"
member this.Name = name
member this.Birthday
with get() = _birthday
and set(value) = _birthday <- value
member internal this.UpdateFoodsEaten(food) = _foodsEaten <- food :: _foodsEaten
member internal this.ResetLastEaten() = _lastEaten <- DateTime.Now
member this.IsHungry = (DateTime.Now - _lastEaten).TotalSeconds >= 5.0
member this.GetFoodsEaten() = _lastEaten
member this.Feed(food) =
this.UpdateFoodsEaten(food)
this.ResetLastEaten()
this.Speak()
此類包含幾個公共、私有和內部成員。但是,此類的使用者只能訪問公共成員;當使用者使用此類時,他們會看到以下介面
type Monkey =
class
new : name:string * birthday:DateTime -> Monkey
member Feed : food:string -> unit
member GetFoodsEaten : unit -> DateTime
member Speak : unit -> unit
member Birthday : DateTime
member IsHungry : bool
member Name : string
member Birthday : DateTime with set
end
請注意 _birthday、_lastEaten、_foodsEaten、UpdateFoodsEaten 和 ResetLastEaten 成員對外部世界不可訪問,因此它們不屬於此物件的公共介面。
到目前為止,您看到的所有介面都與特定物件內在相關。但是,F# 和許多其他 OO 語言允許使用者將介面定義為獨立型別,這使我們能夠有效地將物件的介面與其實現分離。
根據F# 規範,介面使用以下語法定義
type type-name =
interface
inherits-decl
member-defns
end
- 注意: 當使用 #light 語法選項時,可以省略 interface/end 標記,在這種情況下,型別種類推斷 (§10.1) 用於確定型別的種類。任何非抽象成員或建構函式的存在意味著型別不是介面型別。
例如
type ILifeForm = (* .NET convention recommends the prefix 'I' on all interfaces *)
abstract Name : string
abstract Speak : unit -> unit
abstract Eat : unit -> unit
由於它們只定義了一組公共方法簽名,使用者需要建立一個物件來實現介面。以下是在 fsi 中實現 ILifeForm 介面的三個類
> type ILifeForm =
abstract Name : string
abstract Speak : unit -> unit
abstract Eat : unit -> unit
type Dog(name : string, age : int) =
member this.Age = age
interface ILifeForm with
member this.Name = name
member this.Speak() = printfn "Woof!"
member this.Eat() = printfn "Yum, doggy biscuits!"
type Monkey(weight : float) =
let mutable _weight = weight
member this.Weight
with get() = _weight
and set(value) = _weight <- value
interface ILifeForm with
member this.Name = "Monkey!!!"
member this.Speak() = printfn "Ook ook"
member this.Eat() = printfn "Bananas!"
type Ninja() =
interface ILifeForm with
member this.Name = "Ninjas have no name"
member this.Speak() = printfn "Ninjas are silent, deadly killers"
member this.Eat() =
printfn "Ninjas don't eat, they wail on guitars because they're totally sweet";;
type ILifeForm =
interface
abstract member Eat : unit -> unit
abstract member Speak : unit -> unit
abstract member Name : string
end
type Dog =
class
interface ILifeForm
new : name:string * age:int -> Dog
member Age : int
end
type Monkey =
class
interface ILifeForm
new : weight:float -> Monkey
member Weight : float
member Weight : float with set
end
type Ninja =
class
interface ILifeForm
new : unit -> Ninja
end
通常,我們稱介面為抽象,任何實現介面的類都被稱為具體實現。在上面的示例中,ILifeForm 是一個抽象,而 Dog、Monkey 和 Ninja 是具體實現。
值得注意的是,介面只定義物件上的例項成員簽名。換句話說,它們不能定義靜態成員簽名或建構函式簽名。
對於新手程式設計師來說,介面是一個謎(畢竟,建立沒有實現的型別的意義何在?),但是它們對於許多面向物件程式設計技術來說是必不可少的。介面允許程式設計師將函式泛化為實現特定功能的所有類,該功能由介面描述,即使這些類不一定彼此繼承。
例如,上面定義的 Dog、Monkey 和 Ninja 類包含我們在 ILifeForm 介面中定義的共享行為。如程式碼所示,每個類如何說話或吃飯沒有定義,但對於實現介面的每個類,我們都知道它們可以吃飯、說話並有名字。現在,我們可以編寫一個只接受 ILifeForm 介面的方法,而無需擔心它是如何實現的、是否實現了(始終是,編譯器會處理這個問題)或它實際上是什麼型別的物件。任何其他實現相同介面的類(無論其其他方法如何)都會自動得到此方法的支援。
let letsEat (lifeForm: ILifeForm) = lifeForm.Eat()
請注意,在 F# 中,介面是顯式實現的,而在 C# 中,它們通常是隱式實現的。因此,要呼叫期望介面的函式或方法,您必須進行顯式轉換
let myDog = Dog()
letsEat (myDog :> ILifeForm)
您可以透過讓編譯器幫助找到合適的介面來簡化此操作,方法是使用 _ 佔位符
let myDog = Dog()
letsEat (myDog :> _)
介面對於在其他類之間共享實現邏輯片段非常有用,但是為臨時介面定義和實現新類可能非常麻煩。物件表示式允許使用者使用以下語法在匿名類上實現介面
{ new ty0 [ args-expr ] [ as base-ident ] [ with
val-or-member-defns end ]
interface ty1 with [
val-or-member-defns1
end ]
…
interface tyn with [
val-or-member-defnsn
end ] }
使用具體示例,.NET BCL 具有一個名為 System.Array.Sort<T>(T array, IComparer<T>) 的方法,其中 IComparer<T> 公開了一個名為 Compare 的方法。假設我們想使用此方法按臨時方式對陣列進行排序;與其在我們的程式碼中亂扔一次性使用類,我們可以使用物件表示式來動態定義匿名類
> open System
open System.Collections.Generic
type person = { name : string; age : int }
let people =
[|{ name = "Larry"; age = 20 };
{ name = "Moe"; age = 30 };
{ name = "Curly"; age = 25 } |]
let sortAndPrint msg items (comparer : System.Collections.Generic.IComparer<person>) =
Array.Sort(items, comparer)
printf "%s: " msg
Seq.iter (fun x -> printf "(%s, %i) " x.name x.age) items
printfn ""
(* sorting by age *)
sortAndPrint "age" people { new IComparer<person> with member this.Compare(x, y) = x.age.CompareTo(y.age) }
(* sorting by name *)
sortAndPrint "name" people { new IComparer<person> with member this.Compare(x, y) = x.name.CompareTo(y.name) }
(* sorting by name descending *)
sortAndPrint "name desc" people { new IComparer<person> with member this.Compare(x, y) = y.name.CompareTo(x.name) };;
type person =
{ name: string;
age: int; }
val people : person array
val sortAndPrint : string -> person array -> IComparer<person> -> unit
age: (Larry, 20) (Curly, 25) (Moe, 30)
name: (Curly, 25) (Larry, 20) (Moe, 30)
name desc: (Moe, 30) (Larry, 20) (Curly, 25)
與繼承不同,可以實現多個介面
open System
type Person(name : string, age : int) =
member this.Name = name
member this.Age = age
(* IComparable is used for ordering instances *)
interface IComparable<Person> with
member this.CompareTo(other) =
(* sorts by name, then age *)
match this.Name.CompareTo(other.Name) with
| 0 -> this.Age.CompareTo(other.Age)
| n -> n
(* Used for comparing this type against other types *)
interface IEquatable<string> with
member this.Equals(othername) = this.Name.Equals(othername)
在物件表示式中實現多個介面也一樣容易。
介面可以在一種介面層次結構中擴充套件其他介面。例如
type ILifeForm =
abstract member location : System.Drawing.Point
type 'a IAnimal = (* interface with generic type parameter *)
inherit ILifeForm
inherit System.IComparable<'a>
abstract member speak : unit -> unit
type IFeline =
inherit IAnimal<IFeline>
abstract member purr : unit -> unit
當使用者建立 IFeline 的具體實現時,他們需要提供對 IAnimal、IComparable 和 ILifeForm 介面中定義的所有方法的實現。
- 注意: 介面層次結構偶爾有用,但是深層、複雜的層次結構可能難以使用。
open System
type ILifeForm =
abstract Name : string
abstract Speak : unit -> unit
abstract Eat : unit -> unit
type Dog(name : string, age : int) =
member this.Age = age
interface ILifeForm with
member this.Name = name
member this.Speak() = printfn "Woof!"
member this.Eat() = printfn "Yum, doggy biscuits!"
type Monkey(weight : float) =
let mutable _weight = weight
member this.Weight
with get() = _weight
and set(value) = _weight <- value
interface ILifeForm with
member this.Name = "Monkey!!!"
member this.Speak() = printfn "Ook ook"
member this.Eat() = printfn "Bananas!"
type Ninja() =
interface ILifeForm with
member this.Name = "Ninjas have no name"
member this.Speak() = printfn "Ninjas are silent, deadly killers"
member this.Eat() =
printfn "Ninjas don't eat, they wail on guitars because they're totally sweet"
let lifeforms =
[(new Dog("Fido", 7) :> ILifeForm);
(new Monkey(500.0) :> ILifeForm);
(new Ninja() :> ILifeForm)]
let handleLifeForm (x : ILifeForm) =
printfn "Handling lifeform '%s'" x.Name
x.Speak()
x.Eat()
printfn ""
let main() =
printfn "Processing...\n"
lifeforms |> Seq.iter handleLifeForm
printfn "Done."
main()
此程式具有以下型別
type ILifeForm =
interface
abstract member Eat : unit -> unit
abstract member Speak : unit -> unit
abstract member Name : string
end
type Dog =
class
interface ILifeForm
new : name:string * age:int -> Dog
member Age : int
end
type Monkey =
class
interface ILifeForm
new : weight:float -> Monkey
member Weight : float
member Weight : float with set
end
type Ninja =
class
interface ILifeForm
new : unit -> Ninja
end
val lifeforms : ILifeForm list
val handleLifeForm : ILifeForm -> unit
val main : unit -> unit
此程式輸出以下內容
Processing... Handling lifeform 'Fido' Woof! Yum, doggy biscuits! Handling lifeform 'Monkey!!!' Ook ook Bananas! Handling lifeform 'Ninjas have no name' Ninjas are silent, deadly killers Ninjas don't eat, they wail on guitars because they're totally sweet Done.
我們可以在類和函式定義中將泛型型別約束為特定介面。例如,假設我們想要建立一個滿足以下屬性的二叉樹:二叉樹中的每個節點都有兩個子節點,left 和 right,其中 left 中的所有子節點都小於其父節點,而 right 中的所有子節點都大於其父節點。
我們可以實現一個具有這些屬性的二叉樹,定義一個將樹約束為 IComparable<T> 介面的二叉樹。
- 注意: .NET 在 BCL 中定義了許多介面,包括非常重要的
IComparable<T> 介面。IComparable 公開了一個方法,objectInstance.CompareTo(otherInstance),當objectInstance分別大於、小於或等於otherInstance時,該方法應返回 1、-1 或 0。.NET 框架中的許多類(包括所有數值資料型別、字串和日期時間)已經實現了 IComparable。
例如,使用 fsi
> open System
type tree<'a> when 'a :> IComparable<'a> =
| Nil
| Node of 'a * 'a tree * 'a tree
let rec insert (x : #IComparable<'a>) = function
| Nil -> Node(x, Nil, Nil)
| Node(y, l, r) as node ->
if x.CompareTo(y) = 0 then node
elif x.CompareTo(y) = -1 then Node(y, insert x l, r)
else Node(y, l, insert x r)
let rec contains (x : #IComparable<'a>) = function
| Nil -> false
| Node(y, l, r) as node ->
if x.CompareTo(y) = 0 then true
elif x.CompareTo(y) = -1 then contains x l
else contains x r;;
type tree<'a> when 'a :> IComparable<'a>> =
| Nil
| Node of 'a * tree<'a> * tree<'a>
val insert : 'a -> tree<'a> -> tree<'a> when 'a :> IComparable<'a>
val contains : #IComparable<'a> -> tree<'a> -> bool when 'a :> IComparable<'a>
> let x =
let rnd = new Random()
[ for a in 1 .. 10 -> rnd.Next(1, 100) ]
|> Seq.fold (fun acc x -> insert x acc) Nil;;
val x : tree<int>
> x;;
val it : tree<int>
= Node
(25,Node (20,Node (6,Nil,Nil),Nil),
Node
(90,
Node
(86,Node (65,Node (50,Node (39,Node (32,Nil,Nil),Nil),Nil),Nil),Nil),
Nil))
> contains 39 x;;
val it : bool = true
> contains 55 x;;
val it : bool = false
依賴注入是指向軟體元件提供外部依賴的過程。例如,假設我們有一個類,在發生錯誤時,它會向網路管理員傳送電子郵件,我們可能會編寫一些類似這樣的程式碼
type Processor() =
(* ... *)
member this.Process items =
try
(* do stuff with items *)
with
| err -> (new Emailer()).SendMsg("admin@company.com", "Error! " + err.Message)
Process 方法建立了 Emailer 的例項,因此我們可以說 Processor 類依賴於 Emailer 類。
假設我們正在測試 Processor 類,並且我們不想一直向網路管理員傳送電子郵件。與其在測試時註釋掉我們不想執行的程式碼行,不如用一個虛擬類替換 Emailer 依賴項。我們可以透過建構函式傳遞依賴項來實現這一點
type IFailureNotifier =
abstract Notify : string -> unit
type Processor(notifier : IFailureNotifier) =
(* ... *)
member this.Process items =
try
// do stuff with items
with
| err -> notifier.Notify(err.Message)
(* concrete implementations of IFailureNotifier *)
type EmailNotifier() =
interface IFailureNotifier with
member Notify(msg) = (new Emailer()).SendMsg("admin@company.com", "Error! " + msg)
type DummyNotifier() =
interface IFailureNotifier with
member Notify(msg) = () // swallow message
type LogfileNotifier(filename : string) =
interface IFailureNotifer with
member Notify(msg) = System.IO.File.AppendAllText(filename, msg)
現在,我們建立一個處理器,並傳入我們感興趣的 FailureNotifier 型別。在測試環境中,我們將使用 `new Processor(new DummyNotifier())`;在生產環境中,我們將使用 `new Processor(new EmailNotifier())` 或 `new Processor(new LogfileNotifier(@"C:\log.txt"))`。
為了演示使用一個稍微人為的例子進行依賴注入,以下 fsi 中的程式碼展示瞭如何熱交換一個介面實現。
> #time;;
--> Timing now on
> type IAddStrategy =
abstract add : int -> int -> int
type DefaultAdder() =
interface IAddStrategy with
member this.add x y = x + y
type SlowAdder() =
interface IAddStrategy with
member this.add x y =
let rec loop acc = function
| 0 -> acc
| n -> loop (acc + 1) (n - 1)
loop x y
type OffByOneAdder() =
interface IAddStrategy with
member this.add x y = x + y - 1
type SwappableAdder(adder : IAddStrategy) =
let mutable _adder = adder
member this.Adder
with get() = _adder
and set(value) = _adder <- value
member this.Add x y = this.Adder.add x y;;
type IAddStrategy =
interface
abstract member add : int -> (int -> int)
end
type DefaultAdder =
class
interface IAddStrategy
new : unit -> DefaultAdder
end
type SlowAdder =
class
interface IAddStrategy
new : unit -> SlowAdder
end
type OffByOneAdder =
class
interface IAddStrategy
new : unit -> OffByOneAdder
end
type SwappableAdder =
class
new : adder:IAddStrategy -> SwappableAdder
member Add : x:int -> (int -> int)
member Adder : IAddStrategy
member Adder : IAddStrategy with set
end
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
> let myAdder = new SwappableAdder(new DefaultAdder());;
val myAdder : SwappableAdder
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
> myAdder.Add 10 1000000000;;
Real: 00:00:00.001, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 1000000010
> myAdder.Adder <- new SlowAdder();;
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : unit = ()
> myAdder.Add 10 1000000000;;
Real: 00:00:01.085, CPU: 00:00:01.078, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 1000000010
> myAdder.Adder <- new OffByOneAdder();;
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : unit = ()
> myAdder.Add 10 1000000000;;
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 1000000009