代码改变世界

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

2008-09-30 12:07 Anders Cui 阅读(...) 评论(...) 编辑 收藏

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 - 使用记录类型模拟对象


这段代码将在窗体上画出一个圆和正方形。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版本下可能不能通过编译。

F#系列随笔索引页面

参考:
《Foundations of F#》 by Robert Pickering
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino
F# Specs