第五章 面向对象编程(二)
定义类
我们已经看到过一部分使用.NET BCL 库函数中类的示例了,下面,将学习如何定义我们自己的类。在面向对象编程中,类应该创建一些概念模型,用于我们将创建的程序或库函数中。例如,字符串类以字符的集合为模型,进程类以操作系统进程为模型。
类是类型,因此,类定义使用关键字 type,加类名,加类的构造函数的参数,放在括号中再加等号,加类的成员定义。类的最基本的成员称为方法(method),这是一个函数,能够访问类的参数。
下面的示例定义一个类,表示用户。用户类的构造函数有两个参数:用户名和用户密码的哈希值;有两个成员方法:Authenticate,用于检查用户密码是否正确,和LogonMessage,用于获取指定用户的登录消息:
open Strangelights.Samples.Helpers
// a class that represents a user
// it‘s constructor takes two parameters,the user‘s
// name and a hash of their password
type User(name, passwordHash) =
//hashs the users password and checks it against
//the known hash
memberx.Authenticate(password) =
lethashResult = hash (password, "sha1")
passwordHash= hashResult
//gets the users logon message
memberx.LogonMessage() =
Printf.sprintf"Hello, %s" name
// create a new instance of our user class
let user = User("Robert","AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")
let main() =
//authenticate user and print appropriate message
ifuser.Authenticate("badpass") then
printfn"%s" (user.LogonMessage())
else
printfn"Logon failed"
do main()
示例的后半部分演示了如何使用类,其行为就如我们已经看过的来自.NET BCL 中的其他类一样。我们可以用关键字 new 创建 User 的实例,然后,调用它的成员方法。
定义只在类的内部使用的值,通常很有用。比如,可能需要一个预先计算好的值,在几个成员方法之间共享;或者也可能从外部数据源读取一些对象的数据。有些对象可能有只在对象内部的 let ,但是,需要在对象的所有成员之间共享,要做到这一点,就要把这个 let 绑定放在类定义的开头,即等号之后,第一个成员定义之前。这些 let 构成一个隐式构造,当这个对象构造时执行;如果 let 有任何的副作用,那么,在对象构造时也会发生。如果需要调用空类型的函数,比如,记录对象的构造,必须在函数调用的前面加关键字 do。
下面的示例演示了私有 let 绑定,拿原来的 User 类,并做稍许修改。现在,类的构造函数用firstName 和 lastName,在 let 绑定中生成用户的全名(fullName)。要想看到调用有副作用的函数时会发生什么,可以把用户的命名输出到控制台:
open Strangelights.Samples.Helpers
// a class that represents a user
// it‘s constructor takes three parameters,the user‘s
// first name, last name and a hash oftheir password
type User(firstName, lastName,passwordHash) =
//calculate the user‘s full name and store of later use
letfullName = Printf.sprintf "%s %s" firstName lastName
//print users fullname as object is being constructed
doprintfn "User: %s" fullName
//hashs the users password and checks it against
//the known hash
memberx.Authenticate(password) =
lethashResult = hash (password, "sha1")
passwordHash= hashResult
//retrieves the users full name
memberx.GetFullname() = fullName
注意成员还能访问类的 let 绑定,成员 GetFullName 返回已经计算好的 fullName 值。
通常需要能够在类的内部改变值,比如,可能需要在 User 类中提供 ChangePassword 方法重置用户的密码。F# 提供了两种方法。在处理不可变对象,就复制对象的参数,改变适当的值。这种方法通常是考虑为了更好地适应函数风格编程,但是,如果对象有很多的参数,或者创建参数耗费巨大,可能就不方便了。例如,可能是需要大量计算,或者是需要大量输入输出才能构造。下面的示例演示了这种方法,注意,在ChangePassword 方法中如何对 password 参数调用 hash 函数,连同用户名一起,传递给 User 对象的构造函数:
open Strangelights.Samples.Helpers
// a class that represents a user
// it‘s constructor takes two parameters,the user‘s
// name and a hash of their password
type User(name, passwordHash) =
//hashs the users password and checks it against
//the known hash
memberx.Authenticate(password) =
lethashResult = hash (password, "sha1")
passwordHash= hashResult
// gets the users logon message
member x.LogonMessage() =
Printf.sprintf"Hello, %s" name
// creates a copy of the user with thepassword changed
member x.ChangePassword(password) =
newUser(name, hash password)
对于处理不可变对象的另一种方法,是你想改变的值可变,通过把它绑定到可变的 let 绑定,在下面的示例中可以看到,把类的参数passwordHash 绑定到同名的可变绑定上:
open Strangelights.Samples.Helpers
// a class that represents a user
// it‘s constructor takes two parameters,the user‘s
// name and a hash of their password
type User(name, passwordHash) =
//store the password hash in a mutable let
//binding, so it can be changed later
letmutable passwordHash = passwordHash
//hashs the users password and checks it against
//the known hash
memberx.Authenticate(password) =
lethashResult = hash (password, "sha1")
passwordHash = hashResult
//gets the users logon message
memberx.LogonMessage() =
Printf.sprintf"Hello, %s" name
//changes the users password
memberx.ChangePassword(password) =
passwordHash<- hash password
即,你可以自由修改passwordHash 的 let 绑定,如同在ChangePassword 方法中做的一样。
可选参数(Optional Parameters)
类的成员方法(其类型的成员方法除外)和类构造函数的参数是可选的,它可以用来设置默认的输入值。这样,类的用户不必要指定所有的参数,可以使客户端代码看起来更整洁而不凌乱。
标记参数为可选的方法是在它的前面加问号;可以有多个可选参数,但是,可选参数必须总是在参数列表的最后;如果有一个成员方法,其参数不止一个,且可选参数也不止一个,那么,参数必须使用元组风格,即,把可选参数用括号括起来,用逗号分隔。可选参数可以有(也可以没有)类型注释;类型注释放在参数名的后面,用分号隔开;可选参数的类型总是 option<‘a> 类型,因此,不必要放在类型注释中。
下面是一个可选参数的示例,定义了一个类 AClass,其构造函数有一个可选的整型参数;它有一个成员方法 PrintState,有两参数(第二个参数是可选的)。正如我们所想的一样,用针对option<‘a> 类型的模式匹配,来测试可选参数是否作为参数传递了:
type AClass(?someState:int) =
letstate =
matchsomeState with
|Some x -> string x
|None -> "<no input>"
memberx.PrintState (prefix, ?postfix) =
matchpostfix with
|Some x -> printfn "%s %s %s" prefix state x
|None -> printfn "%s %s" prefix state
let aClass = new AClass()
let aClass‘ = new AClass(109)
aClass.PrintState("There was ")
aClass‘.PrintState("Input was:",", which is nice.")
示例的岳半部分演示的是类的客户端代码,创建类的两个实例:第一个没有传递任何参数给构造函数,第二个把值 109 传递给构造函数;接着,调用类的 PrintState 成员,前面一个调用没有可选参数,后面一个调用有可选参数。示例的吓:
There was <no input>
Input was: 109 , which is nice.
目前,用 let 绑定定义的函数还不能有可选参数,正在研究,如何在未来版本的语言中,增加函数的参数也可能是可选的。
定义接口
接口只能包含抽象方法和属性,或者使用关键字abstract 声明的方法。接口定义一个约定(contract),适用于所有实现该接口的类,提供组件给客户端使用,但隐藏了真实地实现。一个类可以只能从一个基类继承,但可实现任意多个接口。因为,任何类实现的接口,都可以看作是接口类型,接口提供了相似于多类继承(multiple-class inheritance)的行为,但避免了其实现的复杂性。
定义接口的方法是定义一个没有构造函数的类型,且所有成员都是抽象的。下面的示例定义一个接口,它声明了两个方法:Authenticate 和LogonMessage。注意,接口名的首字母为 I,这个命名约定严格遵循.NET BCL 的规则,我们在自己的代码也应该遵循这个规则,因为,它能有助于在阅读代码时,其他程序能区分类和接口:
// an interface "IUser"
type IUser =
//hashs the users password and checks it against
//the known hash
abstractAuthenticate: evidence: string -> bool
//gets the users logon message
abstractLogonMessage: unit -> string
let logon (user: IUser) =
//authenticate user and print appropriate message
ifuser.Authenticate("badpass") then
printfn"%s" (user.LogonMessage())
else
printfn"Logon failed"
示例的后半部分体现了接口的优势,使用接口定义的函数,可以不需要知道实现的细节。定义的函数 logon 使用 IUser 参数,执行登录;然后,这个函数处理任何 IUser 的实现。这在许多情况下是非常有用的,比如,可以写出一组客户端代码,重用这个接口,而有不同的实现。
接口实现
实现接口,使用关键字interface,加接口名,加关键字with,加实现接口成员的代码;成员定义的前面关键字加member,其他方面与方法或属性的定义相同。实现接口,可以通过类,也可以用结构。接下来的部分就要讨论如何创建类;本章后面“结构”一节细讨论有关结构的内容。
下面的示例演示如何创建、实现和使用接口。这个接口与前面一节中实现的接口 IUser 相同,这里,在类 User 中实现这个接口:
open Strangelights.Samples.Helpers
// an interface "IUser"
type IUser =
//hashs the users password and checks it against
//the known hash
abstractAuthenticate: evidence: string -> bool
//gets the users logon message
abstractLogonMessage: unit -> string
// a class that represents a user
// it‘s constructor takes two parameters,the user‘s
// name and a hash of their password
type User(name, passwordHash) =
interfaceIUser with
//Authenticate implementation
memberx.Authenticate(password) =
let hashResult = hash (password, "sha1")
passwordHash = hashResult
//LogonMessage implementation
memberx.LogonMessage() =
Printf.sprintf "Hello, %s" name
// create a new instance of the user
let user = User("Robert","AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")
// cast to the IUser interface
let iuser = user :> IUser
// get the logon message
let logonMessage = iuser.LogonMessage()
let logon (iuser: IUser) =
//authenticate user and print appropriate message
ifiuser.Authenticate("badpass") then
printfn"%s" logonMessage
else
printfn"Logon failed"
do logon user
注意在示例的中间我们首次看到 casting(强制类型转换),在本章的最后“类型转换”会有详细讨论。但是,这里简要说明一下:标识符 user,通过向下转换运算符(:?>)转换成接口 IUser:
// create a new instance of the user
let user = User("Robert","AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")
// cast to the IUser interface
let iuser = user :?> IUser
这是必须的,因为在F# 中接口是显式实现的。在能够使用方法 LogonMessage 之前,必须有一个标识符,它不仅是实现了 IUser 的类,而且它的类型也要是 IUser。向后,到示例的结束,有不同的解决方案。函数 logon 的参数类型为 IUser:
let logon (iuser: IUser) =
当用实现了 IUser 的类调用 logon 时,这个类被隐式向下转换成 IUser。
可以在类定义中增加接口成员,这样就能够在实现接口的类中,直接使用类的成员,而不必强制类的用户,以某种方法把对象转换成接口。修改一下这个示例,简单地增加两个方面作为类的成员:Authenticate 和 LogonMessage。现在就不再需要强制转换标识符 user 了(在本章的后面“类和方法”一节中,将学习如何给方法添加成员):
open Strangelights.Samples.Helpers
// a class that represents a user
// it‘s constructor takes two parameters,the user‘s
// name and a hash of their password
type User(name, passwordHash) =
interfaceIUser with
//Authenticate implementation
memberx.Authenticate(password) =
let hashResult = hash (password, "sha1")
passwordHash = hashResult
//LogonMessage implementation
memberx.LogonMessage() =
Printf.sprintf "Hello, %s" name
//Expose Authenticate implementation
memberx.Authenticate(password) = x.Authenticate(password)
//Expose LogonMessage implementation
memberx.LogonMessage() = x.LogonMessage()
类和继承(Inheritance)
我们在“对象表达式”和“实现接口”两节已经讨论了一点继承。继承能够扩展已经定义的类,既可以添加新的功能,也可以调整或替换原有的功能。像大多数现代面向对象语言一样,F# 只允许单继承(从一个基类),实现多接口除外(参见前面“定义接口”和“实现接口”)。
这一节将讨论基本的继承,从一个基类继承,增加新的功能;下一节“方法和继承”将讨论如何实现方法,以便充分利用继承。
指定继承,使用关键字 inherit,必须紧跟在等号后面,加类的构造函数关键字 class 的后面;在关键字inherit [ 原文可能有误,inheritance ]的后面,加想要继承的类名,加想要传递类的构造函数的参数。让我们撇开一些细节,看一个简单的两个 F# 类型之间继承的例子。下面示例中有一个 F# 类 sub,从一个基类Base 派生;类Base 有一个方法GetState,类 Sub 也有一个方法GetOtherState。这个例子演示了派生类Sub 如何使用两个方法,因为GetState 是从基类继承而来。
type Base() =
memberx.GetState() = 0
type Sub() =
inheritBase()
memberx.GetOtherState() = 0
let myObject = new Sub()
printfn
"myObject.state= %i, myObject.otherState = %i"
(myObject.GetState())
(myObject.GetOtherState())
示例的运行结果如下:
myObject.state = 0, myObject.otherState = 0
方法和继承
前面一节我们看到了基本的类之间的继承。现在,看一下如何充分利用面向对象编程,覆盖方法,改变其行为。派生类除了覆盖继承自基类的方法外,还可以定义新的方法。
定义方法使用下面四个关键字中的一个:member、override、abstract、default。我们已经看过用关键字member 和 abstract 来定义方法;关键字member 定义一个简单的有实现的但不能被覆盖的方法;而关键字abstract 定义的方法没有实现,必须在派生类中被覆盖;关键字override 定义的方法覆盖被继承的、在基类中已经实现的方法;最后,关键字 default 的意思与 override 相似,但是,它只用于覆盖抽象方法。
下面的例子演示了这四种方法:
// a base class
type Base() =
//some internal state for the class
letmutable state = 0
//an ordinary member method
memberx.JiggleState y = state <- y
//an abstract method
abstractWiggleState: int -> unit
//a default implementation for the abstract method
defaultx.WiggleState y = state <- y + state
memberx.GetState() = state
// a sub class
type Sub() =
inheritBase()
//override the abstract method
defaultx.WiggleState y = x.JiggleState (x.GetState() &&& y)
// create instances of both methods
let myBase = new Base()
let mySub = new Sub()
// a small test for our classes
let testBehavior (c : #Base) =
c.JiggleState1
printfn"%i" (c.GetState())
c.WiggleState3
printfn"%i" (c.GetState())
// run the tests
let main() =
printfn"base class: "
testBehaviormyBase
printfn"sub class: "
testBehaviormySub
do main()
运行结果如下:
base class:
1
4
sub class:
1
1
首先,在 Base 类中,实现方法 JiggleState,这个方法不能被覆盖,因此,所有的派生类都继承了这个实现;然后,定义抽象方法 WiggleState,它可被派生类覆盖(实际上,必须覆盖)。要新定义一个可以被覆盖的方法,需要用到关键字 abstract 和 default 的组合。就是说,在基类中使用abstract,而在派生类中使用 default。然而,它们在同一个类中通常在一起使用,就如前面的例子一样。这要求程序员必须为需要被覆盖的方法显式指定类型。虽然 F# 通常并不要求程序员显式指定类型,工作留给编译器,但是,编译器却没有方法推断出这些类型,因此,就必须显式指定。
就如上面的结果所示,当调用 JiggleState 时,在基类和派生类中保持相同的行为;相比之下,WiggleState 的行为由于被覆盖而改变。
原文:http://blog.csdn.net/hadstj/article/details/22915589