Sdílet prostřednictvím


Pravidla návrhu komponent jazyka F#

Tento dokument je sada pokynů pro návrh součástí pro programování jazyka F# na základě pokynů pro návrh komponent jazyka F#, v14, Microsoft Research a verze, která byla původně kurátorována a udržována programem F# Software Foundation.

Tento dokument předpokládá, že znáte programování v jazyce F#. Mnoho díky komunitě F# za své příspěvky a užitečnou zpětnou vazbu k různým verzím tohoto průvodce.

Přehled

Tento dokument se zabývá některými problémy souvisejícími s návrhem a kódováním komponent jazyka F#. Komponenta může znamenat některou z následujících možností:

  • Vrstva v projektu jazyka F#, která má v rámci daného projektu externí uživatele.
  • Knihovna určená ke spotřebě kódu jazyka F# přes hranice sestavení.
  • Knihovna určená ke spotřebě libovolného jazyka .NET přes hranice sestavení.
  • Knihovna určená k distribuci prostřednictvím úložiště balíčků, například NuGet.

Techniky popsané v tomto článku se řídí pěti principy dobrého kódu jazyka F#, a proto podle potřeby využívají funkční i objektové programování.

Bez ohledu na metodologii čelí návrhář komponent a knihovny řadě praktických a prosaických problémů při pokusu o vytvoření rozhraní API, které je nejsnážnější pro vývojáře. Konzistentní aplikace pokynů pro návrh knihovny .NET vás povede k vytvoření konzistentní sady rozhraní API, která jsou příjemná k využívání.

Obecné pokyny

Existuje několik univerzálních pokynů, které platí pro knihovny jazyka F# bez ohledu na zamýšlenou cílovou skupinu knihovny.

Informace o pokynech k návrhu knihovny .NET

Bez ohledu na to, jaký typ kódování jazyka F# děláte, je důležité mít funkční znalosti pokynů pro návrh knihovny .NET. Většina ostatních programátorů jazyka F# a .NET bude s těmito pokyny obeznámena a očekává, že kód .NET bude odpovídat těmto pravidlům.

Pokyny k návrhu knihovny .NET poskytují obecné pokyny týkající se pojmenování, navrhování tříd a rozhraní, návrhu členů (vlastností, metod, událostí atd.) a dalších a jsou užitečným prvním referenčním bodem pro celou řadu pokynů k návrhu.

Přidání komentářů dokumentace XML do kódu

Dokumentace XML k veřejným rozhraním API zajišťuje, že uživatelé můžou získat skvělé technologie IntelliSense a Rychlé informace při použití těchto typů a členů a povolit vytváření souborů dokumentace pro knihovnu. Podívejte se do dokumentace XML o různých značkách XML, které lze použít pro další revize v komentářích xmldoc.

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

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

Můžete použít buď krátké komentáře XML (/// comment), nebo standardní komentáře XML (///<summary>comment</summary>).

Zvažte použití explicitních souborů podpisů (.fsi) pro stabilní rozhraní API knihoven a komponent.

Použití explicitních souborů podpisů v knihovně jazyka F# poskytuje stručné shrnutí veřejného rozhraní API, které pomáhá zajistit, že znáte celý veřejný povrch knihovny a poskytuje čisté oddělení mezi veřejnou dokumentací a interními podrobnostmi implementace. Soubory podpisu přidávají třecí plochy ke změně veřejného rozhraní API tím, že vyžadují, aby se změny provedly v implementaci i v souborech podpisu. V důsledku toho by soubory podpisů měly být obvykle zavedeny pouze v případě, že se rozhraní API zvýsnilo a už se očekává, že se výrazně nezmění.

Postupujte podle osvědčených postupů pro používání řetězců v .NET.

Pokud rozsah projektu zaručuje jeho rozsah, postupujte podle osvědčených postupů pro používání řetězců v .NET . Konkrétně explicitně uvádí kulturní záměr při převodu a porovnání řetězců (pokud je to možné).

Pokyny pro knihovny určené pro jazyk F#

Tato část obsahuje doporučení pro vývoj veřejných knihoven určených pro jazyk F#; to znamená, že knihovny zveřejňující veřejná rozhraní API, která mají být spotřebována vývojáři jazyka F#. Pro jazyk F# platí celá řada doporučení pro návrh knihovny. V případě, že nejsou k dispozici konkrétní doporučení, která následují, jsou pokyny pro návrh knihovny .NET náhradními pokyny.

Zásady vytváření názvů

Použití konvencí pojmenování a velkých písmen v .NET

Následující tabulka se řídí konvencí pojmenování a velkých písmen .NET. K zahrnutí konstruktorů jazyka F# existují i malé dodatky. Tato doporučení jsou určená zejména pro rozhraní API, která překračují hranice F#-to-F#, která jsou vhodná pro idiomy z .NET BCL a většiny knihoven.

Konstrukce Velikost písmen Část Příklady Notes
Typy betonu PascalCase Podstatná jména a přídavná jména List, Double, Complex Konkrétní typy jsou struktury, třídy, výčty, delegáty, záznamy a sjednocení. I když názvy typů jsou tradičně malými písmeny v OCaml, jazyk F# přijal schéma pojmenování .NET pro typy.
Knihovny DLL PascalCase Fabrikam.Core.dll
Značky sjednocení PascalCase Podstatné jméno Některé, přidat, úspěch Nepoužívejte předponu ve veřejných rozhraních API. Volitelně můžete použít předponu, pokud je interní, například "typ Teams = TAlpha | TBeta | TDelta".
Událost PascalCase Verb (Příkaz) ValueChanged / ValueChanging
Výjimky PascalCase Webexception Název by měl končit výjimkou.
Pole PascalCase Podstatné jméno CurrentName
Typy rozhraní PascalCase Podstatná jména a přídavná jména Idisposable Název by měl začínat na "I".
metoda PascalCase Verb (Příkaz) ToString
Obor názvů PascalCase Microsoft.FSharp.Core Obecně platí <Organization>.<Technology>[.<Subnamespace>], že pokud je technologie nezávislá na organizaci, zahoďte organizaci.
Parametry camelCase Podstatné jméno typeName, transform, range
let values (internal) camelCase nebo PascalCase Podstatné jméno nebo sloveso getValue, myTable
let values (external) camelCase nebo PascalCase Podstatné jméno nebo sloveso List.map, Dates.Today Hodnoty let-bound jsou často veřejné při sledování tradičních funkčních vzorů návrhu. Obecně však použijte PascalCase, pokud lze identifikátor použít z jiných jazyků .NET.
Vlastnost PascalCase Podstatná jména a přídavná jména IsEndOfFile, BackColor Logické vlastnosti obecně používají Is a Can a měly by být pozitivní, jako v IsEndOfFile, nikoli IsNotEndOfFile.

Vyhněte se zkratkám

Pokyny k .NET nedoporučuje používat zkratky (například "použít OnButtonClick místo OnBtnClick"). Běžné zkratky, například Async "Asynchronní", jsou tolerovány. Toto vodítko je někdy ignorováno pro funkční programování; List.iter Například používá zkratku pro "iterovat". Z tohoto důvodu je použití zkratek v programování F#-to-F# snášeno na vyšší úroveň, ale obecně by se mělo vyhnout návrhu veřejných komponent.

Vyhněte se kolizím názvů velikostí

Pokyny pro .NET říkají, že k nejednoznačnosti kolizí názvů nelze použít samotné velikosti písmen, protože některé jazyky klienta (například Visual Basic) nerozlišují malá a velká písmena.

V případě potřeby používejte zkratky.

Zkratky, jako je XML, nejsou zkratky a běžně se používají v knihovnách .NET v neapitalizované podobě (Xml). Měly by se použít pouze dobře známé, široce známé zkratky.

Použití PascalCase pro obecné názvy parametrů

Pro obecné názvy parametrů ve veřejných rozhraních API, včetně pro knihovny určené pro jazyk F#, použijte PascalCase. Konkrétně používejte názvy jako T, , UT1, T2 pro libovolné obecné parametry a pokud mají konkrétní názvy smysl, pak pro knihovny určené pro jazyk F#používají názvy jako Key, ( ValueArg ale ne napříkladTKey).

Použití jazyka PascalCase nebo camelCase pro veřejné funkce a hodnoty v modulech jazyka F#

camelCase se používá pro veřejné funkce, které jsou navrženy tak, invalidArgaby byly použity nekvalifikované (například ) a pro "standardní funkce kolekce" (například List.map). V obou těchto případech názvy funkcí fungují podobně jako klíčová slova v jazyce.

Návrh objektů, typů a modulů

Použití oborů názvů nebo modulů k zahrnutí typů a modulů

Každý soubor F# v komponentě by měl začínat deklarací oboru názvů nebo deklarací modulu.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

nebo

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

Rozdíly mezi používáním modulů a oborů názvů k uspořádání kódu na nejvyšší úrovni jsou následující:

  • Obory názvů můžou zahrnovat více souborů.
  • Obory názvů nemohou obsahovat funkce jazyka F#, pokud nejsou v interním modulu.
  • Kód pro každý daný modul musí být obsažen v jednom souboru.
  • Moduly nejvyšší úrovně můžou obsahovat funkce jazyka F# bez nutnosti vnitřního modulu.

Volba mezi oborem názvů nejvyšší úrovně nebo modulem má vliv na kompilovanou formu kódu, a tím ovlivní zobrazení z jiných jazyků .NET, pokud vaše rozhraní API nakonec spotřebuje mimo kód jazyka F#.

Použití metod a vlastností pro operace vnitřní typy objektů

Při práci s objekty je nejlepší zajistit, aby se spotřební funkce implementovala jako metody a vlastnosti daného typu.

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

Většina funkcí daného člena nemusí být v daném členu implementována, ale spotřební část této funkce by měla být.

Použití tříd k zapouzdření proměnlivých stavů

V jazyce F# to stačí provést pouze v případě, že tento stav ještě není zapouzdřen jiným konstruktorem jazyka, jako je uzavření, pořadový výraz nebo asynchronní výpočty.

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

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

K reprezentaci sady operací použijte typy rozhraní. Tato možnost je upřednostňovaná pro jiné možnosti, například řazené kolekce členů funkcí nebo záznamů funkcí.

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

Předvolba:

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

Rozhraní jsou prvotřídní koncepty v .NET, které můžete použít k dosažení toho, co by vám funktory normálně poskytly. Kromě toho je možné je použít ke kódování existenčních typů do programu, což záznamy funkcí nemohou.

Použití modulu k seskupení funkcí, které fungují na kolekcích

Při definování typu kolekce zvažte poskytnutí standardní sady operací jako CollectionType.map a CollectionType.iter) pro nové typy kolekcí.

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

Pokud takový modul zahrnete, postupujte podle standardních zásad vytváření názvů pro funkce nalezené v FSharp.Core.

Použití modulu k seskupení funkcí pro běžné, kanonické funkce, zejména v matematických knihovnách a knihovnách DSL

Jedná se například o automaticky otevřenou kolekci funkcí nejvyšší úrovně (například Microsoft.FSharp.Core.Operatorsabs a sin) poskytovaných FSharp.Core.dll.

Stejně tak může knihovna statistik zahrnovat modul s funkcemi erf a erfc, kde je tento modul navržen tak, aby byl explicitně nebo automaticky otevřen.

Zvažte použití RequireQualifiedAccess a pečlivě použijte atributy AutoOpen.

Přidání atributu [<RequireQualifiedAccess>] do modulu označuje, že modul nemusí být otevřen a že odkazy na prvky modulu vyžadují explicitní kvalifikovaný přístup. Modul má například Microsoft.FSharp.Collections.List tento atribut.

To je užitečné, když funkce a hodnoty v modulu mají názvy, které jsou pravděpodobně v konfliktu s názvy v jiných modulech. Vyžadování kvalifikovaného přístupu může výrazně zvýšit dlouhodobou udržovatelnost a zvolnost knihovny.

Důrazně doporučujeme mít [<RequireQualifiedAccess>] atribut pro vlastní moduly, které rozšiřují moduly poskytované FSharp.Core (například Seq, List, Array), protože tyto moduly se používají v kódu jazyka F# a [<RequireQualifiedAccess>] jsou na nich definovány. Obecněji se nedoporučuje definovat vlastní moduly, které nemají atribut, pokud takové stíny modulu nebo rozšiřují další moduly, které mají atribut.

Přidání atributu [<AutoOpen>] do modulu znamená, že se modul otevře při otevření obsahujícího oboru názvů. Atribut [<AutoOpen>] lze také použít na sestavení, které označuje modul, který se automaticky otevře při odkazování sestavení.

Například knihovna statistik MathsHeaven.Statistics může obsahovat module MathsHeaven.Statistics.Operators obsahující funkce erf a erfc. Tento modul je rozumné označit jako [<AutoOpen>]. To znamená open MathsHeaven.Statistics , že tento modul také otevře a přenese názvy erf do erfc oboru. Dalším dobrým využitím [<AutoOpen>] modulů, které obsahují rozšiřující metody.

Nadměrné využití [<AutoOpen>] vede k znečistěným oborům názvů a atribut by se měl používat s opatrností. U konkrétních knihoven v konkrétních doménách může uvážlivé použití [<AutoOpen>] vést k lepší použitelnosti.

Zvažte definování členů operátoru u tříd, kde je vhodné použít známé operátory.

Třídy se někdy používají k modelování matematických konstruktorů, jako jsou vektory. Když modelovaná doména obsahuje dobře známé operátory, je užitečné je definovat jako členy vnitřní třídy.

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

Tyto pokyny odpovídají obecným pokynům k .NET pro tyto typy. V kódování jazyka F# ale může být navíc důležité, protože to umožňuje použití těchto typů ve spojení s funkcemi a metodami jazyka F#s omezeními členů, jako je List.sumBy.

Zvažte použití CompiledName k poskytnutí . Popisný název NET pro ostatní uživatele jazyka .NET

Někdy můžete chtít něco pojmenovat v jednom stylu pro uživatele jazyka F# (například statický člen v malých písmenech, aby se zobrazil jako funkce vázané na modul), ale při kompilaci do sestavení má jiný styl pro název. Atribut můžete použít k poskytnutí jiného [<CompiledName>] stylu pro kód bez jazyka F#, který sestavení využívá.

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

Pomocí tohoto [<CompiledName>]příkazu můžete pro uživatele sestavení, kteří nejsou příjemci jazyka F#, použít konvence vytváření názvů .NET.

Pokud to uděláte, použijte přetížení metody pro členské funkce, pokud to uděláte, poskytuje jednodušší rozhraní API.

Přetížení metody je výkonný nástroj pro zjednodušení rozhraní API, které může vyžadovat podobné funkce, ale s různými možnostmi nebo argumenty.

type Logger() =

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

V jazyce F# je častější přetížení počtu argumentů místo typů argumentů.

Skrytí reprezentací typů záznamů a sjednocení, pokud se návrh těchto typů bude pravděpodobně vyvíjet

Vyhněte se odhalení konkrétních reprezentací objektů. Například konkrétní reprezentace DateTime hodnot není odhalena externím veřejným rozhraním API návrhu knihovny .NET. Modul CLR (Common Language Runtime) ví potvrzenou implementaci, která se použije během provádění. Zkompilovaný kód ale sám nevybírá závislosti na konkrétní reprezentaci.

Vyhněte se použití dědičnosti implementace pro rozšiřitelnost

V jazyce F# se dědičnost implementace používá zřídka. Hierarchie dědičnosti jsou navíc při příchodu nových požadavků často složité a obtížně se mění. Implementace dědičnosti stále existuje v jazyce F# kvůli kompatibilitě a vzácným případům, kdy je nejlepším řešením problému, ale při navrhování polymorfismu, jako je implementace rozhraní, by se ve vašich programech jazyka F# měly hledat alternativní techniky.

Podpisy funkcí a členů

Použití řazených kolekcí členů pro vrácené hodnoty při vrácení malého počtu několika nesouvisejících hodnot

Tady je dobrý příklad použití řazené kolekce členů ve návratovém typu:

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

U návratových typů obsahujících mnoho součástí nebo pokud se komponenty vztahují k jedné identifikovatelné entitě, zvažte použití pojmenovaného typu místo řazené kolekce členů.

Použití Async<T> pro asynchronní programování na hranicích rozhraní API F#

Pokud existuje odpovídající synchronní operace s názvem Operation , která vrací T, pak by asynchronní operace měla být pojmenována AsyncOperation , pokud se vrátí Async<T> nebo OperationAsync pokud vrátí Task<T>. U běžně používaných typů .NET, které zpřístupňují metody Begin/End, zvažte použití Async.FromBeginEnd metod rozšíření jako fasády k poskytnutí asynchronního programovacího modelu F# těmto rozhraním API .NET.

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

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

Výjimky

Informace o vhodném použití výjimek, výsledků a možností najdete v tématu Správa chyb.

Členové rozšíření

Pečlivě použijte členy rozšíření F# v komponentách F#-to-F#.

Členy rozšíření F# by se obecně měly používat pouze pro operace, které jsou v uzavření vnitřních operací přidružených k typu ve většině jeho režimů použití. Jedním z běžných použití je poskytování rozhraní API, která jsou pro různé typy .NET více idiotikou jazyka F#:

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

Typy sjednocení

Používejte diskriminované sjednocení místo hierarchií tříd pro data strukturovaná stromovou strukturou.

Struktury podobné stromové struktuře jsou rekurzivně definovány. To je trapné s dědičností, ale elegantní s diskriminovanými sjednoceními.

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

Reprezentace dat podobných stromové struktuře s diskriminovanými sjednoceními vám také umožňuje využívat vyčerpávající možnosti porovnávání vzorů.

Používá se [<RequireQualifiedAccess>] u typů sjednocení, jejichž názvy písmen nejsou dostatečně jedinečné.

Možná se nacházíte v doméně, kde je stejný název nejlepším názvem pro různé věci, jako jsou případy diskriminované unie. Můžete použít [<RequireQualifiedAccess>] k nejednoznačnosti názvů velkých a malých písmen, abyste se vyhnuli matoucím chybám způsobeným stínováním závislým na pořadí open příkazů.

Skrýt reprezentace diskriminovaných sjednocení pro binární kompatibilní rozhraní API, pokud je pravděpodobné, že se vývoj návrhu těchto typů

Typy sjednocení se spoléhají na formuláře porovnávání vzorů jazyka F# pro programovací model s výstižnými vzory. Jak už jsme zmínili dříve, měli byste se vyhnout odhalení konkrétních reprezentací dat, pokud se bude pravděpodobně vyvíjet návrh těchto typů.

Například reprezentace diskriminované sjednocení může být skrytá pomocí soukromé nebo interní deklarace nebo pomocí souboru podpisu.

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

Pokud odhalíte diskriminované sjednocení nerozlišeně, může být obtížné vytvořit verzi knihovny bez porušení uživatelského kódu. Místo toho zvažte zobrazení jednoho nebo více aktivních vzorů, které umožňují porovnávání vzorů s hodnotami vašeho typu.

Aktivní vzory představují alternativní způsob, jak uživatelům jazyka F# poskytnout porovnávání vzorů a vyhnout se přímému zveřejnění typů sjednocení F#.

Vložené funkce a omezení členů

Definování obecných číselných algoritmů pomocí vložených funkcí s předpokládanými omezeními členů a staticky vyřešenými obecnými typy

Aritmetická členská omezení a omezení porovnání jazyka F# jsou standardem pro programování v jazyce F#. Představte si například následující kód:

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

Typ této funkce je následující:

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

Toto je vhodná funkce pro veřejné rozhraní API v matematické knihovně.

Vyhněte se použití omezení členů k simulaci tříd typů a psaní kachty

Pomocí omezení členů jazyka F# je možné simulovat "psaní kachny". Členové, kteří tuto funkci používají, by se ale neměli obecně používat v návrzích knihoven F#-to-F#. Důvodem je to, že návrhy knihoven založené na neznámých nebo nestandardních implicitních omezeních mají tendenci způsobit, že uživatelský kód bude nepružný a svázaný s jedním konkrétním vzorem architektury.

Navíc existuje dobrá šance, že vysoké využití omezení členů tímto způsobem může vést k velmi dlouhé době kompilace.

Definice operátorů

Vyhněte se definování vlastních symbolických operátorů

Vlastní operátory jsou v některých situacích nezbytné a jsou vysoce užitečná notační zařízení ve velkém těle implementačního kódu. Pro nové uživatele knihovny se pojmenované funkce často snadněji používají. Kromě toho můžou být vlastní symbolické operátory obtížné dokumentovat a uživatelům se kvůli stávajícím omezením v integrovaném vývojovém prostředí (IDE) a vyhledávacích webech obtížně vyhledávají nápovědu k operátorům.

V důsledku toho je nejlepší publikovat funkce jako pojmenované funkce a členy a navíc zpřístupnit operátory pro tuto funkci pouze v případě, že notační výhody převáží dokumentaci a kognitivní náklady na jejich použití.

Měrné jednotky

Pečlivě používejte měrné jednotky pro zvýšení bezpečnosti typů v kódu F#

Další informace o zadávání měrných jednotek se vymažou při prohlížení jinými jazyky .NET. Mějte na paměti, že komponenty, nástroje a reflexe platformy .NET uvidí typy sans-units. Například uživatelé jazyka C# uvidí float místo float<kg>.

Zkratky typů

Pečlivě používejte zkratky typů ke zjednodušení kódu jazyka F#

Komponenty, nástroje a reflexe rozhraní .NET neuvidí zkrácené názvy typů. Významné použití zkratek typů může také znamenat, že doména bude složitější než ve skutečnosti, což by mohlo zmást spotřebitele.

Vyhněte se zkratkám typů pro veřejné typy, jejichž členy a vlastnosti by měly být vnitřně odlišné od těch, které jsou k dispozici ve zkrácené verzi.

V tomto případě zkrácený typ ukazuje příliš mnoho o reprezentaci skutečného typu, který je definován. Místo toho zvažte zabalení zkratky do typu třídy nebo jednoúčelové diskriminované sjednocení (nebo pokud je výkon nezbytný, zvažte použití typu struktury k zabalení zkratky).

Například je lákavé definovat vícemap jako speciální případ mapy jazyka F#, například:

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

Logické operace tečkování tohoto typu ale nejsou stejné jako operace na mapě – je například vhodné, aby vyhledávací operátor map[key] vrátil prázdný seznam, pokud klíč není ve slovníku, a ne vyvolání výjimky.

Pokyny pro knihovny pro použití z jiných jazyků .NET

Při navrhování knihoven pro použití z jiných jazyků .NET je důležité dodržovat pokyny pro návrh knihovny .NET. V tomto dokumentu jsou tyto knihovny označené jako vanilkové knihovny .NET, nikoli jako knihovny F#, které používají konstruktory jazyka F#, bez omezení. Navrhování knihoven .NET vanilla znamená poskytování známých a idiomatických rozhraní API konzistentních se zbytkem rozhraní .NET Framework minimalizací použití konstruktorů specifických pro F#ve veřejném rozhraní API. Pravidla jsou vysvětlena v následujících částech.

Návrh oboru názvů a typů (pro knihovny pro použití z jiných jazyků .NET)

Použití konvencí vytváření názvů .NET na veřejné rozhraní API vašich komponent

Věnujte zvláštní pozornost použití zkrácených názvů a pokynů pro velká písmena .NET.

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

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

Použití oborů názvů, typů a členů jako primární organizační struktury pro vaše komponenty

Všechny soubory obsahující veřejné funkce by měly začínat namespace deklarací a jediné veřejně přístupné entity v oborech názvů by měly být typy. Nepoužívejte moduly jazyka F#.

Používejte neveřejné moduly k uložení kódu implementace, typů nástrojů a funkcí nástroje.

Statické typy by se měly upřednostňovat před moduly, protože umožňují budoucí vývoj rozhraní API pro použití přetížení a dalších konceptů návrhu rozhraní .NET API, které se nedají použít v modulech jazyka F#.

Například místo následujícího veřejného rozhraní API:

module Fabrikam

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

Zvažte místo toho:

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

Použití typů záznamů F# v rozhraních API .NET vanilla, pokud se návrh typů nebude vyvíjet

Typy záznamů F# se kompilují do jednoduché třídy .NET. Ty jsou vhodné pro některé jednoduché a stabilní typy v rozhraních API. Zvažte použití [<NoEquality>] atributů a [<NoComparison>] potlačení automatického generování rozhraní. Vyhněte se také použití proměnlivých polí záznamů v rozhraních API pro vanilku .NET, protože zpřístupňují veřejné pole. Vždy zvažte, jestli by třída poskytovala flexibilnější možnost pro budoucí vývoj rozhraní API.

Například následující kód F# zveřejňuje veřejné rozhraní API příjemci jazyka C#:

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

Skrytí reprezentace typů sjednocení jazyka F# v rozhraních API pro vanilla .NET

Typy sjednocení F# se běžně nepoužívají napříč hranicemi komponent, ani pro kódování F#-to-F#. Jedná se o vynikající implementační zařízení, které se používá interně v rámci komponent a knihoven.

Při navrhování rozhraní API .NET vanilla zvažte skrytí reprezentace typu sjednocení pomocí privátní deklarace nebo souboru podpisu.

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

Můžete také rozšířit typy, které používají reprezentaci sjednocení interně se členy poskytnout požadované . ROZHRANÍ API pro rozhraní NET.

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)

Návrh grafického uživatelského rozhraní a dalších komponent pomocí vzorů návrhu architektury

V rozhraní .NET je k dispozici mnoho různých architektur, jako jsou WinForms, WPF a ASP.NET. Zásady vytváření názvů a návrhu pro každý by se měly použít, pokud navrhujete komponenty pro použití v těchto architekturách. Například pro programování WPF přebít vzory návrhu WPF pro třídy, které navrhujete. U modelů v programování uživatelského rozhraní použijte vzory návrhu, jako jsou události a kolekce založené na oznámeních, jako jsou ty, které byly nalezeny v System.Collections.ObjectModel.

Návrh objektů a členů (pro knihovny pro použití z jiných jazyků .NET)

Zveřejnění událostí .NET pomocí atributu CLIEvent

Vytvořte s DelegateEvent konkrétním typem delegáta .NET, který přebírá objekt a EventArgs (nikoli Event, který tento typ používá FSharpHandler ve výchozím nastavení), aby se události publikovaly známým způsobem v jiných jazycích .NET.

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

Zveřejnění asynchronních operací jako metod, které vracejí úlohy .NET

Úlohy se v .NET používají k reprezentaci aktivních asynchronních výpočtů. Úkoly jsou obecně méně kompoziční než objekty jazyka F# Async<T> , protože představují "již spuštěné" úkoly a nedají se skládat dohromady způsoby, které provádějí paralelní složení nebo skryjí šíření signálů zrušení a dalších kontextových parametrů.

Navzdory tomu jsou však metody, které vracejí úlohy, standardní reprezentací asynchronního programování v .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

Často budete chtít také přijmout explicitní token zrušení:

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

Použití typů delegátů .NET místo typů funkcí jazyka F#

Tady "Typy funkcí F#" znamenají "šipkové" typy jako int -> int.

Místo toho:

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

Postupujte takto:

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

Typ funkce F# se zobrazuje jako class FSharpFunc<T,U> v jiných jazycích .NET a je méně vhodný pro funkce jazyka a nástroje, které rozumí typům delegátů. Při vytváření metody vyššího řádu, která cílí na rozhraní .NET Framework 3.5 nebo vyšší, System.Func jsou a System.Action delegáti správnými rozhraními API k publikování, aby vývojáři .NET mohli tato rozhraní API využívat nízkým třením. (Při cílení na rozhraní .NET Framework 2.0 jsou typy delegátů definované systémem omezenější. Zvažte použití předdefinovaných typů delegátů, jako System.Converter<T,U> je nebo definování konkrétního typu delegáta.)

Na druhé straně nejsou delegáti .NET pro knihovny F# přirozené (viz další oddíl knihoven s přístupem k jazyku F#). V důsledku toho je běžnou implementační strategií při vývoji metod vyššího řádu pro knihovny vanilla .NET vytvořit veškerou implementaci pomocí typů funkcí jazyka F# a pak vytvořit veřejné rozhraní API pomocí delegátů jako tenké fasády na vrcholu skutečné implementace jazyka F#.

Místo vrácení hodnot možností jazyka F# použijte vzor TryGetValue a jako argumenty upřednostněte přetížení metody, aby hodnoty možností jazyka F# používaly jako argumenty.

Běžné vzory použití pro typ možností jazyka F# v rozhraních API jsou lépe implementovány v rozhraních API pro vanilku .NET pomocí standardních technik návrhu .NET. Místo vrácení hodnoty možnosti F# zvažte použití návratového typu bool plus výstupní parametr jako ve vzoru TryGetValue. Místo použití hodnot možností jazyka F# jako parametrů zvažte použití přetížení metody nebo volitelných 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

Použití typů rozhraní kolekce .NET IEnumerable<T> a IDictionary<Key, Hodnota> pro parametry a návratové hodnoty

Nepoužívejte konkrétní typy kolekcí, jako jsou pole T[].NET, typy list<T>Map<Key,Value> F# a Set<T>konkrétní typy kolekcí .NET, například Dictionary<Key,Value>. Pokyny k návrhu knihovny .NET mají dobrou radu ohledně toho, kdy použít různé typy kolekcí, jako je IEnumerable<T>. Některé použití polí (T[]) je v některých případech přijatelné z důvodů výkonu. Všimněte si zejména, že seq<T> je to jen alias F# pro IEnumerable<T>, a proto seq je často vhodný typ pro vanilla .NET API.

Místo seznamů F#:

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

Použití sekvencí jazyka F#:

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

Použití typu jednotky jako jediného vstupního typu metody k definování metody nulového argumentu nebo jako jediný návratový typ k definování metody void-returning

Vyhněte se dalšímu použití typu jednotky. To jsou dobré:

✔ member this.NoArguments() = 3

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

To je špatné:

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

Kontrola hodnot null v hranicích rozhraní .NET API vanilla

Kód implementace jazyka F# má tendenci mít méně hodnot null kvůli neměnným vzorům návrhu a omezením použití literálů null pro typy jazyka F#. Jiné jazyky .NET často používají hodnotu null jako hodnotu mnohem častěji. Z tohoto důvodu by měl kód jazyka F#, který vystavuje rozhraní API vanilla .NET, zkontrolovat parametry hodnoty null na hranici rozhraní API a zabránit tomu, aby tyto hodnoty přetékaly hlouběji do kódu implementace jazyka F#. Funkci isNull nebo vzor odpovídající vzoru null lze použít.

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

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

Nepoužívejte řazené kolekce členů jako návratové hodnoty.

Místo toho raději vraťte pojmenovaný typ, který obsahuje agregovaná data, nebo použijte parametry pro vrácení více hodnot. Přestože řazené kolekce členů a řazené kolekce členů existují v .NET (včetně podpory jazyka C#pro řazené kolekce členů), nebudou nejčastěji poskytovat ideální a očekávané rozhraní API pro vývojáře .NET.

Vyhněte se použití kariingu parametrů

Místo toho použijte konvence Method(arg1,arg2,…,argN)volání .NET .

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

Tip: Pokud navrhujete knihovny pro použití z libovolného jazyka .NET, neexistuje žádná náhrada za provádění experimentálního programování jazyka C# a Jazyka Visual Basic, aby se zajistilo, že vaše knihovny mají z těchto jazyků pocit, že jsou v pořádku. Můžete také použít nástroje, jako je .NET Reflexe or a Visual Studio Object Browser, a zajistit tak, aby knihovny a jejich dokumentace vypadaly podle očekávání pro vývojáře.

Dodatek

Kompletní příklad návrhu kódu jazyka F# pro použití jinými jazyky .NET

Vezměte v úvahu následující třídu:

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

Odvozený typ F# této třídy je následující:

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

Pojďme se podívat, jak se tento typ jazyka F# jeví programátorovi pomocí jiného jazyka .NET. Například přibližný "podpis" jazyka C# je následující:

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

Existuje několik důležitých bodů, které si můžete všimnout, jak jazyk F# tady představuje konstruktory. Příklad:

  • Metadata, jako jsou názvy argumentů, byly zachovány.

  • Metody F#, které přebírají dva argumenty, se stanou metodami jazyka C#, které přebírají dva argumenty.

  • Funkce a seznamy se stanou odkazy na odpovídající typy v knihovně jazyka F#.

Následující kód ukazuje, jak tento kód upravit tak, aby tyto věci zohlednil.

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

Odvozený typ jazyka F# kódu je následující:

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

Podpis jazyka C# je teď následující:

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

Opravy přípravy tohoto typu pro použití v rámci knihovny vanilla .NET jsou následující:

  • Upravili jsme několik názvů: Point1, n, fla stal RadialPointse , countfactor, a transform, v uvedeném pořadí.

  • Použití návratového typu seq<RadialPoint> místo RadialPoint list změnou konstrukce seznamu pomocí [ ... ] sekvence konstrukce pomocí IEnumerable<RadialPoint>.

  • Používá se typ System.Func delegáta .NET místo typu funkce F#.

Díky tomu je mnohem příjemnější v kódu jazyka C#.