類別 (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 包含其他建構函式、執行個體和靜態方法宣告、介面宣告、抽象繫結,以及屬性和事件宣告。 這些描述於 Members 中。

與選擇性 as 關鍵字搭配使用的 identifier,會提供執行個體變數的名稱或自我識別碼,可用於型別定義中,以參考型別的執行個體。 如需詳細資訊,請參閱這個主題稍後的「自我識別碼」一節。

標記定義開頭和結尾的關鍵字 classend 是選擇性的。

相互遞迴型別是彼此參考的型別,會與 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 繫結

類別定義中的 letdo 繫結會形成主要類別建構函式的主體,因此每當建立類別執行個體時,就會執行這些繫結。 如果 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 語言,您可以視需要命名自我識別碼;不限於 selfMethis 等名稱。

在基底建構函式之後,不會初始化以 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# 中,只允許一個直接基底類別。 類別實作的介面不被視為基底類別。 介面會在 Interfaces 主題中討論。

您可以使用語言關鍵字 base 作為識別碼,後面接著句號 (.) 和成員的名稱,從衍生類別存取基底類別的方法和屬性。

如需詳細資訊,請參閱繼承

Members 區段

您可以在本區段中定義靜態或執行個體方法、屬性、介面實作、抽象成員、事件宣告和其他建構函式。 Let 和 do 繫結無法顯示在本區段中。 由於成員除了類別之外,還可以新增至各種 F# 型別,因此會在個別的主題 Members 中討論。

相互遞迴型別

當您以循環方式定義參考彼此的型別時,您會使用 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 參考型別。 實值型別和參考型別的語意不同,實值型別會以值傳遞。 這表示當其以參數的形式傳遞或從函式傳回時,會以少量的方式複製。 也會儲存在堆疊上,或是如果當做欄位使用,會內嵌在父物件內,而不是儲存在堆積上自己的個別位置。 因此,當存取堆積的額外負荷是一個問題時,結構就適合經常存取的資料。 如需結構的詳細資訊,請參閱結構

另請參閱