訓練
此檔案是針對 F# 程式設計的一組元件設計指導方針,基於由 Microsoft Research 出版的 F# 元件設計指導方針 v14,以及最初由 F# Software Foundation 策劃和維護的版本。
本文件假設您已熟悉 F# 程序設計。 許多人感謝 F# 社群對於本指南各種版本的貢獻和實用意見反應。
本檔會探討與 F# 元件設計和程式代碼撰寫相關的一些問題。 元件可能表示下列任一項:
- 在 F# 專案中,一個具有該專案內部外部使用者的圖層。
- 供 F# 程式碼跨組件邊界使用的函式庫。
- 用於跨組件邊界的任何 .NET 語言的程式庫。
- 程式庫,旨在透過套件存放庫分發,例如 NuGet。
本文所述的技術遵循良好 F# 程式代碼 五個原則,因此會適當地利用功能和對象程序設計。
不論方法為何,元件和函式庫設計者在嘗試設計出開發人員最容易使用的 API 時,都會面臨許多實際且平凡的挑戰。 認真運用 .NET 程式庫設計指導方針, 將引導您建立一組一致且令人滿意的 API。
不論連結庫的目標對象為何,都有一些適用於 F# 連結庫的通用指導方針。
不論您從事何種 F# 程式設計,了解 .NET 函式庫設計指導方針都是有價值的。 大部分的其他 F# 和 .NET 程式設計人員都會熟悉這些指導方針,並預期 .NET 程式代碼符合這些指導方針。
.NET 連結庫設計指導方針提供有關命名、設計類別和介面、成員設計(屬性、方法、事件等)等的一般指引,並且是各種設計指引的實用第一個參考點。
公共 API 的 XML 文件確保使用者在使用這些類型和成員時,可以取得絕佳的 Intellisense 和 Quickinfo,並允許建置程式庫的文件檔案。 參閱 XML 文件,了解有關各種 XML 標籤的信息,這些標籤可用於 xmldoc 注釋中的額外標註。
/// 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>
)。
使用 F# 連結庫中的明確簽章檔案提供公用 API 的簡潔摘要,有助於確保您知道連結庫的完整公用介面,並提供公用文件和內部實作詳細數據之間的清楚分隔。 簽名檔案會通過要求在實作和簽名檔案中進行變更,來增加變更公用 API 所需的阻力。 因此,簽章檔案通常只有在 API 已鞏固且不再預期會大幅變更時才會引進。
在專案範圍合適時,請遵循 關於在 .NET 中使用字串的最佳做法,以及 指導方針。 特別是,在字串轉換和比較中明確說明 文化意圖(適用的話)。
本節提供開發公用 F#面向連結庫的建議;也就是說,公開要供 F# 開發人員取用之公用 API 的連結庫。 有各種不同的函式庫設計建議特別適用於 F#。 當缺乏後續特定建議時,.NET 程式庫設計指導方針便成為預設的指導方針。
下表遵循 .NET 命名和大寫慣例。 也有小型新增內容來納入 F# 建構。 這些建議特別適用於跨越 F# 界限的 API,並且符合 .NET BCL 和大多數函式庫的慣用語。
構建 | 箱 | 部分 | 例子 | 筆記 |
---|---|---|---|---|
具體類型 | PascalCase | 名詞/ 形容詞 | List、Double、Complex | 具體類型是結構、類別、列舉、委託、記錄類型和聯合。 雖然類型名稱在 OCaml 中傳統上是小寫,但 F# 已針對類型採用 .NET 命名配置。 |
DLLs | PascalCase | Fabrikam.Core.dll | ||
聯合標籤 | PascalCase | 名詞 | 部分,新增,成功 | 請勿在公用 API 中使用前置詞。 選擇性地在內部時使用前綴,例如「類型 Teams = TAlpha | TBeta | TDelta」。 |
事件 | PascalCase | 動詞 | 值已變更/值正在變更 | |
例外 | PascalCase | WebException(網頁異常) | 名稱結尾應為 「例外狀況」。 | |
田 | PascalCase | 名詞 | CurrentName | |
介面類型 | PascalCase | 名詞/ 形容詞 | IDisposable(可處置介面) | 名稱應該以 「I」 開頭。 |
方法 | PascalCase | 動詞 | ToString | |
Namespace | PascalCase | Microsoft.FSharp.Core | 一般使用 <Organization>.<Technology>[.<Subnamespace>] ,但如果技術與組織無關,則省略組織。 |
|
參數 | camelCase | 名詞 | 型別名稱,轉換,範圍 | |
let 值 (內部) | camelCase 或 PascalCase | 名詞/ 動詞 | getValue、 myTable | |
let 值(外部) | camelCase 或 PascalCase | 名詞/動詞 | List.map,Dates.Today | 遵循傳統功能設計模式時,let 系結值通常會公開。 不過,當標識碼可從其他 .NET 語言使用時,通常會使用PascalCase。 |
財產 | PascalCase | 名詞/ 形容詞 | IsEndOfFile、BackColor | 布爾值屬性通常會使用Is和 Can,而且應該是肯定的,如同IsEndOfFile,而不是IsNotEndOfFile。 |
.NET 指導方針禁止使用縮寫(例如,「使用 OnButtonClick
而不是 OnBtnClick
」。 接受常用的縮寫,例如將「異步」縮寫為 Async
。 此指導方針有時會忽略函數式程式設計;例如,List.iter
使用了「iterate」的縮寫。 基於這個理由,在 F# 到 F# 的程式設計中,使用縮寫通常會有較高的容忍度,但在公用元件設計中仍然應該避免。
.NET 指導方針表示,單獨大小寫無法用來釐清名稱衝突,因為某些客戶端語言(例如 Visual Basic) 不區分大小寫。
像 XML 這樣的首字母縮略詞不是一般的縮寫,並且在 .NET 程式庫中以未資本化的形式 (Xml) 廣泛使用。 應該只使用廣為人知、廣為人知的縮略字。
請針對公用 API 中的泛型參數名稱使用 PascalCase,包括針對 F# 的函式庫。 特別是,請針對任意泛型參數使用 T
、U
、T1
、T2
等名稱,而當特定名稱有意義時,F# 相關的庫則應使用像 Key
、Value
、Arg
這樣的名稱,但不要使用 TKey
。
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# 函式,而不需要內部模組
在最上層命名空間或模組之間選擇將影響程式碼的編譯形式,因此,如果您的 API 最終在 F# 之外被取用,其他 .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 中的一流概念,可用來達成函式通常會為您提供哪些功能。 此外,它們可以用來將存在類型編碼到您的程式,而函式的紀錄卻無法做到。
當您定義集合類型時,請考慮為新的集合類型提供一組標準作業,例如 CollectionType.map
和 CollectionType.iter
。
module CollectionType =
let map f c =
...
let iter f c =
...
如果您包含這類模組,請遵循 FSharp.Core 中找到之函式的標準命名慣例。
例如,Microsoft.FSharp.Core.Operators
是由 FSharp.Core.dll提供的自動開啟最上層函式集合(例如 abs
和 sin
)。
同樣地,統計資料庫可能包含函式 erf
和 erfc
的模組,其中此模組的設計目的是明確或自動開啟。
將 [<RequireQualifiedAccess>]
屬性新增至模組表示模組可能無法開啟,而且對模組元素的參考需要明確的限定存取權。 例如,Microsoft.FSharp.Collections.List
模組具有此屬性。
當模組中的函式和值具有可能與其他模組中名稱衝突的名稱時,這會很有用。 要求合格的存取權可大幅提高連結庫的長期維護性和可演進性。
強烈建議您擁有自定義模組的 [<RequireQualifiedAccess>]
屬性,這些模組會擴充 FSharp.Core
所提供的模組(例如 Seq
、List
、Array
),因為這些模組在 F# 程式代碼中普遍使用,且已定義 [<RequireQualifiedAccess>]
;一般而言,當這類模組遮蔽或擴充具有 屬性的其他模組時,不建議定義缺少 屬性的自定義模組。
將 [<AutoOpen>]
屬性新增至模組表示開啟包含的命名空間時,將會開啟模組。
[<AutoOpen>]
屬性也可以套用至元件,以指出參考元件時自動開啟的模組。
例如,統計資料庫 MathsHeaven.Statistics 可能包含包含函數 erf
和 erfc
的 module MathsHeaven.Statistics.Operators
。 將此課程模組標示為 [<AutoOpen>]
是合理的。 這表示 open MathsHeaven.Statistics
也會開啟此課程模組,並將名稱 erf
和 erfc
納入範圍。 另一個良好的 [<AutoOpen>]
用法是用於包含擴充方法的模組。
過度使用 [<AutoOpen>]
會導致污染的命名空間,而且應該小心使用 屬性。 針對特定領域中的特定程式庫,明智地使用 [<AutoOpen>]
能夠提升使用性。
有時候類別是用來模型化數學建構,例如 Vectors。 當模型化的定義域具有已知的運算子時,將其定義為 類別內建的成員會很有説明。
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。
有時候,您可能會想要以某種樣式為使用 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,但有不同的選項或自變數。
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
對於包含許多元件的傳回型別,或當元件與單一可識別實體相關時,請考慮使用具名類型,而不是 tuple。
如果有名為 Operation
的對應同步作業傳回 T
,則異步作業如果傳回 Async<T>
,應命名為 AsyncOperation
;如果傳回 Task<T>
,應命名為 OperationAsync
。 針對公開 Begin/End 方法的常用 .NET 類型,請考慮使用 Async.FromBeginEnd
將擴充方法撰寫為外觀,以提供 F# 異步程式設計模型給這些 .NET API。
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# 擴充成員通常只能用於關閉與大部分使用模式中類型相關聯之內部作業的作業。 其中一個常見用途是為了各種 .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>]
來消除案例名稱的歧義,以避免因 open
語句順序導致的遮蔽問題而觸發混淆的錯誤。
等位類型依賴 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# 到 F# 的程式庫設計中,不應使用利用這項功能的成員。 這是因為以不熟悉或非標準隱含條件約束為基礎的連結庫設計往往會導致使用者程式代碼變得不靈活,並系結至一個特定的架構模式。
此外,如此一來,大量使用成員限制條件可能會產生很長的編譯時間。
在某些情況下,自定義運算符相當重要,而且在大型實作程式代碼主體中非常實用的表示法裝置。 對於程式庫的新使用者,名稱函式通常更容易使用。 此外,自定義符號運算符可能很難記錄,而且用戶發現,由於IDE和搜尋引擎中的現有限制,查詢運算符的協助會比較困難。
因此,最好將功能發佈為具名函式和成員,並且只有在符號表示的優點超過文件和認知成本時,才會公開此功能的運算符。
當其他 .NET 語言檢視時,會清除測量單位的其他輸入資訊。 請注意,.NET 元件、工具和反射將看到不帶單位的類型。 例如,C# 取用者會看到 float
,而不是 float<kg>
。
.NET 元件、工具和反映不會看到類型的縮寫名稱。 類型縮寫的顯著用法也可以讓網域看起來比實際更為複雜,這可能會混淆取用者。
在此情況下,縮寫的型別會過多透露所定義的實際型別的表示方式。 相反地,請考慮將縮寫包裝在類別型別或單一案例辨別聯集中(或者,當效能至關重要時,請考慮使用結構來包裝縮寫)。
例如,將多重映射定義為 F# 對應的特殊案例很誘人。
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
不過,此類型上的邏輯點表示法作業與 Map 上的作業不同,例如,如果索引鍵不在字典中,查閱運算符 map[key]
傳回空白清單是合理的,而不是引發例外狀況。
設計其他 .NET 語言使用的連結庫時,請務必遵循 .NET 連結庫設計指導方針。 在此文件中,這些函式庫被標示為 vanilla .NET 函式庫,以區別於那些使用 F# 結構且不受限制的 F# 面向函式庫。 設計 vanilla .NET 程式庫意味著通過在公用 API 中盡量減少使用 F#特定建構,來提供與 .NET Framework 其餘部分一致、熟悉且慣用的 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
F# 記錄類型會編譯成簡單的 .NET 類別。 這些適用於 API 中的一些簡單穩定類型。 請考慮使用 [<NoEquality>]
和 [<NoComparison>]
屬性來隱藏介面的自動產生。 也請避免在 vanilla .NET API 中使用可變記錄欄位,因為這些欄位會公開公用字段。 請務必考慮類別是否會為 API 的未來演進提供更有彈性的選項。
例如,下列 F# 程式代碼會將公用 API 公開給 C# 取用者:
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; }
}
F# 等位類型通常不會跨越元件邊界使用,即使在 F# 對 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)
.NET 中有許多不同的架構,例如 WinForms、WPF 和 ASP.NET。 如果您要設計元件以用於這些架構,則應該使用每個元件的命名和設計慣例。 例如,針對 WPF 程式設計,針對您要設計的類別採用 WPF 設計模式。 針對使用者介面程序設計中的模型,請使用事件和通知型集合等設計模式,例如在 System.Collections.ObjectModel中找到的集合。
構造一个具有特定 .NET 委派類型的 DelegateEvent
,該類型接受物件和 EventArgs
(而非預設僅使用 FSharpHandler
類型的 Event
),以熟悉的方式將事件發佈給其他 .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 中會使用工作來表示作用中的異步計算。 工作通常比 F# Async<T>
物件的組合性低,因為這些工作代表「已經在執行」的狀態,無法合併以進行平行組合,也無法隱藏取消信號和其他上下文參數的傳播。
不過,儘管如此,傳回Tasks的方法就是 .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)
這個處的 「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.Func
和 System.Action
委派是適合發佈的 API,可讓 .NET 開發人員輕鬆使用這些 API。 (以 .NET Framework 2.0 為目標時,系統定義的委派類型會更加有限;請考慮使用預先定義的委派類型,例如 System.Converter<T,U>
或定義特定的委派類型。
另一方面,.NET 委派對於針對 F# 的函式庫來說並不自然(請參閱有關針對 F# 的函式庫的下一節)。 因此,開發 Vanilla .NET 連結庫的較高順序方法時,常見的實作策略是使用 F# 函式類型撰寫所有實作,然後使用委派作為實際 F# 實作頂端的精簡外觀建立公用 API。
在 API 中,使用 F# 選項類型的常見模式可以更好地通過在標準版 .NET API 中實作標準 .NET 設計技術來實現。 請考慮使用bool傳回型別加上 out 參數,如 「TryGetValue」 模式所示,而不是傳回 F# 選項值。 而且,請考慮使用方法多載或選擇性自變數,而不採用 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 陣列 T[]
、F# 類型 list<T>
、Map<Key,Value>
和 Set<T>
等具象集合類型,以及 Dictionary<Key,Value>
等 .NET 具體集合類型。 .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>) =
...
避免使用單位類型的其他用法。 這些是不錯的:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
這很糟糕:
member this.WrongUnit( x: unit, z: int) = ((), ())
F# 的實作程式碼通常會有較少的 null 值,這是由於不可變設計模式和對 F# 類型 null 值使用的限制。 其他 .NET 語言通常更頻繁地使用 null 作為一個值。 因此,公開 Vanilla .NET API 的 F# 程式代碼應該檢查 API 界限上是否有 Null 的參數,並防止這些值更深入地流入 F# 實作程式代碼。 您可以使用 isNull
函式或在 null
模式上進行模式比對。
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
從 F# 9 開始,您可以利用新的 | null
語法,讓編譯程式指出可能的 Null 值,以及它們需要處理的位置:
let checkNonNull argName (arg: obj | null) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj | null) =
if isNull arg then nullArg argName
else ()
在 F# 9 中,編譯程式會在偵測到可能 Null 值未處理時發出警告:
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
// `ReadLine` may return null here - when the stream is finished
let line = sr.ReadLine()
// nullness warning: The types 'string' and 'string | null'
// do not have equivalent nullability
printLineLength line
這些警告應該使用 F# null 模式來解決, 比對:
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
let line = sr.ReadLine()
match line with
| null -> ()
| s -> printLineLength s
相反地,偏好傳回包含匯總數據的具名型別,或使用 out 參數傳回多個值。 雖然 Tuple 和結構元組存在於 .NET 中(包括結構 Tuple 的 C# 語言支援),但它們通常不會為 .NET 開發人員提供理想且預期的 API。
請改用 .NET 呼叫慣例 Method(arg1,arg2,…,argN)
。
member this.TupledArguments(str, num) = String.replicate num str
提示:如果您正在設計程式庫以供任何 .NET 語言使用,別無他法,只有進行一些實驗性 C# 和 Visual Basic 程式設計,才能確保您的程式庫從這些語言使用起來很自然。 您也可以使用 .NET Reflector 和 Visual Studio 物件瀏覽器等工具,確保連結庫及其檔如開發人員預期般出現。
請考慮下列類別:
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; }
}
準備此類型以作為標準 .NET 程式庫的一部分使用的修正如下:
已調整數個名稱:
Point1
、n
、l
和f
分別RadialPoint
、count
、factor
和transform
。使用
seq<RadialPoint>
作為返回類型,而不是RadialPoint list
,並將使用[ ... ]
的清單建構更改為使用IEnumerable<RadialPoint>
的序列建構。使用 .NET 委派類型
System.Func
,而不是 F# 函式類型。
這讓在 C# 程式碼中取用更方便。