Sdílet prostřednictvím


Pokyny k návrhu komponent 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 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. Svědomité použití pokynů pro návrh knihovny .NET vás povede k vytvoření konzistentní sady rozhraní API, která je příjemná pro uživatele.

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 druh programování v jazyce F#, který provádíte, je cenné mít praktické 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 API zajišťuje, že uživatelé mohou získat skvělé funkce IntelliSense a Quickinfo při použití těchto typů a členů a umožnit vytváření dokumentačních souborů pro knihovnu. Podívejte se na dokumentaci XML o různých xml značkách, které lze použít pro další strukturování 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í xml komentáře (///<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í překážky ke změně API tím, že změny musí být provedeny jak v implementaci, tak 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.

Když to rozsah projektu vyžaduje, postupujte podle osvědčených postupů a pokynů 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.

Konvence pojmenování

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. Jsou zde malé dodatky i pro zahrnutí konstrukcí jazyka F#. Tato doporučení jsou určena zejména pro rozhraní API, která přesahují rámec pouze F#-to-F#, přičemž se hodí k idiomům z .NET BCL a většiny knihoven.

Konstruovat Případ Část Příklady Poznámky
Typy betonu PascalCase Podstatné jméno/ přídavné jméno Seznam, Dvojitý, Komplexní Konkrétní typy jsou struktury, třídy, výčty, delegáty, záznamy a unie. 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řidání, Ú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 Sloveso ZměnaHodnoty / MěnícíSeHodnota
Výjimky PascalCase Výjimka WebException Název by měl končit výjimkou.
Pole PascalCase Podstatné jméno CurrentName
Typy rozhraní PascalCase Podstatné jméno/přídavné jméno IDisposable Název by měl začínat na "I".
Metoda PascalCase Sloveso ToString
Namespace PascalCase Microsoft.FSharp.Core Obecně používejte <Organization>.<Technology>[.<Subnamespace>], ale pokud je technologie nezávislá na organizaci, organizaci vynechte.
Parametry camelCase Podstatné jméno typeName, transformace, rozsah
let values (internal) camelCase nebo PascalCase Podstatné jméno nebo sloveso getValue, myTable
let values (external) camelCase nebo PascalCase Podstatné jméno/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éno / přídavné jméno IsEndOfFile, BarvaPozadí 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žívejte OnButtonClick místo OnBtnClick"). Běžné zkratky, například Async pro "Asynchronní", jsou tolerovány. Toto vodítko je někdy ignorováno pro funkční programování; například List.iter používá zkratku pro "iterovat". Z tohoto důvodu bývá použití zkratek v programování F#-to-F# do jisté míry tolerováno, ale při návrhu veřejných komponent by se jim mělo obecně vyhnout.

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

Podle pokynů pro .NET nelze k vyřešení kolizí názvů použít samotné změny velikosti písmen, protože některé klientské jazyky, 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, U, T1, 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, Value, Arg (ale ne například TKey).

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, aby byly použity bez kvalifikace (například invalidArg), a pro funkce standardní 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žijte obory názvů nebo moduly k obsahování vašich 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 jmenných prostorů k uspořádání kódu na nejvyšší úrovni jsou následující:

  • Jmenné prostory mohou zahrnovat více souborů.
  • Prostory jmen nemohou obsahovat funkce jazyka F#, nejsou-li 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 nadřazeným oborem názvů nebo modulem má vliv na kompilovanou formu kódu, a tím ovlivní zobrazení z jiných jazyků .NET, pokud bude vaše rozhraní API nakonec používáno mimo kód jazyka F#.

Používejte metody a vlastnosti pro provádění operací vlastní typům 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žijte třídy k zapouzdření proměnlivého stavu

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ňována před jinými možnostmi, jako jsou n-tice funkcí nebo záznamy 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 k zakódování existenčních typů do vašeho programu, což záznamy funkcí nemohou.

Použijte modul k seskupení funkcí, které působí na kolekcích.

Při definování typu kolekce zvažte poskytnutí standardní sady operací, jako je 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

Například Microsoft.FSharp.Core.Operators je automaticky otevřená kolekce funkcí nejvyšší úrovně (například abs a sin), kterou poskytuje FSharp.Core.dll.

Podobně 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. Například modul Microsoft.FSharp.Collections.List má 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 rozšiřitelnost knihovny.

Důrazně doporučujeme mít atribut [<RequireQualifiedAccess>] pro vlastní moduly, které rozšiřují moduly poskytované FSharp.Core (například Seq, List, Array), protože tyto moduly se v kódu jazyka F# běžně používají a mají na nich definované [<RequireQualifiedAccess>]; 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 při odkazování na sestavení automaticky otevře.

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

Nadměrné využití [<AutoOpen>] vede k znečistěným oborům názvů a atribut by měl být použit s opatrností. U konkrétních knihoven v konkrétních doménách může 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 pro poskytnutí názvu přátelského pro .NET pro ostatní uživatele jazyků .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. Pomocí atributu [<CompiledName>] můžete poskytnout jiný styl pro kód, který používá něco jiného než F#, aby využíval sestavení.

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í [<CompiledName>]můžete pro uživatele sestavení bez F# použít konvence vytváření názvů .NET.

Použijte přetížení metod pro členské funkce, pokud to vede k jednoduššímu rozhraní API.

Přetížení metody je výkonný nástroj pro zjednodušení rozhraní API, které může vyžadovat podobnou funkcionalitu, 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ů.

Skrýt reprezentace typů záznamů a sjednocovacích typů, 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í. Nicméně zkompilovaný kód sám o sobě nezahrnuje 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žijte n-tice pro vrácení hodnot při vracení malého počtu nesouvisejících hodnot.

Tady je dobrý příklad, jak se používá n-tice v návratovém typu:

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

Pro návratové typy obsahující mnoho součástí nebo pokud se komponenty vztahují k jedné identifikovatelné entitě, zvažte použití pojmenovaného typu místo n-tice.

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

Pokud existuje odpovídající synchronní operace s názvem Operation, která vrací T, měla by být asynchronní operace pojmenována AsyncOperation, pokud vrací Async<T> nebo OperationAsync, pokud vrací Task<T>. U běžně používaných typů .NET, které zpřístupňují metody Begin/End, zvažte použití Async.FromBeginEnd k zápisu rozšiřujících metod 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í jazyka F# v komponentách vytvářených v rámci F#-to-F#.

Členy rozšíření F# by se obecně měly používat pouze pro operace, které jsou v rámci základních operací spojených s typem ve většině způsobů použití. Jedním z běžných použití je poskytování rozhraní API, která jsou idomatičtější pro jazyk F# u různých typů .NET.

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é unie místo hierarchií tříd pro data strukturovaná stromečkovou strukturou.

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

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

Reprezentace stromových dat s diskriminovanými uniemi vám také umožňuje těžit z vyčerpávajícího porovnávání vzorů.

Použijte [<RequireQualifiedAccess>] u typů sjednocení, jejichž názvy případů 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. Pomocí [<RequireQualifiedAccess>] můžete rozlišit názvy případů, abyste se vyhnuli matoucím chybám způsobeným stínováním v závislosti na pořadí příkazů open.

Skrýt reprezentace rozdělených sjednocení pro binárně kompatibilní rozhraní API, pokud je pravděpodobné, že se návrh těchto typů bude vyvíjet.

Typy sjednocení se spoléhají na porovnávání vzorů v jazyce F# pro stručný programovací model. 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 reprezentaci diskriminačního sjednocení lze skrýt pomocí soukromé nebo interní deklarace nebo pomocí souboru se signaturami.

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

Pokud zpřístupníte diskriminované sjednocení nerozlišeně, může být obtížné aktualizovat verzi knihovny, aniž by došlo k naruš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 typových tříd a duck typing.

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 bez jednotek. Například uživatelé jazyka C# uvidí místo float<kg>float .

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 jednoprvkové diskriminované unie (nebo pokud je výkon důležitý, zvažte použití struktury k zabalení zkratky).

Například je lákavé definovat vícenásobnou mapu jako speciální případ mapy v jazyce F#.

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

Logické operace tečkové notace u tohoto typu však 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, spíše 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 základní knihovny .NET, nikoli jako knihovny orientované na F#, které používají konstrukce jazyka F# bez jakýchkoli 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 k použití v jiných jazycích .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žívejte jmenné prostory, typy a členy jako primární organizační strukturu pro vaše komponenty.

Všechny soubory obsahující veřejné funkce by měly začínat deklarací namespace 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žijte záznamové typy F# ve standardních rozhraních .NET API, 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í atributů [<NoEquality>] a [<NoComparison>] k potlačení automatického generování rozhraní. Vyhněte se také použití mutovatelných polí záznamů ve standardních rozhraních .NET API, protože tato 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í interně reprezentaci sjednocení, členy, abyste poskytli požadované rozhraní API pro .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 při programování ve WPF používejte návrhové vzory WPF pro třídy, které navrhujete. Pro modely 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 například 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 DelegateEvent s konkrétním typem delegáta .NET, který přebírá objekt a EventArgs (místo Event, který ve výchozím nastavení používá FSharpHandler typ), 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ž spouštěné" úkoly a nelze je 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> do jiných jazyků .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 cílené na rozhraní .NET Framework 3.5 nebo vyšší jsou delegáti System.Func a System.Action vhodnými rozhraními API k publikování, aby vývojáři .NET mohli tato rozhraní API využívat snadno a bez komplikací. (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 jsou System.Converter<T,U> nebo definování konkrétního typu delegáta.)

Na druhou stranu delegáti .NET nejsou přirození pro knihovny určené pro F# (viz následující oddíl o knihovnách pro jazyk 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 typu F# option použijte vzor TryGetValue a preferujte přetěžování metod před přijímáním hodnot typu F# option jako argumentů.

Běžné vzory použití pro F# option type v rozhraních API je lépe implementovat ve standardních .NET API pomocí běžných návrhových technik .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<Klíč, Hodnota> pro parametry a návratové hodnoty

Nepoužívejte konkrétní typy kolekcí, jako jsou .NET pole T[], typy F# list<T>, Map<Key,Value> a Set<T>, a .NET konkrétní typy kolekcí, jako jsou Dictionary<Key,Value>. Pokyny k návrhu knihovny .NET mají dobrou radu, pokud chcete použít různé typy kolekcí, jako je IEnumerable<T>. Použití některých polí (T[]) je za určitých okolností přijatelné z důvodů výkonu. Všimněte si zejména, že seq<T> je pouze alias F# pro IEnumerable<T>, a proto seq je často vhodným typem 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žijte typ jednotky jako jediný vstupní typ metody k definování metody bez argumentů nebo jako jediný návratový typ k definování metody, která nevrací žádnou hodnotu.

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#. Lze použít funkci isNull nebo porovnávání vzorů null.

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

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

Počínaje jazykem F# 9 můžete využít novou syntaxi | null, abyste přiměli kompilátor indikovat možné hodnoty null a naznačit, kde je třeba je zpracovat.

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

V jazyce F# 9 kompilátor vygeneruje upozornění, když zjistí, že možná hodnota null není zpracována:

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

Tato upozornění by se měla řešit pomocí vzoru null jazyka F# odpovídajících hodnot:

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

Nepoužívejte n-tice 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 n-tice a strukturální n-tice existují v .NET (včetně podpory jazyka C# pro strukturální n-tice), nejčastěji neposkytnou ideální a očekávané API pro vývojáře .NET.

Vyhněte se použití transformace parametrů pomocí curryingu

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

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

Tip: Pokud navrhujete knihovny pro použití z jakéhokoliv jazyka .NET, nenahradí nic, než experimentování v jazycích C# a Visual Basic, abyste zajistili, že vaše knihovny z těchto jazyků působily správně. K zajištění toho, aby se knihovny a jejich dokumentace zobrazovaly vývojářům podle očekávání, můžete také použít nástroje, jako je .NET Reflector a Visual Studio Object Browser.

Příloha

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

Je několik důležitých bodů, které stojí za zmínku ohledně toho, jak jazyk F# zde reprezentuje konstrukty. Napří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, la f se staly RadialPoint, count, factora transform.

  • Při použití návratového typu seq<RadialPoint> místo RadialPoint list došlo ke změně konstrukce seznamu pomocí [ ... ] na konstrukci sekvence pomocí IEnumerable<RadialPoint>.

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

Díky tomu se kód v jazyce C# stává mnohem příjemnějším pro použití.