赞
踩
第五章 面向对象编程(三)
访问基类
当访问类中的虚拟方法时,派生类中的方法的版本被调用,就是说,如果你想调用基类中的方法,而它已经被派生类覆盖,那么,就会自动调用派生类中的版本。这通常用于调用想被覆盖的方法的基本实现。这并不总是需要,但是,对于库函数设计是需要的;因为,如果不这样做,它会导致基类的出问题。
为了访问基类中的方法,要使用关键字 base。下面的例子实现一个类,从 System.Windows.Form 中派生。使用了隐式类构造,类型 MySquareForm 有一个参数 color:
module File3
open System.Drawing
openSystem.Windows.Forms
//define a class that inherits from 'Form'
type MySquareForm(color)=
inherit Form()
// override the OnPaint method to draw on the form
override x.OnPaint(e) =
e.Graphics.DrawRectangle(color,
10, 10,
x.Width - 35,
x.Height - 60)
base.OnPaint(e)
// override the OnResize method to respond to resizing
override x.OnResize(e) =
x.Invalidate()
base.OnResize(e)
//create a new instance of the form
let form = newMySquareForm(Pens.Blue)
//show the form
doApplication.Run(form)
[
在交互式窗口中使用下面的语句:
form.ShowDialog()
否则,会出错:
System.InvalidOperationException:在单个线程上开始另一个消息循环是无效操作。
]
在这个窗体中,我们覆盖了两个方法OnPaint 和 OnResize;在这些方法中,使用了关键字 base,它授权访问基类,用来调用这个方法的基类实现。
属性和索引器
属性是一种特别类型的方法,对于调用它的代码来说,它看起来就像一个值;索引器也实现了类似的用途,对于调用它的代码来说,使方法更有一点儿像集合。属性和索引器都有存取器(accessors),包括一个用于读的取(get accessors)和用于写的存(set accessors)。
属性的定义,与方法相同,用关键字 member,加表示对象的参数,加点,加成员名;之后,不用方法参数,而是用关键字 with,加 get 或 set;后面是参数,get 方法的参数是空类型,set 方法必须有一个唯一的参数;加等号,加表达式,构成方法体。如果需要第二个方法,就用关键字 and 把它们连接到一起。
下面的例子定义了一个类,有唯一的属性 MyProp,返回一个随机数。存属性重置随机数生成的种子:
// aclass with properties
type Properties() =
let mutable rand = new System.Random()
// a property definition
member x.MyProp
with get () = rand.Next()
and set y = rand <- new System.Random(y)
//create a new instance of our class
let prop = new Properties()
//run some tests for the class
prop.MyProp<- 12
printfn"%d" prop.MyProp
printfn"%d" prop.MyProp
printfn"%d" prop.MyProp
下面是运行的结果:
[
说是随机数,但是,可能不太像,因为每次运行结果都完全一样。
]
2137491492
726598452
334746691
也可以声明抽象属性,语法是相似的,只是关键字 member 换成了 abstract;省略表示对象的参数,就像在方法中做的一样;成员名的后面是用冒号隔开的类型名;加关键字 with,之加 get 或 set,表示继承的方法必须实现 get 或 set,用逗号隔开。对于调用它的代码来说,属性跟字段完全一样。
下面的例子是对前面代码的修改,现在使用接口IAbstractProperties。注意,派生类ConcreteProperties 必须使用关键字 with 和 and 实现 get 和 set 方法。
// aninterface with an abstract property
type IAbstractProperties=
abstract MyProp: int
with get, set
// aclass that implements our interface
typeConcreteProperties() =
let mutable rand = new System.Random()
interface IAbstractProperties with
member x.MyProp
with get() = rand.Next()
and set(y) = rand <- new System.Random(y)
[
let prop = newConcreteProperties() :> IAbstractProperties
//run some tests for the class
prop.MyProp<- 12
printfn"%d" prop.MyProp
printfn"%d" prop.MyProp
printfn"%d" prop.MyProp
]
索引器是有两个以上参数的属性,一个表示在伪集合中元素的位置,其他的表示在集合中的索引。在 C# 中,所有的索引器在底层实现上都是Item,但是,程序员们从来就不用这个名字,因为它总是隐含的;在 F# 中,程序员可以选择索引器属性的名字。如果选择的名字 Item,那么,会提供专门的语法访问这个属性。
创建索引器的语法与属性相似,但是,get 方法可有一个或多个参数,而 set 方法,有两个或多个参数;下一步是访问索引器中的元素,如果它的名字叫是Item,那么,就使用专门的类似访问数组的语法,只是把圆括号换成方括号:
// aclass with indexers
typeIndexers(vals:string[]) =
// a normal indexer
member x.Item
with get y = vals.[y]
and set y z = vals.[y] <- z
// an indexer with an unusual name
member x.MyString
with get y = vals.[y]
and set y z = vals.[y] <- z
//create a new instance of the indexer class
let index = new Indexers [|"One"; "Two"; "Three"; "Four"|]
//test the set indexers
index.[0]<- "Five";
index.Item(2)<- "Six";
index.MyString(3)<- "Seven";
//test the get indexers
printfn"%s" index.[0]
printfn"%s" (index.Item(1))
printfn"%s" (index.MyString(2))
printfn"%s" (index.MyString(3))
运行结果如下:
Five
Two
Six
Seven
注意,当索引器的名字不是 Item时,应该记住,其他 .Net 语言想使用这个类就很困难了。
覆盖非 F# 库中的方法
覆盖非 F# 库中的方法,方法定义必须是以元组的形式,就是必须用括号括起来,用逗号分隔。
下面的例子定义的类实现接口 System.Net.ICredentials,它只有一个方法GetCredential,有两个参数,就在接口实现的后面。如何在方法 GetCredentialList 中把接口当作值使用:
[
为这个方法创建一个 F# 函数
]
typeCredentialsFactory() = class
interface System.Net.ICredentials with
member x.GetCredential(uri, authType) =
new System.Net.NetworkCredential("rob", "whatever", "F#credentials")
member x.GetCredentialList uri authTypes =
let y = (x :> System.Net.ICredentials)
let getCredential s = y.GetCredential(uri, s)
List.map getCredential authTypes
end
在第十四章,将学习更多有关 F# 与 C# 签名之间关系的内容。
抽象类
在 F# 中定义约定(contract)通常可接受的方法就是接口,在大多数情况下都能很好的工作。但是,接口有一个致命的问题:任何一点对接口定义的改变,对客户端代码来说都是破坏性的。这对于将要新建的应用程序来说可能不是问题,因为所有的底层代码对我们来讲,都是可控的。事实上,它甚至是有用的,因为,编译器会自动通知你所有需要改变的代码。然而,如果我们准备发布接口,作为库函数的一部分,那么,改变接口的定义,就会引起问题。例如,假设有一个接口,抽象类定义成约定,这里重要的差别在于,抽象的基类可能有具体的方法和属性,这使得版本化一个抽象基类比接口要容易一些,因为,可以添加具体的成员而不需要做重大改变。不像接口,抽象类可以有具体成员,意思是,类只能从抽象类继承。
抽象类的语法与类完全相同,只是抽象类可以有抽象成员。为了保证添加抽象成员不犯错误,没有提供实现,需要为抽象类加上 [<AbstactClass>] 属性。如果选择使用抽象类,前面的示例User 可能看起来就像这样:
// a abstract class that represents a user
// it's constructor takes one parameters,
// the user's name
[<AbstractClass>]
type User(name) =
//the implmentation of this method should hashs the
//users password and checks it against the known hash
abstract Authenticate: evidence: string -> bool
//gets the users logon message
member x.LogonMessage() =
Printf.sprintf "Hello, %s" name
类和静态方法
静态方法类似实例方法,但是它不指向类的任何实例,因此,不能访问类的字段。
创建静态方法,用关键字 static,加关键字 member,加方法名,加参数,加等号,加方法定义。与声明实例方法基本相同,只是多了一个关键字 static,少了表示对象的参数。不要表示对象的参数,是逻辑上的需要,因为,方法不需要访问对象的属性。
静态方法提供了另一种途径来创建对象的新实例,F# 没有提供重载类构造函数的功能,因此,提供静态方法来调用类的构造函数。下面再返回到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
memberx.LogonMessage() =
Printf.sprintf"Hello, %s" name
//a static member that provides an alterative way
//of creating the object
staticmember FromDB id =
letname, ph = getUserFromDB id
newUser(name, ph)
let user = User.FromDB 1
注意,调用静态方法,用的是和方法相关联的类型名,而不是和方法相关联的类型的值。
静态方法也可以为使用的类提供运算符。声明运算符的基本语法与声明其他静态方法相同,但是方法名换成了放在括号中的运算符。运算符的参数必须是元组,通常,需要使用类型注释,来指示其类型。
下面的例子假设你想在类 MyInt 中重新实现整型,并在类中定义加法:
type MyInt(state:int) = class
memberx.State = state
staticmember ( + ) (x:MyInt, y:MyInt) : MyInt = new MyInt(x.State + y.State)
overridex.ToString() = string state
end
let x = new MyInt(1)
let y = new MyInt(1)
printfn "(x + y) = %A" (x + y)
运行结果如下:
(x + y) = 2
有明确字段和构造函数的类
本章到这里,关注还只是类的隐式语法,但是,F# 还有另一种类型的语法:显式语法。隐式语法通常更好的原因:第一,往往比显式语法更短;第二,F# 编译器能够优化这种类。隐式语法通常就是 let 绑定,和给类的构造函数的参数不会成为类中的字段;有了隐式语法,只要有可能(使对象在内存中更小),编译器可以自由地删除。然而,这些优化有时也会有问题。某些使用反射(reflection)的APIs,要求类和它们的实例有选定的字段;这样,作为程序员就需要使用显式语法才能有额外的控制能力。
在显式语法中,在类型名的后面不需要类的构造函数;相反,类型名后直接加等号,加关键字 class,加类的定义(比如,type User =class ...)。当使用显式语法时,类的结尾,用关键字end。
定义字段,使用关键字 val,加字段名和类型名,之间用冒号隔开。默认情况下,类中的字段是不可变的,就是说,一旦绑定到一个值,这个值只能重新绑定到另外的值。偶尔,重新绑定到其他值,也可能是有用的;这样,F# 就提供了关键字 mutable;当字段用关键字 mutable 定义,无论程序员什么时候选择,都能重新绑定。
为了能够创建类的一个实例,必须显式增加构造函数。这样做,需要增加一个成员,它总是命名为 new,加构造函数[ ?,好像应该是构造函数的参数],用括号括起来;加等号,加程序块(用大括号括起来),包含初始化中类中的每一个字段的表达式。重载构造函数是可能的,通过增加额外的有不同数量参数的构造函数实现;如果想用不同类型的参数重载,那么,必须提供类型注释。
下面的例子,重写了我们的第一个类,用显式语法,定义了简单的 User 类,代码多了几行;
open System.Web.Security
// give shorte name to password hashingmethod
let hash =FormsAuthentication.HashPasswordForStoringInConfigFile
// a class that represents a user
// it's constructor takes two parameters,the user's
// name and a hash of their password
type User = class
//the class' fields
valname: string
valpasswordHash: string
//the class' constructor
new(name, passwordHash) =
{name = name; passwordHash = passwordHash }
//hashs the users password and checks it against
//the known hash
memberx.Authenticate(password) =
lethashResult = hash (password, "sha1")
x.passwordHash= hashResult
//gets the users logon message
memberx.LogonMessage() =
Printf.sprintf"Hello, %s" x.name
end
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。