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 OnBtnClick
van'). 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
, , , T1
voor T2
willekeurige algemene parameters, en wanneer specifieke namen zinvol zijn, gebruiken voor F#-gerichte bibliotheken namen zoals Key
, Value
Arg
(maar niet bijvoorbeeld TKey
U
).
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
Interfaces gebruiken om gerelateerde bewerkingen te groeperen
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
,l
enf
werdenRadialPoint
,count
factor
en respectievelijk , entransform
.Gebruikt een retourtype van
seq<RadialPoint>
in plaats vanRadialPoint list
door een lijstconstructie te wijzigen in[ ... ]
een sequentieconstructie met behulp vanIEnumerable<RadialPoint>
.Het .NET-gemachtigdentype
System.Func
gebruikt in plaats van een F#-functietype.
Dit maakt het veel leuker om te gebruiken in C#-code.