类 是表示可以具有属性、方法和事件的对象的类型。
语法
// 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
其他构造函数、实例和静态方法声明、接口声明、抽象绑定以及属性和事件声明。 这些内容在 成员中介绍。
与 identifier
可选关键字一起使用的实例 as
变量或自标识符的名称,该名称可用于类型定义来引用该类型的实例。 有关详细信息,请参阅本主题后面的“自我标识符”部分。
用于标记定义开始和结尾的关键字class
end
是可选的。
相互递归类型(相互引用的类型)与 and
关键字联接在一起,就像相互递归函数一样。 有关示例,请参阅“相互递归类型”部分。
构造函数
构造函数是创建类类型的实例的代码。 类的构造函数在 F# 中的工作方式与其他 .NET 语言稍有不同。 在 F# 类中,始终有一个主构造函数,其参数在parameter-list
类型名称后面进行描述,其正文由类声明开头的 (和let rec
do
) 绑定和后面的绑定组成let
。 主构造函数的参数在整个类声明范围内。
可以使用关键字添加成员来添加其他构造函数 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 Bindings
let
类定义中的绑定do
构成主类构造函数的主体,因此每当创建类实例时,它们就会运行。
let
如果绑定是函数,则会将其编译为成员。
let
如果绑定是任何函数或成员中未使用的值,则会将其编译为构造函数的局部变量。 否则,它将编译为类的字段。 后面的 do
表达式编译为主构造函数,并为每个实例执行初始化代码。 由于任何其他构造函数始终调用主构造函数,因此无论调用哪个构造函数, let
绑定和 do
绑定都会始终执行。
绑定 let
创建的字段可以在整个类的方法和属性中访问;但是,即使静态方法采用实例变量作为参数,也无法从静态方法访问它们。 如果存在,则无法使用自我标识符来访问它们。
自我标识符
自我标识符是表示当前实例的名称。 自标识符类似于 this
C# 或 C++ 或 Me
Visual Basic 中的关键字。 可以通过两种不同的方式定义自我标识符,具体取决于是希望自标识符位于整个类定义的范围内,还是只针对单个方法。
若要定义整个类的自标识符,请在构造函数参数列表的右括号后面使用 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.
将在运行时引发。 可以在基本构造函数(如绑定或do
绑定中let
)后自由使用自标识符。
泛型类型参数
泛型类型参数以尖括号(<
和 >
)的形式指定,格式为单引号后跟标识符。 多个泛型类型参数用逗号分隔。 泛型类型参数在整个声明中处于范围内。 下面的代码示例演示如何指定泛型类型参数。
type MyGenericClass<'a>(x: 'a) =
do printfn "%A" x
使用类型时,将推断类型自变量。 在以下代码中,推断的类型是元组序列。
let g1 = MyGenericClass(seq { for i in 1..10 -> (i, i * i) })
指定继承
该 inherit
子句标识直接基类(如果有)。 在 F# 中,只允许一个直接基类。 类实现的接口不被视为基类。 接口主题中将讨论 接口 。
可以使用语言关键字 base
作为标识符,后跟句点(.)和成员的名称,从派生类访问基类的方法和属性。
有关详细信息,请参阅继承。
“成员”部分
可以在本节中定义静态或实例方法、属性、接口实现、抽象成员、事件声明和其他构造函数。 让我们和做绑定不能出现在本节中。 由于除了类之外,成员还可以添加到各种 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 引用类型。 值类型和引用类型的语义不同,值类型按值传递。 这意味着,当作为参数传递或从函数返回时,将按位复制它们。 它们也会存储在堆栈上,或者,如果用作字段,则嵌入父对象中,而不是存储在堆上自己的单独位置。 因此,当访问堆的开销出现问题时,结构适用于经常访问的数据。 有关结构的详细信息,请参阅 结构。