Designriktlinjer för F#-komponenter
Det här dokumentet är en uppsättning riktlinjer för komponentdesign för F#-programmering, baserat på designriktlinjerna för F#-komponenter, v14, Microsoft Research och en version som ursprungligen granskades och underhålls av F# Software Foundation.
Det här dokumentet förutsätter att du är bekant med F#-programmering. Stort tack till F#-communityn för deras bidrag och användbar feedback om olika versioner av den här guiden.
Översikt
Det här dokumentet tittar på några av problemen som rör design och kodning av F#-komponenter. En komponent kan betyda något av följande:
- Ett lager i ditt F#-projekt som har externa konsumenter i projektet.
- Ett bibliotek som är avsett för förbrukning med F#-kod över sammansättningsgränser.
- Ett bibliotek som är avsett för förbrukning av ett .NET-språk över sammansättningsgränser.
- Ett bibliotek som är avsett för distribution via en paketlagringsplats, till exempel NuGet.
Tekniker som beskrivs i den här artikeln följer de fem principerna för bra F#-kod och använder därför både funktionell och objektprogrammering efter behov.
Oavsett metodik står komponenten och biblioteksdesignern inför ett antal praktiska och prosaiska problem när de försöker skapa ett API som är lättast att använda av utvecklare. Samvetsgranna program för designriktlinjerna för .NET-bibliotek hjälper dig att skapa en konsekvent uppsättning API:er som är trevliga att använda.
Allmänna riktlinjer
Det finns några universella riktlinjer som gäller för F#-bibliotek, oavsett den avsedda målgruppen för biblioteket.
Lär dig designriktlinjerna för .NET-bibliotek
Oavsett vilken typ av F#-kodning du gör är det värdefullt att ha en fungerande kunskap om designriktlinjerna för .NET-bibliotek. De flesta andra F#- och .NET-programmerare kommer att känna till dessa riktlinjer och förväntar sig att .NET-koden överensstämmer med dem.
Designriktlinjerna för .NET-bibliotek ger allmän vägledning om namngivning, utformning av klasser och gränssnitt, medlemsdesign (egenskaper, metoder, händelser osv.) och är en användbar första referenspunkt för en mängd olika designvägledning.
Lägga till XML-dokumentationskommentar i koden
XML-dokumentation om offentliga API:er säkerställer att användarna kan få bra Intellisense och Quickinfo när de använder dessa typer och medlemmar och aktivera att skapa dokumentationsfiler för biblioteket. Se XML-dokumentationen om olika XML-taggar som kan användas för ytterligare markering i xmldoc-kommentarer.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Du kan använda xml-kommentarer (/// comment
) eller xml-standardkommenteringar (///<summary>comment</summary>
).
Överväg att använda explicita signaturfiler (.fsi) för stabila biblioteks- och komponent-API:er
Med explicita signaturfiler i ett F#-bibliotek får du en kortfattad sammanfattning av det offentliga API:et, som hjälper dig att se till att du känner till hela bibliotekets offentliga yta och ger en ren uppdelning mellan offentlig dokumentation och intern implementeringsinformation. Signaturfiler lägger till friktion för att ändra det offentliga API:et genom att kräva att ändringar görs i både implementerings- och signaturfilerna. Därför bör signaturfiler vanligtvis bara introduceras när ett API har stelnat och inte längre förväntas ändras avsevärt.
Följ metodtipsen för att använda strängar i .NET
Följ metodtips för att använda strängar i .NET-vägledning när projektets omfattning garanterar det. I synnerhet att uttryckligen ange kulturell avsikt vid konvertering och jämförelse av strängar (i förekommande fall).
Riktlinjer för F#-riktade bibliotek
Det här avsnittet innehåller rekommendationer för att utveckla offentliga F#-riktade bibliotek. det vill: bibliotek som exponerar offentliga API:er som är avsedda att användas av F#-utvecklare. Det finns en mängd olika rekommendationer för biblioteksdesign som är specifika för F#. I avsaknad av de specifika rekommendationer som följer är designriktlinjerna för .NET-bibliotek vägledningen för återställning.
Namngivningskonventioner
Använda .NET-namngivnings- och versaler
Följande tabell följer .NET-namngivnings- och versaler. Det finns små tillägg för att även inkludera F#-konstruktioner. Dessa rekommendationer är särskilt avsedda för API:er som sträcker sig bortom F#-till-F#-gränser, som passar med idiom från .NET BCL och majoriteten av biblioteken.
Konstruera | Skiftläge | Delvis | Exempel | Kommentar |
---|---|---|---|---|
Betongtyper | PascalCase | Substantiv/adjektiv | Lista, Dubbel, Komplex | Betongtyper är structs, klasser, uppräkningar, ombud, poster och fackföreningar. Även om typnamn traditionellt är gemener i OCaml har F# antagit .NET-namngivningsschemat för typer. |
Dlls | PascalCase | Fabrikam.Core.dll | ||
Union-taggar | PascalCase | Substantiv | Vissa, Lägg till, Lyckades | Använd inte ett prefix i offentliga API:er. Du kan också använda ett prefix när det är internt, till exempel "type Teams = TAlpha | TBeta | TDelta". |
Event | PascalCase | Verb | ValueChanged/ValueChanging | |
Undantag | PascalCase | WebException | Namnet ska sluta med "Undantag". | |
Fält | PascalCase | Substantiv | CurrentName | |
Gränssnittstyper | PascalCase | Substantiv/adjektiv | IDisposable | Namnet bör börja med "I". |
Metod | PascalCase | Verb | ToString | |
Namnområde | PascalCase | Microsoft.FSharp.Core | <Organization>.<Technology>[.<Subnamespace>] Använd vanligtvis , men släpp organisationen om tekniken är oberoende av organisationen. |
|
Parametrar | camelCase | Substantiv | typeName, transform, range | |
let-värden (intern) | camelCase eller PascalCase | Substantiv/verb | getValue, myTable | |
let-värden (externa) | camelCase eller PascalCase | Substantiv/verb | List.map, Dates.Today | let-bound-värden är ofta offentliga när du följer traditionella funktionella designmönster. Använd dock vanligtvis PascalCase när identifieraren kan användas från andra .NET-språk. |
Property | PascalCase | Substantiv/adjektiv | IsEndOfFile, BackColor | Booleska egenskaper använder vanligtvis Is and Can och bör vara jakande, som i IsEndOfFile, inte IsNotEndOfFile. |
Undvik förkortningar
.NET-riktlinjerna avråder från att använda förkortningar (till exempel "use OnButtonClick
rather than OnBtnClick
"). Vanliga förkortningar, till exempel Async
för "asynkrona", tolereras. Den här riktlinjen ignoreras ibland för funktionell programmering. använder till exempel List.iter
en förkortning för "iterate". Därför tenderar användning av förkortningar att tolereras i högre grad i F#-till-F#-programmering, men bör fortfarande i allmänhet undvikas i offentlig komponentdesign.
Undvik kollisioner med höljenamn
I .NET-riktlinjerna står det att enbart hölje inte kan användas för att skilja namnkollisioner åt, eftersom vissa klientspråk (till exempel Visual Basic) är skiftlägeskänsliga.
Använd akronymer där det är lämpligt
Förkortningar som XML är inte förkortningar och används ofta i .NET-bibliotek i icke-kapitaliserad form (XML). Endast välkända, allmänt erkända förkortningar bör användas.
Använda PascalCase för generiska parameternamn
Använd PascalCase för generiska parameternamn i offentliga API:er, inklusive för F#-riktade bibliotek. I synnerhet använder du namn som T
, U
, T1
, T2
för godtyckliga generiska parametrar och när specifika namn är meningsfulla använder F#-riktade bibliotek namn som Key
, Value
Arg
, (men inte till exempel TKey
).
Använd antingen PascalCase eller camelCase för offentliga funktioner och värden i F#-moduler
camelCase används för offentliga funktioner som är utformade för att användas okvalificerade (till exempel invalidArg
), och för "standardsamlingsfunktioner" (till exempel List.map). I båda dessa fall fungerar funktionsnamnen ungefär som nyckelord på språket.
Design av objekt, typ och modul
Använda namnområden eller moduler för att innehålla dina typer och moduler
Varje F#-fil i en komponent bör börja med antingen en namnområdesdeklaration eller en moduldeklaration.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
eller
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
Skillnaderna mellan att använda moduler och namnområden för att organisera kod på den översta nivån är följande:
- Namnområden kan sträcka sig över flera filer
- Namnområden får inte innehålla F#-funktioner om de inte finns i en inre modul
- Koden för en viss modul måste finnas i en enda fil
- Moduler på den översta nivån kan innehålla F#-funktioner utan behov av en inre modul
Valet mellan ett toppnivånamnområde eller en modul påverkar kodens kompilerade form och påverkar därför vyn från andra .NET-språk om ditt API så småningom används utanför F#-koden.
Använda metoder och egenskaper för åtgärder som är inbyggda i objekttyper
När du arbetar med objekt är det bäst att se till att förbrukningsbara funktioner implementeras som metoder och egenskaper för den typen.
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) = ...
Huvuddelen av funktionerna för en viss medlem behöver inte nödvändigtvis implementeras i den medlemmen, men den förbrukningsbara delen av den funktionen bör vara.
Använda klasser för att kapsla in föränderligt tillstånd
I F#behöver detta bara göras om tillståndet inte redan är inkapslat av en annan språkkonstruktion, till exempel en stängning, sekvensuttryck eller asynkron beräkning.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Använda gränssnitt för grupprelaterade åtgärder
Använd gränssnittstyper för att representera en uppsättning åtgärder. Detta är att föredra framför andra alternativ, till exempel tupplar av funktioner eller poster av funktioner.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
I stället för att:
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
Gränssnitt är förstklassiga begrepp i .NET, som du kan använda för att uppnå det som functors normalt skulle ge dig. Dessutom kan de användas för att koda existentiella typer i ditt program, vilka poster av funktioner inte kan.
Använda en modul för att gruppera funktioner som fungerar i samlingar
När du definierar en samlingstyp bör du överväga att tillhandahålla en standarduppsättning åtgärder som CollectionType.map
och CollectionType.iter
) för nya samlingstyper.
module CollectionType =
let map f c =
...
let iter f c =
...
Om du inkluderar en sådan modul följer du standardnamnkonventionerna för funktioner som finns i FSharp.Core.
Använda en modul för att gruppera funktioner för vanliga, kanoniska funktioner, särskilt i matematik- och DSL-bibliotek
Är till exempel Microsoft.FSharp.Core.Operators
en automatiskt öppnad samling funktioner på den översta nivån (som och sin
) som abs
tillhandahålls av FSharp.Core.dll.
På samma sätt kan ett statistikbibliotek innehålla en modul med funktioner erf
och erfc
, där den här modulen är utformad för att uttryckligen eller automatiskt öppnas.
Överväg att använda RequireQualifiedAccess och noggrant tillämpa AutoOpen-attribut
[<RequireQualifiedAccess>]
Att lägga till attributet i en modul anger att modulen kanske inte öppnas och att referenser till elementen i modulen kräver explicit kvalificerad åtkomst. Modulen Microsoft.FSharp.Collections.List
har till exempel det här attributet.
Detta är användbart när funktioner och värden i modulen har namn som sannolikt kommer att vara i konflikt med namn i andra moduler. Att kräva kvalificerad åtkomst kan avsevärt öka bibliotekets långsiktiga underhållbarhet och utvecklingsmöjligheter.
Det rekommenderas starkt att ha [<RequireQualifiedAccess>]
attributet för anpassade moduler som utökar de som Seq
tillhandahålls av FSharp.Core
(till exempel , List
, Array
), eftersom dessa moduler används i F#-kod och har [<RequireQualifiedAccess>]
definierats på dem. Mer allmänt rekommenderas inte att definiera anpassade moduler som saknar attributet, när sådana modul skuggar eller utökar andra moduler som har attributet.
[<AutoOpen>]
Om du lägger till attributet i en modul öppnas modulen när det innehållande namnområdet öppnas. Attributet [<AutoOpen>]
kan också tillämpas på en sammansättning för att ange en modul som öppnas automatiskt när sammansättningen refereras.
Till exempel kan ett statistikbibliotek MathsHeaven.Statistics innehålla funktioner module MathsHeaven.Statistics.Operators
erf
och erfc
. Det är rimligt att markera den här modulen som [<AutoOpen>]
. Det innebär open MathsHeaven.Statistics
också att du öppnar den här modulen och tar med namnen erf
och erfc
i omfånget. En annan bra användning av [<AutoOpen>]
är för moduler som innehåller tilläggsmetoder.
Överanvändning av [<AutoOpen>]
leads till förorenade namnområden och attributet bör användas med försiktighet. För specifika bibliotek i specifika domäner kan omdömesgill användning av [<AutoOpen>]
leda till bättre användbarhet.
Överväg att definiera operatörsmedlemmar i klasser där det är lämpligt att använda välkända operatorer
Ibland används klasser för att modellera matematiska konstruktioner som vektorer. När domänen som modelleras har välkända operatorer är det användbart att definiera dem som medlemmar som är inbyggda i klassen.
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
Den här vägledningen motsvarar allmän .NET-vägledning för dessa typer. Det kan dock vara extra viktigt i F#-kodning eftersom det gör att dessa typer kan användas tillsammans med F#-funktioner och metoder med medlemsbegränsningar, till exempel List.sumBy.
Överväg att använda CompiledName för att ange en . NET-eget namn för andra .NET-språkanvändare
Ibland kanske du vill namnge något i ett format för F#-konsumenter (till exempel en statisk medlem i gemener så att det ser ut som om det vore en modulbunden funktion), men har ett annat format för namnet när det kompileras till en sammansättning. Du kan använda attributet [<CompiledName>]
för att ange ett annat format för icke F#-kod som använder sammansättningen.
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
Med hjälp [<CompiledName>]
av kan du använda .NET-namngivningskonventioner för icke F#-användare av sammansättningen.
Använd metodöverlagring för medlemsfunktioner, om det ger ett enklare API
Metodöverlagring är ett kraftfullt verktyg för att förenkla ett API som kan behöva utföra liknande funktioner, men med olika alternativ eller argument.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
I F# är det vanligare att överbelasta antalet argument i stället för typer av argument.
Dölj representationer av post- och unionstyper om designen av dessa typer sannolikt kommer att utvecklas
Undvik att avslöja konkreta representationer av objekt. Den konkreta representationen av DateTime värden visas till exempel inte av det externa offentliga API:et för .NET-biblioteksdesignen. Vid körning vet Common Language Runtime vilken implementering som ska användas under körningen. Kompilerad kod hämtar dock inte själv beroenden för den konkreta representationen.
Undvik att använda implementeringsarv för utökningsbarhet
I F#används implementeringsarv sällan. Dessutom är arvshierarkier ofta komplexa och svåra att ändra när nya krav kommer. Arvsimplementering finns fortfarande i F# för kompatibilitet och sällsynta fall där det är den bästa lösningen på ett problem, men alternativa tekniker bör sökas i dina F#-program när du utformar för polymorfism, till exempel gränssnittsimplementering.
Funktions- och medlemssignaturer
Använd tupplar för returvärden när du returnerar ett litet antal orelaterade värden
Här är ett bra exempel på hur du använder en tuppeln i en returtyp:
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
För returtyper som innehåller många komponenter, eller där komponenterna är relaterade till en enda identifierbar entitet, bör du överväga att använda en namngiven typ i stället för en tuppel.
Använd Async<T>
för asynkron programmering vid F#API-gränser
Om det finns en motsvarande synkron åtgärd med namnet Operation
som returnerar en T
ska asynkron åtgärden namnges AsyncOperation
om den returnerar Async<T>
eller OperationAsync
om den returnerar Task<T>
. För vanliga .NET-typer som exponerar Begin/End-metoder kan du använda Async.FromBeginEnd
för att skriva tilläggsmetoder som en fasad för att tillhandahålla programmeringsmodellen F# async till dessa .NET-API:er.
type SomeType =
member this.Compute(x:int): int =
...
member this.AsyncCompute(x:int): Async<int> =
...
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
...
Undantag
Mer information om lämplig användning av undantag, resultat och alternativ finns i Felhantering .
Tilläggsmedlemmar
Tillämpa F#-tilläggsmedlemmar noggrant i F#-to-F#-komponenter
F#-tilläggsmedlemmar bör vanligtvis endast användas för åtgärder som är i stängning av inbyggda åtgärder som är associerade med en typ i de flesta av dess användningslägen. En vanlig användning är att tillhandahålla API:er som är mer idiomatiska för F# för olika .NET-typer:
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-typer
Använda diskriminerade fackföreningar i stället för klasshierarkier för trädstrukturerade data
Trädliknande strukturer definieras rekursivt. Detta är besvärligt med arv, men elegant med diskriminerade fackföreningar.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
Genom att representera trädliknande data med diskriminerade fackföreningar kan du också dra nytta av fullständighet i mönstermatchning.
Använd [<RequireQualifiedAccess>]
på unionstyper vars skiftlägesnamn inte är tillräckligt unika
Du kanske befinner dig i en domän där samma namn är det bästa namnet för olika saker, till exempel Fall av diskriminerad union. Du kan använda [<RequireQualifiedAccess>]
för att skilja skiftlägesnamn för att undvika att utlösa förvirrande fel på grund av open
skuggning beroende på ordningen på instruktioner
Dölj representationer av diskriminerade fackföreningar för binärkompatibla API:er om designen av dessa typer sannolikt kommer att utvecklas
Unionstyper förlitar sig på F#-mönstermatchningsformulär för en kortfattad programmeringsmodell. Som tidigare nämnts bör du undvika att avslöja konkreta datarepresentationer om designen av dessa typer sannolikt kommer att utvecklas.
Till exempel kan representationen av en diskriminerad union döljas med hjälp av en privat eller intern deklaration eller med hjälp av en signaturfil.
type Union =
private
| CaseA of int
| CaseB of string
Om du avslöjar diskriminerade fackföreningar urskillningslöst kan det vara svårt att versionshantera biblioteket utan att bryta användarkoden. Överväg i stället att avslöja ett eller flera aktiva mönster för att tillåta mönstermatchning över värden av din typ.
Aktiva mönster är ett alternativt sätt att ge F#-konsumenter mönstermatchning och samtidigt undvika att exponera F#-unionstyper direkt.
Infogade funktioner och medlemsbegränsningar
Definiera generiska numeriska algoritmer med hjälp av infogade funktioner med underförstådda medlemsbegränsningar och statiskt lösta generiska typer
Aritmetiska medlemsbegränsningar och F#-jämförelsebegränsningar är en standard för F#-programmering. Tänk till exempel på följande kod:
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
Typen av den här funktionen är följande:
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
Det här är en lämplig funktion för ett offentligt API i ett matematiskt bibliotek.
Undvik att använda medlemsbegränsningar för att simulera typklasser och ankskrivning
Det är möjligt att simulera "ankskrivning" med hjälp av F#-medlemsbegränsningar. Medlemmar som använder detta bör dock i allmänhet inte användas i F#-to-F#-biblioteksdesign. Det beror på att biblioteksdesign som baseras på obekanta eller icke-standardmässiga implicita begränsningar tenderar att göra att användarkoden blir oflexibel och kopplad till ett visst ramverksmönster.
Dessutom finns det en god chans att stor användning av medlemsbegränsningar på det här sättet kan leda till mycket långa kompileringstider.
Operatordefinitioner
Undvik att definiera anpassade symboliska operatorer
Anpassade operatorer är viktiga i vissa situationer och är mycket användbara notationsenheter i en stor del av implementeringskoden. För nya användare av ett bibliotek är namngivna funktioner ofta enklare att använda. Dessutom kan anpassade symboliska operatorer vara svåra att dokumentera, och användarna har svårare att söka efter hjälp för operatörer på grund av befintliga begränsningar i IDE och sökmotorer.
Därför är det bäst att publicera dina funktioner som namngivna funktioner och medlemmar, och dessutom exponera operatorer för den här funktionen endast om notationsfördelarna uppväger dokumentationen och de kognitiva kostnaderna för att ha dem.
Enheter
Använd måttenheter noggrant för extra typsäkerhet i F#-kod
Ytterligare skrivinformation för måttenheter raderas när de visas av andra .NET-språk. Tänk på att .NET-komponenter, verktyg och reflektion kommer att se typer-sans-units. Till exempel ser float
C#-konsumenter i stället float<kg>
för .
Skriv förkortningar
Använd noggrant typförkortningar för att förenkla F#-kod
.NET-komponenter, verktyg och reflektion ser inte förkortade namn för typer. Betydande användning av typförkortningar kan också göra att en domän verkar mer komplex än den faktiskt är, vilket kan förvirra konsumenterna.
Undvik typförkortningar för offentliga typer vars medlemmar och egenskaper i sig bör skilja sig från dem som är tillgängliga för den typ som förkortas
I det här fallet visar den förkortade typen för mycket om representationen av den faktiska typen som definieras. Överväg i stället att omsluta förkortningen i en klasstyp eller en union med en enda ärendediskriminering (eller, när prestanda är nödvändigt, överväg att använda en structtyp för att omsluta förkortningen).
Det är till exempel frestande att definiera en multikarta som ett specialfall för en F#-karta, till exempel:
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
De logiska dot-notationsåtgärderna för den här typen är dock inte samma som åtgärderna på en karta, till exempel är det rimligt att uppslagsoperatorn map[key]
returnerar den tomma listan om nyckeln inte finns i ordlistan, i stället för att skapa ett undantag.
Riktlinjer för bibliotek för användning från andra .NET-språk
När du utformar bibliotek för användning från andra .NET-språk är det viktigt att följa designriktlinjerna för .NET-bibliotek. I det här dokumentet är dessa bibliotek märkta som vanilj-.NET-bibliotek, till skillnad från F#-riktade bibliotek som använder F#-konstruktioner utan begränsningar. Att utforma .NET-bibliotek med vanilj innebär att tillhandahålla välbekanta och idiomatiska API:er som överensstämmer med resten av .NET Framework genom att minimera användningen av F#-specifika konstruktioner i det offentliga API:et. Reglerna beskrivs i följande avsnitt.
Namnområdes- och typdesign (för bibliotek för användning från andra .NET-språk)
Tillämpa namngivningskonventionerna för .NET på det offentliga API:et för dina komponenter
Var särskilt uppmärksam på användningen av förkortade namn och riktlinjerna för .NET-versaler.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Använd namnområden, typer och medlemmar som den primära organisationsstrukturen för dina komponenter
Alla filer som innehåller offentliga funktioner bör börja med en namespace
deklaration och de enda offentliga entiteterna i namnområden ska vara typer. Använd inte F#-moduler.
Använd icke-offentliga moduler för att lagra implementeringskod, verktygstyper och verktygsfunktioner.
Statiska typer bör föredras framför moduler, eftersom de gör det möjligt för framtida utveckling av API:et att använda överlagring och andra designbegrepp för .NET API som kanske inte används i F#-moduler.
I stället för följande offentliga API:
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Överväg i stället:
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
Använd F#-posttyper i vanilj-.NET-API:er om designen av typerna inte utvecklas
F#-posttyper kompileras till en enkel .NET-klass. Dessa är lämpliga för vissa enkla, stabila typer i API:er. Överväg att använda attributen [<NoEquality>]
och [<NoComparison>]
för att förhindra automatisk generering av gränssnitt. Undvik också att använda föränderliga postfält i vanilj-.NET-API:er eftersom dessa exponerar ett offentligt fält. Fundera alltid på om en klass skulle ge ett mer flexibelt alternativ för framtida utveckling av API:et.
Följande F#-kod exponerar till exempel det offentliga API:et för en C#-konsument:
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; }
}
Dölj representationen av F#-uniontyper i vanilj-.NET-API:er
F#-uniontyper används inte ofta över komponentgränser, inte ens för F#-till-F#-kodning. De är en utmärkt implementeringsenhet när den används internt i komponenter och bibliotek.
När du utformar ett .NET-API för vanilj bör du överväga att dölja representationen av en unionstyp med hjälp av antingen en privat deklaration eller en signaturfil.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Du kan också utöka typer som använder en facklig representation internt med medlemmar för att tillhandahålla en önskad . NET-riktad 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)
Utforma GUI och andra komponenter med hjälp av ramverkets designmönster
Det finns många olika ramverk som är tillgängliga i .NET, till exempel WinForms, WPF och ASP.NET. Namngivnings- och designkonventioner för var och en bör användas om du utformar komponenter för användning i dessa ramverk. För WPF-programmering kan du till exempel använda WPF-designmönster för de klasser som du utformar. För modeller i programmering av användargränssnitt använder du designmönster som händelser och meddelandebaserade samlingar, till exempel de som finns i System.Collections.ObjectModel.
Objekt- och medlemsdesign (för bibliotek för användning från andra .NET-språk)
Använda CLIEvent-attributet för att exponera .NET-händelser
Skapa en DelegateEvent
med en specifik .NET-delegattyp som tar ett objekt och EventArgs
(i stället för en Event
, som bara använder FSharpHandler
typen som standard) så att händelserna publiceras på det välbekanta sättet till andra .NET-språk.
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
Exponera asynkrona åtgärder som metoder som returnerar .NET-uppgifter
Uppgifter används i .NET för att representera aktiva asynkrona beräkningar. Uppgifter är i allmänhet mindre sammansatta än F#- Async<T>
objekt, eftersom de representerar "redan kör"-uppgifter och inte kan sammanställas på sätt som utför parallell sammansättning, eller som döljer spridningen av annulleringssignaler och andra kontextuella parametrar.
Trots detta är metoder som returnerar Uppgifter standardrepresentationen av asynkron programmering på .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
Du vill ofta också acceptera en explicit annulleringstoken:
/// 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)
Använda .NET-ombudstyper i stället för F#-funktionstyper
Här betyder "F#-funktionstyper" "piltyper" som int -> int
.
I stället för detta:
member this.Transform(f: int->int) =
...
Gör så här:
member this.Transform(f: Func<int,int>) =
...
Funktionstypen F# visas som class FSharpFunc<T,U>
för andra .NET-språk och är mindre lämplig för språkfunktioner och verktyg som förstår ombudstyper. När du redigerar en metod med högre ordning för .NET Framework 3.5 eller senare är ombuden System.Func
och System.Action
rätt API:er att publicera så att .NET-utvecklare kan använda dessa API:er på ett sätt med låg friktion. (När du riktar in dig på .NET Framework 2.0 är de systemdefinierade ombudstyperna mer begränsade. Överväg att använda fördefinierade ombudstyper som System.Converter<T,U>
eller definiera en specifik delegattyp.)
Å andra sidan är .NET-ombud inte naturliga för F#-riktade bibliotek (se nästa avsnitt i F#-riktade bibliotek). Därför är en gemensam implementeringsstrategi när du utvecklar metoder med högre ordning för vanilj.NET-bibliotek att skapa all implementering med hjälp av F#-funktionstyper och sedan skapa det offentliga API:et med ombud som en tunn fasad ovanpå den faktiska F#-implementeringen.
Använd TryGetValue-mönstret i stället för att returnera F#-alternativvärden och föredra metodöverlagring framför att ta F#-alternativvärden som argument
Vanliga användningsmönster för F#-alternativtypen i API:er implementeras bättre i vanilj-.NET-API:er med hjälp av standardmetoder för .NET-design. I stället för att returnera ett F#-alternativvärde bör du överväga att använda returtypen bool plus en out-parameter som i mönstret "TryGetValue". I stället för att ta F#-alternativvärden som parametrar bör du överväga att använda metodöverlagring eller valfria argument.
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
Använd .NET-samlingsgränssnittstyperna IEnumerable<T> och IDictionary<Key,Värde> för parametrar och returvärden
Undvik att använda betongsamlingstyper som .NET-matriserT[]
, F#-typer Map<Key,Value>
list<T>
och Set<T>
, och .NET-betongsamlingstyper som Dictionary<Key,Value>
. Designriktlinjerna för .NET-bibliotek har goda råd om när du ska använda olika samlingstyper som IEnumerable<T>
. Viss användning av matriser (T[]
) är acceptabel i vissa fall, av prestandaskäl. Observera särskilt att seq<T>
det bara är F#-aliaset för IEnumerable<T>
, och därför är seq ofta en lämplig typ för ett .NET-API för vanilj.
I stället för F#-listor:
member this.PrintNames(names: string list) =
...
Använd F#-sekvenser:
member this.PrintNames(names: seq<string>) =
...
Använd enhetstypen som den enda indatatypen för en metod för att definiera en nollargumentsmetod, eller som den enda returtypen för att definiera en void-returning-metod
Undvik andra användningar av enhetstypen. Dessa är bra:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
Detta är dåligt:
member this.WrongUnit( x: unit, z: int) = ((), ())
Sök efter null-värden på .NET API-gränser för vanilj
F#-implementeringskoden tenderar att ha färre null-värden på grund av oföränderliga designmönster och begränsningar för användning av null-literaler för F#-typer. Andra .NET-språk använder ofta null som ett värde mycket oftare. Därför bör F#-kod som exponerar ett vanilj-.NET-API kontrollera parametrarna för null vid API-gränsen och förhindra att dessa värden flödar djupare in i F#-implementeringskoden. Funktionen isNull
eller mönstermatchningen i null
mönstret kan användas.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull` argName (arg: obj) =
if isNull arg then nullArg argName
else ()
Undvik att använda tupplar som returvärden
I stället föredrar du att returnera en namngiven typ som innehåller aggregerade data eller använda utparametrar för att returnera flera värden. Även om tupplar och struct tupplar finns i .NET (inklusive C#-språkstöd för struct tupplar), kommer de oftast inte att tillhandahålla det idealiska och förväntade API:et för .NET-utvecklare.
Undvik användning av currying av parametrar
Använd i stället .NET-anropskonventioner Method(arg1,arg2,…,argN)
.
member this.TupledArguments(str, num) = String.replicate num str
Tips: Om du utformar bibliotek för användning från ett .NET-språk finns det inget substitut för att faktiskt utföra lite experimentell C#- och Visual Basic-programmering för att säkerställa att dina bibliotek "känns rätt" från dessa språk. Du kan också använda verktyg som .NET Reflector och Visual Studio Object Browser för att säkerställa att bibliotek och deras dokumentation visas som förväntat för utvecklare.
Bilaga
Exempel från slutpunkt till slutpunkt för att utforma F#-kod för användning av andra .NET-språk
Överväg följande klass:
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) ]
Den här klassens här anförda F#-typ är följande:
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
Nu ska vi ta en titt på hur den här F#-typen visas för en programmerare med ett annat .NET-språk. Den ungefärliga C#-signaturen är till exempel följande:
// 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; }
}
Det finns några viktiga saker att lägga märke till om hur F# representerar konstruktioner här. Till exempel:
Metadata som argumentnamn har bevarats.
F#-metoder som tar två argument blir C#-metoder som tar två argument.
Funktioner och listor blir referenser till motsvarande typer i F#-biblioteket.
Följande kod visar hur du justerar den här koden för att ta hänsyn till dessa saker.
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) }
Kodens härledda F#-typ är följande:
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
C#-signaturen är nu följande:
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 korrigeringar som görs för att förbereda den här typen för användning som en del av ett vanilla .NET-bibliotek är följande:
Justerat flera namn:
Point1
,n
,l
, ochf
blevRadialPoint
,count
,factor
ochtransform
, respektive.Använde en returtyp
seq<RadialPoint>
i stället förRadialPoint list
genom att ändra en listkonstruktion med en[ ... ]
sekvenskonstruktion med hjälp avIEnumerable<RadialPoint>
.Använde .NET-ombudstypen
System.Func
i stället för en F#-funktionstyp.
Detta gör det mycket trevligare att använda i C#-kod.