Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Ez a dokumentum az F#-programozás összetevőinek tervezési irányelveit tartalmazza az F#-összetevők tervezési irányelvei, a 14-es verzió, a Microsoft Research és az F# Software Foundation által eredetileg válogatott és karbantartott verzió alapján.
Ez a dokumentum feltételezi, hogy ismeri az F#-programozást. Sok köszönet az F# közösségnek az útmutató különböző verzióival kapcsolatos közreműködésükért és hasznos visszajelzésükért.
Áttekintés
Ez a dokumentum az F#-összetevők tervezésével és kódolásával kapcsolatos problémák némelyikét ismerteti. Az összetevők a következők bármelyikét jelenthetik:
- Az F#-projekt egy olyan rétege, amelyben a projekten belül külső felhasználók találhatók.
- Egy F# kód által használt könyvtár, amely az assembly határain átnyúló használatra készült.
- Bármely .NET-nyelv általi használatra szánt kódtár, amelyet az assembly határokon keresztül lehet használni.
- Csomagtáron keresztüli terjesztésre szánt kódtár, például NuGet.
Az ebben a cikkben ismertetett technikák a A jó F#-kódöt alapelvét követik, és így a funkcionális és az objektumprogramozást is szükség szerint használják.
A módszertantól függetlenül az összetevő és a kódtár tervezője számos gyakorlati és prózai problémával szembesül, amikor olyan API-t próbál létrehozni, amelyet a fejlesztők a legkönnyebben használnak. A .NET-kódtár tervezési irányelveinek lelkiismeretes alkalmazása a kellemesen használható API-k konzisztens készletének létrehozására irányít.
Általános irányelvek
Az F#-kódtárakra néhány univerzális irányelv vonatkozik, függetlenül attól, hogy milyen célközönséget szeretne használni a tárhoz.
A .NET-kódtár tervezési irányelveinek megismerése
Függetlenül attól, hogy milyen F# kódolást végez, hasznos, ha ismeri a .NET-kódtárak tervezési irányelveit. A legtöbb F# és .NET programozó ismeri ezeket az irányelveket, és elvárja, hogy a .NET-kód megfeleljen ezeknek.
A .NET-kódtár tervezési irányelvei általános útmutatást nyújtanak az elnevezéssel, az osztályok és felületek tervezésével, a tagtervezéssel (tulajdonságok, módszerek, események stb.) kapcsolatban, és hasznos első referenciaként szolgálnak a különféle tervezési útmutatókhoz.
XML-dokumentációs megjegyzések hozzáadása a kódhoz
A nyilvános API-k XML-dokumentációja biztosítja, hogy a felhasználók nagyszerű Intellisense- és Quickinfo-információt kaphassanak, amikor ezeket a típusokat és ezeket a tagokat használják, és lehetővé teszik a dokumentációs fájlok létrehozását a könyvtár számára. Nézze meg az XML-dokumentációt, amely az xmldoc megjegyzésekben további jelölésekhez használható XML-tag-ekről szól.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Használhatja a rövid formátumú XML-megjegyzéseket (/// comment), vagy a standard XML-megjegyzéseket (///<summary>comment</summary>).
Fontolja meg explicit aláírásfájlok (.fsi) használatát a stabil kódtárhoz és az összetevő API-khoz
Az explicit aláírási fájlok használata az F#-kódtárakban a nyilvános API tömör összegzését biztosítja, amely segít a tár teljes nyilvános felületének megismerésében, valamint a nyilvános dokumentáció és a belső megvalósítás részletei közötti tiszta elkülönítésben. Az aláírási fájlok súrlódást gördíthetnek a nyilvános API módosításához azáltal, hogy módosításokat kell végrehajtani mind a megvalósítási, mind az aláírási fájlokban. Ennek eredményeképpen az aláírási fájlokat általában csak akkor kell bevezetni, ha egy API megszilárdult, és a továbbiakban nem várható jelentős változás.
A sztringek .NET-ben való használatához ajánlott eljárások követése
Kövesse ajánlott eljárásokat a sztringek .NET-ben való használatához, útmutatást, ha a projekt hatóköre ezt indokolja. Különösen a sztringek konvertálásában és összehasonlításában kifejezett kulturális szándékot (ahol alkalmazható).
F#-elérésű kódtárakra vonatkozó irányelvek
Ez a szakasz a nyilvános F#-elérésű kódtárak fejlesztésére vonatkozó javaslatokat ismerteti; vagyis olyan nyilvános API-kat közzétenő kódtárak, amelyeket F#-fejlesztőknek szántak. Számos könyvtártervezési javaslat létezik, amelyek kifejezetten az F#-ra vonatkoznak. Az alábbi konkrét javaslatok hiányában a .NET-kódtár tervezési irányelvei a tartalék útmutatók.
Elnevezési konvenciók
.NET elnevezési és nagybetűsítési konvenciók használata
Az alábbi táblázat a .NET elnevezési és nagybetűsítési konvencióit követi. Kis kiegészítések vannak, amelyek az F# szerkezeteket is tartalmazzák. Ezek a javaslatok kifejezetten olyan API-k számára szólnak, amelyek túllépik az F#–F# határokat, és illeszkednek a .NET BCL és a kódtárak többségének kifejezéseivel.
| Épít | Eset | Rész | Példák | Jegyzetek |
|---|---|---|---|---|
| Betontípusok | PascalCase | Főnév/ melléknév | Lista, dupla, összetett | A konkrét típusok a szerkezetek, osztályok, enumok, delegáltak, rekordok és uniók. Bár a típusnevek hagyományosan kisbetűsek az OCamlben, az F# elfogadta a .NET elnevezési sémát a típusok esetében. |
| DLL-ek | PascalCase | Fabrikam.Core.dll | ||
| Unió címkéi | PascalCase | Főnév | Néhány, Hozzáadás, Siker | Ne használjon előtagot nyilvános API-kban. Ha belsőleg használ egy előtagot, például: "type Teams = TAlpha | TBeta | TDelta". |
| Esemény | PascalCase | Ige | ÉrtékMegváltozott / ÉrtékVáltozik | |
| Kivételek | PascalCase | WebException | A névnek "Exception" (Kivétel) végződnie kell. | |
| Mező | PascalCase | Főnév | JelenlegiNév | |
| Interfésztípusok | PascalCase | Főnév/ melléknév | IDisposable (egy .NET keretrendszerbeli interfész erőforrások felszabadítására) | A névnek az "I" betűvel kell kezdődnie. |
| Módszer | PascalCase | Ige | ToString (objektum sztringgé alakítása) | |
| Namespace | PascalCase | Microsoft.FSharp.Core | Általában használja a <Organization>.<Technology>[.<Subnamespace>]-t, de ha a technológia független a szervezettől, hagyja el a szervezet említését. |
|
| Paraméterek | camelCase | Főnév | típusNév, átalakítás, tartomány | |
| értékek (belső) | camelCase vagy PascalCase | Főnév/ ige | getValue, myTable | |
| Külső értékeket beállít | camelCase vagy PascalCase | Főnév/ige | List.map, Mai dátum | A let-bound értékek gyakran nyilvánosak, ha hagyományos funkcionális tervezési mintákat követnek. A PascalCaset azonban általában akkor érdemes használni, ha az azonosító más .NET-nyelvekről is használható. |
| Ingatlan | PascalCase | Főnév/ melléknév | FájlVégeEllenőrzés, HáttérSzín | A logikai tulajdonságok általában az "Is" és "Can" szavakkal kezdődnek, és állító formában kell lenniük, mint például "IsEndOfFile", nem pedig "IsNotEndOfFile". |
A rövidítések elkerülése
A .NET irányelvei nem akadályozzák a rövidítések használatát (például :"OnButtonClick használata OnBtnClickhelyett"). A gyakori rövidítések, mint például a Async az "Aszinkron", tolerálhatók. Ezt az útmutatót néha figyelmen kívül hagyja a funkcionális programozás; például List.iter az "iterátum" rövidítését használja. Emiatt a rövidítések használata általában nagyobb mértékben tolerálható az F#-to-F# programozásban, de a nyilvános összetevők tervezésekor általában kerülni kell.
A névütközések elkerülése
A .NET-irányelvek szerint a kis- és nagybetűk önmagukban nem használhatók a névütközések egyértelműsítésére, mivel egyes ügyfélnyelvek (például a Visual Basic) nem érzékenyek a kis- és nagybetűkre.
Szükség esetén használjon betűszókat
Az XML-hez hasonló mozaikszavak nem rövidítések, és széles körben használják a .NET könyvtárakban kisbetűsen (Xml). Csak jól ismert, széles körben ismert rövidítéseket szabad használni.
A PascalCase használata általános paraméternevekhez
Használja a PascalCaset a nyilvános API-k általános paraméterneveihez, beleértve az F#-elérésű kódtárakat is. Különösen az T, U, T1, T2 neveket használja tetszőleges általános paraméterekhez, és amikor bizonyos nevek alkalmazása indokolt, akkor az F#-hez tartozó kódtárakban használjon olyan neveket, mint a Key, Value, Arg (de például nem TKey).
PascalCase vagy camelCase használata nyilvános függvényekhez és értékekhez az F#-modulokban
A camelCase olyan nyilvános függvényekhez használatos, amelyeket nem minősített használatra terveztek (például invalidArg), valamint a "standard gyűjteményfüggvényekhez" (például List.map). Mindkét esetben a függvénynevek ugyanúgy viselkednek, mint a kulcsszavak a nyelvben.
Objektum-, típus- és modulterv
Névtereket vagy modulokat használjon a típusok és modulok tárolására.
Egy összetevő minden F#-fájlja névtérdeklarációval vagy moduldeklarációval kezdődik.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
vagy
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
A modulok és a névterek felső szintű rendszerezésére szolgáló használatának különbségei a következők:
- A névterek több fájlra is kiterjedhetnek
- A névterek csak belső modulban tartalmazhatnak F# függvényeket
- Az adott modul kódjának egyetlen fájlban kell lennie
- A legfelső szintű modulok tartalmazhatnak F# függvényeket anélkül, hogy belső modulra van szükség
A legfelső szintű névtér vagy modul közötti választás befolyásolja a kód lefordított formáját, és így hatással lesz más .NET-nyelvek nézetére, ha az API-t végül az F# kódon kívül kell használni.
Metódusok és tulajdonságok használata az objektumtípusokhoz kapcsolódó műveletekhez
Objektumok használatakor a legjobb, ha biztosítja, hogy a használható funkciók metódusokként és tulajdonságokként implementálva legyenek az adott típuson.
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) = ...
Az adott tag funkcióinak nagy részét nem feltétlenül kell implementálnunk az adott tagban, de ennek a funkciónak a fogyasztható része legyen.
A mutable állapot beágyazása osztályok használatával
Az F#-ban ezt csak akkor kell elvégezni, ha ezt az állapotot még nem foglalja bele egy másik nyelvi szerkezet, például egy záró, egy sorozatkifejezés vagy egy aszinkron számítás.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Interfészek használata a kapcsolódó műveletek csoportosításához
Felülettípusok használata műveletek halmazának ábrázolásához. Ezt előnyben részesítik más lehetőségek, például a függvények tuple-jei vagy a függvények rekordjai.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
A következő beállításokat részesíti előnyben:
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
A felületek első osztályú fogalmak a .NET-ben, amelyekkel elérheti, amit a Functors általában adna Önnek. Emellett az egzisztenciális típusokat is kódolhatja a programba, amelyeket a függvények rekordjai nem.
Gyűjteményeken működő függvények csoportosítása modul használatával
Gyűjteménytípus definiálásakor érdemes lehet szabványos műveleteket , például CollectionType.map és CollectionType.iter) biztosítani az új gyűjteménytípusokhoz.
module CollectionType =
let map f c =
...
let iter f c =
...
Ha ilyen modult is tartalmaz, kövesse az FSharp.Core-ban található függvények szabványos elnevezési konvencióit.
A modult használja a függvények csoportosítására a gyakori, kanonikus függvények számára, különösen matematikai és DSL könyvtárakban.
A Microsoft.FSharp.Core.Operators például a FSharp.Core.dlláltal biztosított legfelső szintű függvények (például abs és sin) automatikusan megnyitott gyűjteménye.
Hasonlóképpen, a statisztikai kódtárak tartalmazhatnak egy erf és erfcfüggvényeket tartalmazó modult is, ahol a modul kifejezetten vagy automatikusan megnyitható.
Fontolja meg a RequireQualifiedAccess használatát, és körültekintően alkalmazza az AutoOpen attribútumokat
Ha hozzáadja a [<RequireQualifiedAccess>] attribútumot egy modulhoz, az azt jelzi, hogy a modul nem nyitható meg, és a modul elemeire mutató hivatkozások explicit minősített hozzáférést igényelnek. A Microsoft.FSharp.Collections.List modul például ezt az attribútumot tartalmazza.
Ez akkor hasznos, ha a modul függvényei és értékei olyan neveket tartalmaznak, amelyek valószínűleg ütköznek más modulok neveivel. A minősített hozzáférés megkövetelése jelentősen növelheti a tárak hosszú távú karbantarthatóságát és evolvitását.
Erősen ajánlott az [<RequireQualifiedAccess>] attribútum használata az olyan egyéni modulokhoz, amelyek kibővítik a FSharp.Core által biztosított modulokat (például Seq, List, Array), mivel ezeket a modulokat gyakran használják F#-kódban, és [<RequireQualifiedAccess>] definiálva vannak rajtuk; általánosabban nem célszerű az attribútumot nem tartalmazó egyéni modulokat definiálni, ha az ilyen modul árnyékot árnyékolt, vagy kiterjeszti az attribútummal rendelkező többi modult.
Ha hozzáadja a [<AutoOpen>] attribútumot egy modulhoz, az azt jelenti, hogy a modul megnyílik a névtér megnyitásakor. A [<AutoOpen>] attribútum egy szerelvényre is alkalmazható, amely egy olyan modult jelez, amely automatikusan megnyílik a szerelvény hivatkozásakor.
A MathsHeaven.Statistics statisztikai kódtár például tartalmazhat egy module MathsHeaven.Statistics.Operators, amely függvényeket erf és erfctartalmaz. Ezt a modult ésszerű [<AutoOpen>]ként megjelölni. Ez azt jelenti, hogy open MathsHeaven.Statistics is megnyitja ezt a modult, és behozza a neveket erf és erfc a hatókörbe. A [<AutoOpen>] másik jó felhasználása a bővítménymetelyeket tartalmazó modulok.
A [<AutoOpen>] túlhasználása szennyezett névterekhez vezet, és az attribútumot körültekintően kell használni. Az adott tartományokban lévő adott kódtárak esetében a [<AutoOpen>] megfontolt használata jobb használhatósághoz vezethet.
Fontolja meg az operátortagok meghatározását olyan osztályokban, ahol jól ismert operátorok használata megfelelő
Néha az osztályokat olyan matematikai szerkezetek modellezésére használják, mint a vektorok. Ha a modellezett tartomány jól ismert operátorokkal rendelkezik, hasznos, ha azokat az osztály tagjaiként definiálja.
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
Ez az útmutató az ilyen típusok általános .NET-útmutatójának felel meg. Az F#-kódolásban azonban ez is fontos lehet, mivel ez lehetővé teszi, hogy ezek a típusok az F# függvényekkel és a tagkorlátozásokkal rendelkező metódusokkal (például List.sumBy) együtt használhatók legyenek.
Fontolja meg a CompiledName használatát egy . NET-barát név más .NET-nyelvfelhasználók számára
Néha előfordulhat, hogy az F#-felhasználók számára egy stílusban szeretne valamit elnevíteni (például egy statikus tagot kisbetűvel úgy, hogy úgy jelenjen meg, mintha egy modulhoz kötött függvény lenne), de más stílussal rendelkezik a névhez, amikor egy szerelvénybe van lefordítva. A [<CompiledName>] attribútummal más stílust adhat meg az assemblyt használó F#-en kívüli kódokhoz.
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
A [<CompiledName>]használatával .NET-elnevezési konvenciók használhatók a szerelvény nem F#-felhasználói számára.
Használja a metódusok túlterhelését a tagfüggvényeknél, ha ez egyszerűbb API-t biztosít.
A metódusok túlterhelése hatékony eszköz egy olyan API egyszerűsítésére, amelynek hasonló funkciókat kell végrehajtania, de különböző lehetőségekkel vagy argumentumokkal.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
Az F#-ban gyakoribb az argumentumok száma túlterhelése az argumentumtípusok helyett.
A rekord- és uniótípusok ábrázolásait rejtse el, ha ezeknek a típusoknak a tervezése valószínűleg változni fog.
Kerülje a tárgyak konkrét ábrázolásának felfedését. Az DateTime értékek konkrét ábrázolását például nem fedi fel a .NET-kódtár külső, nyilvános API-ja. Futásidőben a Common Language Runtime ismeri a végrehajtás során használt véglegesített implementációt. A lefordított kód azonban önmagában nem veszi fel a konkrét ábrázolás függőségeit.
A megvalósítási öröklés használatának elkerülése a bővíthetőség érdekében
Az F#-ban ritkán használják a megvalósítás öröklését. Emellett az öröklési hierarchiák gyakran összetettek és nehezen módosíthatók új követelmények érkezésekor. Az öröklés implementációja továbbra is létezik az F#-ban a kompatibilitás érdekében, és ritka esetekben, amikor ez a legjobb megoldás egy problémára, de alternatív technikákat kell keresni az F#-programokban a polimorfizmus tervezésekor, például a felület implementálásánál.
Függvény- és tag szignatúrák
Tuples használata a visszaadott értékekhez, ha kis számú, egymástól független értéket ad vissza
Íme egy jó példa a tuple visszatérési típusban való használatára:
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
A sok összetevőt tartalmazó visszatérési típusok esetén, vagy ahol az összetevők egyetlen azonosítható entitáshoz kapcsolódnak, fontolja meg, hogy elnevezett típust használjon a többszörös helyett.
A Async<T> használata aszinkron programozáshoz az F# API határainál
Ha van egy megfelelő szinkron művelet, amelyet Operation-nak neveznek, és amely egy T-et ad vissza, akkor az aszinkron műveletet AsyncOperation-nek kell nevezni, ha Async<T>-at ad vissza, vagy OperationAsync-nek, ha Task<T>-öt ad vissza. A gyakran használt .NET-típusok esetében, amelyek Begin/End metódusokat közzétesznek, érdemes lehet a Async.FromBeginEnd használatát fontolóra venni kiterjesztési metódusok írásához burkolatként, hogy az F# aszinkron programozási modellt biztosítsuk ezekhez a .NET API-khoz.
type SomeType =
member this.Compute(x:int): int =
...
member this.AsyncCompute(x:int): Async<int> =
...
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
...
Kivételek
A kivételek, eredmények és beállítások megfelelő használatáról a Hibakezelési című témakörben olvashat.
Kiterjesztett tagok
Gondosan alkalmazza az F# bővítménytagokat az F#-to-F# összetevőkben
Az F#-bővítménytagokat általában csak olyan műveletekhez szabad használni, amelyek a használati módok többségében egy típushoz társított belső műveletek lezárása alatt állnak. Az egyik gyakori használat az, hogy olyan API-kat biztosítunk, amelyek idiomatikusabbak az F# számára a különböző .NET-típusok esetében:
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
Unió típusok
Az osztályhierarchiák helyett diszkriminált uniók használata faszerkezetű adatokhoz
A faszerű struktúrák rekurzívan vannak definiálva. Ez kínos az örökléssel, de elegáns a diszkriminált uniókkal.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
A faszerű adatok diszkriminált uniókkal való ábrázolása lehetővé teszi a mintaegyeztetés teljességének előnyeit is.
Használjon [<RequireQualifiedAccess>] olyan uniótípusokhoz, amelyek esettípusai nem elég egyediek.
Előfordulhat, hogy olyan tartományban találja magát, ahol ugyanaz a név a legjobb név különböző dolgokhoz, például a diszkriminált uniós esetekhez. A [<RequireQualifiedAccess>] használatával egyértelművé teheti az esetneveket az open utasítások sorrendjétől függő árnyékolás okozta zavaró hibák elkerülése érdekében.
Rejtse el a bináris kompatibilis API-k diszkriminált unióinak ábrázolását, ha ezeknek a típusoknak a kialakítása valószínűleg fejlődni fog
Az egyesítési típusok az F# mintaillesztési formákra támaszkodnak egy tömör programozási modellhez. Ahogy korábban említettük, el kell kerülnie a konkrét adatmegjelenítések feltárását, ha az ilyen típusok kialakítása valószínűleg fejlődni fog.
A diszkriminált unió ábrázolása például elrejthető privát vagy belső nyilatkozattal, vagy aláírási fájl használatával.
type Union =
private
| CaseA of int
| CaseB of string
Ha válogatás nélkül fedi fel a diszkriminált uniókat, előfordulhat, hogy a kódtárat nehezen tudja a felhasználói kód feltörése nélkül verziószámozni. Ehelyett érdemes lehet egy vagy több aktív mintát felfedni, hogy a típusod értékei feletti mintavételt tegye lehetővé.
Az aktív minták alternatív módot biztosítanak az F#-felhasználók számára a mintaegyeztetés biztosítására, miközben elkerülik az F# uniótípusok közvetlen felfedését.
Beágyazott függvények és tagkorlátozások
Általános numerikus algoritmusok definiálása beágyazott függvények használatával hallgatólagos tagkorlátozásokkal és statikusan feloldott általános típusok használatával
Az Aritmetikai tagkorlátozások és az F#-összehasonlítási kényszerek az F#-programozás standardjai. Vegyük például a következő kódot:
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
A függvény típusa a következő:
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
Ez egy matematikai kódtár nyilvános API-jának megfelelő függvénye.
Kerülje a tagkorlátozások használatát a típusosztályok és a duck typing szimulálásához.
A "kacsagépelés" szimulálható F# tagkorlátozásokkal. Az ezt használó tagok azonban általában nem használhatók az F#-to-F# kódtártervekben. Ennek az az oka, hogy a nem ismert vagy nem standard implicit korlátozásokon alapuló kódtár-kialakítások általában a felhasználói kód rugalmatlanná válását okozzák, és egy adott keretrendszermintához kapcsolódnak.
Emellett jó esély van arra, hogy a tagi korlátozások ilyen használata nagyon hosszú fordítási időt eredményezhet.
Operátordefiníciók
Kerülje az egyéni szimbolikus operátorok definiálását
A testreszabott operátorok bizonyos helyzetekben nélkülözhetetlenek, és rendkívül hasznos jelölésbeli eszközök a nagymértékű megvalósítási kódon belül. A kódtár új felhasználói számára a nevesített függvények gyakran egyszerűbben használhatók. Emellett az egyéni szimbolikus operátorokat nehéz dokumentálni, és a felhasználók nehezebben keresnek segítséget az operátorokhoz az IDE és a keresőmotorok meglévő korlátozásai miatt.
Ennek eredményeképpen a legjobb, ha megnevezett függvényekként és tagokként teszi közzé a funkciókat, és csak akkor teszi elérhetővé az operátorokat ehhez a funkcióhoz, ha a jelölési előnyök meghaladják a dokumentációt és a kognitív költségeket.
Mértékegységek
Körültekintően használjon mértékegységeket a hozzáadott típusbiztonsághoz az F# kódban
A mértékegységek további gépelési információi törlődnek, ha más .NET-nyelvek is megtekintik. Vegye figyelembe, hogy a .NET-összetevők, eszközök és tükröződés típusok-sans-egységeket fognak látni. A C#-felhasználók például floathelyett float<kg> fognak látni.
Rövidítéstípusok
Az F#-kód egyszerűsítése érdekében körültekintően használjon típus rövidítéseket
A .NET-összetevők, az eszközök és a tükröződés nem jelennek meg a típusok rövidített neveiben. A típus rövidítések jelentős használata összetettebbé teheti a tartományt, mint amilyen valójában, ami összezavarhatja a fogyasztókat.
Kerülje a rövidítéseket olyan nyilvános típusok esetében, amelyek tagjai és tulajdonságai belsőleg különböznek a rövidített típuson elérhető típustól
Ebben az esetben a rövidített típus túl sokat elárul a definiált tényleges típus ábrázolásáról. Ehelyett érdemes lehet a rövidítést egy osztálytípusba vagy egy egyes eset diszkriminált unióba burkolni (vagy ha a teljesítmény elengedhetetlen, fontolja meg egy struktúratípus használatát a rövidítés burkolására).
Csábító például egy többtérképet egy F#-térkép speciális eseteként definiálni, például:
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
Azonban az ilyen típusú logikai pontjelölési műveletek nem egyeznek meg a térképen végzett műveletekkel – például ésszerű, hogy a keresési operátor map[key] az üres listát adja vissza, ha a kulcs nem szerepel a szótárban, ahelyett hogy kivételt dobna.
Más .NET-nyelvekből származó kódtárakra vonatkozó irányelvek
Amikor könyvtárakat tervezünk használatra más .NET nyelvekből, fontos követni a .NET-könyvtártervezési irányelveket. Ebben a dokumentumban ezek a kódtárak vanília .NET-kódtárakként vannak címkézve, szemben az F#-elérésű kódtárakkal, amelyek korlátozás nélkül használnak F# szerkezeteket. A vanília .NET-kódtárak tervezése azt jelenti, hogy a nyilvános API-ban az F#-specifikus szerkezetek használatának minimalizálásával a .NET-keretrendszer többi részével konzisztens, ismerős és idiomatikus API-k érhetők el. A szabályokat a következő szakaszok ismertetik.
Névtér- és típusterv (más .NET-nyelvekből származó kódtárakhoz)
A .NET elnevezési konvenciók alkalmazása az összetevők nyilvános API-jára
Különös figyelmet kell fordítani a rövidített nevek használatára és a .NET nagybetűsítési irányelveire.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Névterek, típusok és tagok használata az összetevők elsődleges szervezeti struktúrájaként
A nyilvános funkciókat tartalmazó fájloknak namespace deklarációval kell kezdődnie, és a névterekben csak a nyilvánosan elérhető entitások lehetnek típusok. Ne használjon F#-modulokat.
Nem nyilvános modulok használata implementációs kód, segédprogramtípusok és segédprogramfüggvények tárolására.
A statikus típusokat előnyben kell részesíteni a moduloknál, mivel lehetővé teszik az API jövőbeli fejlődését, hogy túlterhelést és más .NET API tervezési fogalmakat használjanak, amelyek nem használhatók az F#-modulokban.
Például a következő nyilvános API helyett:
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Fontolja meg inkább a következőt:
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# rekordtípusok használata alapértelmezett .NET API-kban, ha a típusok kialakítása nem változik
Az F#-rekordtípusok egyszerű .NET-osztályba állnak össze. Ezek az API-k néhány egyszerű, stabil típusához alkalmasak. Fontolja meg a [<NoEquality>] és [<NoComparison>] attribútumok használatát az interfészek automatikus generálásának letiltásához. Ne használjon mutable rekordmezőket a vanilla .NET API-kban, mivel ezek nyilvános mezőt fednek fel. Mindig gondolja át, hogy egy osztály rugalmasabb lehetőséget biztosít-e az API jövőbeli fejlődéséhez.
A következő F#-kód például egy C#-felhasználó számára teszi elérhetővé a nyilvános API-t:
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; }
}
Az F# uniótípusok ábrázolásának elrejtése a szokásos .NET API-kban
Az F#-egyesítési típusokat gyakran nem használják az összetevők határán, még az F#-to-F# kódoláshoz sem. Kiváló implementációs eszköz, ha belsőleg használják az összetevőkön és kódtárakon belül.
A vanília .NET API tervezésekor fontolja meg egy unió típus reprezentációjának elrejtését privát deklaráció vagy aláírási fájl használatával.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Olyan típusokat is kiegészíthet, amelyek belső unió reprezentációt használnak, hogy egy kívánt .NET-hez kapcsolódó API-t hozzanak létre.
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)
Grafikus felhasználói felület és egyéb összetevők tervezése a keretrendszer tervezési mintáival
A .NET-en belül számos különböző keretrendszer érhető el, például WinForms, WPF és ASP.NET. Az egyes összetevők elnevezési és tervezési konvencióit akkor érdemes használni, ha összetevőket tervez az ezekben a keretrendszerekben való használatra. A WPF-programozáshoz például fogadjon el WPF-tervezési mintákat a megtervezett osztályokhoz. A felhasználói felület programozásában használt modellekhez használjon olyan tervezési mintákat, mint az események és az értesítésalapú gyűjtemények, például a System.Collections.ObjectModel.
Objektum- és tagterv (más .NET-nyelvekből származó kódtárakhoz)
.NET-események közzététele a CLIEvent attribútum használatával
Hozzon létre egy DelegateEvent egy adott .NET-delegálttípussal, amely egy objektumot és EventArgs használ (nem Event, amely alapértelmezés szerint csak a FSharpHandler típust használja), hogy az események más .NET-nyelvekben is jól ismert módon legyenek közzétéve.
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
Aszinkron műveletek elérhetővé tehetők .NET-feladatokat visszaadó metódusokként
A feladatok a .NET-ben aktív aszinkron számítások megjelenítésére szolgálnak. A feladatok általában kevésbé kompozíciósak, mint az F# Async<T> objektumok, mivel "már végrehajtott" feladatokat jelölnek, és nem állíthatók össze olyan módon, hogy párhuzamos összeállítást végezzenek, vagy amelyek elrejtik a lemondási jelek és egyéb környezeti paraméterek propagálását.
Ennek ellenére a Feladatokat visszaadó metódusok a .NET-en futó aszinkron programozás szabványos reprezentációi.
/// 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
Gyakran szeretne explicit lemondási jogkivonatot is elfogadni:
/// 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-delegálttípusok használata F# függvénytípusok helyett
Itt az "F# függvénytípusok" a "nyíl" típusokat jelentik, például int -> int.
A következő helyett:
member this.Transform(f: int->int) =
...
Tegye a következőt:
member this.Transform(f: Func<int,int>) =
...
Az F# függvénytípus class FSharpFunc<T,U> jelenik meg más .NET-nyelvek számára, és kevésbé alkalmas a delegált típusokat megértő nyelvi funkciókhoz és eszközökhöz. Ha egy .NET-keretrendszer 3.5-ös vagy újabb verziójára célzó, magasabbrendű metódust hoz létre, a System.Func és System.Action delegálások a megfelelő API-k a közzétételhez, lehetővé téve, hogy a .NET-fejlesztők zökkenőmentesen használhassák ezeket az API-kat. (A .NET-keretrendszer 2.0-s verziójának megcélzásakor a rendszer által definiált delegálási típusok korlátozottabbak; érdemes lehet előre definiált delegálási típusokat használni, például System.Converter<T,U> vagy egy adott delegálttípus definiálását.)
Másrészt a .NET-delegáltak nem természetes választások az F#-kódtárak számára (lásd az F#-kódtárak következő szakaszát). Ennek eredményeképpen a vanilla .NET-kódtárak magasabb rendű metódusainak fejlesztésekor gyakori implementációs stratégia az összes implementáció létrehozása F#-függvénytípusok használatával, majd a nyilvános API létrehozása meghatalmazottak használatával a tényleges F# implementáció tetején, vékony homlokzatként.
Használja a TryGetValue mintát az F# opció értékek visszaadása helyett, és részesítse előnyben a metódus túlterhelést az F# opció értékek argumentumként való használata helyett.
Az F# opció típus általános használati mintái az API-kban hagyományos .NET API-kban jobban megvalósíthatóak standard .NET tervezési technikákkal. F#-beállításérték visszaadása helyett fontolja meg a bool visszatérési típus és egy out paraméter használatát, mint a "TryGetValue" mintában. Az F#-beállításértékek paraméterként való használata helyett érdemes lehet metódustúlterhelést vagy választható argumentumokat használni.
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
A .NET gyűjteményfelület IEnumerable<T> és IDictionary<Key,Value> használata paraméterekhez és visszatérési értékekhez
Kerülje az olyan betongyűjtési típusok használatát, mint a .NET-tömbök T[], az F# típusú list<T>, a Map<Key,Value> és a Set<T>, valamint a .NET betongyűjtési típusok, például a Dictionary<Key,Value>. A .NET-kódtár tervezési irányelvei jó tanácsokat adnak a különböző gyűjteménytípusok, például a IEnumerable<T>használatának időpontjával kapcsolatban. A tömbök (T[]) egyes használata bizonyos körülmények között elfogadható teljesítménybeli okokból. Különösen vegye figyelembe, hogy a seq<T> valójában IEnumerable<T>F# aliasa, így a seq gyakran megfelelő típus az alapértelmezett .NET API-hoz.
F#-listák helyett:
member this.PrintNames(names: string list) =
...
F#-sorozatok használata:
member this.PrintNames(names: seq<string>) =
...
Nulla argumentumú metódus definiálásához használja az egységtípust egy metódus egyetlen bemeneti típusaként, vagy az egyetlen visszatérési típusként a null értékű visszatérési metódus definiálásához
Kerülje az egységtípus egyéb használatát. Ezek jók.
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
Ez rossz:
member this.WrongUnit( x: unit, z: int) = ((), ())
Null értékek ellenőrzése az alapértelmezett .NET API-interfészeken
Az F# implementációs kód általában kevesebb null értékkel rendelkezik, mivel az F# típusú null literálok használatára vonatkozó nem módosítható tervezési minták és korlátozások miatt kevesebb null értéket használnak. Más .NET-nyelvek gyakran a null értéket használják sokkal gyakrabban. Emiatt a vanilla .NET API-t feltáró F# kódnak ellenőriznie kell a null érték paramétereit az API határán, és meg kell akadályoznia, hogy ezek az értékek mélyebben befolyjanak az F# implementációs kódjába. A isNull függvényt vagy a null mintához tartozó mintaillesztést lehet használni.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
Az F# 9-től kezdve használhatja az új | nullszintaxist annak érdekében, hogy a fordító a lehetséges null értékeket jelezze, és ahol kezelni kell őket:
let checkNonNull argName (arg: obj | null) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj | null) =
if isNull arg then nullArg argName
else ()
Az F# 9-ben a fordító figyelmeztetést ad ki, ha azt észleli, hogy egy lehetséges null érték nincs kezelve:
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
// `ReadLine` may return null here - when the stream is finished
let line = sr.ReadLine()
// nullness warning: The types 'string' and 'string | null'
// do not have equivalent nullability
printLineLength line
Ezeket a figyelmeztetéseket F# null mintával kell kezelni, egyezésben:
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
let line = sr.ReadLine()
match line with
| null -> ()
| s -> printLineLength s
Ne használjon tömböt visszatérési értékként
Ehelyett inkább az összesítő adatokat tartalmazó névvel ellátott típust adja vissza, vagy használjon ki paramétereket több érték visszaadásához. Bár a tuples és a struct tuples a .NET-ben is létezik (beleértve a C# nyelv támogatását a struct tupleshez), ezek többnyire nem biztosítják az ideális és várt API-t a .NET-fejlesztők számára.
Kerülje el a paraméterek currying-elését
Ehelyett használjon .NET-hívási konvenciók Method(arg1,arg2,…,argN).
member this.TupledArguments(str, num) = String.replicate num str
Tipp: Ha bármilyen .NET-nyelvből való használatra tervez könyvtárakat, nincs jobb módja annak, mint kísérleti C# és Visual Basic programozást végezni, hogy biztosítsa, hogy a könyvtárak megfelelően illeszkedjenek ezekhez a nyelvekhez. Az olyan eszközöket is használhatja, mint a .NET Reflector és a Visual Studio Object Browser, hogy a kódtárak és azok dokumentációja a várt módon jelenjen meg a fejlesztők számára.
Függelék
Teljes körű példa az F#-kód más .NET-nyelvek általi használatára való tervezésére
Vegye figyelembe a következő osztályt:
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) ]
Ennek az osztálynak az F#-típusa a következő:
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
Nézzük meg, hogyan jelenik meg ez az F# típus egy másik .NET-nyelvet használó programozó számára. A C# hozzávetőleges aláírása például a következő:
// 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; }
}
Itt néhány fontos szempontot kell megfigyelni azzal kapcsolatban, hogy az F# hogyan jelöli a szerkezeteket. Például:
A metaadatok, például az argumentumnevek megmaradtak.
A két argumentumot tartalmazó F# metódusok két argumentumot tartalmazó C# metódussá válnak.
A függvények és listák az F#-kódtár megfelelő típusaira mutató hivatkozásokká válnak.
Az alábbi kód bemutatja, hogyan módosíthatja ezt a kódot, hogy ezeket figyelembe vegye.
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) }
A kód F#-jának kikövetkedő típusa a következő:
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
A C#-aláírás a következő:
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; }
}
Az ilyen típusú javítások a vanilla .NET-kódtár részeként való használatra való előkészítéséhez a következők:
Számos nevet módosított:
Point1,n,lésfRadialPoint,count,factoréstransformlett.A
seq<RadialPoint>listaszerkezet helyettRadialPoint listszekvenciaszerkezetet használva[ ... ]visszatérési típust használt aIEnumerable<RadialPoint>helyett.A .NET delegált
System.Functípust használták az F# függvénytípus helyett.
Ez sokkal kellemesebbé teszi a C#-kódban való felhasználást.