跳轉到內容

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_foodsEatenUpdateFoodsEatenResetLastEaten 成員對外部世界不可訪問,因此它們不屬於此物件的公共介面。

到目前為止,您看到的所有介面都與特定物件內在相關。但是,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 是一個抽象,而 DogMonkeyNinja 是具體實現。

值得注意的是,介面只定義物件上的例項成員簽名。換句話說,它們不能定義靜態成員簽名或建構函式簽名。

介面的用途和使用方法?

[編輯 | 編輯原始碼]

對於新手程式設計師來說,介面是一個謎(畢竟,建立沒有實現的型別的意義何在?),但是它們對於許多面向物件程式設計技術來說是必不可少的。介面允許程式設計師將函式泛化為實現特定功能的所有類,該功能由介面描述,即使這些類不一定彼此繼承。

例如,上面定義的 DogMonkeyNinja 類包含我們在 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 的具體實現時,他們需要提供對 IAnimalIComparableILifeForm 介面中定義的所有方法的實現。

注意: 介面層次結構偶爾有用,但是深層、複雜的層次結構可能難以使用。

將函式泛化為多個類

[編輯 | 編輯原始碼]
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.

在泛型型別定義中使用介面

[編輯 | 編輯原始碼]

我們可以在類和函式定義中將泛型型別約束為特定介面。例如,假設我們想要建立一個滿足以下屬性的二叉樹:二叉樹中的每個節點都有兩個子節點,leftright,其中 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
前一個:繼承 索引 下一個:事件
華夏公益教科書