类 (F#)
类是表示可具有属性、方法和事件的对象的类型。
// Class definition:
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] =
[ class ]
[ inherit base-type-name(base-constructor-args) ]
[ let-bindings ]
[ do-bindings ]
member-list
...
[ end ]
// Mutually recursive class definitions:
type [access-modifier] type-name1 ...
and [access-modifier] type-name2 ...
...
备注
类表示 .NET 对象类型的基本描述;类是 F# 中支持面向对象编程的主要类型概念。
在前面的语法中,type-name 是任何有效的标识符。 type-params 描述可选的泛型类型参数。 它由类型参数名称和用尖括号(< 和 >)括起的约束组成。 有关更多信息,请参见泛型 (F#)和约束 (F#)。 parameter-list 描述构造函数参数。 第一次访问修饰符与类型相关;第二次与主的构造函数相关。 在两种情况下,默认值为 public。
可使用 inherit 关键字指定类的基类。 必须为基类构造函数提供参数(用括号引起)。
可使用 let 绑定来声明类的本地字段或函数值,并且必须遵循 let 绑定的一般规则。 do-bindings 部分包含要在构造对象时执行的代码。
member-list 包含附加构造函数、实例和静态方法声明、接口声明、抽象绑定以及属性和事件声明。 这些内容在成员 (F#) 中介绍。
与可选的 as 关键字一起使用的 identifier 为实例变量或自我标识符(可在类型定义中用来引用类型的实例)指定名称。 有关更多信息,请参见本主题后面的“自我标识符”一节。
用于标记定义的开头和结尾的关键字 class 和 end 是可选的。
正如互相递归函数一样,互相递归类型(互相引用的类型)是使用 and 关键字联接在一起的。 有关示例,请参见“互相递归类型”一节。
构造函数
构造函数是用来创建类类型的实例的代码。 类构造函数在 F# 中的工作方式与在其他 .NET 语言中略有不同。 在 F# 类中,始终有一个主构造函数,其参数在类型名称后的 parameter-list 中描述,并且函数体包含类声明开始处的 let(和 let rec)绑定以及随后的 do 绑定。 主构造函数的参数在整个类声明的范围内。
您可以通过使用 new 关键字添加成员来添加其他构造函数,如下所示:
new(argument-list) = constructor-body
新构造函数的主体必须调用在类声明的顶部指定的主构造函数。
以下示例阐释了此概念。 在下面的代码中,MyClass 包含两个构造函数:一个采用两个参数的主构造函数和一个不采用任何参数的构造函数。
type MyClass1(x: int, y: int) =
do printfn "%d %d" x y
new() = MyClass1(0, 0)
let 绑定和 do 绑定
类定义中的 let 和 do 绑定构成了主类构造函数的主体,因此只要创建类实例,就会运行这两个绑定。 如果 let 绑定是函数,则将它编译为一个成员。 如果 let 绑定是一个不会在任何函数或成员中使用的值,则将它编译为构造函数的局部变量。 否则,将它编译成类的一个字段。 后面的 do 表达式将被编译为主构造函数,并为每个实例执行初始化代码。 由于任何附加构造函数始终会调用主构造函数,因此总是执行 let 绑定和 do 绑定,而不管调用的是哪一个构造函数。
可以通过类的方法和属性来访问 let 绑定所创建的字段;但无法通过静态方法来访问这些字段,即使静态方法采用实例变量作为参数也是如此。 无法使用自我标识符(如果有)访问这些字段。
自我标识符
自我标识符是一个表示当前实例的名称。 自我标识符类似于 C# 或 C++ 中的 this 关键字或 Visual Basic 中的 Me。 可以通过两种不同的方式来定义自我标识符,具体取决于您希望自我标识符在整个类定义的范围内,还是仅在单个方法的范围内。
若要为整个类定义自我标识符,请在构造函数参数列表的右括号后使用 as 关键字,并指定标识符名称。
若要仅为一个方法定义一个自我标识符,请在成员声明中的该方法名称前提供该自我标识符,并提供一个句点 (.) 作为分隔符。
下面的代码示例说明了创建自我标识符的两种方式。 在第一行中,as 关键字用于定义自我标识符。 在第五行中,标识符 this 用于定义范围限制在方法 PrintMessage 内的自我标识符。
type MyClass2(dataIn) as self =
let data = dataIn
do
self.PrintMessage()
member this.PrintMessage() =
printf "Creating MyClass2 with Data %d" data
与在其他 .NET 语言中不同,您可以根据自己的需要来命名自我标识符,而不限制使用类似 self、Me 或 this 这样的名称。
在执行 let 绑定之前,不会初始化用 as 关键字声明的自我标识符。 因此,不能在 let 绑定中使用该自我标识符。 可以在 do 绑定部分使用该自我标识符。
泛型类型参数
泛型类型参数是在尖括号(< 和 >)中指定的,其格式为一个单引号后跟一个标识符。 多个泛型类型参数之间以逗号分隔。 泛型类型参数在整个声明范围内。 下面的代码示例演示了如何指定泛型类型参数。
type MyGenericClass<'a> (x: 'a) =
do printfn "%A" x
使用类型时,将会推断类型参数。 在下面的代码中,推断的类型是一个元组序列。
let g1 = MyGenericClass( seq { for i in 1 .. 10 -> (i, i*i) } )
指定继承
inherit 子句标识直接基类(如果有)。 在 F# 中,只允许有一个直接基类。 类实现的接口不被视为基类。 接口在接口 (F#) 主题中讨论。
可以通过以下方式从派生类访问基类的方法和属性:使用语言关键字 base 作为标识符,后跟一个句点 (.) 和成员名称。
有关更多信息,请参见继承 (F#)。
成员部分
可以在此部分定义静态或实例方法、属性、接口实现、抽象成员、事件声明以及其他构造函数。 此部分不能出现 let 和 do 绑定。 由于成员不仅可以添加到类中,还可以添加到各种 F# 类型中,因此将在单独的主题成员 (F#) 中讨论。
互相递归类型
在定义按照循环方式互相引用的类型时,可使用 and 关键字将各个类型定义连接在一起。 and 关键字将替换所有定义(第一个定义除外)中的 type 关键字,如下所示。
open System.IO
type Folder(pathIn: string) =
let path = pathIn
let filenameArray : string array = Directory.GetFiles(path)
member this.FileArray = Array.map (fun elem -> new File(elem, this)) filenameArray
and File(filename: string, containingFolder: Folder) =
member this.Name = filename
member this.ContainingFolder = containingFolder
let folder1 = new Folder(".")
for file in folder1.FileArray do
printfn "%s" file.Name
输出为当前目录中所有文件的列表。
何时使用类、联合、记录和结构
由于有多种类型可供选择,因此您需要很好地了解每种类型的用途,才能为特定情形选择适当的类型。 类用在面向对象编程的上下文中。 面向对象编程是在针对 .NET Framework 编写的应用程序中使用的主要范例。 在您的 F# 代码必须与 .NET Framework 或另一个面向对象的库紧密合作的情况下,特别是在您必须从一个面向对象的类型系统(如 UI 库)进行扩展时,类可能适用。
如果您不需要与面向对象的代码进行紧密的交互操作,或您编写的代码是自包含的,因而需要防止与面向对象的代码频繁交互,则您应考虑使用记录和可区分联合。 通常,可以将一个精心构思的可区分联合与适当的模式匹配代码结合使用,以作为对象层次结构的更简单的替代方案。 有关可区分联合的更多信息,请参见可区分联合 (F#)。
记录具有比类更简单的优点,但是,当记录的简单性无法满足类型的要求时,记录将不适用。 记录基本上是值的简单聚合,其中不包含可执行自定义操作的单独构造函数、隐藏字段以及继承或接口实现。 虽然可将属性和方法等成员添加到记录中以使其行为更为复杂,但记录中存储的字段仍为值的简单聚合。 有关记录的更多信息,请参见记录 (F#)。
结构对于少量数据聚合也很有用,但与类和记录的不同之处在于,结构是 .NET 值类型。 类和记录是 .NET 引用类型。 值类型的语义和引用类型的语义的不同之处在于,值类型是通过值来传递的。 这意味着,在将值类型作为参数传递或从函数返回值类型时,将按位复制值类型。 值类型还存储在堆栈中,如果它们用作字段,则嵌入在父对象内部,而不是存储在堆中各自不同的位置。 因此,在需要频繁访问数据时,如果访问堆所产生的开销是一个需考虑的因素,则适合使用结构。 有关结构的更多信息,请参见结构 (F#)。