Share via


Ontwerprichtlijnen voor F#-onderdelen

Dit document is een set ontwerprichtlijnen voor onderdelen voor F#-programmering, op basis van de F#-richtlijnen voor het ontwerpen van onderdelen, v14, Microsoft Research en een versie die oorspronkelijk is samengesteld en onderhouden door de F# Software Foundation.

In dit document wordt ervan uitgegaan dat u bekend bent met F#-programmering. Veel dank aan de F#-community voor hun bijdragen en nuttige feedback over verschillende versies van deze handleiding.

Overzicht

In dit document worden enkele problemen behandeld met betrekking tot het ontwerpen en coderen van F#-onderdelen. Een onderdeel kan een van de volgende zaken betekenen:

  • Een laag in uw F#-project met externe consumenten binnen dat project.
  • Een bibliotheek die is bedoeld voor gebruik door F#-code over assemblygrenzen.
  • Een bibliotheek die is bedoeld voor gebruik door elke .NET-taal binnen de assemblygrenzen.
  • Een bibliotheek die is bedoeld voor distributie via een pakketopslagplaats, zoals NuGet.

Technieken die in dit artikel worden beschreven, volgen de vijf principes van goede F#-code en maken dus gebruik van zowel functionele als objectprogrammering, indien van toepassing.

Ongeacht de methodologie heeft de ontwerpfunctie voor onderdelen en bibliotheken te maken met een aantal praktische en prosaïsche problemen bij het maken van een API die het gemakkelijkst kan worden gebruikt door ontwikkelaars. De zorgvuldige toepassing van de ontwerprichtlijnen voor .NET-bibliotheken leidt u naar het maken van een consistente set API's die prettig zijn om te gebruiken.

Algemene richtlijnen

Er zijn enkele universele richtlijnen die van toepassing zijn op F#-bibliotheken, ongeacht de beoogde doelgroep voor de bibliotheek.

Meer informatie over de ontwerprichtlijnen voor .NET-bibliotheken

Ongeacht het type F#-codering dat u doet, is het waardevol om een werkende kennis te hebben van de ontwerprichtlijnen voor .NET-bibliotheken. De meeste andere F# en .NET-programmeurs zullen bekend zijn met deze richtlijnen en verwachten dat .NET-code aan hen voldoet.

De ontwerprichtlijnen voor .NET-bibliotheken bieden algemene richtlijnen met betrekking tot naamgeving, het ontwerpen van klassen en interfaces, ontwerp van leden (eigenschappen, methoden, gebeurtenissen, enzovoort) en zijn een nuttig eerste referentiepunt voor verschillende ontwerprichtlijnen.

Xml-documentatieopmerkingen toevoegen aan uw code

XML-documentatie over openbare API's zorgt ervoor dat gebruikers geweldige IntelliSense en Quickinfo kunnen krijgen bij het gebruik van deze typen en leden en het inschakelen van documentatiebestanden voor de bibliotheek. Zie de XML-documentatie over verschillende XML-tags die kunnen worden gebruikt voor extra markeringen in xmldoc-opmerkingen.

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

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

U kunt de korte XML-opmerkingen (/// comment) of standaard XML-opmerkingen (///<summary>comment</summary>) gebruiken.

Overweeg expliciete handtekeningbestanden (.fsi) te gebruiken voor stabiele bibliotheek- en onderdeel-API's

Het gebruik van expliciete handtekeningenbestanden in een F#-bibliotheek biedt een beknopt overzicht van de openbare API, waarmee u ervoor kunt zorgen dat u de volledige openbare oppervlakte van uw bibliotheek kent en een schone scheiding biedt tussen openbare documentatie en interne implementatiedetails. Handtekeningbestanden zorgen voor wrijving bij het wijzigen van de openbare API, doordat wijzigingen moeten worden aangebracht in zowel de implementatie- als handtekeningbestanden. Als gevolg hiervan moeten handtekeningbestanden doorgaans alleen worden geïntroduceerd wanneer een API is gestolde en naar verwachting niet meer aanzienlijk zal veranderen.

Volg de aanbevolen procedures voor het gebruik van tekenreeksen in .NET

Volg aanbevolen procedures voor het gebruik van tekenreeksen in .NET-richtlijnen wanneer het bereik van het project dit garandeert. Met name expliciet de culturele intentie in de conversie en vergelijking van tekenreeksen (indien van toepassing).

Richtlijnen voor F#-gerichte bibliotheken

Deze sectie bevat aanbevelingen voor het ontwikkelen van openbare F#-bibliotheken; Dat wil gezegd, bibliotheken die openbare API's weergeven die bedoeld zijn om te worden gebruikt door F#-ontwikkelaars. Er zijn diverse aanbevelingen voor bibliotheekontwerp die specifiek van toepassing zijn op F#. Als er geen specifieke aanbevelingen volgen, zijn de ontwerprichtlijnen voor .NET-bibliotheken de terugvalrichtlijnen.

Naamconventies

.NET-naamgevings- en hoofdletterconventies gebruiken

De volgende tabel volgt .NET-naamgevings- en hoofdletterconventies. Er zijn kleine toevoegingen om ook F#-constructies op te nemen. Deze aanbevelingen zijn vooral bedoeld voor API's die zich buiten de grenzen van F#-to-F# bevinden, met idiomen van .NET BCL en het merendeel van de bibliotheken.

Bouwen Case Onderdeel Voorbeelden Opmerkingen
Betontypen PascalCase Zelfstandig naamwoord/bijvoeglijk naamwoord Lijst, dubbel, complex Betontypen zijn structs, klassen, opsommingen, gemachtigden, records en samenvoegingen. Hoewel typenamen traditioneel kleine letters in OCaml zijn, heeft F# het .NET-naamgevingsschema voor typen gebruikt.
Dlls PascalCase Fabrikam.Core.dll
Samenvoegtags PascalCase Zelfstandig naamwoord Sommige, toevoegen, geslaagd Gebruik geen voorvoegsel in openbare API's. Gebruik eventueel een voorvoegsel wanneer intern, zoals 'type Teams = TAlpha | TBeta | TDelta".
Gebeurtenis PascalCase Term ValueChanged /ValueChanging
Uitzonderingen PascalCase WebException De naam moet eindigen op Uitzondering.
Veld PascalCase Zelfstandig naamwoord CurrentName
Interfacetypen PascalCase Zelfstandig naamwoord/bijvoeglijk naamwoord IDisposable De naam moet beginnen met 'I'.
Wijze PascalCase Term ToString
Naamruimte PascalCase Microsoft.FSharp.Core Over het algemeen wordt de organisatie niet meer gebruikt <Organization>.<Technology>[.<Subnamespace>]als de technologie onafhankelijk is van de organisatie.
Parameters camelCase Zelfstandig naamwoord typeName, transform, range
waarden laten (intern) camelCase of PascalCase Zelfstandig naamwoord/werkwoord getValue, myTable
waarden laten (extern) camelCase of PascalCase Zelfstandig naamwoord/werkwoord List.map, Dates.Today let-bound values zijn vaak openbaar bij het volgen van traditionele functionele ontwerppatronen. Gebruik echter meestal PascalCase wanneer de id kan worden gebruikt uit andere .NET-talen.
Eigenschappen PascalCase Zelfstandig naamwoord/bijvoeglijk naamwoord IsEndOfFile, BackColor Booleaanse eigenschappen gebruiken over het algemeen Is en Can en moeten bevestigend zijn, zoals in IsEndOfFile, niet IsNotEndOfFile.

Afkortingen vermijden

De .NET-richtlijnen ontmoedigen het gebruik van afkortingen (bijvoorbeeld 'gebruik OnButtonClick in plaats OnBtnClickvan'). Veelgebruikte afkortingen, zoals Async voor 'Asynchroon', worden getolereerd. Deze richtlijn wordt soms genegeerd voor functioneel programmeren; Gebruikt bijvoorbeeld List.iter een afkorting voor 'herhalen'. Daarom wordt het gebruik van afkortingen meestal getolereerd tot een grotere mate van F#-naar-F#-programmering, maar moet in het algemeen nog steeds worden vermeden in het ontwerp van openbare onderdelen.

Voorkomen dat hoofdletters en naamconflicten voorkomen

De .NET-richtlijnen zeggen dat alleen hoofdlettergebruik niet kan worden gebruikt om naamconflicten niet eenduidig te maken, omdat sommige clienttalen (bijvoorbeeld Visual Basic) niet hoofdlettergevoelig zijn.

Gebruik waar nodig acroniemen

Acroniemen zoals XML zijn geen afkortingen en worden veel gebruikt in .NET-bibliotheken in niet-ingekapitaliseerde vorm (XML). Alleen bekende, algemeen herkende acroniemen moeten worden gebruikt.

PascalCase gebruiken voor algemene parameternamen

Gebruik PascalCase voor algemene parameternamen in openbare API's, waaronder voor F#-gerichte bibliotheken. Gebruik met name namen zoals T, , , T1voor T2 willekeurige algemene parameters, en wanneer specifieke namen zinvol zijn, gebruiken voor F#-gerichte bibliotheken namen zoals Key, ValueArg (maar niet bijvoorbeeld TKeyU).

PascalCase of camelCase gebruiken voor openbare functies en waarden in F#-modules

camelCase wordt gebruikt voor openbare functies die zijn ontworpen om niet-gekwalificeerd te worden gebruikt (bijvoorbeeld invalidArg), en voor de 'standaardverzamelingsfuncties' (bijvoorbeeld List.map). In beide gevallen fungeren de functienamen net als trefwoorden in de taal.

Object-, type- en moduleontwerp

Naamruimten of modules gebruiken om uw typen en modules te bevatten

Elk F#-bestand in een onderdeel moet beginnen met een naamruimtedeclaratie of een moduledeclaratie.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

or

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

De verschillen tussen het gebruik van modules en naamruimten om code op het hoogste niveau te organiseren, zijn als volgt:

  • Naamruimten kunnen meerdere bestanden omvatten
  • Naamruimten kunnen geen F#-functies bevatten tenzij ze zich in een binnenste module bevinden
  • De code voor een bepaalde module moet zich in één bestand bevinden
  • Modules op het hoogste niveau kunnen F#-functies bevatten zonder dat er een interne module nodig is

De keuze tussen een naamruimte of module op het hoogste niveau is van invloed op de gecompileerde vorm van de code en heeft dus invloed op de weergave uit andere .NET-talen als uw API uiteindelijk buiten F#-code wordt gebruikt.

Methoden en eigenschappen gebruiken voor bewerkingen die intrinsiek zijn voor objecttypen

Wanneer u met objecten werkt, is het raadzaam ervoor te zorgen dat de verbruiksfunctionaliteit wordt geïmplementeerd als methoden en eigenschappen voor dat type.

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) = ...

Het grootste deel van de functionaliteit voor een bepaald lid hoeft niet noodzakelijkerwijs in dat lid te worden geïmplementeerd, maar het verbruikbare deel van die functionaliteit moet zijn.

Klassen gebruiken om de onveranderbare status in te kapselen

In F# hoeft dit alleen te worden gedaan wanneer die status nog niet is ingekapseld door een andere taalconstructie, zoals een sluiting, reeksexpressie of asynchrone berekening.

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

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

Gebruik interfacetypen om een set bewerkingen weer te geven. Dit is de voorkeur aan andere opties, zoals tuples van functies of records van functies.

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

Voorkeur voor:

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

Interfaces zijn eersteklas concepten in .NET, die u kunt gebruiken om te bereiken wat Functors u normaal gesproken zou geven. Daarnaast kunnen ze worden gebruikt om existentiële typen te coderen in uw programma, die records van functies niet kunnen.

Een module gebruiken om functies te groeperen die reageren op verzamelingen

Wanneer u een verzamelingstype definieert, kunt u overwegen een standaardset bewerkingen zoals CollectionType.map en CollectionType.iter) op te geven voor nieuwe verzamelingstypen.

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

Als u een dergelijke module opneemt, volgt u de standaardnaamconventies voor functies in FSharp.Core.

Een module gebruiken om functies te groeperen voor algemene, canonieke functies, met name in wiskundige en DSL-bibliotheken

Is bijvoorbeeld Microsoft.FSharp.Core.Operators een automatisch geopende verzameling functies op het hoogste niveau (zoals abs en sin) die worden geleverd door FSharp.Core.dll.

Op dezelfde manier kan een statistiekenbibliotheek een module met functies erf bevatten en erfc, waarbij deze module expliciet of automatisch wordt geopend.

Overweeg het gebruik van RequireQualifiedAccess en pas de kenmerken van AutoOpen zorgvuldig toe

Het toevoegen van het [<RequireQualifiedAccess>] kenmerk aan een module geeft aan dat de module mogelijk niet wordt geopend en dat verwijzingen naar de elementen van de module expliciete gekwalificeerde toegang vereisen. De module heeft bijvoorbeeld Microsoft.FSharp.Collections.List dit kenmerk.

Dit is handig wanneer functies en waarden in de module namen bevatten die waarschijnlijk conflicteren met namen in andere modules. Het vereisen van gekwalificeerde toegang kan de onderhoudbaarheid op lange termijn en de mogelijkheden van een bibliotheek aanzienlijk verhogen.

Het wordt ten zeerste aangeraden om het [<RequireQualifiedAccess>] kenmerk voor aangepaste modules te hebben die worden geleverd door FSharp.Core (zoals Seq, List, Array), omdat deze modules vaak worden gebruikt in F#-code en zijn [<RequireQualifiedAccess>] gedefinieerd. Over het algemeen wordt het afgeraden om aangepaste modules te definiëren die ontbreken aan het kenmerk, wanneer dergelijke moduleschaduwen of andere modules met het kenmerk uitbreiden.

Als u het [<AutoOpen>] kenmerk aan een module toevoegt, wordt de module geopend wanneer de naamruimte wordt geopend. Het [<AutoOpen>] kenmerk kan ook worden toegepast op een assembly om aan te geven dat er automatisch een module wordt geopend wanneer naar de assembly wordt verwezen.

Een statistiekenbibliotheek MathsHeaven.Statistics kan bijvoorbeeld een module MathsHeaven.Statistics.Operators met functies erf bevatten en erfc. Het is redelijk om deze module te markeren als [<AutoOpen>]. Dit betekent dat open MathsHeaven.Statistics deze module ook wordt geopend en dat de namen erf en erfc het bereik worden bereikt. Een ander goed gebruik hiervan [<AutoOpen>] is voor modules die extensiemethoden bevatten.

Overgebruik van [<AutoOpen>] leidt tot vervuilende naamruimten en het kenmerk moet zorgvuldig worden gebruikt. Voor specifieke bibliotheken in specifieke domeinen kan het gebruik van [<AutoOpen>] een goed gebruik leiden tot een betere bruikbaarheid.

Overweeg om operatorleden te definiëren voor klassen waar het gebruik van bekende operators geschikt is

Soms worden klassen gebruikt om wiskundige constructies zoals vectoren te modelleren. Wanneer het domein dat wordt gemodelleerd bekende operators heeft, is het handig om ze te definiëren als leden die intrinsiek zijn voor de klasse.

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

Deze richtlijnen komen overeen met algemene .NET-richtlijnen voor deze typen. Het kan echter ook belangrijk zijn in F#-codering, omdat deze typen kunnen worden gebruikt in combinatie met F#-functies en -methoden met lidbeperkingen, zoals List.sumBy.

Overweeg het gebruik van CompiledName om een . NET-beschrijvende naam voor andere .NET-taalgebruikers

Soms wilt u een naam in één stijl voor F#-gebruikers (zoals een statisch lid in kleine letters, zodat deze lijkt alsof het een modulegebonden functie is), maar een andere stijl hebben voor de naam wanneer deze in een assembly wordt gecompileerd. U kunt het [<CompiledName>] kenmerk gebruiken om een andere stijl op te geven voor niet-F#-code die de assembly gebruikt.

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

Met behulp van [<CompiledName>], kunt u .NET-naamconventies gebruiken voor niet-F#-consumenten van de assembly.

Gebruik overbelasting van methoden voor lidfuncties als dit een eenvoudigere API biedt

Overbelasting van methoden is een krachtig hulpprogramma voor het vereenvoudigen van een API die mogelijk vergelijkbare functionaliteit moet uitvoeren, maar met verschillende opties of argumenten.

type Logger() =

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

In F# is het gebruikelijker om het aantal argumenten te overbelasten in plaats van typen argumenten.

Verberg de weergaven van record- en samenvoegtypen als het ontwerp van deze typen waarschijnlijk zal veranderen

Vermijd het onthullen van concrete representaties van objecten. De concrete weergave van DateTime waarden wordt bijvoorbeeld niet weergegeven door de externe, openbare API van het .NET-bibliotheekontwerp. Tijdens runtime kent de Common Language Runtime de vastgelegde implementatie die tijdens de uitvoering wordt gebruikt. Gecompileerde code haalt echter zelf geen afhankelijkheden op van de concrete representatie.

Vermijd het gebruik van implementatieovername voor uitbreidbaarheid

In F# wordt overname van implementatie zelden gebruikt. Bovendien zijn overnamehiërarchieën vaak complex en moeilijk te wijzigen wanneer nieuwe vereisten binnenkomen. Overname-implementatie bestaat nog steeds in F# voor compatibiliteit en zeldzame gevallen waarbij het de beste oplossing voor een probleem is, maar alternatieve technieken moeten worden gezocht in uw F#-programma's bij het ontwerpen voor polymorfisme, zoals interface-implementatie.

Functie- en lidhandtekeningen

Tuples gebruiken voor retourwaarden bij het retourneren van een klein aantal niet-gerelateerde waarden

Hier volgt een goed voorbeeld van het gebruik van een tuple in een retourtype:

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

Voor retourtypen die veel onderdelen bevatten of waarbij de onderdelen zijn gerelateerd aan één identificeerbare entiteit, kunt u overwegen een benoemd type te gebruiken in plaats van een tuple.

Gebruiken Async<T> voor asynchrone programmering bij F#-API-grenzen

Als er een overeenkomstige synchrone bewerking is die een retourneertOperation, moet de asynchrone bewerking worden genoemd AsyncOperation als deze retourneert Async<T> of OperationAsync als deze retourneertTask<T>.T Voor veelgebruikte .NET-typen die begin-/eindmethoden beschikbaar maken, kunt u overwegen Async.FromBeginEnd om extensiemethoden te schrijven als een gevel om het F#async-programmeermodel aan die .NET-API's te bieden.

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

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

Uitzonderingen

Zie Foutbeheer voor meer informatie over het juiste gebruik van uitzonderingen, resultaten en opties.

Extensieleden

Pas de leden van de F#-extensie zorgvuldig toe in F#-op-F#-onderdelen

F#-uitbreidingsleden mogen over het algemeen alleen worden gebruikt voor bewerkingen die zich in de sluiting van intrinsieke bewerkingen bevinden die zijn gekoppeld aan een type in het merendeel van de gebruiksmodi. Een veelvoorkomend gebruik is om API's te bieden die meer idiotisch zijn voor F# voor verschillende .NET-typen:

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

Samenvoegtypen

Gediscrimineerde samenvoegingen gebruiken in plaats van klassehiërarchieën voor structuurgestructureerde gegevens

Structuurachtige structuren worden recursief gedefinieerd. Dit is onhandig met overname, maar elegant met gediscrimineerde unies.

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

Door structuurachtige gegevens met gediscrimineerde unions weer te geven, kunt u ook profiteren van uitputtendheid in patroonkoppeling.

Gebruik [<RequireQualifiedAccess>] voor samenvoegtypen waarvan de naam niet voldoende uniek is

U kunt zich in een domein bevinden waarin dezelfde naam de beste naam is voor verschillende zaken, zoals gediscrimineerde uniezaken. U kunt namen [<RequireQualifiedAccess>] van hoofdletters niet eenduidig maken om verwarrende fouten te voorkomen als gevolg van schaduwen die afhankelijk zijn van de volgorde van open instructies

Verberg de representaties van gediscrimineerde samenvoegingen voor binaire compatibele API's als het ontwerp van deze typen waarschijnlijk zal evolueren

Samenvoegtypen zijn afhankelijk van F#-patroonkoppelingsformulieren voor een beknopt programmeermodel. Zoals eerder vermeld, moet u voorkomen dat concrete gegevensweergaven worden weergegeven als het ontwerp van deze typen waarschijnlijk zal evolueren.

De weergave van een gediscrimineerde vereniging kan bijvoorbeeld worden verborgen met behulp van een persoonlijke of interne verklaring, of met behulp van een handtekeningbestand.

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

Als u ongediscrimineerde vakbonden openbaar maakt, is het misschien lastig om uw bibliotheek te versien zonder dat u gebruikerscode hoeft te breken. Overweeg in plaats daarvan een of meer actieve patronen weer te geven om patroonkoppelingen toe te passen op waarden van uw type.

Actieve patronen bieden een alternatieve manier om F#-gebruikers een patroonkoppeling te bieden, terwijl F#-samenvoegtypen niet rechtstreeks beschikbaar worden gemaakt.

Inlinefuncties en ledenbeperkingen

Algemene numerieke algoritmen definiëren met inlinefuncties met impliciete lidbeperkingen en statische opgeloste algemene typen

Rekenkundige lidbeperkingen en F#-vergelijkingsbeperkingen zijn een standaard voor F#-programmering. Denk bijvoorbeeld aan de volgende 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

Het type van deze functie is als volgt:

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

Dit is een geschikte functie voor een openbare API in een wiskundige bibliotheek.

Vermijd het gebruik van lidbeperkingen om typeklassen en eendentypen te simuleren

Het is mogelijk om 'eenden typen' te simuleren met F#-lidbeperkingen. Leden die hiervan gebruikmaken, mogen echter niet in het algemeen worden gebruikt in F#-naar-F#-bibliotheekontwerpen. Dit komt doordat bibliotheekontwerpen op basis van onbekende of niet-standaard impliciete beperkingen ertoe leiden dat gebruikerscode inflexibel wordt en is gekoppeld aan een bepaald frameworkpatroon.

Daarnaast is er een goede kans dat intensief gebruik van lidbeperkingen op deze manier kan leiden tot zeer lange compilatietijden.

Operatordefinities

Vermijd het definiëren van aangepaste symbolische operators

Aangepaste operators zijn in sommige situaties essentieel en zijn zeer nuttige notatieapparaten binnen een grote hoofdtekst van de implementatiecode. Voor nieuwe gebruikers van een bibliotheek zijn benoemde functies vaak gemakkelijker te gebruiken. Bovendien kunnen aangepaste symbolische operators moeilijk te documenteren zijn en vinden gebruikers het moeilijker om hulp op te zoeken bij operators, vanwege bestaande beperkingen in IDE en zoekmachines.

Als gevolg hiervan kunt u uw functionaliteit het beste publiceren als benoemde functies en leden en bovendien operators voor deze functionaliteit beschikbaar maken als de notatievoordelen opwegen tegen de documentatie en cognitieve kosten van het hebben ervan.

Eenheden

Gebruik zorgvuldig maateenheden voor extra typeveiligheid in F#-code

Aanvullende typegegevens voor maateenheden worden gewist wanneer ze worden bekeken door andere .NET-talen. Houd er rekening mee dat .NET-onderdelen, hulpprogramma's en weerspiegeling typen-sans-eenheden zullen zien. C#-consumenten zien float bijvoorbeeld in plaats float<kg>van .

Afkortingen typen

Gebruik zorgvuldig type afkortingen om F#-code te vereenvoudigen

.NET-onderdelen, hulpprogramma's en weerspiegeling zien geen verkorte namen voor typen. Een aanzienlijk gebruik van type afkortingen kan er ook voor zorgen dat een domein complexer wordt dan het daadwerkelijk is, wat consumenten kan verwarren.

Vermijd type afkortingen voor openbare typen waarvan de leden en eigenschappen intrinsiek moeten verschillen van de typen die beschikbaar zijn voor het type dat wordt afgekort

In dit geval blijkt uit het type dat wordt afgekort te veel over de weergave van het werkelijke type dat wordt gedefinieerd. In plaats daarvan kunt u de afkorting verpakken in een klassetype of een gediscrimineerde samenvoeging in één geval (of, wanneer de prestaties essentieel zijn, kunt u overwegen om een structtype te gebruiken om de afkorting te verpakken).

Het is bijvoorbeeld verleidelijk om een multi-map te definiëren als een speciaal geval van een F#-kaart, bijvoorbeeld:

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

De logische punt-notatiebewerkingen voor dit type zijn echter niet hetzelfde als de bewerkingen op een kaart. Het is bijvoorbeeld redelijk dat de opzoekoperator map[key] de lege lijst retourneert als de sleutel zich niet in de woordenlijst bevindt, in plaats van een uitzondering te genereren.

Richtlijnen voor bibliotheken voor gebruik vanuit andere .NET-talen

Bij het ontwerpen van bibliotheken voor gebruik vanuit andere .NET-talen is het belangrijk om te voldoen aan de ontwerprichtlijnen voor .NET-bibliotheken. In dit document worden deze bibliotheken gelabeld als vanille .NET-bibliotheken, in plaats van F#-bibliotheken die gebruikmaken van F#-constructies zonder beperking. Het ontwerpen van vanille .NET-bibliotheken betekent dat vertrouwde en idiomatische API's consistent zijn met de rest van het .NET Framework door het gebruik van F#-specifieke constructies in de openbare API te minimaliseren. De regels worden in de volgende secties uitgelegd.

Ontwerp van naamruimte en type (voor bibliotheken voor gebruik vanuit andere .NET-talen)

De .NET-naamconventies toepassen op de openbare API van uw onderdelen

Let vooral op het gebruik van verkorte namen en de richtlijnen voor .NET-hoofdlettergebruik.

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

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

Naamruimten, typen en leden gebruiken als de primaire organisatiestructuur voor uw onderdelen

Alle bestanden met openbare functionaliteit moeten beginnen met een namespace declaratie en de enige openbare entiteiten in naamruimten moeten typen zijn. Gebruik geen F#-modules.

Gebruik niet-openbare modules voor implementatiecode, hulpprogrammatypen en hulpprogrammafuncties.

Statische typen moeten de voorkeur hebben voor modules, omdat ze toekomstige ontwikkeling van de API mogelijk maken om overbelasting en andere .NET API-ontwerpconcepten te gebruiken die mogelijk niet worden gebruikt in F#-modules.

Bijvoorbeeld in plaats van de volgende openbare API:

module Fabrikam

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

Overweeg in plaats daarvan:

namespace Fabrikam

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

F#-recordtypen gebruiken in vanille .NET-API's als het ontwerp van de typen niet verandert

F#-recordtypen worden gecompileerd naar een eenvoudige .NET-klasse. Deze zijn geschikt voor enkele eenvoudige, stabiele typen in API's. Overweeg het gebruik van de [<NoEquality>] en [<NoComparison>] kenmerken om de automatische generatie van interfaces te onderdrukken. Vermijd ook het gebruik van onveranderbare recordvelden in vanille .NET-API's, omdat deze een openbaar veld beschikbaar maken. Overweeg altijd of een klasse een flexibelere optie zou bieden voor toekomstige evolutie van de API.

Met de volgende F#-code wordt de openbare API bijvoorbeeld beschikbaar gemaakt voor een C#-consument:

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

De weergave van F#-samenvoegtypen verbergen in .NET-API's vanille

F#-samenvoegtypen worden niet vaak gebruikt voor onderdeelgrenzen, zelfs niet voor F#-naar-F#-codering. Ze zijn een uitstekend implementatieapparaat wanneer ze intern worden gebruikt binnen onderdelen en bibliotheken.

Bij het ontwerpen van een vanille .NET-API kunt u overwegen om de weergave van een samenvoegtype te verbergen met behulp van een persoonlijke declaratie of een handtekeningbestand.

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

U kunt ook typen uitbreiden die intern een samenvoeging met leden gebruiken om een gewenste waarde te bieden. NET-gerichte API.

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

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

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

Ontwerp-GUI en andere onderdelen met behulp van de ontwerppatronen van het framework

Er zijn veel verschillende frameworks beschikbaar in .NET, zoals WinForms, WPF en ASP.NET. Naamgevings- en ontwerpconventies voor elk moeten worden gebruikt als u onderdelen ontwerpt voor gebruik in deze frameworks. Voor WPF-programmering gebruikt u bijvoorbeeld WPF-ontwerppatronen voor de klassen die u ontwerpt. Gebruik voor modellen in het programmeren van gebruikersinterfaces ontwerppatronen zoals gebeurtenissen en verzamelingen op basis van meldingen, zoals die in System.Collections.ObjectModel.

Object- en lidontwerp (voor bibliotheken voor gebruik vanuit andere .NET-talen)

Het CLIEvent-kenmerk gebruiken om .NET-gebeurtenissen beschikbaar te maken

Maak een DelegateEvent met een specifiek .NET-gemachtigde type dat een object gebruikt en EventArgs (in plaats van een Event, die standaard het FSharpHandler type gebruikt), zodat de gebeurtenissen op de vertrouwde manier worden gepubliceerd naar andere .NET-talen.

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

Asynchrone bewerkingen beschikbaar maken als methoden die .NET-taken retourneren

Taken worden gebruikt in .NET om actieve asynchrone berekeningen weer te geven. Taken zijn in het algemeen minder compositie dan F# Async<T> -objecten, omdat ze 'al uitgevoerde' taken vertegenwoordigen en niet samen kunnen worden samengesteld op manieren die parallelle samenstelling uitvoeren of die de doorgifte van annuleringssignalen en andere contextuele parameters verbergen.

Ondanks dit zijn methoden die taken retourneren echter de standaardweergave van asynchrone programmering op .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

U wilt vaak ook een expliciet annuleringstoken accepteren:

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

.NET-gemachtigdentypen gebruiken in plaats van F#-functietypen

Hier betekent 'F#-functietypen' 'pijltypen', zoals int -> int.

In plaats van dit:

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

Ga als volgt te werk:

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

Het F#-functietype lijkt op class FSharpFunc<T,U> andere .NET-talen en is minder geschikt voor taalfuncties en hulpprogramma's die gemachtigdentypen begrijpen. Bij het ontwerpen van een methode met een hogere volgorde die gericht is op .NET Framework 3.5 of hoger, zijn de System.Func en System.Action gedelegeerden de juiste API's om .NET-ontwikkelaars in staat te stellen deze API's op een lage wrijving te gebruiken. (Wanneer u zich richt op .NET Framework 2.0, zijn de door het systeem gedefinieerde gedelegeerdentypen beperkter. Overweeg het gebruik van vooraf gedefinieerde gedelegeerdentypen, zoals System.Converter<T,U> of het definiëren van een specifiek gedelegeerdetype.)

Aan de zijkant zijn .NET-gemachtigden niet natuurlijk voor F#-gerichte bibliotheken (zie de volgende sectie over F#-gerichte bibliotheken). Als gevolg hiervan is een algemene implementatiestrategie bij het ontwikkelen van methoden met een hogere volgorde voor vanille .NET-bibliotheken het ontwerpen van alle implementaties met F#-functietypen en het maken van de openbare API met behulp van gedelegeerden als een dunne gevel boven op de daadwerkelijke F#-implementatie.

Gebruik het TryGetValue-patroon in plaats van F#-optiewaarden te retourneren en geef de voorkeur aan overbelasting van methoden om F#-optiewaarden als argumenten te gebruiken

Veelvoorkomende gebruikspatronen voor het F#-optietype in API's worden beter geïmplementeerd in vanille .NET API's met behulp van standaard .NET-ontwerptechnieken. In plaats van een F#-optiewaarde te retourneren, kunt u overwegen om het retourtype bool plus een outparameter te gebruiken, zoals in het patroon TryGetValue. En in plaats van F#-optiewaarden als parameters te gebruiken, kunt u overwegen om overbelasting van methoden of optionele argumenten te gebruiken.

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

De .NET-verzamelingsinterfacetypen IEnumerable<T> en IDictionary<Key gebruiken, Waarde> voor parameters en retourwaarden

Vermijd het gebruik van betonverzamelingstypen zoals .NET-matricesT[], F#-typen list<T>Map<Key,Value> en Set<T>.NET-betonverzamelingstypen zoals Dictionary<Key,Value>. De ontwerprichtlijnen voor .NET-bibliotheken hebben een goed advies over het gebruik van verschillende verzamelingstypen, zoals IEnumerable<T>. Sommige gebruik van matrices (T[]) is in sommige omstandigheden aanvaardbaar, op grond van prestaties. Let vooral op: seq<T> alleen de F#-alias voor IEnumerable<T>, en dus seq is vaak een geschikt type voor een vanilla .NET-API.

In plaats van F#-lijsten:

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

F#-reeksen gebruiken:

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

Gebruik het eenheidstype als het enige invoertype van een methode om een methode met nul argumenten te definiëren, of als het enige retourtype om een ongeldige retourmethode te definiëren

Vermijd andere toepassingen van het eenheidstype. Deze zijn goed:

✔ member this.NoArguments() = 3

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

Dit is slecht:

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

Controleren op null-waarden voor vanille .NET API-grenzen

F#-implementatiecode heeft meestal minder null-waarden, vanwege onveranderbare ontwerppatronen en beperkingen voor het gebruik van null-letterlijke waarden voor F#-typen. In andere .NET-talen wordt vaak null gebruikt als een waarde die veel vaker wordt gebruikt. Daarom moet F#-code die een vanille .NET-API weergeeft, parameters controleren op null op de API-grens en voorkomen dat deze waarden dieper in de F#-implementatiecode stromen. De isNull functie of het patroon dat overeenkomt met het null patroon kan worden gebruikt.

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

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

Vermijd het gebruik van tuples als retourwaarden

Geef in plaats daarvan de voorkeur aan het retourneren van een benoemd type met de geaggregeerde gegevens of het gebruik van parameters om meerdere waarden te retourneren. Hoewel tuples en struct-tuples bestaan in .NET (inclusief C#-taalondersteuning voor struct tuples), bieden ze meestal niet de ideale en verwachte API voor .NET-ontwikkelaars.

Vermijd het gebruik van kerrie van parameters

Gebruik in plaats daarvan .NET-aanroepconventies Method(arg1,arg2,…,argN).

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

Tip: Als u bibliotheken ontwerpt voor gebruik vanuit een .NET-taal, is er geen vervanging voor het uitvoeren van experimentele C# en Visual Basic-programmering om ervoor te zorgen dat uw bibliotheken zich 'goed voelen' uit deze talen. U kunt ook hulpprogramma's zoals .NET Reflector en Visual Studio Object Browser gebruiken om ervoor te zorgen dat bibliotheken en hun documentatie worden weergegeven zoals verwacht voor ontwikkelaars.

Bijlage

End-to-end-voorbeeld van het ontwerpen van F#-code voor gebruik door andere .NET-talen

Houd rekening met de volgende 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) ]

Het uitgestelde F#-type van deze klasse is als volgt:

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

Laten we eens kijken hoe dit F#-type voor een programmeur wordt weergegeven met een andere .NET-taal. De C# -handtekening is bijvoorbeeld als volgt:

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

Er zijn enkele belangrijke punten om te zien hoe F# hier constructies vertegenwoordigt. Voorbeeld:

  • Metagegevens zoals argumentnamen zijn behouden.

  • F#-methoden waarbij twee argumenten worden gebruikt, worden C#-methoden die twee argumenten gebruiken.

  • Functies en lijsten worden verwijzingen naar bijbehorende typen in de F#-bibliotheek.

De volgende code laat zien hoe u deze code aanpast om rekening te houden met deze zaken.

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

Het uitgestelde F#-type van de code is als volgt:

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

De C#-handtekening is nu als volgt:

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

De oplossingen die zijn aangebracht om dit type voor te bereiden voor gebruik als onderdeel van een vanilla .NET-bibliotheek zijn als volgt:

  • Verschillende namen aangepast: Point1, n, len f werden RadialPoint, countfactoren respectievelijk , en transform.

  • Gebruikt een retourtype van seq<RadialPoint> in plaats van RadialPoint list door een lijstconstructie te wijzigen in [ ... ] een sequentieconstructie met behulp van IEnumerable<RadialPoint>.

  • Het .NET-gemachtigdentype System.Func gebruikt in plaats van een F#-functietype.

Dit maakt het veel leuker om te gebruiken in C#-code.