第五章 面向对象编程(四)
强制类型转换(Casting)
我们已经遇到过强制类型转换,那是在本章“实现接口”一节有过简短的讨论。强制类型转换是显式改变值的静态类型,既可以通过隐藏信息的方法,称为向上转换(upcasting),也可以重新发现信息,称为向下转换(downcasting)。在 F# 中,向上转换和向下转换都有各自的运算符。类型层次最顶层是 obj(或 System.Object),所有的子类都在它的下面。向上转换是把类型层次向上层移动,向下转换是把类型层次向下移动。
向上转换改变值的静态类到它的一个祖先类型,这种操作是安全的,编译器总是能够告诉它是否工作,因为编译总是知道一个类型的祖先,因此,可以使用静态分析,确定向上转换是否成功完成。向上转换用冒号加大于号(:>)来表示。
下面的代码是用upcast将string转换到obj:
let myObject = ("This is astring" :> obj)
通常,当定义的集合中包含不同的类型时,就需要使用向上转换;如果不使用向上转换,编译器推断出的类型,即集合中的第一个元素的类型,如果集合中还有其他类型,会编译出错。下面的示例演示了如何创建控件数组,这是使用 Windows 窗体应用程序时十分常见的任务。注意,所有单独的控件都向上转换成它们通常的基类 Control:
open System.Windows.Forms
let myControls =
[| (new Button() :> Control);
(new TextBox() :> Control);
(new Label() :> Control) |]
对任何值类型,向上转换还有自动装箱(boxing)的效果。值类型占居内存中的程序栈(program stack),而不是托管堆(managed heap)。装箱把值压入托管堆,因此,能够作为引用被传递。下面代码演示把值将箱:
let boxedInt = (1 :> obj)
向下转换把值的静态类型改变到它的后代类型。这样,向下转换可以恢复由向上转换隐藏的信息。向下转换是危险的,因为编译器没有办法静态决定,一个类型的实例是否和它的派生类型兼容。就是说,可能出错,会在运行时引发不正确的强制转换意外(System.InvalidCastException)的问题 。由于向下转换固有的危险性,许多开发人员宁可用对 .NET类型的模式匹配来取代,如第三章中的演示。尽管如此,向下转换在某些情况下也是有用的。向下转换也有运算符,它的组成,冒号加问号加大于号(:?>)。下面的例子演示向下转换操作:
open System.Windows.Forms
let moreControls =
[|(new Button() :> Control);
(new TextBox() :> Control) |]
let control =
lettemp = moreControls.[0]
temp.Text <- "Click Me!"
temp
let button =
lettemp = (control :?> Button)
temp.DoubleClick.Add(fun e -> MessageBox.Show("Hello")|> ignore)
temp
这个例子创建了一个数组,有两个 Windows 控件对象,把它们向上转换成基类 Control;然后,绑定第一个控件到标识符 control,再把它向下转换成指定的类型 Button;再给它的双击(DoubleClick)事件添加事件处理程序,Control 类上没有事件。
类型测试
与强制类型转换概念最相近的是类型测试。可以把标识符绑定到派生类型的对象上,前面已经看过。下面的例子是把字符串绑定到obj 类型的标识符上:
let myObject = ("This is astring" :> obj)
可以把标识符绑定到派生类型的对象上,因此,通常用于测试类型是什么。为实现这个功能,F# 专门提供了类型测试运算符,由冒号、加问号(:?)组成。要想编译,运算符和运算数必须用括号括起来。如果类型测试中的标识符是特定的类型或它的派生类型,则运算符返回真;否则,返回假。下面的例子是两个类型测试,一个返回真一个返回假。
let anotherObject = ("This is a string":> obj)
if (anotherObject :? string) then
printfn "This object is a string"
else
printfn "This object is not a string"
if (anotherObject :? string[]) then
printfn "This object is a string array"
else
printfn "This object is not a string array"
首先,创建一个标识符anotherObject,是obj 类型,绑定到字符串;然后,测试anotherObject 是否是字符串,结果返回真;接着,测试是否是字符串数组,当然返回假。
子类型的类型注释
就像在第三章看到的,类型注释是为了限制标符的,通常,函数的参数,为固定类型。对于面向对象的程序员来说,在第三章引入类型注释的形式是严格的,有违常规;换句话说,没有考虑到继承的层次。这就是说,如果类型注释应用到表达式,那么,这个表达式静态地必须有一个准确的类型,派生类型不适合。下面的例子演示这个内容:
open System.Windows.Forms
let showForm (form : Form) =
form.Show()
// PrintPreviewDialog is defined in the BCLand is
// derived directly the Form class
let myForm = new PrintPreviewDialog()
showForm myForm
编译这个程序,会报错:
Prog.fs(11,10): error: FS0001: Thisexpression has type
PrintPreviewDialog
but is here used with type
Form
用有严格的类型注释的参数调用函数,是在函数被调用的地方显式地使用向上转换,改变类型与函数参数类型一致。下面的这一行代码把myForm 的类型改成与showForm 的参数类型一致。
showForm (myForm :> Form)
虽然向上转换参数,对showForm 来说是一种办法,但并不好,因为,有向上转换的客户代码显得混乱。因此,F# 提供了另一种类型注释,派生类型注释(derived type annotation),即,在类型名的前面加井号(#);它的效果是强制标识符为一种特定类型或它的派生类型。这样,我们重写前面的例子,在调用代码中去掉显式向上转换,这对于任何想使这个函数的人来说,都有极大的好处:
open System
open System.Windows.Forms
let showFormRevised (form : #Form) =
form.Show()
// ThreadExceptionDialog is define in theBCL and is
// directly derived type of the Form class
let anotherForm = newThreadExceptionDialog(new Exception())
showFormRevised anotherForm
可以用这种类型注释的方法去整理使用了大量有强制类型转换的代码。例如,在创建有共用基类型的集合时,经常需要大量的强制类型转换,这会让代码看起来有点笨重(在本章前面“强制类型转换”一节,有很好的演示)。删些重复的强制类型转换,是一个好方法,把重复代码段定义成函数:
open System.Windows.Forms
let myControls =
[|(new Button() :> Control);
(newTextBox() :> Control);
(newLabel() :> Control) |]
let uc (c : #Control) = c :> Control
let myConciseControls =
[|uc (new Button()); uc (new TextBox()); uc (new Label()) |]
这个例子定义两个控件数组,第一个myControls,对每个控件使用向上转换;第二个myConciseControls,把这个任务委托给函数。这能成为好的技巧,是有一前提,数组越大,能为你节省的代码和工作也越多。对于使用Windows 窗体编程,这样的数组通常都相当大。
定义委托(Delegates)
委托是 C# 和 VB 用来将方法当作值处理的一种机制。委托主要是作为包装了方法、提供执行方法的 .NET 对象,因此,方法可以被调用。在F# 中,很少需要定义委托,因为,F# 不需要任何包装,就可以把函数当作值看待。然而,有时委托还是有用的,比如,当需要定义委托,以更加友好的方式,提供 F# 的功能给其他的 .NET 语言使用,或者定义回调函数(callback),直接从 F# 中调用 C 代码。
定义委托,使用关键字 delegate,加关键字 of,加委托的特征类型(the type of the delegate’s signature),它符合 F# 类型注释的标准。
下面的例子定义一个委托MyDelegate,接收整型参数,返回空类型;然后,创建委托的实例,并把它应用到一个整型列表上。就像我们在第三章中所见到的,在 F# 中实现这个功能,有更加简短的方法。
type MyDelegate = delegate of int ->unit
let inst = new MyDelegate (fun i ->print_int i)
let ints = [1 ; 2 ; 3 ]
ints
|> List.iter (fun i -> inst.Invoke(i))
运行结果如下:
123
结构(Structs)
定义结构与定义类很相似,把关键字 class 换成 struct;主要的不同是,对象分配的内存空间不同。当用作局部变量或参数时,结构在堆栈(stack)中分配,而类在托管堆(managed heap)中分配。因为结构在堆栈中分配,因此,不需要垃圾回收,当函数退出时,自动回收。通常,访问结构中的字段要比类稍许快一点,而将其传递给方法,则稍许慢一点;而这些差异并不明显。因为结构在堆栈中分配,为避免堆栈举出,结构往往只有少量的字段。当用结构实现时,不能使用继承,因此,结构不能定义虚方法或抽象方法。
下面的例子定义一个表示 IP 地址的结构,注意,与定义类的唯一不同是使用了关键字 struct:
type IpAddress = struct
val first : byte
val second : byte
val third : byte
val fourth : byte
new(first, second, third, fourth) =
{first = first;
second = second;
third = third;
fourth = fourth }
overridex.ToString() =
Printf.sprintf "%O.%O.%O.%O"x.first x.second x.third x.fourth
member x.GetBytes() = x.first, x.second,x.third, x.fourth
end
问题是,什么时候应该使用类,什么时候应该使用类结构?一个很好的经验法则就是:应该避免用结构,只在真地需要的时候才用,比如,当与非托管的 C、C++ 代码互操作时(在第十三章,会有更详细的讨论)。
枚举(Enums)
枚举是由标识符的有限集组成的类,每一个标识符对应一个整数,它定义的类型,可以获得和已定义的任何一个标识符相关联的值。
定义枚举,给定标识符名,加等号,加和标识符相关联的常量;枚举成员的标识符用竖线隔开。下面的例子定义一个枚举 Scale:
type Scale =
| C = 1
| D = 2
| E = 3
| F = 4
| G = 5
| A = 6
| B = 7
定义枚举用于逻辑组合,是很常见的。要实现,选择的常量的每一个数由单独一位(bit)表示,或数字0、1、2、4、8,等等。在这里,F# 的二进制文字就大有帮助了,可以很容易看到是如何组合常量的。
[<System.Flags>]
type ChordScale =
| C = 0b0000000000000001
| D = 0b0000000000000010
| E = 0b0000000000000100
| F = 0b0000000000001000
| G = 0b0000000000010000
| A = 0b0000000000100000
| B = 0b0000000001000000
F# 中的模块 Enum 提供了处理枚举的功能(将在第七章中学习这个模块)。
第五章小结
到现在为止,我们已经学习了F# 中的三种主要编程范式,F# 为使用混合模式编程提供了灵活性。下一章,我们将学习如何组织代码,如何注释和引用。
原文:http://blog.csdn.net/hadstj/article/details/23270913