Freigeben über


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 OnBtnClickverwenden"). 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 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 erfcenthalten, 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 erfcenthä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 Tzurü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 | nullSyntax 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, und f wurden RadialPoint, count, factor, bzw. transform.

  • Verwendet einen Rückgabetyp von seq<RadialPoint> anstelle von RadialPoint list durch Ändern einer Listenkonstruktion mithilfe von [ ... ] in eine Sequenzkonstruktion mithilfe von IEnumerable<RadialPoint>.

  • Verwendet .NET-Delegattyp System.Func anstelle eines F#-Funktionstyps.

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