分享方式:


F# 元件設計指導

本文件是根據 F# 元件設計指導方針、v14、Microsoft Research,以及 F# Software Foundation 最初策劃及維護的版本,為 F# 程式設計提供一組元件設計指導方針。

本文件假設您已熟悉 F# 程式設計。 非常感謝 F# 社群對於本指南各種版本的貢獻和實用意見反應。

概觀

本文件會探討一些 F# 元件設計和程式碼撰寫的相關問題。 元件可能表示下列任一項:

  • 在專案內有外部取用者的 F# 專案中圖層。
  • 供 F# 程式碼跨組件界限取用的程式庫。
  • 供任何 .NET 語言跨組件界限取用的程式庫。
  • 可透過套件存放庫 (例如 NuGet) 散發的程式庫。

本文中所述的技術遵循良好 F# 程式碼的五個原則,因此會適當地利用功能和物件程式設計。

不論方法為何,元件和程式庫設計工具在嘗試製作最易於開發人員使用的 API 時,都會面臨許多實際且專業的問題。 請謹慎應用 .NET 程式庫設計指導方針,它會引導您建立一組一致的 API,以供您愉快地取用。

一般 指導方針

有一些適用於 F# 程式庫的通用指導方針,不論程式庫的目標適用對象為何皆可適用。

了解 .NET 程式庫設計指導方針

不論您執行的 F# 程式碼撰寫類型為何,具備 .NET 程式庫設計指導方針的實作知識都非常重要。 大部分其他的 F# 和 .NET 程式設計人員都會熟悉這些指導方針,並預期 .NET 程式碼符合這些指導方針。

.NET 程式庫設計指導方針提供有關命名、設計類別和介面、成員設計 (屬性、方法、事件等) 等的一般指導,而且是各種設計指導實用的第一個參考點。

將 XML 文件註解新增至您的程式碼

公用 API 上的 XML 文件可確保使用者在使用這些類型和成員時,能夠取得絕佳的 Intellisense 和 Quickinfo,並且能夠建置程式庫的文件檔案。 請參閱 XML 文件,了解可用於 xmldoc 註解內其他標記的各種 xml 標記。

/// A class for representing (x,y) coordinates
type Point =

    /// Computes the distance between this point and another
    member DistanceTo: otherPoint:Point -> float

您可以使用簡短格式的 XML 註解 (/// comment),或使用標準 XML 註解 (///<summary>comment</summary>)。

請考慮使用明確簽章檔案 (.fsi) 以獲得穩定的程式庫和元件 API

在 F# 程式庫中使用明確簽章檔案能提供簡潔的公用 API 摘要,有助於確保您知道程式庫的完整公用介面,並提供公用文件和內部實作詳細資料之間的清楚分隔。 簽章檔案會要求在實作和簽章檔案中進行變更,藉此增加變更公用 API 的衝突。 因此,通常僅在 API 已穩固且不再預期會大幅變更時,才會引用簽章檔案。

請遵循在 .NET 中使用字串的最佳做法

如果專案範圍許可,請遵循在 .NET 中使用字串的最佳做法指導。 特別是,明確陳述轉換及比較字串的文化意圖 (在適用時)。

F# 面向程式庫的指導方針

本節提供開發公用 F# 面向程式庫的建議;也就是,將要供 F# 開發人員取用的公用 API 公開的程式庫。 特別適用於 F# 的各種程式庫設計建議。 如果沒有遵循的特定建議,.NET 程式庫設計指導方針就是後援指導。

命名規範

使用 .NET 命名和大小寫慣例

下表遵循 .NET 命名和大小寫慣例。 還有一些新增項目,會將 F# 建構包含在內。 這些建議特別適用於跨越 F# 對 F# 界限的 API,適用於來自 .NET BCL 和大部分程式庫的慣用語。

建構 大小寫 部分 範例 備註
具體類型 PascalCase 名詞/形容詞 List、Double、Complex 具體類型為結構、類別、列舉、委派、記錄和等位。 雖然類型名稱在 OCaml 中傳統為小寫,但 F# 已針對型別採用 .NET 命名配置。
DLL PascalCase Fabrikam.Core.dll
等位標籤 PascalCase 名詞 Some、Add、Success 請勿在公用 API 中使用前置詞。 選擇性地在內部使用前置詞,例如「型別 Teams = TAlpha |TBeta |TDelta」。
Event PascalCase 動詞命令 ValueChanged / ValueChanging
例外狀況 PascalCase WebException 名稱應以「例外狀況」結尾。
欄位 PascalCase 名詞 CurrentName
介面類型 PascalCase 名詞/形容詞 IDisposable 名稱應以「I」開頭。
方法 PascalCase 動詞命令 ToString
Namespace PascalCase Microsoft.FSharp.Core 一般而言使用 <Organization>.<Technology>[.<Subnamespace>],但如果技術與組織無關,則會捨棄組織。
參數 camelCase 名詞 typeName、transform、range
let 值 (內部) camelCase 或 PascalCase 名詞/動詞 getValue、myTable
let 值 (外部) camelCase 或 PascalCase 名詞/動詞 List.map、Dates.Today 遵循傳統功能設計模式時,let-bound 值通常為公用的。 不過,當您可從其他 .NET 語言使用識別碼時,通常會使用 PascalCase。
屬性 PascalCase 名詞/形容詞 IsEndOfFile、BackColor 布林值屬性通常會使用 Is 和 Can,且應該為肯定,如同 IsEndOfFile,而不是 IsNotEndOfFile。

避免縮寫

.NET 指導方針不建議使用縮寫 (例如「使用 OnButtonClick 而非 OnBtnClick」)。 可容許常見的縮寫,例如「非同步」縮寫為 Async。 在進行功能性程式設計時,有時會忽略本指導方針;例如,List.iter 會使用「iterate」的縮寫。 基於這個理由,使用縮寫在 F#-to-F# 程式設計中的容許程度通常會更大,但通常仍應該在公用元件設計中加以避免。

避免大小寫名稱衝突

.NET 指導方針指出,光是大小寫並無法用來釐清名稱衝突,因為某些用戶端語言 (例如,Visual Basic) 不區分大小寫。

建議在適當時使用縮略字

XML 之類的縮略字並非縮寫,且在 .NET 程式庫中廣泛使用非大寫形式格式 (Xml)。 應該僅使用已知且廣為公認的縮略字。

使用 PascalCase 做為泛型參數名稱

請使用 PascalCase 做為公用 API 中的泛型參數名稱,包括 F# 面向程式庫。 特別是,針對任意泛型參數使用諸如 T, UT1T2 等名稱,而當特定名稱有意義時,則針對 F# 面向程式庫,請使用諸如 KeyValueArg 等名稱 (但不使用例如,TKey 的名稱)。

針對 F# 模組中的公用函式和值使用 PascalCase 或 camelCase

camelCase 用於設計為非限定使用的公用函式 (例如,invalidArg),以及用於「標準集合函式」(例如,List.map)。 在這兩種情況下,函式名稱的運作方式非常類似於語言中的關鍵字。

物件、類型和模組設計

使用命名空間或模組來包含您的類型和模組

元件中的每個 F# 檔案開頭都應為命名空間宣告或模組宣告。

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

使用模組及命名空間來組織最上層程式碼之間的差異如下:

  • 命名空間可以跨越多個檔案
  • 除非命名空間位於內部模組內,否則無法包含 F# 函式
  • 任何指定模組的程式碼都必須包含在單一檔案內
  • 最上層模組即可包含 F# 函式,而不需要內部模組

最上層命名空間或模組之間的選擇會影響程式碼的編譯形式,因此,如果最終會在 F# 程式碼之外取用 API,就會影響以其他 .NET 語言的檢視。

針對物件類型內建的作業使用方法和屬性

使用物件時,最好能確保可取用的功能會實作為該類型的方法和屬性。

type HardwareDevice() =

    member this.ID = ...

    member this.SupportedProtocols = ...

type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =

    member this.Add(key, value) = ...

    member this.ContainsKey(key) = ...

    member this.ContainsValue(value) = ...

特定成員的大量功能不必在該成員中實作,而是應該實作該功能的可取用部分。

使用類別來封裝可變動狀態

在 F# 中,只需要在另一種語言建構尚未封裝該狀態的位置完成這個動作 (例如關閉、序列運算式或非同步計算)。

type Counter() =
    // let-bound values are private in classes.
    let mutable count = 0

    member this.Next() =
        count <- count + 1
        count

使用介面類別來表示一組作業。 這是其他選項的慣用選項,例如函式的元組或函式記錄。

type Serializer =
    abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
    abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T

喜好設定為:

type Serializer<'T> = {
    Serialize: bool -> 'T -> string
    Deserialize: bool -> string -> 'T
}

介面是 .NET 中的第一級概念,您可以使用這個概念來達成 Functor 一般可提供給您的內容。 此外,這些概念可用來將存在的類型編碼到您的程式中,而函式的記錄則無法如此。

使用模組將作用於集合的函式進行分組

當您定義集合類型時,請考慮為新的集合類型提供一組標準作業 (例如 CollectionType.mapCollectionType.iter)。

module CollectionType =
    let map f c =
        ...
    let iter f c =
        ...

如果您包含這種模組,請遵循 FSharp.Core 中所找到函式的標準命名慣例。

使用模組將函式分組為通用標準函式,特別是在數學和 DSL 程式庫中

例如,Microsoft.FSharp.Core.Operators 是 FSharp.Core.dll 所提供的最上層函式自動開啟集合 (例如 abssin)。

同樣地,統計程式庫可能包含具有 erferfc 函式的模組,此模組在其中設計為要明確或自動開啟。

請考慮使用 RequireQualifiedAccess 並仔細套用 AutoOpen 屬性

[<RequireQualifiedAccess>] 屬性新增至模組表示模組可能未開啟,且對模組元素的參考需要明確的限定存取權。 例如,Microsoft.FSharp.Collections.List 模組具有這個屬性。

當模組中的函式和值所使用的名稱可能與其他模組中的名稱衝突時,這會非常有用。 要求限定的存取權可大幅提升長期維護性,以及程式庫的發展能力。

強烈建議擴充 FSharp.Core 所提供的模組 (例如 SeqListArray) 的自訂模組需有 [<RequireQualifiedAccess>] 屬性,因為這些模組在 F# 程式碼中普遍使用,而且已定義 [<RequireQualifiedAccess>];更一般而言,當這類模組隱藏或擴充具有該屬性的其他模組時,不建議定義缺少該屬性的自訂模組。

[<AutoOpen>] 屬性新增至模組表示當開啟內含的命名空間時,將會開啟模組。 [<AutoOpen>] 屬性也可以套用至組件,以指出參考組件時會自動開啟的模組。

例如,統計程式庫 MathsHeaven.Statistics 可能包含 module MathsHeaven.Statistics.Operators (其包含 erferfc 函式)。 將此課程模組標示為 [<AutoOpen>] 是合理的。 這表示 open MathsHeaven.Statistics 也會開啟此課程模組,並將 erferfc 名稱移入範圍。 [<AutoOpen>] 的另一個絕佳用法是用於包含擴充方法的模組。

過度使用 [<AutoOpen>] 會導致命名空間受干擾,且應該謹慎使用屬性。 對於特定網域中的特定程式庫,審慎使用 [<AutoOpen>] 可能會提升可用性。

請考慮在適合使用已知運算子的類別上定義運算子成員

有時候,會使用類別來建立數學建構 (例如向量) 的模型。 當模型化的網域具有已知的運算子時,將其定義為類別內建的成員會很有用。

type Vector(x: float) =

    member v.X = x

    static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)

    static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)

let v = Vector(5.0)

let u = v * 10.0

本指導會對應至這些型別的一般 .NET 指導。 不過,這在 F# 編碼中格外重要,因為這可讓這些類型與 F# 函式搭配使用,以及方法與成員條件約束 (例如 List.sumBy) 搭配使用。

請考慮使用 CompiledName 為其他 .NET 語言取用者提供 .NET 易記名稱

有時候,您可能需要以某個樣式為 F# 取用者命名某個項目 (例如使用小寫的靜態成員,使其看起來像是模組繫結函式),但在將其編譯成組件時,名稱卻樣式不同。 您可以使用 [<CompiledName>] 屬性,為取用組件的非 F# 程式碼提供不同的樣式。

type Vector(x:float, y:float) =

    member v.X = x
    member v.Y = y

    [<CompiledName("Create")>]
    static member create x y = Vector (x, y)

let v = Vector.create 5.0 3.0

您可以使用 [<CompiledName>],針對組件的非 F# 取用者使用 .NET 命名慣例。

若這麼做能提供更簡單的 API,請使用成員函式的方法多載

方法多載是一項功能強大的工具,可簡化可能需要執行類似功能的 API,但需使用不同的選項或引數。

type Logger() =

    member this.Log(message) =
        ...
    member this.Log(message, retryPolicy) =
        ...

在 F# 中,多載引數數目而非引數類型較為常見。

如果這些類型的設計可能有所改進,請隱藏記錄和等位類型的表示法

避免顯示物件的具體表示法。 例如,.NET 程式庫設計的外部公用 API 不會顯示 DateTime 值的具體表示法。 在執行階段中,Common Language Runtime 知道在整個執行中將要使用的認可實作。 不過,編譯的程式碼本身不會挑選具體表示法的相依性。

避免使用實作繼承來進行擴充性

在 F# 中,很少使用實作繼承。 此外,當新需求送達時,繼承階層通常很複雜且難以變更。 F# 中仍有繼承實作以取得相容性,且繼承實作在罕見的情況下為問題的最佳解決方案,和,但在針對多型 (例如介面實作) 設計時,應該在 F# 程式中尋找替代技術。

函式和成員簽章

傳回少數多個不相關的值時,請使用元組來傳回值

以下是在傳回型別中使用元組的絕佳範例:

val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger

對於包含許多元件的傳回型別,或元件與單一可識別實體相關的案例中,請考慮使用具名類型,而不是元組。

針對 F# API 界限的非同步程式設計,請使用 Async<T>

如果有名為 Operation 的對應同步作業傳回 T,則如果非同步作業傳回 Async<T> 則應該命名為 AsyncOperation,或如果非同步作業傳回 Task<T> 則應該命名為 OperationAsync。 針對公開 Begin/End 方法的常用 .NET 型別,請考慮使用 Async.FromBeginEnd 將擴充方法撰寫為外觀,以向這些 .NET API 提供 F# 非同步程式設計模型。

type SomeType =
    member this.Compute(x:int): int =
        ...
    member this.AsyncCompute(x:int): Async<int> =
        ...

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        ...

例外狀況

請參閱錯誤管理,以了解例外狀況、結果和選項的適當用法。

擴充成員

仔細套用 F#-to-F# 元件中的 F# 擴充成員

F# 擴充成員通常只能用於在大部分使用模式中,關閉與類型相關聯的內建作業。 其中一個常見用法是針對各種 .NET 型別提供 F# 更慣用的 API:

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        Async.FromBeginEnd(this.BeginReceive, this.EndReceive)

type System.Collections.Generic.IDictionary<'Key,'Value> with
    member this.TryGet key =
        let ok, v = this.TryGetValue key
        if ok then Some v else None

等位類型

針對樹狀結構資料,使用區分等位而非類別階層

會以遞迴方式來定義樹狀結構。 這對於繼承而言使用不便,但對於區分等位而言卻是簡潔的方式。

type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T>

使用區分等位來表示類似樹狀的資料,也可讓您受益於模式比對的鉅細靡遺。

在大小寫名稱不夠唯一的等位類型上使用 [<RequireQualifiedAccess>]

您可能會在某個網域中發現相同名稱是不同項目的最佳名稱,例如區分等位案例。 您可以使用 [<RequireQualifiedAccess>] 來釐清大小寫名稱,以避免觸發因遮蔽 open 陳述式順序的相依性而令人混淆的錯誤

如果這些類型的設計可能有所改進,請隱藏二進位相容 API 的區分等位表示法

等位類型仰賴 F# 模式比對形式來進行簡潔的程式設計模型。 如先前所述,如果這些類型的設計可能有所改進,您就應該避免顯示具體資料表示法。

例如,您可以使用私人或內部宣告,或是使用簽章檔案,來隱藏區分等位表示法。

type Union =
    private
    | CaseA of int
    | CaseB of string

如果您不小心顯示區分等位,您可能會發現在不中斷使用者程式碼的情況下,很難為程式庫設定版本。 相反地,請考慮顯示一或多個現用模式,以允許對您類型的值進行模式比對。

現用模式會提供替代方式來向 F# 取用者提供模式比對,同時避免直接公開 F# 等位類型。

內嵌函式和成員條件約束

使用內嵌函數搭配隱含成員條件約束和靜態解析的泛型型別來定義泛型數值演算法

算術成員條件約束和 F# 比較條件約束是 F# 程式設計的標準。 例如,請考慮下列程式碼:

let inline highestCommonFactor a b =
    let rec loop a b =
        if a = LanguagePrimitives.GenericZero<_> then b
        elif a < b then loop a (b - a)
        else loop (a - b) b
    loop a b

此函式的類型如下所示:

val inline highestCommonFactor : ^T -> ^T -> ^T
                when ^T : (static member Zero : ^T)
                and ^T : (static member ( - ) : ^T * ^T -> ^T)
                and ^T : equality
                and ^T : comparison

這是數學程式庫中公用 API 的適用函式。

避免使用成員條件約束來模擬類型類別和無條件類型

您可以使用 F# 成員條件約束來模擬「無條件類型」。 不過,使用此功能的成員通常不應該用於 F#-to-F# 程式庫設計。 這是因為根據不熟悉或非標準隱含條件約束的程式庫設計通常會導致使用者程式碼變得不具彈性,並繫結至一個特定的架構模式。

此外,大量使用成員條件約束很有可能會導致很長的編譯時間。

運算子定義

避免定義自訂符號運算子

在某些情況下,自訂運算子是不可或缺的,且在大型實作程式碼主體內是非常實用的標記法裝置。 對於程式庫的新使用者而言,具名函式通常較容易使用。 此外,自訂符號運算子可能難以記錄,且使用者會發現,由於 IDE 和搜尋引擎中的現有限制,而難以查閱運算子的相關說明。

因此,最好將您的功能發佈為具名函式和成員,且僅在標記法優點超過文件和擁有運算子的認知成本時,才公開此功能的運算子。

測量單位

在 F# 程式碼中,謹慎使用測量單位來提升型別安全

以其他 .NET 語言檢視時,會清除測量單位的其他型別資訊。 請注意,.NET 元件、工具和反映將會看到 types-sans-units。 例如,C# 取用者會看到 float 而不是 float<kg>

類型縮寫

謹慎使用類型縮寫可簡化 F# 程式碼

.NET 元件、工具和反映不會看到型別的縮寫名稱。 類型縮寫的顯著用法也可能會使網域看起來比實際更複雜,這可能會使取用者感到混淆。

避免使用公用類型的類型縮寫,公用類型的成員和屬性應該與縮寫類型上可用的類型本質不同

在此案例中,所縮寫的類型會顯示過多所定義實際類型的表示法。 相反地,請考慮將縮寫包裝在類別類型或單一大小寫區分等位 (或者,當您非常需要效能時,請考慮使用結構類型來包裝縮寫) 中。

例如,想要將多對應定義為 F# 對應的特殊案例,例如:

type MultiMap<'Key,'Value> = Map<'Key,'Value list>

不過,此類型的邏輯點標記法運算與對應上的作業不同,例如,如果索引鍵並不在字典中,則查閱運算子 map[key] 就會傳回空白清單,而不是引發例外狀況。

以其他 .NET 語言使用的程式庫指導方針

設計要以其他 .NET 語言使用的程式庫時,請務必遵守 .NET 程式庫設計指導方針。 在本文件中,這些程式庫會標記為 vanilla .NET 程式庫,而不是使用 F# 建構且不受限制的 F# 面向程式庫。 設計 vanilla .NET 程式庫表示盡可能不使用公用 API 中的 F# 特定建構,以提供與 .NET 架構其餘部分一致的熟悉和慣用 API。 下列各節將說明規則。

命名空間和型別設計 (供程式庫以其他 .NET 語言使用)

將 .NET 命名慣例套用至您元件的公用 API

請特別注意使用縮寫的名稱和 .NET 大寫指導方針。

type pCoord = ...
    member this.theta = ...

type PolarCoordinate = ...
    member this.Theta = ...

使用命名空間、類型和成員做為元件的主要組織結構

包含公用功能的所有檔案都應該以 namespace 宣告為開頭,而命名空間中唯一的公開實體應為類型。 請勿使用 F# 模組。

使用非公用模組來保存實作程式碼、公用程式類型及公用程式函式。

靜態型別應該優先於模組,因為其允許 API 未來演進使用多載和其他可能無法在 F# 模組中使用的 .NET API 設計概念。

例如,取代下列公用 API:

module Fabrikam

module Utilities =
    let Name = "Bob"
    let Add2 x y = x + y
    let Add3 x y z = x + y + z

請考慮改用:

namespace Fabrikam

[<AbstractClass; Sealed>]
type Utilities =
    static member Name = "Bob"
    static member Add(x,y) = x + y
    static member Add(x,y,z) = x + y + z

如果型別的設計並未演進,請在 vanilla .NET API 中使用 F# 記錄型別

F# 記錄型別會編譯成簡單的 .NET 類別。 這些適用於 API 中一些簡單而穩定的類型。 請考慮使用 [<NoEquality>][<NoComparison>] 屬性來隱藏自動產生介面。 另外,也請避免在 vanilla .NET API 中使用可變動的記錄欄位,因為這些欄位會公開公用欄位。 請一律考慮類別是否會為 API 未來的演進提供更有彈性的選項。

例如,下列 F# 程式碼會向 C# 取用者公開公用 API:

F#:

[<NoEquality; NoComparison>]
type MyRecord =
    { FirstThing: int
        SecondThing: string }

C#:

public sealed class MyRecord
{
    public MyRecord(int firstThing, string secondThing);
    public int FirstThing { get; }
    public string SecondThing { get; }
}

隱藏 vanilla .NET API 中 F# 等位型別的表示法

F# 等位類型通常不會跨元件界限使用,即使用於 F#-to-F# 程式碼也一樣。 在元件和程式庫內部使用這些時,是絕佳的實作裝置。

設計 Vanilla .NET API 時,請考慮使用私人宣告或簽章檔案來隱藏等位型別的表示法。

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

您也可以將在內部搭配成員使用等位表示法的型別擴大,來提供所需的 .NET 面向 API。

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

    /// A public member for use from C#
    member x.Evaluate =
        match x with
        | And(a,b) -> a.Evaluate && b.Evaluate
        | Not a -> not a.Evaluate
        | True -> true

    /// A public member for use from C#
    static member CreateAnd(a,b) = And(a,b)

使用架構的設計模式來設計 GUI 和其他元件

在 .NET 內提供許多不同的架構,例如 WinForms、WPF 和 ASP .NET。 如果您要設計元件以用於這些架構,則應該使用每個架構的命名和設計慣例。 例如,針對 WPF 程式設計,針對您要設計的類別採用 WPF 設計模式。 針對使用者介面程式設計中的模型,請使用設計模式,例如事件和通知型集合,例如在 System.Collections.ObjectModel 中找到的集合。

物件和成員設計 (供程式庫以其他 .NET 語言使用)

使用 CLIEvent 屬性來公開 .NET 事件

使用採用物件和 EventArgs 的特定 .NET 委派型別 (而非預設只使用 FSharpHandler 型別的 Event) 來建構 DelegateEvent,讓事件以熟悉的方式發佈給其他 .NET 語言。

type MyBadType() =
    let myEv = new Event<int>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

type MyEventArgs(x: int) =
    inherit System.EventArgs()
    member this.X = x

    /// A type in a component designed for use from other .NET languages
type MyGoodType() =
    let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

將非同步作業公開為傳回 .NET 工作的方法

.NET 中會使用工作來表示作用中的非同步計算。 工作通常比 F# Async<T> 物件更不具複合性,因為其代表「已執行」的工作,且無法以執行平行組合的方式,或隱藏傳播取消訊號和其他內容參數的方式組合在一起。

不過,儘管如此,傳回工作的方法也是 .NET 上非同步程式設計的標準表示法。

/// A type in a component designed for use from other .NET languages
type MyType() =

    let compute (x: int): Async<int> = async { ... }

    member this.ComputeAsync(x) = compute x |> Async.StartAsTask

您經常也需要接受明確的取消權杖:

/// A type in a component designed for use from other .NET languages
type MyType() =
    let compute(x: int): Async<int> = async { ... }
    member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)

使用 .NET 委派型別而非 F# 函式型別

此處的「F# 函式類型」表示「arrow」類型,例如 int -> int

而不是這個項目:

member this.Transform(f: int->int) =
    ...

請執行:

member this.Transform(f: Func<int,int>) =
    ...

F# 函式型別會向其他 .NET 語言顯示為 class FSharpFunc<T,U>,因此較不適合了解委派型別的語言功能和工具。 撰寫以 .NET Framework 3.5 或更高版本為目標的較高順序方法時,System.FuncSystem.Action 委派是需要發佈的正確 API,可讓 .NET 開發人員以低負擔方式取用這些 API。 (以 .NET Framework 2.0 為目標時,系統定義的委派型別限制較多;請考慮使用預先定義的委派型別,例如 System.Converter<T,U>,或定義特定的委派型別)。

另一方面,F# 面向程式庫的 .NET 委派並非常態 (請參閱下一節的 F# 面向程式庫)。 因此,開發 Vanilla .NET 程式庫較高順序方法時的常見實作策略,是使用 F# 函式型別來撰寫所有實作,然後使用委派做為實際 F# 實作頂端的精簡外觀來建立公用 API。

使用 TryGetValue 模式,而非傳回 F# 選項值,並偏好方法多載,以採用 F# 選項值做為引數

在 API 中使用 F# 選項型別的常見模式,較適合使用標準 .NET 設計技術在 vanilla .NET API 中實作。 請考慮使用 bool 傳回類型加上 out 參數,而非傳回 F# 選項值,如「TryGetValue」模式所示。 此外,請考慮使用方法多載或選擇性引數,而不採用 F# 選項值來做為參數。

member this.ReturnOption() = Some 3

member this.ReturnBoolAndOut(outVal: byref<int>) =
    outVal <- 3
    true

member this.ParamOption(x: int, y: int option) =
    match y with
    | Some y2 -> x + y2
    | None -> x

member this.ParamOverload(x: int) = x

member this.ParamOverload(x: int, y: int) = x + y

針對參數和傳回值使用 .NET 集合介面類型 IEnumerable<T> 和 IDictionary<Key,Value>

請避免使用具體集合型別,例如 .NET 陣列 T[]、F# 型別 list<T>Map<Key,Value>Set<T>,以及 .NET 具體集合型別,例如 Dictionary<Key,Value>。 .NET 程式庫設計指導方針對於何時要使用各種集合型別 (例如 IEnumerable<T>) 有良好的建議。 在某些情況下,可因效能理由接受陣列 (T[]) 的一些用法。 請特別注意,seq<T> 只是 IEnumerable<T> 的 F# 別名,因此 seq 通常是適合用於 vanilla .NET API 的型別。

而非 F# 清單:

member this.PrintNames(names: string list) =
    ...

使用 F# 序列:

member this.PrintNames(names: seq<string>) =
    ...

使用單位類型做為方法的唯一輸入類型來定義零引數方法,或做為唯一的傳回型別來定義 void 傳回方法

避免其他單位類型用途。 這些是不錯的:

✔ member this.NoArguments() = 3

✔ member this.ReturnVoid(x: int) = ()

這是錯誤的:

member this.WrongUnit( x: unit, z: int) = ((), ())

檢查 Vanilla .NET API 界限上的 Null 值

F# 實作程式碼通常會有較少的 Null 值,因為使用 F# 類型的 Null 常值時不可變動的設計模式和限制。 其他 .NET 語言通常更頻繁使用 Null 做為值。 因此,公開 Vanilla .NET API 的 F# 程式碼應該檢查 API 界限上的 null 參數,並避免這些值更深入流向 F# 實作程式碼。 可以使用 null 模式上的 isNull 函式或模式比對。

let checkNonNull argName (arg: obj) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull` argName (arg: obj) =
    if isNull arg then nullArg argName
    else ()

避免使用元組做為傳回值

相反地,偏好傳回保存彙總資料的具名類型,或使用 out 參數傳回多個值。 雖然元組和結構元組存在於 .NET (包含結構元組的 C# 語言支援),但其通常不會為 .NET 開發人員提供理想且預期中的 API。

避免使用局部調用參數

請改用 .NET 呼叫慣例 Method(arg1,arg2,…,argN)

member this.TupledArguments(str, num) = String.replicate num str

提示:如果您要設計透過任何 .NET 語言使用的程式庫,則沒有什麼方式可以替代實際執行一些實驗性的 C# 和 Visual Basic 程式設計,以確保您的程式庫在這些語言中「感覺正確」。 您也可以使用 .NET 反映程式和 Visual Studio 物件瀏覽器等工具,來確保程式庫及其文件如開發人員預期的方式出現。

附錄

設計 F# 程式碼以供其他 .NET 語言使用的端對端範例

請考慮下列 類別:

open System

type Point1(angle,radius) =
    new() = Point1(angle=0.0, radius=0.0)
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
    member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
    static member Circle(n) =
        [ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]

此類別的推斷 F# 類型如下所示:

type Point1 =
    new : unit -> Point1
    new : angle:double * radius:double -> Point1
    static member Circle : n:int -> Point1 list
    member Stretch : l:double -> Point1
    member Warp : f:(double -> double) -> Point1
    member Angle : double
    member Radius : double

讓我們看看此 F# 型別如何使用其他 .NET 語言向程式設計人員顯示。 例如,大約 C# 「簽章」如下所示:

// C# signature for the unadjusted Point1 class
public class Point1
{
    public Point1();

    public Point1(double angle, double radius);

    public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);

    public Point1 Stretch(double factor);

    public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

關於 F# 如何代表這裡的建構,有一些需要注意的重點。 例如:

  • 已保留引數名稱之類的中繼資料。

  • 採用兩個引數的 F# 方法會變成採用兩個引數的 C# 方法。

  • 函式和清單會變成 F# 程式庫中對應類型的參考。

下列程式碼示範如何調整此程式碼,以將這些事項納入考慮。

namespace SuperDuperFSharpLibrary.Types

type RadialPoint(angle:double, radius:double) =

    /// Return a point at the origin
    new() = RadialPoint(angle=0.0, radius=0.0)

    /// The angle to the point, from the x-axis
    member x.Angle = angle

    /// The distance to the point, from the origin
    member x.Radius = radius

    /// Return a new point, with radius multiplied by the given factor
    member x.Stretch(factor) =
        RadialPoint(angle=angle, radius=radius * factor)

    /// Return a new point, with angle transformed by the function
    member x.Warp(transform:Func<_,_>) =
        RadialPoint(angle=transform.Invoke angle, radius=radius)

    /// Return a sequence of points describing an approximate circle using
    /// the given count of points
    static member Circle(count) =
        seq { for i in 1..count ->
                RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }

此類型的推斷 F# 類型如下所示:

type RadialPoint =
    new : unit -> RadialPoint
    new : angle:double * radius:double -> RadialPoint
    static member Circle : count:int -> seq<RadialPoint>
    member Stretch : factor:double -> RadialPoint
    member Warp : transform:System.Func<double,double> -> RadialPoint
    member Angle : double
    member Radius : double

C# 簽章現在如下所示:

public class RadialPoint
{
    public RadialPoint();

    public RadialPoint(double angle, double radius);

    public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);

    public RadialPoint Stretch(double factor);

    public RadialPoint Warp(System.Func<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

準備這個型別供 Vanilla .NET 程式庫一部分使用的修正如下:

  • 已調整數個名稱:Point1nlf 分別變成 RadialPointcountfactortransform

  • 將使用 [ ... ] 的清單建構變更為使用 IEnumerable<RadialPoint> 的序列建構,以使用 seq<RadialPoint> 的傳回型別,而不是 RadialPoint list

  • 使用 .NET 委派型別 System.Func,而不是 F# 函式型別。

這會使在 C# 程式碼中取用更加理想。