F#-Komponentenentwurfsrichtlinien
Dieses Dokument ist eine Reihe von Komponentenentwurfsrichtlinien für die F#-Programmierung, basierend auf den F#-Komponentenentwurfsrichtlinien, v14, Microsoft Research und einer Version, die ursprünglich von der F# Software Foundation kuratiert und verwaltet wurde.
In diesem Dokument wird davon ausgegangen, dass Sie mit der F#-Programmierung vertraut sind. Vielen Dank an die F#-Community für ihre Beiträge und hilfreiches Feedback zu verschiedenen Versionen dieses Leitfadens.
Überblick
In diesem Dokument werden einige der Probleme im Zusammenhang mit F#-Komponentenentwurf und -Codierung behandelt. Eine Komponente kann eine der folgenden Elemente bedeuten:
- Eine Ebene in Ihrem F#-Projekt, die von externen Nutzern innerhalb dieses Projekts verwendet wird.
- Eine Bibliothek, die für den Verbrauch durch F#-Code über Assemblygrenzen hinweg vorgesehen ist.
- Eine Bibliothek, die für die Verwendung durch jede .NET-Sprache über Assemblygrenzen hinweg vorgesehen ist.
- Eine Bibliothek für die Verteilung über ein Paket-Repository, z. B. NuGet-.
Die in diesem Artikel beschriebenen Techniken folgen den Fünf Prinzipien des guten F#-Codesund verwenden daher sowohl die funktionale als auch die Objektprogrammierung entsprechend.
Unabhängig von der Methodik sieht sich der Komponenten- und Bibliotheksdesigner einer Reihe praktischer und prosaischer Probleme gegenüber, wenn versucht wird, eine API zu erstellen, die von Entwicklern am einfachsten verwendet werden kann. Die gewissenhafte Anwendung der .NET Library Design Guidelines führt Sie dazu, einen konsistenten Satz von APIs zu erstellen, die angenehm zu nutzen sind.
Allgemeine Richtlinien
Es gibt einige universelle Richtlinien, die für F#-Bibliotheken gelten, unabhängig von der vorgesehenen Zielgruppe für die Bibliothek.
Weitere Informationen zu den Designrichtlinien für .NET-Bibliotheken
Unabhängig von der Art der F#-Codierung, die Sie ausführen, ist es nützlich, über kenntnisse der .NET Library Design Guidelineszu verfügen. Die meisten anderen F#- und .NET-Programmierer sind mit diesen Richtlinien vertraut und erwarten, dass .NET-Code sie erfüllt.
Die .NET Library Design Guidelines bieten allgemeine Anleitungen zum Benennen, Entwerfen von Klassen und Schnittstellen, Memberdesign (Eigenschaften, Methoden, Ereignisse usw.) und vieles mehr und sind ein nützlicher erster Referenzpunkt für eine Vielzahl von Entwurfsanleitungen.
Hinzufügen von XML-Dokumentationskommentaren zu Ihrem Code
Die XML-Dokumentation zu öffentlichen APIs stellt sicher, dass Benutzer bei Verwendung dieser Typen und Member großartige IntelliSense- und Quickinfo-Elemente erhalten und die Erstellung von Dokumentationsdateien für die Bibliothek aktivieren können. Informationen zu verschiedenen XML-Tags, die für zusätzliches Markup in xmldoc-Kommentaren verwendet werden können, finden Sie in der XML-Dokumentation.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Sie können entweder die XML-Kurzformkommentare (/// comment
) oder xml-Standardkommentare (///<summary>comment</summary>
) verwenden.
Erwägen Sie die Verwendung expliziter Signaturdateien (FSI) für stabile Bibliotheks- und Komponenten-APIs.
Die Verwendung expliziter Signaturendateien in einer F#-Bibliothek bietet eine prägnante Zusammenfassung der öffentlichen API, mit der Sie sicherstellen können, dass Sie die vollständige öffentliche Oberfläche Ihrer Bibliothek kennen und eine klare Trennung zwischen der öffentlichen Dokumentation und internen Implementierungsdetails bereitstellen. Signaturdateien erschweren das Ändern der öffentlichen API, indem Änderungen sowohl in den Implementierungsdateien als auch in den Signaturdateien vorgenommen werden müssen. Daher sollten Signaturdateien in der Regel nur eingeführt werden, wenn eine API festigt wurde und nicht mehr wesentlich geändert werden soll.
Befolgen Sie die bewährten Methoden für die Verwendung von Zeichenfolgen in .NET
Befolgen Sie -bewährte Methoden für die Verwendung von Zeichenfolgen in .NET-Leitfäden, wenn der Umfang des Projekts dies garantiert. Geben Sie insbesondere beim Konvertieren und Vergleichen von Zeichenfolgen immer explizit die kulturelle Absicht an (sofern zutreffend).
Richtlinien für F#-orientierte Bibliotheken
Dieser Abschnitt enthält Empfehlungen für die Entwicklung öffentlicher F#-bibliotheken; d. h. Bibliotheken, die öffentliche APIs verfügbar machen, die von F#-Entwicklern genutzt werden sollen. Es gibt eine Vielzahl von Empfehlungen für den Bibliotheksentwurf, die speziell für F# gelten. In Ermangelung der folgenden spezifischen Empfehlungen dienen die .NET-Bibliotheksentwurfsrichtlinien als Richtlinien.
Namenskonventionen
Verwenden von .NET-Benennungs- und Großschreibungskonventionen
Die folgende Tabelle folgt den .NET-Benennungs- und Großschreibungskonventionen. Es gibt kleine Ergänzungen, die auch F#-Konstrukte enthalten. Diese Empfehlungen sind insbesondere für APIs gedacht, die über F#-zu-F#-Grenzen hinausgehen und mit Idiomen der .NET BCL sowie der Mehrheit der Bibliotheken übereinstimmen.
Bauen | Case | Teil | Beispiele | Notizen |
---|---|---|---|---|
Betontypen | PascalCase | Substantiv/ Adjektiv | List, Double, Complex | Konkrete Typen sind Strukturen, Klassen, Enumerationen, Stellvertretungen, Datensätze und Vereinigungen. Obwohl Typnamen in OCaml traditionell Kleinbuchstaben sind, hat F# das .NET-Benennungsschema für Typen übernommen. |
DLLs | PascalCase | Fabrikam.Core.dll | ||
Union-Tags | PascalCase | Nomen | Some, Add, Success | Verwenden Sie kein Präfix in öffentlichen APIs. Verwenden Sie optional ein Präfix, wenn es intern ist, z. B. "Typ Teams = TAlpha | TBeta | TDelta". |
Ereignis | PascalCase | Verb | ValueChanged/ValueChanging | |
Ausnahmen | PascalCase | WebException | Der Name sollte mit "Exception" enden. | |
Feld | PascalCase | Nomen | AktuellerName | |
Schnittstellentypen | PascalCase | Substantiv/ Adjektiv | IDisposable | Der Name sollte mit "I" beginnen. |
Methode | PascalCase | Verb | ToString | |
Namespace | PascalCase | Microsoft.FSharp.Core | Verwenden Sie im Allgemeinen <Organization>.<Technology>[.<Subnamespace>] . Lassen Sie die Organisation aber weg, wenn die Technologie unabhängig von der Organisation ist. |
|
Parameter | camelCase | Nomen | typeName, transformation, range | |
let-Werte (intern) | camelCase oder PascalCase | Substantiv/ Verb | getValue, myTable | |
let-Werte (extern) | camelCase oder PascalCase | Substantiv/Verb | List.map, Dates.Today | let-gebundene Werte sind häufig öffentlich, wenn die herkömmlichen funktionalen Entwurfsmuster befolgt werden. Verwenden Sie jedoch in der Regel PascalCase, wenn der Bezeichner aus anderen .NET-Sprachen verwendet werden kann. |
Eigentum | PascalCase | Substantiv/ Adjektiv | IsEndOfFile, BackColor | Boolesche Eigenschaften verwenden in der Regel Is und Can und sollten bejahend sein, wie in IsEndOfFile, nicht IsNotEndOfFile. |
Vermeiden von Abkürzungen
In den .NET-Richtlinien wird die Verwendung von Abkürzungen abgeraten (z. B. "OnButtonClick
anstelle von OnBtnClick
verwenden"). Allgemeine Abkürzungen, z. B. Async
für "Asynchron", werden toleriert. Diese Richtlinie wird manchmal für die funktionale Programmierung ignoriert; beispielsweise verwendet List.iter
eine Abkürzung für "iterate". Aus diesem Grund wird die Verwendung von Abkürzungen in der F#-to-F#-Programmierung in größerem Maße toleriert, sollte aber im Allgemeinen im öffentlichen Komponentenentwurf vermieden werden.
Vermeiden von Namenskonflikten durch Groß-/Kleinschreibung
Die .NET-Richtlinien besagen, dass Groß-/Kleinschreibung nicht allein verwendet werden darf, um Namen voneinander zu unterscheiden, da die Groß-/Kleinschreibung bei einigen Clientsprachen (z. B. Visual Basic) nicht beachtet wird.
Gegebenenfalls Akronyme verwenden
Akronyme wie XML sind keine Abkürzungen und werden in .NET-Bibliotheken in nicht gecapitalisierter Form (Xml) häufig verwendet. Nur bekannte, weit verbreitete Akronyme sollten verwendet werden.
Verwenden von PascalCase für generische Parameternamen
Verwenden Sie PascalCase für generische Parameternamen in öffentlichen APIs, einschließlich für F#-bezogene Bibliotheken. Verwenden Sie insbesondere Namen wie T
, U
, T1
, T2
für beliebige generische Parameter. Wenn spezifische Namen sinnvoll sind, verwenden Sie für F#-bezogene Bibliotheken Namen wie Key
, Value
, Arg
(aber nicht TKey
).
Verwenden von PascalCase oder camelCase für öffentliche Funktionen und Werte in F#-Modulen
camelCase wird für öffentliche Funktionen verwendet, die ohne Qualifizierung verwendet werden sollen (z. B. invalidArg
), sowie für die "Standardsammlungsfunktionen" (z. B. List.map). In beiden Fällen wirken die Funktionsnamen ähnlich wie Schlüsselwörter in der Sprache.
Objekt-, Typ- und Modulentwurf
Verwenden Sie Namespaces oder Module, um Ihre Typen und Module einzuschließen.
Jede F#-Datei in einer Komponente sollte entweder mit einer Namespacedeklaration oder einer Moduldeklaration beginnen.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
oder
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
Die Unterschiede zwischen der Verwendung von Modulen und Namespaces zum Organisieren von Code auf oberster Ebene lauten wie folgt:
- Namespaces können mehrere Dateien umfassen
- Namespaces dürfen keine F#-Funktionen enthalten, es sei denn, sie befinden sich innerhalb eines inneren Moduls
- Der Code für ein bestimmtes Modul muss in einer einzelnen Datei enthalten sein.
- Module der obersten Ebene können F#-Funktionen enthalten, ohne dass ein inneres Modul erforderlich ist
Die Auswahl zwischen einem Namespace oder Modul der obersten Ebene wirkt sich auf die kompilierte Form des Codes aus und wirkt sich daher auf die Ansicht aus anderen .NET-Sprachen aus, wenn Ihre API schließlich außerhalb des F#-Codes verwendet wird.
Verwenden von Methoden und Eigenschaften für vorgänge, die für Objekttypen systemintern sind
Wenn Sie mit Objekten arbeiten, sollten Sie sicherstellen, dass konsumierbare Funktionen als Methoden und Eigenschaften für diesen Typ implementiert werden.
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) = ...
Der Großteil der Funktionalität für ein bestimmtes Mitglied muss nicht unbedingt in diesem Mitglied implementiert werden, aber der konsumierbare Teil dieser Funktionalität sollte es sein.
Verwenden von Klassen zum Kapseln des veränderbaren Zustands
In F# muss dies nur erfolgen, wenn dieser Zustand nicht bereits von einem anderen Sprachkonstrukt gekapselt ist, z. B. ein Schließen, Sequenzausdruck oder asynchrone Berechnung.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Verwenden von Schnittstellen zum Gruppieren verwandter Vorgänge
Verwenden Sie Schnittstellentypen, um eine Gruppe von Vorgängen darzustellen. Dies wird gegenüber anderen Optionen bevorzugt, z. B. Tupel von Funktionen oder Datensätze von Funktionen.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
Vorzugsweise vor:
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
Schnittstellen sind erstklassige Konzepte in .NET, mit denen Sie erreichen können, was Functors normalerweise bieten würden. Darüber hinaus können sie verwendet werden, um existenzielle Typen in Ihr Programm zu codieren, was mit Funktionsaufzeichnungen nicht möglich ist.
Verwenden eines Moduls zum Gruppieren von Funktionen, die auf Sammlungen reagieren
Wenn Sie einen Sammlungstyp definieren, sollten Sie einen Standardsatz von Vorgängen wie CollectionType.map
und CollectionType.iter
) für neue Sammlungstypen bereitstellen.
module CollectionType =
let map f c =
...
let iter f c =
...
Wenn Sie ein solches Modul einschließen, befolgen Sie die Standardbenennungskonventionen für Funktionen in FSharp.Core.
Verwenden eines Moduls zum Gruppieren von Funktionen für allgemeine, kanonische Funktionen, insbesondere in mathematischen und DSL-Bibliotheken
Beispielsweise ist Microsoft.FSharp.Core.Operators
eine automatisch geöffnete Sammlung von Funktionen auf oberster Ebene (z. B. abs
und sin
), die von FSharp.Core.dllbereitgestellt werden.
Ebenso kann eine Statistikbibliothek ein Modul mit Funktionen erf
und erfc
enthalten, in dem dieses Modul explizit oder automatisch geöffnet werden soll.
Erwägen Sie die Verwendung von RequireQualifiedAccess, und wenden Sie AutoOpen-Attribute sorgfältig an.
Das Hinzufügen des [<RequireQualifiedAccess>]
-Attributs zu einem Modul weist darauf hin, dass das Modul möglicherweise nicht geöffnet wird und dass Verweise auf die Elemente des Moduls expliziten qualifizierten Zugriff erfordern. Beispielsweise weist das modul Microsoft.FSharp.Collections.List
dieses Attribut auf.
Dies ist nützlich, wenn Funktionen und Werte im Modul Namen haben, die wahrscheinlich mit Namen in anderen Modulen in Konflikt stehen. Die Notwendigkeit eines qualifizierten Zugriffs kann die langfristige Verwendbarkeit und Volvierbarkeit einer Bibliothek erheblich erhöhen.
Es wird dringend empfohlen, das Attribut [<RequireQualifiedAccess>]
für benutzerdefinierte Module zu haben, die jene erweitern, die von FSharp.Core
bereitgestellt werden (z. B. Seq
, List
, Array
), da diese Module im F#-Code häufig verwendet werden und [<RequireQualifiedAccess>]
darauf definiert ist; im Allgemeinen wird davon abgeraten, benutzerdefinierte Module zu definieren, denen das Attribut fehlt, wenn solche Module andere Module überlagern oder erweitern, die das Attribut besitzen.
Das Hinzufügen des [<AutoOpen>]
-Attributs zu einem Modul bedeutet, dass das Modul geöffnet wird, wenn der enthaltende Namespace geöffnet wird. Das attribut [<AutoOpen>]
kann auch auf eine Assembly angewendet werden, um ein Modul anzugeben, das automatisch geöffnet wird, wenn auf die Assembly verwiesen wird.
Beispielsweise kann eine Statistikbibliothek MathsHeaven.Statistics eine module MathsHeaven.Statistics.Operators
enthalten, die Funktionen erf
und erfc
enthält. Es ist sinnvoll, dieses Modul als [<AutoOpen>]
zu kennzeichnen. Dies bedeutet, dass open MathsHeaven.Statistics
dieses Modul auch öffnen und die Namen erf
und erfc
in den Geltungsbereich bringen wird. Eine weitere gute Verwendung von [<AutoOpen>]
ist für Module mit Erweiterungsmethoden.
Die Übernutzung von [<AutoOpen>]
führt zu verunreinigten Namespaces, und das Attribut sollte mit Bedacht verwendet werden. Für bestimmte Bibliotheken in bestimmten Domänen kann die sorgfältige Verwendung von [<AutoOpen>]
zu einer besseren Benutzerfreundlichkeit führen.
Erwägen Sie die Definition von Operatormitgliedern für Klassen, bei denen die Verwendung bekannter Operatoren geeignet ist.
Manchmal werden Klassen verwendet, um mathematische Konstrukte wie Vectors zu modellieren. Wenn die modellierte Domäne bekannte Operatoren aufweist, ist es hilfreich, sie als Elemente zu definieren, die für die Klasse intrinsisch sind.
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
Dieser Leitfaden entspricht allgemeinen .NET-Richtlinien für diese Typen. Es kann jedoch auch in der F#-Codierung wichtig sein, da diese Typen in Verbindung mit F#-Funktionen und -Methoden mit Membereinschränkungen wie List.sumBy verwendet werden können.
Erwägen der Verwendung von CompiledName, um einen .NET-konformen Namen für Consumer anderer .NET-Sprachen bereitzustellen
Manchmal möchten Sie etwas in einer Formatvorlage für F#-Consumer benennen (z. B. ein statisches Element in Kleinbuchstaben, sodass es so aussieht, als wäre es eine modulgebundene Funktion), aber eine andere Formatvorlage für den Namen haben, wenn sie in eine Assembly kompiliert wird. Mit dem [<CompiledName>]
-Attribut können Sie ein anderes Format für Nicht-F#-Code bereitzustellen, der die Assembly verwendet.
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
Mithilfe von [<CompiledName>]
können Sie .NET-Benennungskonventionen für Verbraucher der Assembly, die kein F# verwenden, anwenden.
Verwenden der Methodenüberladung für Memberfunktionen, um so eine einfachere API bereitzustellen
Die Methodenüberladung ist ein leistungsfähiges Tool zum Vereinfachen einer API, die möglicherweise ähnliche Funktionen ausführen muss, aber mit verschiedenen Optionen oder Argumenten.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
In F# ist es gebräuchlicher, die Anzahl von Argumenten und nicht die Typen von Argumenten zu überladen.
Verbergen der Darstellungen von Datensatz- und Union-Typen, wenn der Entwurf dieser Typen wahrscheinlich weiterentwickelt wird
Vermeiden Sie das Einblenden konkreter Darstellungen von Objekten. Beispielsweise wird die konkrete Darstellung von DateTime Werten nicht durch die externe, öffentliche API des .NET-Bibliotheksdesigns offenbart. Zur Laufzeit kennt die Common Language Runtime die zugesicherte Implementierung, die während der gesamten Ausführung verwendet wird. Kompilierter Code nimmt jedoch keine Abhängigkeiten von der konkreten Darstellung auf.
Vermeiden der Verwendung der Implementierungsvererbung für die Erweiterbarkeit
In F# wird die Implementierungsvererbung selten verwendet. Darüber hinaus sind Vererbungshierarchien oft komplex und schwierig zu ändern, wenn neue Anforderungen eingehen. Die Vererbungsimplementierung ist in F# weiterhin vorhanden, um Kompatibilität zu gewährleisten und in seltenen Fällen, in denen dies die beste Lösung für ein Problem ist, aber in Ihren F#-Programmen sollten alternative Techniken zur Gestaltung polygener Strukturen gesucht werden, wie zum Beispiel die Implementierung von Schnittstellen.
Funktions- und Membersignaturen
Verwenden von Tupeln für Rückgabewerte beim Zurückgeben einer kleinen Anzahl von mehreren nicht verknüpften Werten
Hier sehen Sie ein gutes Beispiel für die Verwendung eines Tupels in einem Rückgabetyp:
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
Bei Rückgabetypen, die viele Komponenten enthalten, oder wenn die Komponenten mit einer einzelnen identifizierbaren Entität verknüpft sind, sollten Sie einen benannten Typ anstelle eines Tupels verwenden.
Verwenden von Async<T>
für die asynchrone Programmierung an F#-API-Grenzen
Wenn es einen entsprechenden synchronen Vorgang namens Operation
gibt, der einen T
zurückgibt, sollte der asynchrone Vorgang AsyncOperation
benannt werden, wenn er Async<T>
oder OperationAsync
zurückgibt, wenn er Task<T>
zurückgibt. Für häufig verwendete .NET-Typen, die Begin/End-Methoden verfügbar machen, sollten Sie Async.FromBeginEnd
verwenden, um Erweiterungsmethoden als Fassade zu schreiben, um das F#-asynchrone Programmiermodell für diese .NET-APIs bereitzustellen.
type SomeType =
member this.Compute(x:int): int =
...
member this.AsyncCompute(x:int): Async<int> =
...
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
...
Ausnahmen
Informationen zur geeigneten Verwendung von Ausnahmen, Ergebnissen und Optionen finden Sie unter Fehlerverwaltung.
Erweiterungsmember
Sorgfältiges Anwenden von F#-Erweiterungsmembern in F#-zu-F#-Komponenten
F#-Erweiterungsmember sollten im Allgemeinen nur für Operationen verwendet werden, die in der Mehrzahl der Verwendungen zum Abschluss von systeminternen Operationen gehören, die einem Typ zugeordnet sind. Eine häufige Verwendung besteht darin, APIs bereitzustellen, die für verschiedene .NET-Typen idiomatischer für F# sind:
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
Union-Typen
Verwenden Sie diskriminierte Vereinigungen (discriminated unions) anstelle von Klassenhierarchien für baumstrukturierte Daten.
Baumartige Strukturen werden rekursiv definiert. Dies ist bei Vererbung umständlich, bei Unterscheidungs-Unions aber elegant.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
Das Darstellen von baumähnlichen Daten mit diskriminierten Vereinigungen ermöglicht es Ihnen auch, von der Vollständigkeit beim Musterabgleich zu profitieren.
Verwenden von [<RequireQualifiedAccess>]
für Union-Typen, deren Fallnamen nicht eindeutig genug sind
Möglicherweise befinden Sie sich in einer Domäne, in der derselbe Name der beste Name für verschiedene Dinge ist, z. B. Diskriminierte Union-Fälle. Sie können [<RequireQualifiedAccess>]
verwenden, um Fallnamen zu unterscheiden und zu vermeiden, dass verwirrende Fehler durch Shadowing abhängig von der Reihenfolge von open
-Anweisungen ausgelöst werden.
Verbergen der Darstellungen von Unterscheidungs-Unions für binärkompatible APIs, wenn der Entwurf dieser Typen wahrscheinlich weiterentwickelt wird
Union-Typen basieren auf F#-Musterabgleichsformen für ein prägnantes Programmiermodell. Wie bereits erwähnt, sollten Sie verhindern, dass konkrete Datendarstellungen angezeigt werden, wenn sich der Entwurf dieser Typen wahrscheinlich weiterentwickelt.
Beispielsweise kann die Darstellung einer diskriminierten Vereinigung mithilfe einer privaten oder internen Deklaration oder mithilfe einer Signaturdatei ausgeblendet werden.
type Union =
private
| CaseA of int
| CaseB of string
Wenn Sie diskriminierende Vereinigungen ohne Unterscheidung offenlegen, kann es schwierig sein, Ihre Bibliothek zu versionieren, ohne den Code der Benutzer zu beeinträchtigen. Erwägen Sie stattdessen, ein oder mehrere aktive Muster offenzulegen, um einen Musterabgleich für die Werte Ihres Typs zuzulassen.
Aktive Muster bieten eine alternative Möglichkeit, F#-Consumern Musterabgleiche zu ermöglichen, ohne F#-Union-Typen direkt verfügbar zu machen.
Inlinefunktionen und Membereinschränkungen
Definieren generischer numerischer Algorithmen mithilfe von Inlinefunktionen mit impliziten Membereinschränkungen und statisch aufgelösten generischen Typen
Arithmetische Membereinschränkungen und F#-Vergleichseinschränkungen sind ein Standard für die F#-Programmierung. Betrachten Sie z. B. den folgenden Code:
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
Der Typ dieser Funktion lautet wie folgt:
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
Dies ist eine geeignete Funktion für eine öffentliche API in einer mathematischen Bibliothek.
Vermeiden der Verwendung von Membereinschränkungen zum Simulieren von Typklassen und Duck-Typing
Es ist möglich, die mit von F#-Membereinschränkungen „Duck-Typing“zu simulieren. Member, die dies nutzen, sollten im Allgemeinen jedoch nicht in F#-zu-F#-Bibliotheksentwürfen verwendet werden. Dies liegt daran, dass Bibliotheksdesigns, die auf unbekannten oder nicht standardmäßigen impliziten Einschränkungen basieren, dazu führen, dass Benutzercode unflexibel und an ein bestimmtes Frameworkmuster gebunden wird.
Darüber hinaus besteht eine gute Chance, dass eine hohe Verwendung von Membereinschränkungen auf diese Weise zu sehr langen Kompilierungszeiten führen kann.
Operatordefinitionen
Vermeiden Sie das Definieren benutzerdefinierter symbolischer Operatoren
Benutzerdefinierte Operatoren sind in einigen Situationen von wesentlicher Bedeutung und sind äußerst nützliche Notationalgeräte innerhalb eines großen Implementierungscodes. Für neue Benutzer einer Bibliothek sind benannte Funktionen häufig einfacher zu verwenden. Darüber hinaus können benutzerdefinierte symbolische Operatoren schwer zu dokumentieren sein, und Benutzer finden es schwieriger, Hilfe zu Operatoren aufgrund vorhandener Einschränkungen in IDE und Suchmaschinen nachzuschlagen.
Daher ist es am besten, die Funktionalität als benannte Funktionen und Member zu veröffentlichen und Operatoren für diese Funktionalität nur dann verfügbar zu machen, wenn die notationalen Vorteile den Dokumentationsaufwand und die kognitiven Kosten dafür überwiegen.
Maßeinheiten
Verwenden Sie Maßeinheiten sorgfältig für die zusätzliche Typsicherheit im F#-Code.
Zusätzliche Eingabeinformationen für Maßeinheiten werden gelöscht, wenn sie von anderen .NET-Sprachen angezeigt werden. Beachten Sie, dass .NET-Komponenten, -Tools und -Reflektionen Typen ohne Einheiten sehen. C#-Consumer sehen z. B. float
anstelle von float<kg>
.
Abkürzungen nach Typ
Verwenden Sie sorgfältig Typkürzel, um F#-Code zu vereinfachen
.NET-Komponenten, -Tools und -Spiegelungen sehen keine abgekürzten Namen für Typen. Eine erhebliche Verwendung von Typkürzeln kann auch dazu führen, dass eine Domäne komplexer erscheint als tatsächlich, was die Verbraucher verwirren könnte.
Vermeiden von Typkürzeln für öffentliche Typen, deren Member und Eigenschaften sich von denen unterscheiden sollten, die für den abgekürzten Typ verfügbar sind
In diesem Fall zeigt der abgekürzte Typ zu viel über die Darstellung des tatsächlich definierten Typs an. Erwägen Sie stattdessen, die Abkürzung in einen Klassentyp oder eine Unterscheidungs-Union für den Einzelfall einzuschließen. (Oder erwägen Sie die Verwendung eines Strukturtyps zum Umschließen der Abkürzung, wenn die Leistung von Bedeutung ist.)
Es ist z. B. verlockend, eine Multimap als Sonderfall einer F#-Map zu definieren, z. B.:
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
Die logischen Punktnotationsvorgänge für diesen Typ sind jedoch nicht mit den Vorgängen auf einer Karte identisch– beispielsweise ist es sinnvoll, dass der Nachschlageoperator map[key]
die leere Liste zurückgibt, wenn sich der Schlüssel nicht im Wörterbuch befindet, anstatt eine Ausnahme zu auslösen.
Richtlinien für Bibliotheken, die in anderen .NET-Sprachen verwendet werden sollen
Beim Entwerfen von Bibliotheken für die Verwendung aus anderen .NET-Sprachen ist es wichtig, die .NET Library Design Guidelineseinzuhalten. In diesem Dokument werden diese Bibliotheken als Vanille .NET-Bibliotheken bezeichnet, im Gegensatz zu F#-bezogenen Bibliotheken, die F#-Konstrukte ohne Einschränkung verwenden. Das Entwerfen von Vanille .NET-Bibliotheken bedeutet, vertraute und idiomatische APIs bereitzustellen, die mit dem Rest von .NET Framework konsistent sind, indem die Verwendung von F#-spezifischen Konstrukten in der öffentlichen API minimiert wird. Die Regeln werden in den folgenden Abschnitten erläutert.
Namespace- und Typentwurf (für Bibliotheken, die in anderen .NET-Sprachen verwendet werden sollen)
Anwenden der .NET-Benennungskonventionen auf die öffentliche API Ihrer Komponenten
Achten Sie besonders auf die Verwendung gekürzter Namen und der .NET-Großschreibungsrichtlinien.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Verwenden von Namespaces, Typen und Mitgliedern als primäre Organisationsstruktur für Ihre Komponenten
Alle Dateien, die öffentliche Funktionen enthalten, sollten mit einer namespace
Deklaration beginnen, und die einzigen öffentlich zugänglichen Entitäten in Namespaces sollten Typen sein. Verwenden Sie keine F#-Module.
Verwenden Sie nicht öffentliche Module, um Implementierungscode, Hilfsprogrammtypen und Hilfsfunktionen zu speichern.
Statische Typen sollten gegenüber Modulen bevorzugt werden, da sie die zukünftige Entwicklung der API zur Verwendung von Überladungen und anderen .NET-API-Entwurfskonzepten ermöglichen, die möglicherweise nicht in F#-Modulen verwendet werden.
Beispiel: anstelle der folgenden öffentlichen API:
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Erwägen Sie stattdessen Folgendes:
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
Verwenden Sie F#-Datensatztypen in Vanille .NET-APIs, wenn sich der Entwurf der Typen nicht weiterentwickelt
F#-Datensatztypen werden in einer einfachen .NET-Klasse kompiliert. Diese eignen sich für einige einfache, stabile Typen in APIs. Erwägen Sie die Verwendung der attribute [<NoEquality>]
und [<NoComparison>]
, um die automatische Generierung von Schnittstellen zu unterdrücken. Vermeiden Sie auch die Verwendung von änderbaren Datensatzfeldern in Vanille .NET-APIs, da diese ein öffentliches Feld verfügbar machen. Überlegen Sie immer, ob eine Klasse eine flexiblere Option für die zukünftige Entwicklung der API bieten würde.
Der folgende F#-Code macht z. B. die öffentliche API für einen C#-Consumer verfügbar:
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; }
}
Verbergen der Darstellung von F#-Union-Typen in Vanilla .NET-APIs
F#-Union-Typen werden nicht häufig über Komponentengrenzen hinweg verwendet, selbst beim F#-zu-F#-Codieren. Sie sind ein hervorragendes Implementierungsgerät, wenn sie intern in Komponenten und Bibliotheken verwendet werden.
Beim Entwerfen einer standardmäßigen .NET-API sollten Sie die Darstellung eines Vereinigungstyps ausblenden, indem Sie entweder eine private Deklaration oder eine Signaturdatei verwenden.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Sie können auch Typen, die eine Union-Darstellung verwenden, intern mit Membern erweitern, um eine gewünschte .NET-orientierte API bereitzustellen.
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)
Entwerfen der GUI und anderer Komponenten mithilfe der Entwurfsmuster des Frameworks
Es gibt viele verschiedene Frameworks in .NET, z. B. WinForms, WPF und ASP.NET. Benennungs- und Entwurfskonventionen für jede sollten verwendet werden, wenn Sie Komponenten für die Verwendung in diesen Frameworks entwerfen. Wenden Sie zum Beispiel für die WPF-Programmierung WPF-Entwurfsmuster auf die Klassen an, die Sie gestalten. Verwenden Sie für Modelle in der Benutzeroberflächenprogrammierung Entwurfsmuster wie Ereignisse und Benachrichtigungsbasierte Auflistungen wie die in System.Collections.ObjectModelgefundenen.
Objekt- und Memberdesign (für Bibliotheken zur Verwendung mit anderen .NET-Sprachen)
Verwenden des CLIEvent-Attributs zum Verfügbarmachen von .NET-Ereignissen
Erstellen Sie eine DelegateEvent
mit einem bestimmten .NET-Delegattyp, der ein Objekt verwendet, und EventArgs
(anstelle einer Event
, die nur den FSharpHandler
Typ standardmäßig verwendet), damit die Ereignisse auf die vertraute Weise in anderen .NET-Sprachen veröffentlicht werden.
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
Verfügbarmachen asynchroner Vorgänge als Methoden, die .NET-Aufgaben zurückgeben
Aufgaben werden in .NET verwendet, um aktive asynchrone Berechnungen darzustellen. Aufgaben sind im Allgemeinen weniger kompositorativ als F#-Async<T>
-Objekte, da sie "bereits ausgeführte" Aufgaben darstellen und nicht so zusammengesetzt werden können, dass parallele Kompositionen ausgeführt werden oder die die Verteilung von Abbruchsignalen und anderen kontextbezogenen Parametern ausblenden.
Trotzdem sind Methoden, die Tasks zurückgeben, die Standarddarstellung der asynchronen Programmierung in .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
Häufig möchten Sie auch ein explizites Abbruchtoken akzeptieren:
/// 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)
Verwenden von .NET-Delegattypen anstelle von F#-Funktionstypen
"F#-Funktionstypen" bedeuten "Pfeil"-Typen wie int -> int
.
Stattdessen:
member this.Transform(f: int->int) =
...
Gehen Sie wie folgt vor:
member this.Transform(f: Func<int,int>) =
...
Der F#-Funktionstyp wird als class FSharpFunc<T,U>
für andere .NET-Sprachen angezeigt und eignet sich weniger für Sprachfeatures und Tools, die Stellvertretungstypen verstehen. Wenn Sie eine höherrangige Methode erstellen, die auf .NET Framework 3.5 oder höher ausgerichtet ist, sind die Delegaten System.Func
und System.Action
die richtigen APIs für die Veröffentlichung, damit .NET-Entwickler diese APIs problemlos nutzen können. (Bei der Zielbestimmung von .NET Framework 2.0 sind die vom System definierten Delegattypen eingeschränkter. Erwägen Sie die Verwendung vordefinierter Delegattypen wie System.Converter<T,U>
oder Definieren eines bestimmten Delegattyps.)
Auf der anderen Seite sind .NET-Delegate für F#-orientierte Bibliotheken unnatürlich (siehe nächster Abschnitt zu F#-orientierten Bibliotheken). Daher besteht eine allgemeine Implementierungsstrategie bei der Entwicklung von Methoden mit höherer Reihenfolge für Vanille .NET-Bibliotheken darin, alle Implementierungen mit F#-Funktionstypen zu erstellen und dann die öffentliche API mithilfe von Delegaten als dünne Fassade auf die tatsächliche F#-Implementierung zu erstellen.
Verwenden Sie das TryGetValue-Muster, anstatt F#-Optionswerte zurückzugeben, und ziehen Sie die Methodenüberladung vor, anstatt F#-Optionswerte als Argumente zu verwenden.
Allgemeine Verwendungsmuster für den F#-Optionstyp in APIs werden besser in Vanille .NET-APIs mit standardmäßigen .NET-Entwurfstechniken implementiert. Anstatt einen F#-Optionswert zurückzugeben, sollten Sie den Bool-Rückgabetyp plus einen Out-Parameter wie im Muster "TryGetValue" verwenden. Und anstatt F#-Optionswerte als Parameter zu verwenden, sollten Sie die Verwendung von Methodenüberladungen oder optionalen Argumenten in Betracht ziehen.
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
Verwenden der .NET-Auflistungsschnittstellentypen IEnumerable<T> und IDictionary<Schlüssel,Wert> für Parameter und Rückgabewerte
Vermeiden Sie die Verwendung konkreter Sammlungstypen wie .NET-Arrays T[]
, F#-Typen list<T>
, Map<Key,Value>
und Set<T>
sowie .NET-Konkretsammlungstypen wie Dictionary<Key,Value>
. Die .NET Library Design Guidelines haben gute Ratschläge, wann verschiedene Sammlungstypen wie IEnumerable<T>
verwendet werden sollen. Einige Verwendung von Arrays (T[]
) ist unter bestimmten Umständen aus Leistungsgründen akzeptabel. Beachten Sie insbesondere, dass seq<T>
nur der F#-Alias für IEnumerable<T>
ist und seq daher oft ein geeigneter Typ für eine Standard-.NET-API ist.
Anstelle von F#-Listen:
member this.PrintNames(names: string list) =
...
Verwenden Sie F#-Sequenzen:
member this.PrintNames(names: seq<string>) =
...
Verwenden Sie den Einheitentyp als einzigen Eingabetyp einer Methode, um eine Null-Argument-Methode zu definieren, oder als einziger Rückgabetyp zum Definieren einer Void-Returning-Methode
Vermeiden Sie andere Verwendungen des Einheitentyps. Dies sind gut:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
Das ist schlecht:
member this.WrongUnit( x: unit, z: int) = ((), ())
Überprüfen auf NULL-Werte an den Grenzen der Vanilla .NET-API
F#-Implementierungscode hat aufgrund unveränderlicher Entwurfsmuster und Einschränkungen bei der Verwendung von Nullliteralen für F#-Typen tendenziell weniger Nullwerte. Andere .NET-Sprachen verwenden häufig null als Wert viel häufiger. Aus diesem Grund sollten F#-Code, der eine Vanille .NET-API verfügbar macht, Parameter auf NULL an der API-Grenze überprüfen und verhindern, dass diese Werte tiefer in den F#-Implementierungscode fließen. Die Funktion isNull
oder der Musterabgleich für das null
-Muster kann verwendet werden.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
Ab F# 9 können Sie die neue | null
Syntax nutzen, um den Compiler auf mögliche NULL-Werte hinzuweisen und wo diese behandelt werden müssen:
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 ()
In F# 9 gibt der Compiler eine Warnung aus, wenn erkannt wird, dass ein möglicher Nullwert nicht behandelt wird:
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
Diese Warnungen sollten mit dem NULL-Muster in F# beim Abgleich behoben werden:
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
Vermeiden der Verwendung von Tupeln als Rückgabewerte
Geben Sie stattdessen einen benannten Typ zurück, der die Aggregatdaten enthält, oder verwenden Sie Parameter, um mehrere Werte zurückzugeben. Obwohl Tupel und Strukturtupel in .NET vorhanden sind (einschließlich C#-Sprachunterstützung für Strukturtupel), stellen sie in den meisten Fällen nicht die ideale und erwartete API für .NET-Entwickler bereit.
Vermeiden der Verstümmelung von Parametern
Verwenden Sie stattdessen .NET-Aufrufkonventionen Method(arg1,arg2,…,argN)
.
member this.TupledArguments(str, num) = String.replicate num str
Tipp: Wenn Sie Bibliotheken für die Verwendung aus einer beliebigen .NET-Sprache entwerfen, gibt es keinen Ersatz für die tatsächlich experimentelle C#- und Visual Basic-Programmierung, um sicherzustellen, dass Sich Ihre Bibliotheken in diesen Sprachen "richtig fühlen". Sie können auch Tools wie .NET Reflector und den Visual Studio-Objektbrowser verwenden, um sicherzustellen, dass Bibliotheken und deren Dokumentation für Entwickler erwartungsgemäß angezeigt werden.
Anhang
End-to-End-Beispiel für das Entwerfen von F#-Code für die Verwendung durch andere .NET-Sprachen
Berücksichtigen Sie die folgende Klasse:
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) ]
Der abgeleitete F#-Typ dieser Klasse lautet wie folgt:
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
Sehen wir uns an, wie dieser F#-Typ einem Programmierer mit einer anderen .NET-Sprache angezeigt wird. Die ungefähre C#-Signatur lautet z. B. wie folgt:
// 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; }
}
Es gibt einige wichtige Punkte zu beachten, wie F# hier Konstrukte darstellt. Zum Beispiel:
Metadaten wie Argumentnamen wurden beibehalten.
F#-Methoden, die zwei Argumente verwenden, werden zu C#-Methoden, die zwei Argumente annehmen.
Funktionen und Listen werden zu Verweisen auf entsprechende Typen in der F#-Bibliothek.
Der folgende Code zeigt, wie Sie diesen Code anpassen, um diese Dinge zu berücksichtigen.
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) }
Der abgeleitete F#-Typ des Codes lautet wie folgt:
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
Die C#-Signatur lautet jetzt wie folgt:
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; }
}
Die Korrekturen, die zum Vorbereiten dieses Typs für die Verwendung als Teil einer Vanille .NET-Bibliothek vorgenommen wurden, sind wie folgt:
Mehrere Namen wurden angepasst:
Point1
,n
,l
, undf
wurdenRadialPoint
,count
,factor
, bzw.transform
.Verwendet einen Rückgabetyp von
seq<RadialPoint>
anstelle vonRadialPoint list
durch Ändern einer Listenkonstruktion mithilfe von[ ... ]
in eine Sequenzkonstruktion mithilfe vonIEnumerable<RadialPoint>
.Verwendet .NET-Delegattyp
System.Func
anstelle eines F#-Funktionstyps.
Dies macht die Nutzung in C#-Code wesentlich einfacher.