第五章 面向对象编程(二)

来源:互联网 发布:法律咨询 知乎 编辑:程序博客网 时间:2024/05/28 15:07

第五章 面向对象编程(二)


定义类

 

我们已经看到过一部分使用.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 的行为由于被覆盖而改变。

0 0