Entwurfsrichtlinien für eine F#-Komponente

Dieses Dokument enthält mehrere Komponentenentwurfsrichtlinien für die F#-Programmierung, die auf den Entwurfsrichtlinien für F#-Komponenten, v14, Microsoft Research und einer Version basieren, die ursprünglich von der F# Software Foundation zusammengestellt und gepflegt wurde.

In diesem Dokument wird vorausgesetzt, 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.

Übersicht

In diesem Dokument werden einige der Probleme im Zusammenhang mit dem Entwurf und der Codierung von F#-Komponenten behandelt. Bei einer Komponente kann es sich handeln um:

  • Eine Ebene in Ihrem F#-Projekt, die über externe Consumer im betreffenden Projekt verfügt.
  • Eine Bibliothek, die für die Nutzung durch F#-Code über Assemblygrenzen hinweg vorgesehen ist.
  • Eine Bibliothek, die für die Nutzung durch eine .NET-Sprache über Assemblygrenzen hinweg vorgesehen ist.
  • Eine Bibliothek, die für die Verteilung über ein Paketrepository vorgesehen ist, z. B. NuGet.

Die in diesem Artikel beschriebenen Techniken folgen den fünf Prinzipien eines guten F#-Codes und nutzen daher je nach Bedarf sowohl die funktionale Programmierung als auch die objektorientierte Programmierung.

Unabhängig von der Methodik steht der Komponenten- und Bibliotheksdesigner vor einer Reihe praktischer und alltäglicher Probleme, wenn er eine API erstellen möchte, die von Entwicklern sehr einfach verwendet werden kann. Die gewissenhafte Anwendung der Entwurfsrichtlinien für .NET-Bibliotheken ermöglicht die Erstellung konsistenter APIs, die unproblematisch genutzt werden können.

Allgemeine Richtlinien

Unabhängig von der Zielgruppe für die Bibliothek gibt es einige universelle Richtlinien, die für F#-Bibliotheken gelten.

Informationen zu den Entwurfsrichtlinien für .NET-Bibliotheken

Unabhängig von der jeweils verwendeten F#-Codierung sind Kenntnisse der Entwurfsrichtlinien für .NET-Bibliotheken hilfreich. Die meisten anderen F#- und .NET-Programmierer sind mit diesen Richtlinien vertraut und erwarten, dass .NET-Code diesen Richtlinien entspricht.

Die Entwurfsrichtlinien für .NET-Bibliotheken bieten allgemeine Hinweise zu Benennung, zum Entwurf von Klassen und Schnittstellen, zum Entwurf von Membern (Eigenschaften, Methoden, Ereignisse usw.) und sind ein nützlicher erster Referenzpunkt für eine Vielzahl von Entwurfsleitfäden.

Hinzufügen von XML-Dokumentationskommentaren zu Ihrem Code

Die XML-Dokumentation zu öffentlichen APIs stellt sicher, dass Benutzer bei Verwendung dieser Typen und Member hervorragende IntelliSense-Informationen und QuickInfos erhalten, und ermöglicht die Erstellung von Dokumentationsdateien für die Bibliothek. 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 XML-Kurzkommentare (/// comment) oder XML-Standardkommentare (///<summary>comment</summary>) verwenden.

Erwägen der Verwendung expliziter Signaturdateien (FSI) für stabile Bibliotheks- und Komponenten-APIs

Die Verwendung expliziter Signaturdateien in einer F#-Bibliothek bietet eine kurz gefasste Übersicht über die öffentlichen API, sodass die gesamte öffentliche Oberfläche der Bibliothek bekannt ist, und ermöglicht eine saubere Trennung zwischen der öffentlichen Dokumentation und internen Implementierungsdetails. Signaturdateien bewirken jedoch Mehraufwand, wenn öffentliche APIs geändert werden, da sowohl in der Implementierung als auch in den Signaturdateien Änderungen vorgenommen werden müssen. Daher sollten Signaturdateien normalerweise nur eingeführt werden, wenn eine API verfestigt wurde und keine wesentlichen Änderungen mehr erwartet werden.

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#-orientierter 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. Wenn keine spezifischen Empfehlungen folgen, können ersatzweise die .NET-Bibliotheksentwurfsrichtlinien verwendet werden.

Benennungskonventionen

Verwenden der Benennungskonventionen und Konventionen für die Groß-/Kleinschreibung von .NET

Die folgende Tabelle folgt den Benennungskonventionen und Konventionen für die Groß-/Kleinschreibung von .NET. Darüber hinaus gibt es geringfügige Ergänzungen, die für F#-Konstrukte gelten. Diese Empfehlungen sind vor allem für APIs gedacht, welche die Grenzen von F# zu F# überschreiten und zu den Idiomen der .NET BCL und der meisten Bibliotheken passen.

Konstrukt Fall Teil Beispiele Notizen
Konkrete Typen PascalCase Nomen/Adjektiv List, Double, Complex Konkrete Typen sind Strukturen, Klassen, Enumerationen, Delegate, Datensätze und Unions. Obwohl Typnamen in OCaml traditionell klein geschrieben werden, 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 CurrentName
Schnittstellentypen PascalCase Nomen/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 Nomen/Verb getValue, myTable
let-Werte (extern) camelCase oder PascalCase Nomen/Verb List.map, Dates.Today let-gebundene Werte sind häufig öffentlich, wenn die herkömmlichen funktionalen Entwurfsmuster befolgt werden. Verwenden Sie im Allgemeinen jedoch PascalCase, wenn der Bezeichner in anderen .NET-Sprachen verwendet werden kann.
Eigenschaft PascalCase Nomen/Adjektiv IsEndOfFile, BackColor Boolesche Eigenschaften verwenden im Allgemeinen „Is“ und „Can“ und sollten affirmativ sein, wie in IsEndOfFile, nicht IsNotEndOfFile.

Vermeiden von Abkürzungen

Die .NET-Richtlinien raten von der Verwendung von Abkürzungen ab (z. B. Verwendung von OnButtonClick anstatt OnBtnClick). Allgemeine Abkürzungen, z. B. Async für „Asynchron“, werden toleriert. Diese Richtlinie wird bei der funktionalen Programmierung manchmal ignoriert; List.iter verwendet beispielsweise eine Abkürzung für „iterate“. Aus diesem Grund wird die Verwendung von Abkürzungen in der F#-zu-F#-Programmierung eher toleriert, sollte aber im Entwurf öffentlicher Komponenten generell 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.

Verwenden von Akronymen, soweit möglich

Akronyme wie XML sind keine Abkürzungen und werden in .NET-Bibliotheken weit verbreitet mit Kleinschreibung (Xml) verwendet. Es sollten nur bekannte, allgemein anerkannte Akronyme verwendet werden.

Verwenden von PascalCase für generische Parameternamen

Verwenden Sie PascalCase für generische Parameternamen in öffentlichen APIs, auch für F#-orientierte Bibliotheken. Verwenden Sie insbesondere Namen wie T, U, T1, T2 für beliebige generische Parameter. Verwenden Sie für F#-orientierte Bibliotheken Namen wie Key, ValueArg (aber beispielsweise nicht TKey), und wenn bestimmte Namen sinnvoll sind.

Verwenden von PascalCase oder camelCase für öffentliche Funktionen und Werte in F#-Modulen

camelCase wird für öffentliche Funktionen, die nicht qualifiziert verwendet werden sollen (z. B. invalidArg), und für die „Standardauflistungsfunktionen“ (z. B. List.map) verwendet. In beiden Fällen fungieren die Funktionsnamen ähnlich wie Schlüsselwörter in der Sprache.

Objekt-, Typ- und Modulentwurf

Verwenden von Namespaces oder Modulen, die Typen und Module enthalten sollen

Jede F#-Datei in einer Komponente sollte 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 der obersten Ebene sind:

  • Namespaces können mehrere Dateien umfassen.
  • Namespaces dürfen keine F#-Funktionen enthalten, es sei denn, sie befinden sich in einem inneren Modul.
  • 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 internes Modul erforderlich ist.

Die Auswahl zwischen einem Namespace oder Modul auf der obersten Ebene wirkt sich auf die kompilierte Form des Codes aus und beeinflusst daher die Sicht aus anderen .NET-Sprachen, falls die API später außerhalb von F#-Code genutzt wird.

Verwenden von Methoden und Eigenschaften für objekttypinterne Operationen

Bei der Arbeit mit Objekten sollte die nutzbare Funktionalität im Idealfall als Methoden und Eigenschaften für den betreffenden 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 einen bestimmten Member muss nicht unbedingt im jeweiligen Member implementiert werden; der nutzbare Teil der Funktionalität sollte jedoch im Member implementiert werden.

Verwenden von Klassen zum Kapseln des änderbaren Zustands

In F# ist dies nur notwendig, wenn der betreffende Zustand nicht bereits von einem anderen Sprachkonstrukt gekapselt ist, z. B. durch einen Endausdruck, einen Sequenzausdruck oder eine asynchrone Berechnung.

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

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

Verwenden Sie Schnittstellentypen, um eine Gruppe von Operationen 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 das erreichen können, was normalerweise Funktoren möglich machen. Darüber hinaus können sie zum Codieren existenzieller Typen im Programm verwendet werden, was Datensätze von Funktionen nicht können.

Verwenden eines Moduls zum Gruppieren von Funktionen, die für Auflistungen verwendet werden

Wenn Sie einen Auflistungstyp definieren, sollten Sie eine Standardgruppe von Operationen wie CollectionType.map und CollectionType.iter für neue Auflistungstypen bereitstellen.

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

Befolgen Sie die Standardbenennungskonventionen für Funktionen in FSharp.Core, wenn Sie ein solches Modul einschließen.

Verwenden eines Moduls zum Gruppieren von Funktionen für allgemeine kanonische Funktionen, insbesondere in mathematischen und DSL-Bibliotheken

Microsoft.FSharp.Core.Operators ist beispielsweise eine automatisch geöffnete Auflistung von Funktionen auf der obersten Ebene (wie abs und sin), die von FSharp.Core.dll bereitgestellt werden.

Ebenso kann eine Statistikbibliothek ein Modul mit den Funktionen erf und erfc enthalten, wobei dieses Modul explizit oder automatisch geöffnet werden kann.

Erwägen der Verwendung von RequireQualifiedAccess und sorgfältige Anwendung von AutoOpen-Attributen

Das Hinzufügen des Attributs [<RequireQualifiedAccess>] zu einem Modul gibt an, dass das Modul möglicherweise nicht geöffnet werden darf und für Verweise auf die Elemente des Moduls expliziter qualifizierter Zugriff erforderlich ist. Das Modul Microsoft.FSharp.Collections.List verfügt beispielsweise über dieses Attribut.

Dies ist nützlich, wenn Funktionen und Werte im Modul Namen aufweisen, die wahrscheinlich mit Namen in anderen Modulen in Konflikt stehen. Durch Forderung eines qualifizierten Zugriffs können die langfristige Wartbarkeit und die Möglichkeiten der Weiterentwicklung einer Bibliothek erheblich verbessert werden.

Es wird dringend empfohlen, das [<RequireQualifiedAccess>]-Attribut für benutzerdefinierte Module zu verwenden, die die von FSharp.Core bereitgestellten Module (z. B. Seq, List, Array) erweitern, da diese Module in F#-Code häufig verwendet und [<RequireQualifiedAccess>] auf in ihnen definiert ist; generell wird davon abgeraten, benutzerdefinierte Module ohne das Attribut zu definieren, wenn ein solches Modul andere Module, die das Attribut haben, überschattet oder erweitert.

Das Hinzufügen des [<AutoOpen>]-Attributs zu einem Modul bedeutet, dass das Modul geöffnet wird, wenn der Namespace geöffnet wird, der das Modul enthält. Das [<AutoOpen>]-Attribut 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 einen Verweis auf ein Modul enthalten (module MathsHeaven.Statistics.Operators), das die Funktionen erf und erfc enthält. Es ist sinnvoll, dieses Modul als [<AutoOpen>] zu markieren. Dies bedeutet, dass open MathsHeaven.Statistics auch dieses Modul öffnet, und die Namen erf und erfc verfügbar werden. Eine weitere gute Verwendung von [<AutoOpen>] sind Module, die Erweiterungsmethoden enthalten.

Eine übermäßige Verwendung von [<AutoOpen>] führt zu unübersichtlichen Namespaces, sodass das Attribut mit Sorgfalt verwendet werden sollte. Für bestimmte Bibliotheken in bestimmten Domänen kann eine umsichtige Verwendung von [<AutoOpen>] die Benutzerfreundlichkeit verbessern.

Erwägen der Definition von Operatormembern für Klassen, die für die Verwendung bekannter Operatoren geeignet sind

Manchmal werden Klassen verwendet, um mathematische Konstrukte wie Vektoren zu modellieren. Wenn die modellierte Domäne über bekannte Operatoren verfügt, ist es hilfreich, diese als klasseninterne Member zu definieren.

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

Diese Richtlinie entspricht den allgemeinen .NET-Richtlinien für diese Typen. Es kann jedoch bei der F#-Codierung zusätzlich 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 kann es wünschenswert sein, einen Namen in einem Format für F#-Consumer zu erstellen (z. B. ein statischer Member in Kleinbuchstaben, sodass er wie eine modulgebundene Funktion aussieht), beim Kompilieren in eine Assembly aber ein anderes Format für den Namen zu verwenden. 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-Namenskonventionen für Nicht-F#-Consumer der Assembly verwenden.

Verwenden der Methodenüberladung für Memberfunktionen, um so eine einfachere API bereitzustellen

Die Methodenüberladung ist ein leistungsfähiges Tool zur Vereinfachung einer API, die möglicherweise ähnliche Funktionen, aber mit unterschiedlichen Optionen oder Argumenten, ausführen muss.

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 die Offenlegung konkreter Darstellungen von Objekten. Beispielsweise wird die konkrete Darstellung von DateTime-Werten nicht durch die externe, öffentliche API des .NET-Bibliotheksentwurfs offen gelegt. Zur Laufzeit kennt die Common Language Runtime die festgelegte Implementierung, die bei der Ausführung verwendet wird. Der kompilierte Code selbst 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 schwer zu ändern, wenn neue Anforderungen auftreten. Die Implementierung der Vererbung ist in F# aus Kompatibilitätsgründen und für seltene Fällen, in denen sie die beste Lösung für ein Problem ist, weiterhin vorhanden. Beim Entwurf für Polymorphie, z. B. Schnittstellenimplementierung, sollten Sie nach alternativen Techniken in Ihren F#-Programmen suchen.

Funktions- und Membersignaturen

Verwenden von Tupeln für Rückgabewerte beim Zurückgeben einer kleinen Anzahl von nicht zusammenhängenden Werten

Hier sehen Sie ein gutes Beispiel für die Verwendung eines Tupels in einem Rückgabetyp:

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

Für Rückgabetypen, die viele Komponenten enthalten, oder bei denen die Komponenten mit einer einzelnen identifizierbaren Entität in Beziehung stehen, sollten Sie anstelle eines Tupels einen benannten Typ verwenden.

Verwenden von Async<T> für die asynchrone Programmierung an F#-API-Grenzen

Wenn es eine entsprechende synchrone Operation namens Operation gibt, die T zurückgibt, sollte der asynchrone Vorgang AsyncOperation genannt werden, wenn Async<T> zurückgegeben wird, bzw. OperationAsync, wenn Task<T> zurückgegeben wird. Für häufig verwendete .NET-Typen, die Begin/End-Methoden verfügbar machen, sollten Sie Async.FromBeginEnd zum Schreiben von Erweiterungsmethoden als Fassade verwenden, um das asynchrone F#-Programmiermodell diesen .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 für verschiedene .NET-Typen bereitzustellen, die für die Sprache F# üblicher 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 von Unterscheidungs-Unions anstelle von Klassenhierarchien für baumstrukturierte Daten

Baumähnliche 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>

Durch die Darstellung von baumähnlichen Daten mit Unterscheidungs-Unions können Sie auch von der umfassenden Funktionalität für Musterabgleiche 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 ein und derselbe Name der beste Name für verschiedene Dinge ist, z. B. Fälle mit Unterscheidungs-Unions. 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 es vermeiden, konkrete Datendarstellungen offenzulegen, wenn der Entwurf dieser Typen wahrscheinlich weiterentwickelt wird.

Beispielsweise kann die Darstellung einer Unterscheidungs-Union mit einer privaten oder internen Deklaration bzw. mit einer Signaturdatei verborgen werden.

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

Wenn Sie Unterscheidungs-Unions wahllos offenlegen, wird die Versionierung der Bibliothek möglicherweise erschwert, ohne den Benutzercode verändern zu müssen. 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 für die F#-Programmierung normal. Beachten Sie z. B. 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 Bibliotheksentwürfe, die auf ungewohnten 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 hohe Wahrscheinlichkeit, dass eine derartige umfangreiche Verwendung von Membereinschränkungen zu sehr langen Kompilierzeiten führen kann.

Operatordefinitionen

Vermeiden der Definition von benutzerdefinierten symbolischen Operatoren

Benutzerdefinierte Operatoren sind in einigen Situationen unerlässlich und sind sehr nützliche notationale Einrichtungen in einem umfangreichen Implementierungscode. Für neue Benutzer einer Bibliothek sind benannte Funktionen oftmals einfacher zu verwenden. Darüber hinaus können benutzerdefinierte symbolische Operatoren schwer zu dokumentieren sein, sodass es für Benutzer aufgrund vorhandener Einschränkungen in IDE und Suchmaschinen schwieriger sein kann, Hilfe für solche Operatoren zu erhalten.

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

Sorgfältige Verwendung von Maßeinheiten für zusätzliche Typsicherheit im F#-Code

Zusätzliche Typisierungsinformationen 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 beispielsweise float anstelle von float<kg>.

Typabkürzungen

Sorgfältige Verwendung von Typkürzeln zum Vereinfachen von F#-Code

.NET-Komponenten, -Tools und -Reflektionen sehen keine abgekürzten Namen für Typen. Die signifikante Verwendung von Typkürzeln kann auch dazu führen, dass eine Domäne komplexer erscheint, als sie tatsächlich ist, was Consumer verwirren kann.

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 gibt der abgekürzte Typ zu viel über die Darstellung des tatsächlich definierten Typs preis. 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 beispielsweise verlockend, eine Mehrfachzuordnung als Sonderfall einer F#-Zuordnung zu definieren, z. B.:

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

Die logischen Operationen mit Punktnotation für diesen Typ sind jedoch nicht mit den Operationen für eine Zuordnung identisch. Beispielsweise ist es sinnvoll, dass der Nachschlageoperator map[key] eine leere Liste zurückgibt, wenn sich der Schlüssel nicht im Wörterbuch befindet, anstatt eine Ausnahme auszulösen.

Richtlinien für Bibliotheken, die in anderen .NET-Sprachen verwendet werden sollen

Beim Entwerfen von Bibliotheken, die in anderen .NET-Sprachen verwendet werden sollen, müssen die Entwurfsrichtlinien für .NET-Bibliotheken unbedingt eingehalten werden. In diesem Dokument werden diese Bibliotheken als Vanilla .NET-Bibliotheken bezeichnet, im Gegensatz zu F#-orientierten Bibliotheken, die F#-Konstrukte ohne Einschränkung verwenden. Der Entwurf von Vanilla .NET-Bibliotheken bedeutet, vertraute und idiomatische APIs bereitzustellen, die mit dem Rest des .NET-Frameworks vereinbar 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 abgekürzter Namen und die .NET-Groß-/Kleinschreibungsrichtlinien.

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

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

Verwenden von Namespaces, Typen und Membern 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 Hilfsprogrammfunktionen zu speichern.

Statische Typen sollten gegenüber Modulen bevorzugt werden, da sie eine zukünftige Weiterentwicklung der API ermöglichen, um Überladung und andere .NET-API-Entwurfskonzepte verwenden zu können, die nicht in F#-Modulen verwendet werden können.

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

Sollten Sie beispielsweise stattdessen in Betracht ziehen:

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 von F#-Datensatztypen in Vanilla .NET-APIs, wenn der Entwurf der Typen nicht weiterentwickelt wird

F#-Datensatztypen werden in eine einfache .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 außerdem die Verwendung von veränderbaren Datensatzfeldern in Vanilla .NET-APIs, da diese ein öffentliches Feld verfügbar machen. Überlegen Sie immer, ob eine Klasse eine flexiblere Option für die zukünftige Weiterentwicklung der API bietet.

Der folgende F#-Code macht beispielsweise 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 häufig nicht über Komponentengrenzen hinweg verwendet, auch nicht für die F#-zu-F#-Codierung. Sie sind eine ausgezeichnete Implementierungseinrichtung, wenn sie intern in Komponenten und Bibliotheken verwendet werden.

Beim Entwurf einer Vanilla .NET-API sollten Sie die Darstellung eines Union-Typs verbergen, indem Sie 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 unter Verwendung der Entwurfsmuster des Frameworks

In .NET sind viele verschiedene Frameworks verfügbar, z. B. WinForms, WPF und ASP.NET. Wenn Sie Komponenten für die Verwendung in diesen Frameworks entwerfen, sollten die jeweiligen Benennungs- und Entwurfskonventionen verwendet werden. Verwenden Sie beispielsweise bei der WPF-Programmierung die WPF-Entwurfsmuster für die von Ihnen entworfenen Klassen. Verwenden Sie für Modelle in der Benutzeroberflächenprogrammierung Entwurfsmuster wie Ereignisse und benachrichtigungsbasierte Auflistungen, z. B. die in System.Collections.ObjectModel.

Objekt- und Memberentwurf (für Bibliotheken, die in anderen .NET-Sprachen verwendet werden sollen)

Verwenden des CLIEvent-Attributs zum Verfügbarmachen von .NET-Ereignissen

Erstellen Sie ein DelegateEvent mit einem bestimmten .NET-Delegattyp, der ein Objekt und EventArgs (anstelle von Event verwendet, das standardmäßig nur den FSharpHandler-Typ verwendet), damit die Ereignisse auf 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 Operationen als Methoden, die .NET-Aufgaben zurückgeben

Aufgaben werden in .NET verwendet, um aktive asynchrone Berechnungen darzustellen. Aufgaben sind im Allgemeinen weniger kompositionell als Async<T>-Objekte von F#, da sie „bereits ausgeführte“ Aufgaben darstellen und nicht so zusammen zusammengesetzt werden können, dass eine parallele Komposition erfolgt, oder die die Weitergabe von Abbruchsignalen und anderen kontextbezogenen Parametern verbergen.

Trotzdem sind Methoden, die Aufgaben 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 kann es auch wünschenswert sein, ein explizites Abbruchtoken zu 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

Hier bedeuten "F#-Funktionstypen" Pfeiltypen wie int -> int.

Anstelle von:

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

Gehen Sie folgendermaßen vor:

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

Der F#-Funktionstyp wird für andere .NET-Sprachen als class FSharpFunc<T,U> angezeigt und eignet sich weniger für Sprachfeatures und Tools, die Delegattypen 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 Ausrichtung auf .NET Framework 2.0 sind die systemdefinierten Delegattypen eingeschränkter. Erwägen Sie die Verwendung vordefinierter Delegattypen wie System.Converter<T,U> oder das Definieren eines speziellen 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 gängige Implementierungsstrategie bei der Entwicklung höherrangiger Methoden für Vanilla .NET-Bibliotheken darin, die gesamte Implementierung mit F#-Funktionstypen zu erstellen und dann die öffentliche API mithilfe von Delegaten als dünne Fassade über der tatsächlichen F#-Implementierung zu erstellen.

Verwenden des TryGetValue-Musters, anstatt F#-Optionswerte zurückzugeben, und Bevorzugen der Methodenüberladung, um F#-Optionswerte als Argumente zu übernehmen

Gängige Verwendungsmuster für den F#-Optionstyp in APIs werden besser in Vanilla .NET-APIs mit .NET-Standardentwurfstechniken implementiert. Anstatt einen F#-Optionswert zurückzugeben, sollten Sie den bool-Rückgabetyp und einen out-Parameter wie im TryGetValue-Muster verwenden. Anstatt F#-Optionswerte als Parameter zu übernehmen, sollten Sie die Verwendung von Methodenüberladungen oder optionalen Argumenten in Erwägung 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 Auflistungstypen wie .NET-Arrays T[], F#-Typen list<T>, Map<Key,Value> und Set<T> sowie konkrete .NET-Auflistungstypen wie Dictionary<Key,Value>. Die Entwurfsrichtlinien für .NET-Bibliotheken enthalten gute Empfehlungen zur Verwendung verschiedener Auflistungstypen wie IEnumerable<T>. Eine gewisse Verwendung von Arrays (T[]) ist unter bestimmten Umständen aus Leistungsgründen akzeptabel. Beachten Sie insbesondere, dass seq<T> lediglich der F#-Alias für IEnumerable<T> ist und „seq“ daher oftmals ein geeigneter Typ für eine Vanilla .NET-API ist.

Anstelle von F#-Listen:

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

Verwenden Sie F#-Sequenzen:

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

Verwenden des unit-Typs als einzigen Eingabetyp einer Methode, um eine Methode ohne Argumente zu definieren, oder als einzigen Rückgabetyp, um eine Methode zu definieren, die „void“ zurückgibt

Vermeiden Sie andere Verwendungen des unit-Typs. Dies ist gut:

✔ member this.NoArguments() = 3

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

Dies ist schlecht:

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

Überprüfen auf NULL-Werte an den Grenzen der Vanilla .NET-API

Der F#-Implementierungscode weist aufgrund unveränderlicher Entwurfsmuster und Einschränkungen bei der Verwendung von NULL-Literalen für F#-Typen in der Regel weniger NULL-Werte auf. Andere .NET-Sprachen verwenden häufiger NULL als Wert. Aus diesem Grund sollte F#-Code, der eine Vanilla .NET-API verfügbar macht, Parameter an der API-Grenze auf NULL ü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 ()

Vermeiden der Verwendung von Tupeln als Rückgabewerte

Geben Sie stattdessen lieber einen benannten Typ zurück, der die aggregierten Daten enthält, oder verwenden Sie out-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 in einer beliebigen .NET-Sprache entwerfen, gibt es keine Alternative zu einer experimentellen C#- und Visual Basic-Programmierung, um sicherzustellen, dass sich Ihre Bibliotheken in diesen Sprachen „wohl fühlen“. Sie können auch Tools wie .NET Reflector und den Visual Studio-Objektkatalog verwenden, um sicherzustellen, dass Bibliotheken und deren Dokumentation für Entwickler wie erwartet angezeigt werden.

Anhang

End-to-End-Beispiel für den Entwurf von F#-Code für die Verwendung durch andere .NET-Sprachen

Betrachten 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 für einen Programmierer, der eine andere .NET-Sprache verwendet, angezeigt wird. Die „C#-Signatur“ sieht beispielsweise ungefähr wie folgt aus:

// 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. Beispiel:

  • Metadaten wie Argumentnamen wurden beibehalten.

  • F#-Methoden, die zwei Argumente übernehmen, werden zu C#-Methoden, die zwei Argumente übernehmen.

  • 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 dieses 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 sieht jetzt wie folgt aus:

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; }
}

Um diesen Typ für die Verwendung als Teil einer Vanilla .NET-Bibliothek aufzubereiten, wurden folgende Korrekturen vorgenommen:

  • Mehrere Namen wurden angepasst: Point1, n, l und f wurden zu RadialPoint, count, factor bzw. transform.

  • Es wurde ein Rückgabetyp von seq<RadialPoint> anstelle von RadialPoint list verwendet, indem eine Listenkonstruktion mit [ ... ] in eine Sequenzkonstruktion IEnumerable<RadialPoint> geändert wurde.

  • Anstelle eines F#-Funktionstyps wurde der .NET-Delegattyp System.Func verwendet.

Dies macht die Nutzung in C#-Code wesentlich einfacher.