F#探险之旅(四):面向对象编程(上)(转)

面向对象编程概述(OOP)

面向对象编程是当今最流行的编程范式,看看TIOBE 2008年9月的编程语言排行榜就很清楚了:

在这些主流语言中,除了C,都或多或少地提供对OOP的支持,而Java和C#更是纯粹的面向对象编程语言,C还有一个子集——Objective-C。值得一提的是Delphi的强势回归。下图则是各个编程范式的占有率:

OOP编程范式是指使用“对象”及“对象”之间的交互来设计应用程序。OOP的基本概念包括类,对象,实例,方法,消息传递,继承,抽象,封装,多 态和解耦(Decoupling)等。“一切皆是对象”这句话曾盛极一时,它也衍生出了像设计模式这样的重要理念。关于面向对象编程,需要很多本书来讲 述,我这里只想说OOP只是设计方式的一种,计算机要解决的问题包罗万象,面对不同的问题,我们也要选择不同的范式,没必要所有问题都要往OO上靠。要了 解OOP的更多内容,可以看这里。

面向对象编程是F#支持的第三种主要范式。我在这里将对其中的基本概念逐一介绍。

类型转换(Casting)

在采用面向对象编程时,我们会面对一个类型的层次结构,在F#中,这个层次结构始自obj(即System.Object)。转换是指改变值的静态 类型,可能是向上转换(upcast),将类型向着基类的方向移动,也可能是向下转换(downcast),将类型向着派生类的方向移动。

向上转换是安全的,编译器总能够了解一个类型的所有基类。它的操作符是“:>”。下面的代码将string值转换为obj。

F# Code
#light
let myStr = "A string value."
let myObj = (myStr :> obj)

向上转换的一个典型应用场景是在定义集合时。如果不使用向上转换,编译器会自动将集合的类型推导为第一个元素的类型,如果其它元素的类型与之不同,就会发生编译错误。我们不得不显式地进行类型转换:

F# Code
#light
open System.Windows.Forms
let myCtrls =
  [| (new Button() :> Control);
    (new TextBox() :> Control);
    (new Label() :> Control) |]

我们知道在.NET中有值类型和引用类型之分,在对值类型进行向上转换时会自动对其装箱。

向下转换将值的静态类型转换为它的一个派生类,其操作符是“:?>”。这个就没那么安全了,因为编译器没法确定一个类的实例是否与其派生类兼 容,如果不兼容(不能转换),程序运行时会抛出一个InvalidCastException。所以使用时要小心一点。看代码吧:

F# Code
let moreCtrls =
  [| (new Button() :> Control);
    (new TextBox() :> Control) |]

let control =
  let temp = moreCtrls.[0]
  temp.Text <- "Click me"
  temp

let button =
  let temp = (control :?> Button)
  temp.DoubleClick.Add(fun e -> MessageBox.Show("Hey") |> ignore)
  temp

如果你担心转换的安全性,可以采用模式匹配来代替向下转换。

 

类型测试

与类型转换相近的概念是类型测试。比如一个窗体类,有时我们会循环它的所有控件,并对Button和TextBox采取不同的处理,这时就需要判断控件的类型。看个简单的例子:

F# Code
let anotherObj = ("Another string value." :> obj)
if (anotherObj :? string) then
  print_endline "This object is a string."
else
  print_endline "This object is not a string."

类型转换操作符:>/:?>和类型测试操作符:?,类似于C#的as和is。

对派生类使用类型标注

函数式编程(中)提到过,可以对标识符(比如参数)使用类型标注(Type Annotation),以限定它的类型。但它也有些“死板”,它不考虑类型的继承层次。如果参数类型标注为Control类型,那就不能传入Button类型的值。

F# Code
#light
open System.Windows.Forms
let showForm(form : Form) =
  form.Show()

// PrintPreviewDialog定义在BCL中,派生自Form类
let myForm = new PrintPreviewDialog()
showForm myForm

编译器会报告错误:“This expression has type PrintPreviewDialog but is here used with type Form”。当然,我们可以在传入参数时将值转换为所标注的类:

F# Code
showForm(myForm :> Form)

这样是可以工作,但毕竟不太漂亮。我们还可以这么做:

F# Code
let showFormRevised(form : #Form) =
  form.Show()

let myForm = new PrintPreviewDialog()
showFormRevised myForm

“#”让代码更优雅,可以避免很多转换。现在重写一下本文开始处的例子:

F# Code
let myCtrls =
  [| (new Button() :> Control);
    (new TextBox() :> Control);
    (new Label() :> Control) |]

let uc(c : #Control) = c :> Control
let myConciseCtrls =
  [| uc(new Button()); uc(new TextBox()); uc(new Label()) |]

使用记录类型模拟对象

记录类型有自己的字段,而字段值可以是函数,这样我们可以使用这个特性来模拟对象的方法。事实上,在函数式编程语言拥有OO结构之前人们就是这样做 的。而且,在定义记录类型时,我们仅需要给出函数的类型而无须实现,这样函数的实现就很容易进行替换(而在OOP中,往往需要定义派生类来覆盖基类的实 现)。

下面用经典的Shape例子来演示这个过程。

F# Code - 使用记录类型模拟对象
#light
open System
open System.Drawing
open System.Windows.Forms
// 定义两个方法,此时仅仅声明函数的类型
type Shape =
  { reposition : Point -> unit;
   draw : Graphics -> unit }
// 创建一个Shape实例,并为它实现了两个方法 
let makeShape initPos draw =
  let currPos = ref initPos in
  {  reposition = (fun newPos -> currPos := newPos);
    draw = (fun g -> draw !currPos g);
  }

// 创建一个circle(Shape)实例,它替换了前面draw的实现
let makeCircle initPos diam =
  makeShape initPos (fun pos g ->
    g.DrawEllipse(Pens.Blue, pos.X, pos.Y, diam, diam))

// 创建一个square(Shape)实例,它替换了前面draw的实现
let makeSquare initPos size =
  makeShape initPos (fun pos g ->
    g.DrawRectangle(Pens.Blue, pos.X, pos.Y, size, size))
let getPoint(x, y) = new Point(x, y)
let shapes =
  [ makeCircle (getPoint(10, 10)) 20; makeSquare (getPoint(30, 30)) 20; ]

let mainForm =
  let form = new Form()
  let rand = new Random()
  form.Paint.Add(fun e ->
    shapes |> List.iter(fun s -> s.draw e.Graphics)
  )
  form.Click.Add(fun e ->
    shapes |> List.iter(fun s ->
      s.reposition(new Point(rand.Next(form.Width), rand.Next(form.Height)));
      form.Invalidate())
  )
  form
[<STAThread>]
do Application.Run(mainForm)

这段代码将在窗体上画出一个圆和正方形。makeShape用于返回一个通用的Shape,makeCircle和makeSquare则用于返回 两种特定的Shape。最后在Form的Paint事件中画出这两个Shape。这样我们可以快速创建不同功能的记录类型,却不用创建额外的类型。这与我 们在C#中的惯用方式不同,下一节中将介绍一种更为自然的方式:向F#类型中添加成员。

向F#类型添加成员

F#中的类型包括记录(Record)和Union类型,两者均可以添加成员。在函数式编程(下)中,我们看到了如何定义类型,要为之添加成员需要在字段定义的末尾处进行。看下面的例子:

F#Code
#light
// 包含两个字段,一个方法
type Point =
  {  mutable top : int;
    mutable left : int }
  with
    member this.Swap() =
      let temp = this.top
      this.top <- this.left
      this.left <- temp
  end

let printAnyNewline x =
  print_any x
  print_newline()
// 定义Point类的实例
let p = { top = 30; left = 40; }
let main() =
  printAnyNewline p
  p.Swap()
  printAnyNewline p

main()

输出结果为:

Output
{ top = 30;
left = 40;}
{ top = 40;
left = 30;}

看看Point的定义,前半部分是字段们的定义,这个跟前面的一样,后半部分是一个with…end代码块,这里通过member关键字定义了方法 Swap。注意Swap前面的this参数,它表示持有该方法的类实例,即Swap通过这个实例被调用。一些语言都有特定的关键字来表示这里的this, 如C#中的this和VB.NET中的Me,但F#要求你选择该参数的名字,该名字没有限制,你完全可以用x代替这里的this。只是如果用惯了 C#,this看起来会更亲切。

Union类型也可以有成员方法。定义方式与记录类型相同。

F# Code
#light
type DrinkAmount =
  | Coffee of int
  | Tea of int
  | Water of int
  with
    override this.ToString() =
      match this with
      | Coffee x -> Printf.sprintf "Coffee: %i" x
      | Tea x -> Printf.sprintf "Tea: %i" x
      | Water x -> Printf.sprintf "Water: %i" x
  end

let t = Tea 2
print_endline (t.ToString())

输出结果为:

Output
Tea: 2

这里为类型添加了成员方法ToString,不过使用的是override而不是member,这意味着覆盖了基类(obj)的ToString实现。

小结

首先对OOP做了简单介绍,然后逐一介绍了类型转换、类型测试、对派生类使用类型标注、使用记录类型模拟对象、向F#类型添加成员方法,通过这些我们能将值和函数封装在类型内部。在下一篇中将介绍接口和继承等相关语言结构。

注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。

参考:

《Foundations of F#》 by Robert Pickering

《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino

《F# Specs》:http://research.microsoft.com/fsharp/manual/spec2.aspx

posted @ 2011-01-14 12:57  董雨  阅读(398)  评论(0编辑  收藏  举报