类 (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
描述可选的泛型类型参数。 它由类型参数名称和括在尖括号(<
和 >
)中的约束组成。 有关详细信息,请参阅泛型和约束。 parameter-list
描述构造函数参数。 第一个访问修饰符与类型有关;第二个与主构造函数有关。 在这两种情况下,默认值都为 public
。
可以使用 inherit
关键字指定类的基类。 必须为基类构造函数提供括在圆括号中的参数。
可以使用 let
绑定声明类的本地字段或函数值,并且必须遵循 let
绑定的通用规则。 do-bindings
节包含构造对象时要执行的代码。
member-list
由其他构造函数、实例和静态方法声明、接口声明、抽象绑定以及属性和事件声明组成。 这些内容已在成员中说明。
与可选的 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
等名称。
使用 as
关键字声明的自我标识符在基本构造函数之后才会被初始化。 因此,如果在基本构造函数之前或内部使用,将在运行时引发 System.InvalidOperationException: The initialization of an object or value resulted in an object or value being accessed recursively before it was fully initialized.
。 你可以在基本构造函数之后随意使用自我标识符,例如用于 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# 中,只允许一个直接基类。 类实现的接口不被视为基类。 接口主题对接口进行了说明。
通过使用语言关键字 base
作为标识符,后跟句点 (.) 和成员名称,可以从派生类访问基类的方法和属性。
有关详细信息,请参阅继承。
成员节
你可以在此节中定义静态或实例方法、属性、接口实现、抽象成员、事件声明和其他构造函数。 Let 和 do 绑定不能出现在此节中。 由于除了类之外,还可以将成员添加到各种 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 库等面向对象的类型系统进行扩展时,类可能是一个不错的选择。
如果不用与面向对象的代码紧密地互操作,或者如果正在编写自包含的代码,并因此避免与面向对象的代码频繁交互,则应考虑混合使用类、记录和可区分联合。 一个经过深思熟虑的可区分联合以及适当的模式匹配代码通常可以用作对象层次结构的更简单替代方案。 有关可区分联合的详细信息,请参阅可区分联合。
记录的优点是比类更简单,但是,当类型的需求超出其简单性所能实现的需求时,记录便不再适用。 记录基本上是简单的值聚合,没有可以执行自定义操作的单独构造函数,没有隐藏字段,也没有继承或接口实现。 尽管可以向记录添加属性和方法等成员,使其行为更加复杂,但存储在记录中的字段仍然是简单的值聚合。 有关记录的详细信息,请参阅记录。
结构对于小型数据聚合也很有用,但它们与类和记录的不同之处在于它们是 .NET 值类型。 类和记录是 .NET 引用类型。 值类型和引用类型的语义不同,值类型是按值传递的。 这意味着,当它们作为参数传递或从函数返回时,它们会被逐位复制。 它们也存储在堆栈中,或者,如果它们用作字段,则嵌入到父对象中,而不是存储在堆中各自不同的位置。 因此,当访问堆所产生的开销成为问题时,结构适用于频繁访问的数据。 有关结构的详细信息,请参阅结构。