跳转到内容

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("[email protected]", "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("[email protected]", "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
前一个:继承 索引 下一个:事件
华夏公益教科书