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

来源:互联网 发布:杰昆.菲尼克斯 知乎 编辑:程序博客网 时间:2024/05/18 00:40

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

 

 

面向对象编程(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属性,这会比正常地解决方案有更多地工作。然而,在不需要为类型添加额外的属性或方法时,该语法就非常有用。

0 0