您的位置:首页 > 编程语言

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

2014-04-02 10:55 274 查看
第五章面向对象编程(一)

面向对象编程(Object-oriented programming)是第三种编程模式(paradigm)。有趋势表明,函数范式与面向对象范式有竞争,但是,我认为,把它们可以在一起很好地工作,能形成互补,这一章中我们会有演示。面向对象编程的核心是几个简单思想,有时也称为原则:封装(encapsulation)、多态(polymorphism)和继承(inheritance)

第一个原则,也可能是最重要的原则就是封装,其思想是这样的,实现和状态应该被封装,或隐藏在明确定义的边界之后,这样,更易于管理程序的结构。在 F# 中,隐藏的办法除了可以简单地定义成本地表达式或类构造之外,还可以使用模块和类型定义签名(signatures)(这两方面,本章后面都会讨论)。

第二个原则,多态,即,可以用多种方法实现抽象实体。我们已经碰到过大量抽象实体了,比如函数类型。函数类型之所以是抽象的,是因为我们可以用许多不同的方法实现一个有指定类型的函数,比如,函数的类型为int -> int,它可以实现为加上给定参数的函数,减少给定参数的函数,或者一个百万数列中的一项。还可以在已有的抽象组件之外构建抽象实体,比如,定义在.NET BCL 中的接口类型;还可能通过用户定义的接口类型,构建更复杂的抽象实体。接口类型的好处是可以按层次组织,也称为接口继承(interface inheritance)。比如,在
.NETBCL 中的集合类型就是分层次的,分布在System.Collections 和System.Collections.Generic 命名空间中。

在面向对象编程中,实现片段也可以分层次组织,这就称为实现继承(implementation inheritance),它在 F# 编程中往往并不重要,因为函数编程本身对定义和共享实现片段提供了更多的灵活性。然而,对于一些特定领域会很重要,比如,图形用户界面(graphical user interface,GUI)的编程。

面向对象编程的原则不仅重要,而且成为围绕系统名词的值组织代码,并针对这些值提供操作的成员、函数或方法的同义词,这通常很简单,就像把以这种风格写的函数,其中函数应用到值(如 String.Length s),重写成点符号(比如 s.Length)一样,这样的简化使代码更好理解。在这一章,我们会看到如何在 F# 中附加成员到类、类型上,只要需要,可以所有代码按面向对象风格重新组织。

F# 提供了丰富的面向对象编程模型,用来创建类、接口和对象,与通过 C# 和 VB.NET 创建的行为非常相似。可能更为重要的是,在 F# 中创建的类,与使用其他语言创建的类,当在一个库函数中,从这个库的使用者来看,是难以区分的。当然,面向对象编程不仅仅是定义对象那么简单,我们将会看到,如何以面向对象风格使用 F# 本地类型编程。

记录作对象(Records AsObjects)

在第三章中,我们看到用记录类型模拟类似对象的行为是可能的,这是因为记录能够有这样的字段,是函数,可以模拟对象的方法。与 F# 的类相比,这项技术既有限制,也有优势。

在记录定义中,只要给出函数的类型(或者叫签名(signature)可能更好),因此,可以很容易交换实现,而不必像在其他面向对象编程中那样定义派生类。在本章后面的“对象表达式”和“继承”中会有更多的讨论。

我们看一个简单的例子,演示如何使用记录作对象。它定义了一个类型 Shape,有两个成员:第一个是 reposition,函数类型,移动图形;第二个 draw,绘制图形。使用函数 makeShap 新建 Sharp 类型的实例;makeShape 函数实现重新定位功能,参数 initPos 保存在可变的引用(ref)单元中,调用函数 reposition 进行更新。这就是说,图形的位置被封装,只能通过成员 reposition 访问,以这种方法隐藏值在 F# 编程中很常用。

open System.Drawing
// aShape record that will act as our object
type Shape =
{ Reposition: Point -> unit;
Draw: unit -> unit }

//create a new instance of Shape
let makeShape initPosdraw =
// currPos is the internal state of the object
let currPos = ref initPos
{ Reposition =
// the Reposition member updates the internal state
(fun newPos
-> currPos := newPos);
Draw =
// draw the shape passing the current position
// to given draw function
(fun ()
-> draw !currPos); }

//"draws" a shape, prints out the shapes name and position
let draw shape (pos:Point) =
printfn "%s, with x = %iand y = %i"
shape pos.X pos.Y

//creates a new circle shape
let circle initPos =
makeShape initPos (draw "Circle")

//creates a new square shape
let square initPos =
makeShape initPos (draw "Square")

//list of shapes in their inital positions
let shapes =
[ circle (new Point (10,10));
square (new Point (30,30)) ]

//draw all the shapes
let drawShapes() =
shapes |> List.iter (fun s
-> s.Draw())

let main() =
drawShapes() // draw the shapes
// move all the shapes
shapes |> List.iter (fun s
-> s.Reposition (new Point (40,40)))
drawShapes() // draw the shapes

//start the program
do main()

运行结果如下:

Circle, with x = 10 and y = 10

Square, with x = 30 and y = 30

Circle, with x = 40 and y = 40

Square, with x = 40 and y = 40

这个例子似乎没有意义,但是,有了这项技术就能做得更多。很自然,下面的例子将在窗体上画出真正的图形。

open System
open System.Drawing
openSystem.Windows.Forms

// aShape record that will act as our object
type Shape =
{ Reposition: Point -> unit;
Draw : Graphics -> unit }

//create a new instance of Shape
let movingShape initPosdraw =
// currPos is the internal state of the object
let currPos = ref initPos
in
{ Reposition =
// the Reposition member updates the internal state
(fun newPos
-> currPos := newPos);
Draw =
// draw the shape passing the current position
// and graphics object to given draw function
(fun g
-> draw !currPos g); }

//create a new circle Shape
let movingCircleinitPos diam =
movingShape initPos (fun pos g
->
g.DrawEllipse(Pens.Blue,pos.X,pos.Y,diam,diam))

//create a new square Shape
let movingSquareinitPos size =
movingShape initPos (fun pos g
->
g.DrawRectangle(Pens.Blue,pos.X,pos.Y,size,size) )

//list of shapes in their inital positions
let shapes =
[ movingCircle (new Point (10,10)) 20;
movingSquare (new Point (30,30)) 20;
movingCircle (new Point (20,20)) 20;
movingCircle (new Point (40,40)) 20;]

//create the form to show the items
let mainForm =
let form =
new Form()
let rand =
new Random()
// add an event handler to draw the shapes
form.Paint.Add(fun e
->
shapes |> List.iter (fun s
->
s.Draw e.Graphics))
// add an event handler to move the shapes
// when the user clicks the form
form.Click.Add(fun e
->
shapes |> List.iter (fun s
->
s.Reposition(newPoint(rand.Next(form.Width),
rand.Next(form.Height)))
form.Invalidate()))
form

//Show the form and start the event loop
[<STAThread>]
doApplication.Run(mainForm)

程序产生一个图形界面,如图 5-1 所示。



图 5-1 用记录模拟对象绘制图形

再次,定义记录类型 Shape,它有两个成员 Reposition 和 Rraw;然后,定义函数 makeCircle 和 makeSquare 绘制不同的图形,并用它定义一个 Shape [ 类型 ]的记录列表;最后,定义窗体处理这些记录。这里,必须多做一些事情,因为没有使用继承,BCL的 System.Winows.Forms.Form 根本不知道有关 Sharp 对象,因此,必须对这个列表进行迭代,显式绘制每一个图形。实际上,也相当简单,只要三行代码,添加一个事件处理程序到mainForm 的 Paint
事件中。

temp.Paint.Add(

fune ->

List.iter (fun s -> s.draw e.Graphics) shapes);

这个例子演示了如何可以快速创建多功能记录,而不用担心由于继承带来的不必要的功能。

在下一节还将看到,以更加自然地方法表现操作:为 F# 类型添加成员。

有成员的F# 类型

可以为F# 的记录和联合类型添加函数。调用添加到记录或联合类型的函数可以用点符号,就像是调用非 F# 写的库函数中的类成员一样;同时,对于向其他.NET 语言提供用F# 定义的类型也是很有用的。(这些会在第十三章有更详细地讨论。)许多程序员可能更喜欢看到在实例值上做函数调用,这为对所有的F# 类型也这样做提供了一种更好的方法。

定义有成员的F# 记录或联合类型的语法与第三章中学过的语法相同,只是这里要包含成员定义,总是放在最后,在关键字with 的后面。成员本身的定义,用关键字member,加标识符,表示成员附属于该类型的参数,加点,加函数名,加函数需要的其他参数;之后是等号,加函数定义,可以是任意F# 表达式。

下面的例子定义一个记录类型Point,有两个字段 Left 和 Top,一个成员函数 Swap。函数Swap 很简单,用交换 Left 和 Top 之后的值产生一个新的点。注意如何使用参数x,放在函数名 Swap 之前,在函数定义的内部,用于访问记录的其他成员。

// A point type

type Point =

{Top: int;

Left: int }

with

// the swap member creates a new point

// with the left/top coords reveresed

member x.Swap() =

{ Top = x.Left;

Left = x.Top }

// create a new point

let myPoint =

{Top = 3;

Left = 7 }

let main() =

//print the inital point

printfn "%A" myPoint

//create a new point with the coords swapped

letnextPoint = myPoint.Swap()

//print the new point

printfn "%A" nextPoint

// start the app

do main()

示例运行结果如下:

{top = 3;

left = 7;}

{top = 7;

left = 3;}

你可能已经注意到,在函数 Swap 定义中的参数x:

member x.Swap() =

{Top = x.Left;

Left = x.Top }

这个参数表示一个对象,函数将对这个对象调用。现在,看一下在值上调用函数:

let nextPoint = myPoint.Swap()

函数调用的值作为参数传递给这个函数。可以这样认为,其逻辑是,函数需要针对调用它的值,能够访问这个值的字段和方法。有些面向对象语言使用专门的关键字,比如this 或Me,但F# 可以让你选择参数名,在关键字member 后面,给它指定一个名字,比如这里的x。

联合类型也可以有成员函数,定义的方法同记录类型。下面的例子定义了一个联合类型DrinkAmount,有一个为它添加的函数:

// a type representing the amount of aspecific drink

type DrinkAmount =

|Coffee of int

|Tea of int

|Water of int

with

// get a string representation of the value

override x.ToString() =

match x with

| Coffee x -> Printf.sprintf "Coffee: %i" x

| Tea x -> Printf.sprintf "Tea: %i" x

| Water x -> Printf.sprintf "Water: %i" x

// create a new instance of DrinkAmount

let t = Tea 2

// print out the string

printfn "%s" (t.ToString())

示例运行结果如下:

Tea: 2

注意如何使用关键字override 代替了member,它有替换、覆盖基类已有函数的效果。但并不是与F# 类型有关的函数成员的通常做法,因为,只有四种方法(ToString、Equals、GetHashCode、Finalize)可以覆盖。每一个.NET 类型都从System.Object 继承,由于这些方法与CLR 的交互问题,只建议覆盖ToString。只有四种方法可用于覆盖,是因为记录和联合类型并不承担基类或派生类的作用,因此,不能继承要覆盖的方法(System.Object 除外)。

对象表达式(Object Expressions)

对象表达式是用F# 简化面向对象编程的核心,它提供了简明的语法,继承已有类型,创建对象,可用于这样几个方面:以简洁的方式实现抽象类、接口,或调整已有的类定义。对象表达式能够在创建对象实例的同时,实现类或接口。

对象表达式的定义用大括号括起来,类或接口的名字,必须加括号,在括号中放需要传递给构造函数的任意值;接口名的后面什么也不要,尽管类名与接口名两者都可以跟类型参数,类型参数要用尖括号(<>)括起来。接下来,加关键字with,实现类、接口的方法定义,声明这些方法就如同在记录或联合类型上声明方法一样(参见前面一节)。声明每一个新方法,使用关键字 member 或 override,加实例参数,加点,加方法名,方法名必须与类或接口定义中的虚拟或抽象方法名相同,参数要用括号括起来,并用逗号分隔,就像.NET 的方法一样(除非方法只有一个参数,可以不用括号)。通常不需要类型注释,但如果基类包含一个方法的几个重载,就可能要加类型注释。在方法名的参数之后,加等号,加方法体的实现,是
F# 表达式,必须匹配方法的返回值。

open System

open System.Collections.Generic

// a comparer that will compare string inthere reversed order

let comparer =

{new IComparer<string>

with

member x.Compare(s1, s2) =

// function to reverse a string

let rev (s: String) =

new String(Array.rev (s.ToCharArray()))

// reverse 1st string

let reversed = rev s1

// compare reversed string to 2nd strings reversed

reversed.CompareTo(rev s2) }

// Eurovision winners in a random order

let winners =

[|"Sandie Shaw"; "Bucks Fizz"; "Dana International";

"Abba"; "Lordi" |]

// print the winners

printfn "%A" winners

// sort the winners

Array.Sort(winners, comparer)

// print the winners again

printfn "%A" winners

运行结果如下:

[|"Sandie Shaw"; "BucksFizz"; "Dana International"; "Abba";"Lordi"|]

[|"Abba"; "Lordi";"Dana International"; "Sandie Shaw"; "BucksFizz"|]

前面的例子实现了一个接口IComparer,它是只有一个方法Comparer 的标识符,有两个参数,返回表示参数比较结果的整数。它接收一个类型参数,这里是一个字符串,可以看到,在标识符comparer 定义的第二行;之后,是方法体的定义,这里,是用反转以后的字符串进行比较;最后,定义一个数组来使用comparer,然后,[ 用comparer ]进行排序,输出前后的结果到控制台。

在一个对象表达式中,实现多个接口,或者一个类与几个接口,是可能的;也可以为一个已有的类附加一个接口,而不改变任何类方法;然而,在一个对象表达式中,实现多个类,则不行,其根本原因是,不论是 F# 还是通用语言运行时,都不允许类有多个继承。不论哪种情况,在第一个接口、类后面的任何其它接口的实现,必须放在第一个接口、类的所有方法定义之后。接口名的前面是关键字 interface,后面是关键字 with。方法的定义与第一个接口、类相同。如果不改变类中的任何方法,就不需要用关键字 with。

open System

open System.Drawing

open System.Windows.Forms

// create a new instance of a numbercontrol

let makeNumberControl (n: int) =

{new TextBox(Tag = n, Width = 32, Height = 16, Text = n.ToString())

//implement the IComparable interface so the controls

//can be compared

interfaceIComparable with

member x.CompareTo(other) =

let otherControl = other :?> Control in

let n1 = otherControl.Tag :?> int in

n.CompareTo(n1) }

// a sorted array of the numbered controls

let numbers =

//initalize the collection

lettemp = new ResizeArray<Control>()

//initalize the random number generator

letrand = new Random()

//add the controls collection

forindex = 1 to 10 do

temp.Add(makeNumberControl(rand.Next(100)))

//sort the collection

temp.Sort()

//layout the controls correctly

letheight = ref 0

temp|> Seq.iter

(func ->

c.Top <- !height

height := c.Height + !height)

//return collection as an array

temp.ToArray()

// create a form to show the numbercontrols

let numbersForm =

lettemp = new Form() in

temp.Controls.AddRange(numbers);

temp

// show the form

[<STAThread>]

do Application.Run(numbersForm)

前面的例子演示了如何定义对象表达式,为文本框类实现接口IComparable。IComparable 使实现这个接口的对象能够进行比较,更好地用来排序。这里,IComparable 的CompareTo 方法实现了根据文本框中文本显示的数字排序控件;实现makeNumberControl 函数之后,创建一个控制数组 numbers。数组 numbers 定义有些复杂,首先初始化,以随机顺序充填全部控件,然后,对数组进行排序,最后,确保每个控件在适当的高度上显示。运行结果如图 5-2 所示。



图 5-2 排序文本框控件

对象表达式中对象的方法也是可以覆盖的,是这样实现的,使用相同的语法,但是对象名后面的关键字用 with。假设我们不用文本框显示数字,而准备自定义绘制,通过覆盖对象的 OnPaint 方法:

open System

open System.Drawing

open System.Windows.Forms

// create a new instance of a numbercontrol

let makeNumberControl (n: int) =

{new Control(Tag = n, Width = 32, Height = 16) with

//override the controls paint method to draw the number

overridex.OnPaint(e) =

let font = new Font(FontFamily.Families.[2], 12.0F)

e.Graphics.DrawString(n.ToString(),

font,

Brushes.Black,

new PointF(0.0F,0.0F))

//implement the IComparable interface so the controls

//can be compared

interfaceIComparable with

member x.CompareTo(other) =

let otherControl = other :?> Control in

let n1 = otherControl.Tag :?> int in

n.CompareTo(n1) }

运行结果如图 5-3 所示:


图 5-3 排序,自定义绘制控件

对象表达式的机制非常有效,能够快速、简洁地把非F# 库函数中的面向对象功能导入到F# 代码中,缺点是能为这些对象添加额外的属性或方法。例如,在前面的例子中,注意,必须把和控件相关的数字放在控件的tag属性,这会比正常地解决方案有更多地工作。然而,在不需要为类型添加额外的属性或方法时,该语法就非常有用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: