F# — wytyczne dotyczące projektowania składników
Ten dokument jest zestawem wytycznych dotyczących projektowania składników dla programowania W języku F#, opartych na wytycznych dotyczących projektowania składników języka F#, wersji 14, Microsoft Research i wersji, która została pierwotnie wyselekcjonowany i obsługiwana przez program F# Software Foundation.
W tym dokumencie założono, że znasz programowanie w języku F#. Dziękujemy społeczności języka F# za współtworzenie i pomocną opinię na temat różnych wersji tego przewodnika.
Omówienie
Ten dokument zawiera omówienie niektórych problemów związanych z projektowaniem i kodowaniem składników języka F#. Składnik może oznaczać dowolny z następujących elementów:
- Warstwa w projekcie języka F#, która ma odbiorców zewnętrznych w tym projekcie.
- Biblioteka przeznaczona do użycia przez kod języka F# w granicach zestawów.
- Biblioteka przeznaczona do użycia przez dowolny język .NET w granicach zestawów.
- Biblioteka przeznaczona do dystrybucji za pośrednictwem repozytorium pakietów, takiego jak NuGet.
Techniki opisane w tym artykule są zgodne z pięcioma zasadami dobrego kodu języka F#, a tym samym wykorzystują zarówno programowanie funkcjonalne, jak i obiektowe zgodnie z potrzebami.
Niezależnie od metodologii projektant składników i bibliotek napotyka szereg praktycznych i prozaicznych problemów podczas próby utworzenia interfejsu API, który jest najbardziej łatwy do użycia przez deweloperów. Sumienne zastosowanie wytycznych dotyczących projektowania biblioteki platformy .NET będzie kierować Cię do tworzenia spójnego zestawu interfejsów API, które są przyjemne do użycia.
Ogólne wskazówki
Istnieje kilka uniwersalnych wytycznych, które mają zastosowanie do bibliotek języka F#, niezależnie od odbiorców przeznaczonych dla biblioteki.
Poznaj wytyczne dotyczące projektowania biblioteki .NET
Niezależnie od rodzaju kodowania języka F#, które wykonujesz, warto mieć działającą wiedzę na temat wytycznych dotyczących projektowania bibliotek platformy .NET. Większość innych programistów języka F# i platformy .NET zapozna się z tymi wytycznymi i oczekuje, że kod platformy .NET będzie zgodny z nimi.
Wytyczne dotyczące projektowania biblioteki .NET zawierają ogólne wskazówki dotyczące nazewnictwa, projektowania klas i interfejsów, projektowania składowych (właściwości, metod, zdarzeń itp.) i nie tylko oraz są przydatnym pierwszym punktem odniesienia dla różnych wskazówek projektowych.
Dodawanie komentarzy dokumentacji XML do kodu
Dokumentacja XML dotycząca publicznych interfejsów API zapewnia, że użytkownicy mogą uzyskać doskonałe informacje intellisense i Quickinfo podczas korzystania z tych typów i elementów członkowskich oraz włączyć tworzenie plików dokumentacji dla biblioteki. Zapoznaj się z dokumentacją XML dotyczącą różnych tagów XML, które mogą służyć do dodatkowego znaczników w komentarzach xmldoc.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Możesz użyć krótkich komentarzy XML (/// comment
) lub standardowych komentarzy XML (///<summary>comment</summary>
).
Rozważ użycie jawnych plików podpisów (fsi) dla stabilnych interfejsów API biblioteki i składników
Użycie jawnych plików podpisów w bibliotece języka F# zawiera zwięzłe podsumowanie publicznego interfejsu API, co pomaga zapewnić, że znasz pełną publiczną powierzchnię biblioteki i zapewnia czyste rozdzielenie między publiczną dokumentacją a wewnętrznymi szczegółami implementacji. Pliki podpisów dodają tarcie do zmiany publicznego interfejsu API, wymagając wprowadzenia zmian w plikach implementacji i podpisu. W związku z tym pliki podpisów powinny być zwykle wprowadzane tylko wtedy, gdy interfejs API stał się solidyfikowany i nie oczekuje się już znacznej zmiany.
Postępuj zgodnie z najlepszymi rozwiązaniami dotyczącymi używania ciągów na platformie .NET
Postępuj zgodnie z najlepszymi rozwiązaniami dotyczącymi używania ciągów na platformie .NET , gdy zakres projektu go uzasadnia. W szczególności jawne stwierdzenie intencji kulturowej w konwersji i porównywaniu ciągów (w stosownych przypadkach).
Wskazówki dotyczące bibliotek języka F#-facing
W tej sekcji przedstawiono zalecenia dotyczące tworzenia publicznych bibliotek języka F#; oznacza to, że biblioteki ujawniające publiczne interfejsy API, które mają być używane przez deweloperów języka F#. Istnieje wiele zaleceń dotyczących projektowania bibliotek, które mają zastosowanie w szczególności w języku F#. W przypadku braku określonych zaleceń, które są zgodne, wytyczne dotyczące projektowania bibliotek platformy .NET są wskazówkami rezerwowymi.
Konwencje nazewnictwa
Używanie konwencji nazewnictwa i wielkości liter platformy .NET
W poniższej tabeli przedstawiono konwencje nazewnictwa i wielkości liter platformy .NET. Istnieją małe dodatki, które zawierają również konstrukcje języka F#. Te zalecenia są szczególnie przeznaczone dla interfejsów API, które wykraczają poza granice języka F#-to-F#, pasujące do idiomów z listy BCL platformy .NET i większości bibliotek.
Konstrukcja | Przypadek | Element | Przykłady | Uwagi |
---|---|---|---|---|
Typy betonowe | PascalCase | Przymiotnik/ przymiotnik | Lista, podwójna, złożona | Typy betonowe to struktury, klasy, wyliczenia, delegaty, rekordy i związki. Chociaż nazwy typów są tradycyjnie małymi literami w OCaml, język F# przyjął schemat nazewnictwa platformy .NET dla typów. |
Biblioteki DLL | PascalCase | Fabrikam.Core.dll | ||
Tagi unii | PascalCase | Rzeczownik | Niektóre, dodawanie, powodzenie | Nie używaj prefiksu w publicznych interfejsach API. Opcjonalnie użyj prefiksu, gdy jest to wewnętrzny, na przykład "type Teams = TAlpha | TBeta | TDelta". |
Zdarzenie | PascalCase | Czasownik | ValueChanged/ValueChanging | |
Wyjątki | PascalCase | Webexception | Nazwa powinna kończyć się ciągiem "Wyjątek". | |
Pole | PascalCase | Rzeczownik | CurrentName | |
Typy interfejsów | PascalCase | Przymiotnik/ przymiotnik | Idisposable | Nazwa powinna zaczynać się od "I". |
Method | PascalCase | Czasownik | ToString | |
Przestrzeń nazw | PascalCase | Microsoft.FSharp.Core | Zazwyczaj należy użyć metody <Organization>.<Technology>[.<Subnamespace>] , choć porzucić organizację, jeśli technologia jest niezależna od organizacji. |
|
Parametry | camelCase | Rzeczownik | typeName, transform, range | |
let wartości (wewnętrzne) | camelCase lub PascalCase | Czasownik/czasownik | getValue, myTable | |
wartości let (zewnętrzne) | camelCase lub PascalCase | Czasownik/czasownik | List.map, Dates.Today | wartości let-bound są często publiczne w przypadku przestrzegania tradycyjnych wzorców projektowych funkcjonalnych. Jednak zazwyczaj należy używać PascalCase, gdy identyfikator może być używany z innych języków platformy .NET. |
Właściwości | PascalCase | Przymiotnik/ przymiotnik | IsEndOfFile, BackColor | Właściwości logiczne zazwyczaj używają właściwości Is i Can i powinny być potwierdzane, jak w isEndOfFile, a nie IsNotEndOfFile. |
Unikaj skrótów
Wytyczne dotyczące platformy .NET zniechęcają do używania skrótów (na przykład "użyj OnButtonClick
, a nie OnBtnClick
"). Typowe skróty, takie jak Async
"Asynchroniczne", są tolerowane. Te wytyczne są czasami ignorowane w przypadku programowania funkcjonalnego; na przykład List.iter
używa skrótu "iteracja". Z tego powodu używanie skrótów zwykle jest tolerowane do większego stopnia w programowaniu F#-to-F#, ale nadal powinno być ogólnie unikane w projekcie składników publicznych.
Unikaj kolizji nazw liter
Wytyczne dotyczące platformy .NET mówią, że nie można używać samej wielkości liter do uściślania kolizji nazw, ponieważ niektóre języki klienta (na przykład Visual Basic) nie są uwzględniane wielkości liter.
Używaj akronimów tam, gdzie jest to konieczne
Akronimy, takie jak XML, nie są skrótami i są powszechnie używane w bibliotekach platformy .NET w formie niekapitalizowanej (Xml). Należy używać tylko dobrze znanych, powszechnie rozpoznawanych akronimów.
Używanie pascalcase dla ogólnych nazw parametrów
Należy użyć PascalCase dla ogólnych nazw parametrów w publicznych interfejsach API, w tym dla bibliotek F#-facing. W szczególności należy używać nazw, takich jak , , T2
dla dowolnych parametrów ogólnych, a gdy określone nazwy mają sens, w przypadku bibliotek F#-facing używają nazw, takich jak Key
, Value
Arg
(ale nie na przykład TKey
). T1
U
T
Użyj metody PascalCase lub camelCase dla funkcji publicznych i wartości w modułach języka F#
CamelCase służy do funkcji publicznych, które są przeznaczone do użycia niekwalifikowanych (na przykład ), i dla "standardowych funkcji kolekcji" (na przykład invalidArg
List.map). W obu tych przypadkach nazwy funkcji działają podobnie jak słowa kluczowe w języku.
Projekt obiektów, typów i modułów
Używanie przestrzeni nazw lub modułów do przechowywania typów i modułów
Każdy plik F# w składniku powinien rozpoczynać się od deklaracji przestrzeni nazw lub deklaracji modułu.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
lub
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
Różnice między używaniem modułów i przestrzeni nazw do organizowania kodu na najwyższym poziomie są następujące:
- Przestrzenie nazw mogą obejmować wiele plików
- Przestrzenie nazw nie mogą zawierać funkcji języka F#, chyba że znajdują się w module wewnętrznym
- Kod dla dowolnego modułu musi być zawarty w jednym pliku
- Moduły najwyższego poziomu mogą zawierać funkcje języka F# bez konieczności korzystania z modułu wewnętrznego
Wybór między przestrzenią nazw najwyższego poziomu lub modułem ma wpływ na skompilowany formularz kodu, a tym samym wpłynie na widok z innych języków platformy .NET, jeśli interfejs API zostanie ostatecznie użyty poza kodem języka F#.
Używanie metod i właściwości operacji wewnętrznych dla typów obiektów
Podczas pracy z obiektami najlepiej upewnić się, że funkcje eksploatacyjne są implementowane jako metody i właściwości tego 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) = ...
Większość funkcji danego elementu członkowskiego nie musi być zaimplementowana w tym elemencie członkowskim, ale elementem eksploatacyjnym tej funkcji powinno być.
Używanie klas do hermetyzacji stanu modyfikowalnego
W języku F# należy to zrobić tylko wtedy, gdy ten stan nie jest jeszcze hermetyzowany przez inną konstrukcję języka, taką jak zamknięcie, wyrażenie sekwencji lub obliczenia asynchroniczne.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Używanie interfejsów do grupowania powiązanych operacji
Użyj typów interfejsów do reprezentowania zestawu operacji. Jest to preferowane dla innych opcji, takich jak krotki funkcji lub rekordów funkcji.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
W preferencjach:
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
Interfejsy są pojęciami pierwszej klasy na platformie .NET, których można użyć do osiągnięcia tego, co zwykle daje Functors. Ponadto mogą służyć do kodowania typów egzystencjalnych w programie, których rekordy funkcji nie mogą.
Używanie modułu do grupowania funkcji, które działają w kolekcjach
Podczas definiowania typu kolekcji rozważ udostępnienie standardowego zestawu operacji, takich jak CollectionType.map
i CollectionType.iter
) dla nowych typów kolekcji.
module CollectionType =
let map f c =
...
let iter f c =
...
Jeśli dołączysz taki moduł, postępuj zgodnie ze standardowymi konwencjami nazewnictwa dla funkcji znalezionych w pliku FSharp.Core.
Używanie modułu do grupowania funkcji dla typowych, kanonicznych funkcji, szczególnie w bibliotekach matematycznych i DSL
Na przykład Microsoft.FSharp.Core.Operators
jest to automatycznie otwarta kolekcja funkcji najwyższego poziomu (takich jak abs
i sin
) udostępnianych przez FSharp.Core.dll.
Podobnie biblioteka statystyk może zawierać moduł z funkcjami erf
i erfc
, gdzie ten moduł jest przeznaczony do jawnego lub automatycznego otwierania.
Rozważ użycie funkcji RequireQualifiedAccess i starannie zastosuj atrybuty AutoOtwórz
Dodanie atrybutu do modułu [<RequireQualifiedAccess>]
wskazuje, że moduł może nie być otwarty i że odwołania do elementów modułu wymagają jawnego kwalifikowanego dostępu. Na przykład Microsoft.FSharp.Collections.List
moduł ma ten atrybut.
Jest to przydatne, gdy funkcje i wartości w module mają nazwy, które mogą powodować konflikt z nazwami w innych modułach. Wymaganie dostępu kwalifikowanego może znacznie zwiększyć długoterminową łatwość utrzymania i możliwości rozwoju biblioteki.
Zdecydowanie zaleca się posiadanie atrybutu [<RequireQualifiedAccess>]
dla modułów niestandardowych, które rozszerzają te dostarczane przez FSharp.Core
(npSeq
. , Array
List
), ponieważ te moduły są powszechnie używane w kodzie języka F# i zostały [<RequireQualifiedAccess>]
zdefiniowane na nich; ogólnie rzecz biorąc, nie zaleca się definiowania modułów niestandardowych, które nie mają atrybutu, gdy takie moduły w tle lub rozszerzają inne moduły, które mają atrybut.
Dodanie atrybutu do modułu [<AutoOpen>]
oznacza, że moduł zostanie otwarty po otwarciu zawierającej przestrzeni nazw. Atrybut [<AutoOpen>]
można również zastosować do zestawu, aby wskazać moduł, który jest automatycznie otwierany podczas odwołowania się do zestawu.
Na przykład biblioteka statystyk MathsHeaven.Statistics może zawierać module MathsHeaven.Statistics.Operators
funkcje zawierające i erf
erfc
. Warto oznaczyć ten moduł jako [<AutoOpen>]
. Oznacza open MathsHeaven.Statistics
to również otwarcie tego modułu i wprowadzenie nazw erf
do erfc
zakresu. Innym dobrym zastosowaniem [<AutoOpen>]
jest moduły zawierające metody rozszerzenia.
Nadmierne użycie [<AutoOpen>]
prowadzi do zanieczyszczonych przestrzeni nazw, a atrybut powinien być używany z ostrożnością. W przypadku określonych bibliotek w określonych domenach rozsądne użycie [<AutoOpen>]
programu może prowadzić do poprawy użyteczności.
Rozważ zdefiniowanie składowych operatorów w klasach, w których używanie dobrze znanych operatorów jest odpowiednie
Czasami klasy są używane do modelowania konstrukcji matematycznych, takich jak Vectors. Gdy modelowana domena ma dobrze znane operatory, pomocne jest zdefiniowanie ich jako elementów członkowskich wewnętrznych klasy.
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
Te wskazówki odnoszą się do ogólnych wskazówek dotyczących platformy .NET dla tych typów. Jednak może to być dodatkowo ważne w kodowaniu języka F#, ponieważ pozwala to na używanie tych typów w połączeniu z funkcjami i metodami języka F# z ograniczeniami składowymi, takimi jak List.sumBy.
Rozważ użycie elementu CompiledName, aby podać element . Przyjazna dla platformy NET nazwa dla innych użytkowników języka platformy .NET
Czasami można nazwać coś w jednym stylu dla użytkowników języka F# (na przykład statycznego elementu członkowskiego w małych literach, tak aby wyglądała tak, jakby była to funkcja powiązana z modułem), ale ma inny styl nazwy podczas kompilowania w zestawie. Możesz użyć atrybutu [<CompiledName>]
, aby podać inny styl dla kodu innego niż F# korzystających z zestawu.
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
Za pomocą programu [<CompiledName>]
można użyć konwencji nazewnictwa platformy .NET dla użytkowników innych niż F# zestawu.
Użyj przeciążenia metody dla funkcji składowych, jeśli w ten sposób udostępnia prostszy interfejs API
Przeciążenie metody to zaawansowane narzędzie do upraszczania interfejsu API, które może wymagać wykonania podobnych funkcji, ale z różnymi opcjami lub argumentami.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
W języku F# częściej przeciąża się liczbą argumentów, a nie typami argumentów.
Ukryj reprezentacje typów rekordów i unii, jeśli projekt tych typów może ewoluować
Unikaj odsłaniania konkretnych reprezentacji obiektów. Na przykład konkretna reprezentacja DateTime wartości nie jest ujawniana przez zewnętrzny, publiczny interfejs API projektu biblioteki platformy .NET. W czasie wykonywania środowisko uruchomieniowe języka wspólnego zna zatwierdzoną implementację, która będzie używana w trakcie wykonywania. Jednak skompilowany kod nie pobiera zależności od konkretnej reprezentacji.
Unikaj używania dziedziczenia implementacji na potrzeby rozszerzalności
W języku F# rzadko jest używane dziedziczenie implementacji. Ponadto hierarchie dziedziczenia są często złożone i trudne do zmiany po nadejściu nowych wymagań. Implementacja dziedziczenia nadal istnieje w języku F# w celu zapewnienia zgodności i rzadkich przypadków, w których jest najlepszym rozwiązaniem problemu, ale w programach języka F# należy szukać alternatywnych technik podczas projektowania pod kątem polimorfizmu, takiego jak implementacja interfejsu.
Podpisy funkcji i składowych
Użyj krotki dla wartości zwracanych podczas zwracania niewielkiej liczby wielu niepowiązanych wartości
Oto dobry przykład użycia krotki w typie zwrotnym:
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
W przypadku typów zwracanych zawierających wiele składników lub gdy składniki są powiązane z pojedynczą rozpoznawalną jednostką, rozważ użycie nazwanego typu zamiast krotki.
Używanie Async<T>
do programowania asynchronicznego w granicach interfejsu API języka F#
Jeśli istnieje odpowiednia operacja synchroniczna o nazwie Operation
, która zwraca wartość , operacja asynchroniczna powinna mieć nazwę AsyncOperation
, jeśli zwraca T
Async<T>
wartość lub OperationAsync
zwraca wartość Task<T>
. W przypadku powszechnie używanych typów platformy .NET, które uwidaczniają metody rozpoczęcia/zakończenia, rozważ użycie Async.FromBeginEnd
metody pisania rozszerzeń jako fasady w celu udostępnienia modelu programowania asynchronicznego języka F# tym interfejsom API platformy .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() =
...
Wyjątki
Zobacz Zarządzanie błędami , aby dowiedzieć się więcej o odpowiednim użyciu wyjątków, wyników i opcji.
Elementy członkowskie rozszerzenia
Starannie zastosuj elementy członkowskie rozszerzeń języka F# w składnikach języka F#-to-F#
Elementy członkowskie rozszerzeń języka F# powinny być zwykle używane tylko dla operacji, które są w zamknięciu operacji wewnętrznych skojarzonych z typem w większości jego trybów użytkowania. Jednym z typowych zastosowań jest zapewnienie interfejsów API, które są bardziej idiotyczne dla języka F# dla różnych typów platformy .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 unii
Używanie związków dyskryminowanych zamiast hierarchii klas dla danych ze strukturą drzewa
Struktury podobne do drzewa są rekursywnie definiowane. Jest to niezręczne z dziedziczeniem, ale eleganckie z dyskryminowanych związków zawodowych.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
Reprezentowanie danych podobnych do drzewa z związkami dyskryminującymi umożliwia również korzystanie z wyczerpującości w dopasowywaniu wzorców.
Używanie [<RequireQualifiedAccess>]
w typach unii, których nazwy liter nie są wystarczająco unikatowe
Możesz znaleźć się w domenie, w której ta sama nazwa jest najlepszą nazwą dla różnych rzeczy, takich jak przypadki unii dyskryminowanej. Można użyć [<RequireQualifiedAccess>]
do uściślania nazw przypadków, aby uniknąć wyzwalania mylących błędów z powodu cieniowania zależnego od kolejności instrukcji open
Ukryj reprezentacje dyskryminowanych związków dla zgodnych binarnych interfejsów API, jeśli projekt tych typów może ewoluować
Typy unii opierają się na formularzach dopasowywania wzorców języka F# dla zwięzłego modelu programowania. Jak wspomniano wcześniej, należy unikać ujawniania konkretnych reprezentacji danych, jeśli projekt tych typów może ewoluować.
Na przykład reprezentacja dyskryminowanego związku może być ukryta przy użyciu prywatnej lub wewnętrznej deklaracji albo przy użyciu pliku podpisu.
type Union =
private
| CaseA of int
| CaseB of string
Jeśli ujawnisz dyskryminowane związki zawodowe bez konieczności masowego wprowadzania wersji biblioteki, może okazać się trudne do użycia bez przerywania kodu użytkownika. Zamiast tego rozważ ujawnienie co najmniej jednego aktywnego wzorca, aby umożliwić dopasowywanie wzorca do wartości typu.
Aktywne wzorce zapewniają alternatywny sposób zapewnienia użytkownikom języka F# dopasowywania wzorców, unikając bezpośredniego uwidaczniania typów unii języka F#.
Funkcje wbudowane i ograniczenia składowe
Definiowanie ogólnych algorytmów liczbowych przy użyciu funkcji wbudowanych z domniemanymi ograniczeniami składowymi i statycznie rozpoznawanych typów ogólnych
Ograniczenia składowe arytmetyczne i ograniczenia porównania języka F# są standardem programowania w języku F#. Rozważmy na przykład następujący kod:
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 tej funkcji jest następujący:
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
Jest to odpowiednia funkcja dla publicznego interfejsu API w bibliotece matematycznej.
Unikaj używania ograniczeń składowych do symulowania klas typów i wpisywania kaczek
Istnieje możliwość symulowania "wpisywania kaczki" przy użyciu ograniczeń składowych języka F#. Jednak elementy członkowskie, które korzystają z tej funkcji, nie powinny być ogólnie używane w projektach bibliotek języka F#-to-F#. Dzieje się tak, ponieważ projekty bibliotek oparte na nieznanych lub nietypowych niejawnych ograniczeniach zwykle powodują, że kod użytkownika staje się nieelastyczny i powiązany z jednym konkretnym wzorcem struktury.
Ponadto istnieje duża szansa, że duże wykorzystanie ograniczeń składowych w ten sposób może spowodować bardzo długie czasy kompilacji.
Definicje operatorów
Unikaj definiowania niestandardowych operatorów symbolicznych
Operatory niestandardowe są niezbędne w niektórych sytuacjach i są wysoce przydatnymi urządzeniami notacyjnymi w dużej części kodu implementacji. W przypadku nowych użytkowników biblioteki nazwane funkcje są często łatwiejsze do użycia. Ponadto niestandardowe operatory symboliczne mogą być trudne do udokumentowania, a użytkownicy mogą łatwiej wyszukiwać pomoc dla operatorów ze względu na istniejące ograniczenia środowiska IDE i wyszukiwarki.
W związku z tym najlepiej opublikować funkcje jako nazwane funkcje i elementy członkowskie, a dodatkowo uwidaczniać operatory dla tej funkcji tylko wtedy, gdy korzyści notacyjne przewyższają dokumentację i koszt poznawczy ich posiadania.
Jednostki miary
Ostrożnie używaj jednostek miary w celu zwiększenia bezpieczeństwa typu w kodzie języka F#
Dodatkowe informacje o wpisywaniu jednostek miary są usuwane w przypadku wyświetlania przez inne języki platformy .NET. Należy pamiętać, że składniki, narzędzia i odbicie platformy .NET będą widzieć typy-sans-units. Na przykład użytkownicy języka C# zobaczą float
, a nie float<kg>
.
Skróty typów
Ostrożnie używaj skrótów typów, aby uprościć kod języka F#
Składniki, narzędzia i odbicie platformy .NET nie będą widzieć skróconych nazw typów. Znaczące użycie skrótów typów może również sprawić, że domena będzie bardziej złożona niż w rzeczywistości, co może mylić konsumentów.
Unikaj skrótów typów dla typów publicznych, których składowe i właściwości powinny być wewnętrznie inne niż te dostępne w typie, który jest skracany
W tym przypadku skrót typu ujawnia zbyt wiele informacji o reprezentacji zdefiniowanego typu rzeczywistego. Zamiast tego należy rozważyć zawijanie skrótu w typie klasy lub unii dyskryminowanej pojedynczej wielkości liter (lub, gdy wydajność jest niezbędna, rozważ użycie typu struktury do zawijania skrótu).
Na przykład kuszące jest zdefiniowanie wielomapy jako specjalny przypadek mapy języka F#, na przykład:
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
Jednak logiczne operacje notacji kropkowej na tym typie nie są takie same jak operacje na mapie — na przykład rozsądnie jest, że operator map[key]
wyszukiwania zwraca pustą listę, jeśli klucz nie znajduje się w słowniku, zamiast zgłaszać wyjątek.
Wskazówki dotyczące bibliotek do użycia z innych języków platformy .NET
Podczas projektowania bibliotek do użycia z innych języków platformy .NET należy przestrzegać wytycznych dotyczących projektowania bibliotek platformy .NET. W tym dokumencie te biblioteki są oznaczone jako biblioteki waniliowe .NET, w przeciwieństwie do bibliotek języka F#, które używają konstrukcji języka F# bez ograniczeń. Projektowanie bibliotek platformy .NET waniliowych oznacza zapewnienie znanych i idiotycznych interfejsów API spójnych z resztą programu .NET Framework przez zminimalizowanie użycia konstrukcji specyficznych dla języka F#w publicznym interfejsie API. Reguły zostały wyjaśnione w poniższych sekcjach.
Przestrzeń nazw i projekt typu (w przypadku bibliotek do użycia z innych języków platformy .NET)
Stosowanie konwencji nazewnictwa platformy .NET do publicznego interfejsu API składników
Zwróć szczególną uwagę na stosowanie skróconych nazw i wytycznych dotyczących wielkich liter platformy .NET.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Użyj przestrzeni nazw, typów i elementów członkowskich jako podstawowej struktury organizacyjnej składników
Wszystkie pliki zawierające funkcje publiczne powinny zaczynać się od namespace
deklaracji, a jedynymi publicznymi jednostkami w przestrzeniach nazw powinny być typy. Nie używaj modułów języka F#.
Użyj modułów innych niż publiczne do przechowywania kodu implementacji, typów narzędzi i funkcji narzędziowych.
Typy statyczne powinny być preferowane w przypadku modułów, ponieważ umożliwiają one przyszłe ewolucję interfejsu API do używania przeciążenia i innych pojęć projektowych interfejsu API platformy .NET, które mogą nie być używane w modułach języka F#.
Na przykład zamiast następującego publicznego interfejsu API:
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Rozważ zamiast tego:
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
Używanie typów rekordów języka F# w waniliowych interfejsach API platformy .NET, jeśli projektowanie typów nie będzie ewoluować
Typy rekordów języka F# są kompilowane do prostej klasy .NET. Są one odpowiednie dla niektórych prostych, stabilnych typów w interfejsach API. Rozważ użycie [<NoEquality>]
atrybutów i [<NoComparison>]
, aby pominąć automatyczne generowanie interfejsów. Należy również unikać używania pól rekordów modyfikowalnego w interfejsach API platformy .NET, ponieważ uwidacznia to pole publiczne. Zawsze należy rozważyć, czy klasa zapewni bardziej elastyczną opcję przyszłej ewolucji interfejsu API.
Na przykład następujący kod języka F# uwidacznia publiczny interfejs API użytkownikowi języka 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; }
}
Ukryj reprezentację typów unii języka F# w waniliowych interfejsach API platformy .NET
Typy unii języka F# nie są często używane w granicach składników, nawet w przypadku kodowania F#-to-F#. Są to doskonałe urządzenie implementacji używane wewnętrznie w składnikach i bibliotekach.
Podczas projektowania waniliowego interfejsu API platformy .NET rozważ ukrycie reprezentacji typu unii przy użyciu deklaracji prywatnej lub pliku podpisu.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Można również rozszerzyć typy, które używają reprezentacji unii wewnętrznie z elementami członkowskimi, aby zapewnić żądany element . Interfejs API dostępny dla platformy 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)
Projektowanie graficznego interfejsu użytkownika i innych składników przy użyciu wzorców projektowych platformy
Istnieje wiele różnych platform dostępnych na platformie .NET, takich jak WinForms, WPF i ASP.NET. Konwencje nazewnictwa i projektowania dla każdego z nich powinny być używane, jeśli projektujesz składniki do użycia w tych strukturach. Na przykład w przypadku programowania WPF należy przyjąć wzorce projektowe WPF dla klas, które projektujesz. W przypadku modeli w programowaniu interfejsu użytkownika należy używać wzorców projektowych, takich jak zdarzenia i kolekcje oparte na powiadomieniach, takie jak te znalezione w programie System.Collections.ObjectModel.
Projekt obiektów i składowych (w przypadku bibliotek do użycia z innych języków platformy .NET)
Używanie atrybutu CLIEvent do uwidaczniania zdarzeń platformy .NET
Skonstruuj element DelegateEvent
o określonym typie delegata platformy .NET, który przyjmuje obiekt i EventArgs
(a nie Event
, który domyślnie używa FSharpHandler
typu ), aby zdarzenia były publikowane w znany sposób innym językach platformy .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
Uwidaczniaj operacje asynchroniczne jako metody zwracające zadania platformy .NET
Zadania są używane na platformie .NET do reprezentowania aktywnych obliczeń asynchronicznych. Zadania są ogólnie mniej złożone niż obiekty języka F# Async<T>
, ponieważ reprezentują zadania "już wykonywane" i nie mogą być komponowane razem w sposób wykonujący kompozycję równoległą lub które ukrywają propagację sygnałów anulowania i innych parametrów kontekstowych.
Jednak mimo to metody zwracające zadania są standardową reprezentacją programowania asynchronicznego na platformie .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
Często chcesz również zaakceptować jawny token anulowania:
/// 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)
Używanie typów delegatów platformy .NET zamiast typów funkcji języka F#
Tutaj "Typy funkcji F#" oznaczają typy "strzałki", takie jak int -> int
.
Zamiast tego:
member this.Transform(f: int->int) =
...
Wykonaj następujące czynności:
member this.Transform(f: Func<int,int>) =
...
Typ funkcji języka F# jest wyświetlany jako class FSharpFunc<T,U>
inny język .NET i jest mniej odpowiedni dla funkcji języka i narzędzi, które rozumieją typy delegatów. Podczas tworzenia metody o wyższej kolejności przeznaczonej dla platformy .NET Framework 3.5 lub nowszej, delegaty i System.Action
są właściwymi interfejsami API do opublikowania, System.Func
aby umożliwić deweloperom platformy .NET korzystanie z tych interfejsów API w sposób o niskim tarciu. (W przypadku określania wartości docelowej dla programu .NET Framework 2.0 typy delegatów zdefiniowane przez system są bardziej ograniczone; rozważ użycie wstępnie zdefiniowanych typów delegatów, takich jak System.Converter<T,U>
lub zdefiniowanie określonego typu delegata).
Z drugiej strony delegaty platformy .NET nie są naturalne dla bibliotek języka F#-facing (zobacz następną sekcję w bibliotekach F#-facing). W związku z tym powszechną strategią implementacji podczas opracowywania metod wyższej kolejności dla bibliotek platformy .NET jest utworzenie całej implementacji przy użyciu typów funkcji języka F#, a następnie utworzenie publicznego interfejsu API przy użyciu delegatów jako cienkiej fasady na szczycie rzeczywistej implementacji języka F#.
Użyj wzorca TryGetValue zamiast zwracania wartości opcji języka F# i preferuj przeciążenie metody do przyjmowania wartości opcji języka F# jako argumentów
Typowe wzorce użycia typu opcji języka F# w interfejsach API są lepiej implementowane w interfejsach API platformy .NET wanilii przy użyciu standardowych technik projektowania platformy .NET. Zamiast zwracać wartość opcji języka F#, rozważ użycie typu zwracanego wartości logicznej oraz parametru out, jak w wzorzec "TryGetValue". Zamiast przyjmować wartości opcji języka F# jako parametry, rozważ użycie przeciążenia metody lub argumentów opcjonalnych.
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
Użyj typów interfejsów kolekcji .NET IEnumerable<T> i IDictionary<Key,Value> dla parametrów i zwracanych wartości
Unikaj używania konkretnych typów kolekcji, takich jak tablice platformy T[]
.NET, typy Map<Key,Value>
list<T>
języka F# i , oraz Set<T>
typy kolekcji betonowych platformy .NET, takie jak Dictionary<Key,Value>
. Wytyczne dotyczące projektowania biblioteki .NET mają dobre porady dotyczące tego, kiedy należy używać różnych typów kolekcji, takich jak IEnumerable<T>
. Niektóre zastosowania tablic (T[]
) są dopuszczalne w niektórych okolicznościach ze względu na wydajność. Należy pamiętać, że seq<T>
jest to tylko alias języka F# dla IEnumerable<T>
elementu , a zatem seq jest często odpowiednim typem dla waniliowego interfejsu API platformy .NET.
Zamiast list F#:
member this.PrintNames(names: string list) =
...
Użyj sekwencji języka F#:
member this.PrintNames(names: seq<string>) =
...
Użyj typu jednostki jako jedynego typu wejściowego metody, aby zdefiniować metodę zero-argument lub jako jedyny zwracany typ, aby zdefiniować metodę zwracaną przez pustkę
Unikaj innych zastosowań typu jednostki. Są one dobre:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
To jest złe:
member this.WrongUnit( x: unit, z: int) = ((), ())
Sprawdzanie wartości null w granicach interfejsu API platformy .NET
Kod implementacji języka F# zwykle ma mniej wartości null ze względu na niezmienne wzorce projektowe i ograniczenia dotyczące używania literałów null dla typów języka F#. Inne języki platformy .NET często używają wartości null jako wartości znacznie częściej. W związku z tym kod języka F#, który uwidacznia waniliowy interfejs API platformy .NET, powinien sprawdzać parametry o wartości null w granicach interfejsu API i zapobiegać przepływowi tych wartości głębiej do kodu implementacji języka F#. Można isNull
użyć funkcji lub wzorca pasującego do null
wzorca.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull` argName (arg: obj) =
if isNull arg then nullArg argName
else ()
Unikaj używania krotki jako wartości zwracanych
Zamiast tego preferuj zwracanie nazwanego typu zawierającego zagregowane dane lub użycie parametrów wychodzących w celu zwrócenia wielu wartości. Chociaż krotki i krotki struktury istnieją na platformie .NET (w tym obsługę języka C# dla krotki struktur), najczęściej nie zapewniają idealnego i oczekiwanego interfejsu API dla deweloperów platformy .NET.
Unikaj używania currying parametrów
Zamiast tego należy użyć konwencji wywoływania platformy Method(arg1,arg2,…,argN)
.NET.
member this.TupledArguments(str, num) = String.replicate num str
Porada: Jeśli projektujesz biblioteki do użycia z dowolnego języka .NET, nie ma podstawy do rzeczywistego wykonywania eksperymentalnego programowania w języku C# i Visual Basic, aby upewnić się, że biblioteki "czują się dobrze" z tych języków. Możesz również użyć narzędzi, takich jak .NET Emocje or i Visual Studio Object Browser, aby upewnić się, że biblioteki i ich dokumentacja są wyświetlane zgodnie z oczekiwaniami dla deweloperów.
Dodatek
Kompleksowe przykład projektowania kodu F# do użycia przez inne języki platformy .NET
Rozważmy następującą klasę:
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) ]
Wywnioskowany typ języka F# tej klasy jest następujący:
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
Przyjrzyjmy się, jak ten typ języka F# pojawia się dla programisty przy użyciu innego języka .NET. Na przykład przybliżony "podpis" w języku C# jest następujący:
// 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; }
}
Istnieją pewne ważne kwestie, które należy zauważyć, jak język F# reprezentuje konstrukcje tutaj. Na przykład:
Metadane, takie jak nazwy argumentów, zostały zachowane.
Metody języka F#, które przyjmują dwa argumenty, stają się metodami języka C#, które przyjmują dwa argumenty.
Funkcje i listy stają się odwołaniami do odpowiednich typów w bibliotece języka F#.
Poniższy kod pokazuje, jak dostosować ten kod, aby uwzględnić te elementy.
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) }
Wywnioskowany typ języka F# kodu jest następujący:
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 języka C# jest teraz następujący:
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; }
}
Poprawki wprowadzone w celu przygotowania tego typu do użycia w ramach waniliowej biblioteki platformy .NET są następujące:
Skorygowaliśmy kilka nazw:
Point1
,l
n
if
stały sięRadialPoint
odpowiednio ,count
,factor
itransform
.Użyto typu zwracanego
seq<RadialPoint>
zamiastRadialPoint list
przez zmianę konstrukcji listy na[ ... ]
konstrukcję sekwencji przy użyciu poleceniaIEnumerable<RadialPoint>
.Użyto typu
System.Func
delegata platformy .NET zamiast typu funkcji języka F#.
To sprawia, że jest znacznie bardziej przyjemna do korzystania z kodu w języku C#.