F# 编程/类
F#:类和对象 |
在现实世界中,对象是“真实”的事物。猫、人、电脑和一卷胶带都是以有形意义存在的“真实”事物。当我们思考这些事物时,我们可以从多个属性的角度来广泛地描述它们
- 属性:人有姓名,猫有四条腿,电脑有价格标签,胶带是粘性的。
- 行为:人阅读报纸,猫整天睡觉,电脑处理数据,胶带将东西粘到其他东西上。
- 类型/群体成员关系:员工是人的一种类型,猫是宠物,戴尔和 Mac 是电脑的类型,胶带属于更广泛的粘合剂家族。
在编程世界中,“对象”简单来说就是现实世界中某事物的模型。面向对象编程 (OOP) 存在是因为它允许程序员对现实世界的实体进行建模,并在代码中模拟它们的交互。就像它们在现实世界中的对应物一样,计算机编程中的对象具有属性和行为,并且可以根据其类型进行分类。
虽然我们当然可以创建代表猫、人、粘合剂的对象,但对象也可以代表不太具体的事物,例如银行账户或业务规则。
尽管 OOP 的范围已经扩展到包括一些高级概念,例如设计模式和应用程序的大规模体系结构,但此页面将保持简单,并将 OOP 的讨论限制在数据建模。
在创建对象之前,应定义其属性和函数。您在类中定义对象的属性和方法。实际上,定义类的语法有两种:隐式语法和显式语法。
隐式类语法定义如下
type TypeName optional-type-arguments arguments [ as ident ] =
[ inherit type { as base } ]
[ let-binding | let-rec bindings ] *
[ do-statement ] *
[ abstract-binding | member-binding | interface-implementation ] *
- 方括号中的元素是可选的,后跟
*
的元素可以出现零次或多次。
上面的语法并不像看起来那么令人生畏。这是一个用隐式风格编写的简单类
type Account(number : int, holder : string) = class
let mutable amount = 0m
member x.Number = number
member x.Holder = holder
member x.Amount = amount
member x.Deposit(value) = amount <- amount + value
member x.Withdraw(value) = amount <- amount - value
end
上面的代码定义了一个名为Account
的类,它具有三个属性和两个方法。让我们仔细看看以下内容
type Account(number : int, holder : string) = class
下划线代码称为类构造函数。构造函数是一种特殊的函数,用于初始化对象中的字段。在本例中,我们的构造函数定义了两个值number
和holder
,可以在类的任何地方访问它们。您可以使用new
关键字并向构造函数传递适当的参数来创建Account
的实例,如下所示
let bob = new Account(123456, "Bob’s Saving")
此外,让我们看看成员的定义方式
member x.Deposit(value) = amount <- amount + value
上面的x
是当前作用域中对象的别名。大多数面向对象语言都提供隐式this
或self
变量来访问作用域中的对象,但 F# 要求程序员创建自己的别名。
创建了Account
的实例后,可以使用.propertyName
表示法访问其属性。以下是在 FSI 中的示例
> let printAccount (x : Account) =
printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount;;
val printAccount : Account -> unit
> let bob = new Account(123456, "Bob’s Savings");;
val bob : Account
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 0
val it : unit = ()
> bob.Deposit(100M);;
val it : unit = ()
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 100
val it : unit = ()
> bob.Withdraw(29.95M);;
val it : unit = ()
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 70.05
让我们在一个真实的程序中使用这个类
open System
type Account(number : int, holder : string) = class
let mutable amount = 0m
member x.Number = number
member x.Holder = holder
member x.Amount = amount
member x.Deposit(value) = amount <- amount + value
member x.Withdraw(value) = amount <- amount - value
end
let homer = new Account(12345, "Homer")
let marge = new Account(67890, "Marge")
let transfer amount (source : Account) (target : Account) =
source.Withdraw amount
target.Deposit amount
let printAccount (x : Account) =
printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount
let main() =
let printAccounts() =
[homer; marge] |> Seq.iter printAccount
printfn "\nInializing account"
homer.Deposit 50M
marge.Deposit 100M
printAccounts()
printfn "\nTransferring $30 from Marge to Homer"
transfer 30M marge homer
printAccounts()
printfn "\nTransferring $75 from Homer to Marge"
transfer 75M homer marge
printAccounts()
main()
该程序具有以下类型
type Account =
class
new : number:int * holder:string -> Account
member Deposit : value:decimal -> unit
member Withdraw : value:decimal -> unit
member Amount : decimal
member Holder : string
member Number : int
end
val homer : Account
val marge : Account
val transfer : decimal -> Account -> Account -> unit
val printAccount : Account -> unit
该程序输出以下内容
Initializing account x.Number: 12345, x.Holder: Homer, x.Amount: 50 x.Number: 67890, x.Holder: Marge, x.Amount: 100 Transferring $30 from Marge to Homer x.Number: 12345, x.Holder: Homer, x.Amount: 80 x.Number: 67890, x.Holder: Marge, x.Amount: 70 Transferring $75 from Homer to Marge x.Number: 12345, x.Holder: Homer, x.Amount: 5 x.Number: 67890, x.Holder: Marge, x.Amount: 145
do
关键字用于构造函数后初始化。例如,要创建一个表示股票的对象,需要传入股票代码并在构造函数中初始化其余属性
open System
open System.Net
type Stock(symbol : string) = class
let url =
"http://download.finance.yahoo.com/d/quotes.csv?s=" + symbol + "&f=sl1d1t1c1ohgv&e=.csv"
let mutable _symbol = String.Empty
let mutable _current = 0.0
let mutable _open = 0.0
let mutable _high = 0.0
let mutable _low = 0.0
let mutable _volume = 0
do
(* We initialize our object in the do block *)
let webClient = new WebClient()
(* Data comes back as a comma-separated list, so we split it
on each comma *)
let data = webClient.DownloadString(url).Split([|','|])
_symbol <- data.[0]
_current <- float data.[1]
_open <- float data.[5]
_high <- float data.[6]
_low <- float data.[7]
_volume <- int data.[8]
member x.Symbol = _symbol
member x.Current = _current
member x.Open = _open
member x.High = _high
member x.Low = _low
member x.Volume = _volume
end
let main() =
let stocks =
["msft"; "noc"; "yhoo"; "gm"]
|> Seq.map (fun x -> new Stock(x))
stocks |> Seq.iter (fun x -> printfn "Symbol: %s (%F)" x.Symbol x.Current)
main()
该程序具有以下类型
type Stock =
class
new : symbol:string -> Stock
member Current : float
member High : float
member Low : float
member Open : float
member Symbol : string
member Volume : int
end
该程序输出以下内容(您的输出可能会有所不同)
Symbol: "MSFT" (19.130000) Symbol: "NOC" (43.240000) Symbol: "YHOO" (12.340000) Symbol: "GM" (3.660000)
- 注意:在类定义中可以有任意数量的
do
语句,尽管通常不需要超过一个。
以显式风格编写的类遵循以下格式
type TypeName =
[ inherit type ]
[ val-definitions ]
[ new ( optional-type-arguments arguments ) [ as ident ] =
{ field-initialization }
[ then constructor-statements ]
] *
[ abstract-binding | member-binding | interface-implementation ] *
这是一个使用显式语法定义的类
type Line = class
val X1 : float
val Y1 : float
val X2 : float
val Y2 : float
new (x1, y1, x2, y2) =
{ X1 = x1; Y1 = y1;
X2 = x2; Y2 = y2}
member x.Length =
let sqr x = x * x
sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end
每个val
在我们的对象中定义一个字段。与其他面向对象语言不同,F# 不会隐式地将类中的字段初始化为任何值。相反,F# 要求程序员定义一个构造函数,并使用值显式初始化对象中的每个字段。
我们可以使用then
块执行一些构造函数后处理,如下所示
type Line = class
val X1 : float
val Y1 : float
val X2 : float
val Y2 : float
new (x1, y1, x2, y2) as this =
{ X1 = x1; Y1 = y1;
X2 = x2; Y2 = y2;}
then
printfn "Line constructor: {(%F, %F), (%F, %F)}, Line.Length: %F"
this.X1 this.Y1 this.X2 this.Y2 this.Length
member x.Length =
let sqr x = x * x
sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end
请注意,我们必须在构造函数后添加一个别名(new (x1, y1, x2, y2) as this
),以访问正在构造的对象的字段。每次创建Line
对象时,构造函数都会打印到控制台。我们可以使用 fsi 演示这一点
> let line = new Line(1.0, 1.0, 4.0, 2.5);;
val line : Line
Line constructor: {(1.000000, 1.000000), (4.000000, 2.500000)}, Line.Length: 3.354102
由于构造函数是显式定义的,因此我们可以选择提供多个构造函数。
open System
open System.Net
type Car = class
val used : bool
val owner : string
val mutable mileage : int
(* first constructor *)
new (owner) =
{ used = false;
owner = owner;
mileage = 0 }
(* another constructor *)
new (owner, mileage) =
{ used = true;
owner = owner;
mileage = mileage }
end
let main() =
let printCar (c : Car) =
printfn "c.used: %b, c.owner: %s, c.mileage: %i" c.used c.owner c.mileage
let stevesNewCar = new Car("Steve")
let bobsUsedCar = new Car("Bob", 83000)
let printCars() =
[stevesNewCar; bobsUsedCar] |> Seq.iter printCar
printfn "\nCars created"
printCars()
printfn "\nSteve drives all over the state"
stevesNewCar.mileage <- stevesNewCar.mileage + 780
printCars()
printfn "\nBob commits odometer fraud"
bobsUsedCar.mileage <- 0
printCars()
main()
该程序具有以下类型
type Car =
class
val used: bool
val owner: string
val mutable mileage: int
new : owner:string * mileage:int -> Car
new : owner:string -> Car
end
请注意,我们的val
字段包含在类定义的公共接口中。
该程序输出以下内容
Cars created c.used: false, c.owner: Steve, c.mileage: 0 c.used: true, c.owner: Bob, c.mileage: 83000 Steve drives all over the state c.used: false, c.owner: Steve, c.mileage: 780 c.used: true, c.owner: Bob, c.mileage: 83000 Bob commits odometer fraud c.used: false, c.owner: Steve, c.mileage: 780 c.used: true, c.owner: Bob, c.mileage: 0
您可能已经猜到,两种语法之间的主要区别与构造函数有关:显式语法强制程序员提供显式构造函数,而隐式语法将主构造函数与类体融合在一起。但是,还有一些其他的细微差别
- 显式语法不允许程序员声明
let
和do
绑定。 - 即使
val
字段可以在隐式语法中使用,它们也必须具有属性[<DefaultValue>]
并且是可变的。在这种情况下,使用let
绑定更方便。当需要公开时,可以添加公共member
访问器。 - 在隐式语法中,主构造函数参数在整个类体中可见。通过使用此功能,无需编写将构造函数参数复制到实例成员的代码。
- 虽然两种语法都支持多个构造函数,但当使用隐式语法声明其他构造函数时,它们必须调用主构造函数。在显式语法中,所有构造函数都使用 new() 声明,并且没有需要从其他构造函数中引用的主构造函数。
具有主(隐式)构造函数的类 | 仅具有显式构造函数的类 |
---|---|
// The class body acts as a constructor
type Car1(make : string, model : string) = class
// x.Make and x.Model are property getters
// (explained later in this chapter)
// Notice how they can access the
// constructor parameters directly
member x.Make = make
member x.Model = model
// This is an extra constructor.
// It calls the primary constructor
new () = Car1("default make", "default model")
end
|
type Car2 = class
// In this case, we need to declare
// all fields and their types explicitly
val private make : string
val private model : string
// Notice how field access differs
// from parameter access
member x.Make = x.make
member x.Model = x.model
// Two constructors
new (make : string, model : string) = {
make = make
model = model
}
new () = {
make = "default make"
model = "default model"
}
end
|
通常,程序员可以使用隐式或显式语法来定义类。但是,在实践中隐式语法使用得更多,因为它往往会导致更短、更易读的类定义。
F# 的#light
语法允许程序员省略类定义中的class
和end
关键字,此功能通常称为类推断或类型种类推断。例如,以下类定义之间没有区别
类推断 | 显式类 |
---|---|
type Product(make : string, model : string) =
member x.Make = make
member x.Model = model
|
type Car(make : string, model : string) = class
member x.Make = make
member x.Model = model
end
|
这两个类都编译成相同的字节码,但是使用类推断的代码允许我们省略一些不必要的关键字。
类推断和显式类样式都被认为是可以接受的。至少,在编写 F# 库时,不要使用类推断定义一半的类,而使用显式类样式定义另一半的类——选择一种样式,并在整个项目中所有类中一致地使用它。
您可以向对象添加两种类型的成员
- 实例成员,只能从使用
new
关键字创建的对象实例中调用。 - 静态成员,不与任何对象实例关联。
以下类包含一个静态方法和一个实例方法
type SomeClass(prop : int) = class
member x.Prop = prop
static member SomeStaticMethod = "This is a static method"
end
我们使用className.methodName
的形式调用静态方法。我们通过创建类的实例并使用classInstance.methodName
调用方法来调用实例方法。以下是在fsi中的演示
> SomeClass.SomeStaticMethod;; (* invoking static method *)
val it : string = "This is a static method"
> SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)
SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)
^^^^^^^^^^^^^^^
stdin(78,1): error FS0191: property 'Prop' is not static.
> let instance = new SomeClass(5);;
val instance : SomeClass
> instance.Prop;; (* now we have an instance, we can call our instance method *)
val it : int = 5
> instance.SomeStaticMethod;; (* can't invoke static method from instance *)
instance.SomeStaticMethod;; (* can't invoke static method from instance *)
^^^^^^^^^^^^^^^^^^^^^^^^^^
stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.
当然,我们也可以从传递到静态方法中的对象调用实例方法,例如,假设我们在上面定义的对象中添加了一个Copy
方法
type SomeClass(prop : int) = class
member x.Prop = prop
static member SomeStaticMethod = "This is a static method"
static member Copy (source : SomeClass) = new SomeClass(source.Prop)
end
我们可以在fsi中试验此方法
> let instance = new SomeClass(10);;
val instance : SomeClass
> let shallowCopy = instance;; (* copies pointer to another symbol *)
val shallowCopy : SomeClass
> let deepCopy = SomeClass.Copy instance;; (* copies values into a new object *)
val deepCopy : SomeClass
> open System;;
> Object.ReferenceEquals(instance, shallowCopy);;
val it : bool = true
> Object.ReferenceEquals(instance, deepCopy);;
val it : bool = false
Object.ReferenceEquals
是System.Object
类上的一个静态方法,它确定两个对象实例是否为同一个对象。如上所示,我们的Copy
方法接受一个SomeClass
的实例并访问其Prop
属性。
何时应该使用静态方法而不是实例方法?
当.NET框架的设计人员设计System.String
类时,他们必须决定Length
方法应该放在哪里。他们可以选择将属性设为实例方法(s.Length
)或设为静态方法(String.GetLength(s)
)。.NET设计人员选择将Length
设为实例方法,因为它是一个字符串的内在属性。
另一方面,String
类还有一些静态方法,包括String.Concat
,它接受一个字符串列表并将它们连接在一起。连接字符串与实例无关,它不依赖于任何特定字符串的实例成员。
以下一般原则适用于所有面向对象语言
- 实例成员应该用于访问对象的内在属性,例如
stringInstance.Length
。 - 当实例方法依赖于特定对象实例的状态时,应该使用实例方法,例如
stringInstance.Contains
。 - 当预计程序员希望在派生类中重写该方法时,应该使用实例方法。
- 静态方法不应依赖于对象的特定实例,例如
Int32.TryParse
。 - 只要输入保持不变,静态方法应该返回相同的值。
- 常量,即对于任何类实例都不变的值,应该声明为静态成员,例如
System.Boolean.TrueString
。
获取器和设置器是特殊的函数,允许程序员使用方便的语法读取和写入成员。获取器和设置器使用以下格式编写
member alias.PropertyName
with get() = some-value
and set(value) = some-assignment
这是一个使用fsi的简单示例
> type IntWrapper() = class
let mutable num = 0
member x.Num
with get() = num
and set(value) = num <- value
end;;
type IntWrapper =
class
new : unit -> IntWrapper
member Num : int
member Num : int with set
end
> let wrapper = new IntWrapper();;
val wrapper : IntWrapper
> wrapper.Num;;
val it : int = 0
> wrapper.Num <- 20;;
val it : unit = ()
> wrapper.Num;;
val it : int = 20
获取器和设置器用于将私有成员暴露给外部世界。例如,我们的Num
属性允许用户读取/写入内部num
变量。由于获取器和设置器是美化的函数,因此我们可以在写入值到内部变量之前使用它们来清理输入。例如,我们可以通过修改我们的类如下,修改我们的IntWrapper
类来将我们的值限制在0到10之间
type IntWrapper() = class
let mutable num = 0
member x.Num
with get() = num
and set(value) =
if value > 10 || value < 0 then
raise (new Exception("Values must be between 0 and 10"))
else
num <- value
end
我们可以在fsi中使用此类
> let wrapper = new IntWrapper();;
val wrapper : IntWrapper
> wrapper.Num <- 5;;
val it : unit = ()
> wrapper.Num;;
val it : int = 5
> wrapper.Num <- 20;;
System.Exception: Values must be between 0 and 10
at FSI_0072.IntWrapper.set_Num(Int32 value)
at <StartupCode$FSI_0076>.$FSI_0076._main()
stopped due to error
向记录和联合类型添加成员也同样容易。
记录示例
> type Line =
{ X1 : float; Y1 : float;
X2 : float; Y2 : float }
with
member x.Length =
let sqr x = x * x
sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2))
member x.ShiftH amount =
{ x with X1 = x.X1 + amount; X2 = x.X2 + amount }
member x.ShiftV amount =
{ x with Y1 = x.Y1 + amount; Y2 = x.Y2 + amount };;
type Line =
{X1: float;
Y1: float;
X2: float;
Y2: float;}
with
member ShiftH : amount:float -> Line
member ShiftV : amount:float -> Line
member Length : float
end
> let line = { X1 = 1.0; Y1 = 2.0; X2 = 5.0; Y2 = 4.5 };;
val line : Line
> line.Length;;
val it : float = 4.716990566
> line.ShiftH 10.0;;
val it : Line = {X1 = 11.0;
Y1 = 2.0;
X2 = 15.0;
Y2 = 4.5;}
> line.ShiftV -5.0;;
val it : Line = {X1 = 1.0;
Y1 = -3.0;
X2 = 5.0;
Y2 = -0.5;}
联合示例
> type shape =
| Circle of float
| Rectangle of float * float
| Triangle of float * float
with
member x.Area =
match x with
| Circle(r) -> Math.PI * r * r
| Rectangle(b, h) -> b * h
| Triangle(b, h) -> b * h / 2.0
member x.Scale value =
match x with
| Circle(r) -> Circle(r + value)
| Rectangle(b, h) -> Rectangle(b + value, h + value)
| Triangle(b, h) -> Triangle(b + value, h + value);;
type shape =
| Circle of float
| Rectangle of float * float
| Triangle of float * float
with
member Scale : value:float -> shape
member Area : float
end
> let mycircle = Circle(5.0);;
val mycircle : shape
> mycircle.Area;;
val it : float = 78.53981634
> mycircle.Scale(7.0);;
val it : shape = Circle 12.0
可以创建接受泛型类型的类
type 'a GenericWrapper(initialVal : 'a) = class
let mutable internalVal = initialVal
member x.Value
with get() = internalVal
and set(value) = internalVal <- value
end
我们可以在FSI中使用此类,如下所示
> let intWrapper = new GenericWrapper<_>(5);;
val intWrapper : int GenericWrapper
> intWrapper.Value;;
val it : int = 5
> intWrapper.Value <- 20;;
val it : unit = ()
> intWrapper.Value;;
val it : int = 20
> intWrapper.Value <- 2.0;; (* throws an exception *)
intWrapper.Value <- 2.0;; (* throws an exception *)
--------------------^^^^
stdin(156,21): error FS0001: This expression has type
float
but is here used with type
int.
> let boolWrapper = new GenericWrapper<_>(true);;
val boolWrapper : bool GenericWrapper
> boolWrapper.Value;;
val it : bool = true
泛型类帮助程序员将类泛化以对多种不同类型进行操作。它们的使用方式与已经在F#中看到的其他所有泛型类型(如列表、集合、映射和联合类型)基本相同。
虽然我们不能像对列表和联合类型那样完全以相同的方式根据对象的结构来匹配对象,但F#允许程序员使用以下语法根据类型进行匹配
match arg with
| :? type1 -> expr
| :? type2 -> expr
这是一个使用类型测试的示例
type Cat() = class
member x.Meow() = printfn "Meow"
end
type Person(name : string) = class
member x.Name = name
member x.SayHello() = printfn "Hi, I'm %s" x.Name
end
type Monkey() = class
member x.SwingFromTrees() = printfn "swinging from trees"
end
let handlesAnything (o : obj) =
match o with
| null -> printfn "<null>"
| :? Cat as cat -> cat.Meow()
| :? Person as person -> person.SayHello()
| _ -> printfn "I don't recognize type '%s'" (o.GetType().Name)
let main() =
let cat = new Cat()
let bob = new Person("Bob")
let bill = new Person("Bill")
let phrase = "Hello world!"
let monkey = new Monkey()
handlesAnything cat
handlesAnything bob
handlesAnything bill
handlesAnything phrase
handlesAnything monkey
handlesAnything null
main()
此程序输出
Meow Hi, I'm Bob Hi, I'm Bill I don't recognize type 'String' I don't recognize type 'Monkey' <null>