F#-összetevők tervezési irányelvei

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

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és fRadialPoint, count, factorés transformlett.

  • A seq<RadialPoint> listaszerkezet helyett RadialPoint listszekvenciaszerkezetet használva [ ... ] visszatérési típust használt a IEnumerable<RadialPoint> helyett.

  • A .NET delegált System.Func típust használták az F# függvénytípus helyett.

Ez sokkal kellemesebbé teszi a C#-kódban való felhasználást.