Události
Vytváření inteligentních aplikací
17. 3. 23 - 21. 3. 23
Připojte se k řadě meetupů a vytvořte škálovatelná řešení AI založená na skutečných případech použití s kolegy vývojáři a odborníky.
ZaregistrovatTento prohlížeč se už nepodporuje.
Upgradujte na Microsoft Edge, abyste mohli využívat nejnovější funkce, aktualizace zabezpečení a technickou podporu.
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.
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í:
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.
Existuje několik univerzálních pokynů, které platí pro knihovny jazyka F# bez ohledu na zamýšlenou cílovou skupinu knihovny.
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.
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>
).
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í.
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é).
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.
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. |
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.
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.
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.
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
).
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.
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í:
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#.
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.
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.
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.
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.
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.
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.
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.
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ů.
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.
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.
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.
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() =
...
Informace o vhodném použití výjimek, výsledků a možností najdete v tématu Správa chyb.
Č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
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ů.
[<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
.
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#.
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ě.
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.
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í.
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
.
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.
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.
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.
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 = ...
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
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; }
}
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)
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.
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
Ú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)
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#.
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
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>) =
...
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) = ((), ())
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
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.
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.
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
, l
a f
se staly RadialPoint
, count
, factor
a 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í.
Zpětná vazba k produktu .NET
.NET je open source projekt. Vyberte odkaz pro poskytnutí zpětné vazby:
Události
Vytváření inteligentních aplikací
17. 3. 23 - 21. 3. 23
Připojte se k řadě meetupů a vytvořte škálovatelná řešení AI založená na skutečných případech použití s kolegy vývojáři a odborníky.
ZaregistrovatŠkolení
Modul
Zabezpečení null v jazyce C# - Training
Naučte se kódovat postupy, které pomáhají zabránit výskytu NullReferenceException.