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("[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