Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Ten dokument jest zestawem wytycznych dotyczących projektowania składników dla programowania w języku F#, opartych na wytycznych F# Component Design Guidelines, wersji 14, Microsoft Research i wersji, która została pierwotnie wyselekcjonowana i utrzymywana przez Fundację Oprogramowania F#.
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.
Przegląd
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 przez granice zestawów.
- Biblioteka przeznaczona do dystrybucji za pośrednictwem repozytorium pakietów, na przykład NuGet.
Techniki opisane w tym artykule są zgodne z Pięć zasad dobrego kodu F#, a zatem korzystają zarówno z programowania funkcjonalnego, jak i obiektowego 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 wytyczne
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 w języku F#, warto mieć podstawową wiedzę na temat wytycznych dotyczących projektowania biblioteki 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 użytkownikom doskonałe wsparcie Intellisense i Quickinfo podczas korzystania z tych typów i członków oraz umożliwia tworzenie plików dokumentacji dla biblioteki. Zobacz dokumentację XML dotyczącą różnych tagów XML, które można wykorzystać do dodatkowego oznakowania 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 sygnatur (.fsi) dla stabilnych interfejsów API bibliotek i komponentó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 utrudniają zmiany w publicznym interfejsie API, ponieważ wymagają wprowadzania zmian zarówno w plikach implementacji, jak 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 wskazówki, gdy zakres projektu go uzasadnia. W szczególności wyraźne określenie intencji kulturowej w konwersji i porównywaniu ciągów (w stosownych przypadkach).
Wskazówki dotyczące bibliotek przeznaczonych dla języka F#
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 następują, wytyczne projektu bibliotek platformy .NET są zapasowym wytycznym.
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 przekraczających granice zastosowania F#, dopasowujących się do idiomów platformy BCL .NET i większości bibliotek.
| Konstruować | Przypadek | Część | Przykłady | Notatki |
|---|---|---|---|---|
| Typy betonowe | PascalCase | Rzeczownik/ przymiotnik | Lista, podwójna, złożona | Typy betonowe to struktury, klasy, wyliczenia, delegaty, rekordy i unie. 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, Dodaj, 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 | ZmienionoWartość/ZmieniaSięWartość | |
| Wyjątki | PascalCase | Wyjątek typu WebException | Nazwa powinna kończyć się na "Exception". | |
| Pole | PascalCase | Rzeczownik | CurrentName | |
| Typy interfejsów | PascalCase | Rzeczownik/ przymiotnik | IDisposable | Nazwa powinna zaczynać się od "I". |
| Metoda | PascalCase | Czasownik | ToString | |
| Namespace | PascalCase | Microsoft.FSharp.Core | Zazwyczaj należy używać <Organization>.<Technology>[.<Subnamespace>], z pominięciem organizacji, jeśli technologia jest niezależna od organizacji. |
|
| Parametry | camelCase | Rzeczownik | nazwaTypu, przekształcenie, zakres | |
| let wartości (wewnętrzne) | camelCase lub PascalCase | Rzeczownik/czasownik | getValue, myTable | |
| wartości let (zewnętrzne) | camelCase lub PascalCase | Rzeczownik/czasownik | Mapa.Lista, Dzisiejsza.Data | 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łasność | PascalCase | Rzeczownik/ przymiotnik | CzyKoniecPliku, KolorTła | Właściwości boolowskie zazwyczaj używają przedrostków Is i Can i powinny być w formie twierdzącej, 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 dla "Asynchronicznego", są tolerowane. Te wytyczne są czasami ignorowane w przypadku programowania funkcjonalnego; na przykład List.iter używa skrótu "iteracja". Dlatego używanie skrótów zwykle jest tolerowane w większym stopniu w programowaniu F#-to-F#, ale nadal powinno być generalnie unikane w projektowaniu publicznych komponentów.
Unikaj kolizji nazw wynikających z różnic w wielkości liter
Wytyczne dotyczące platformy .NET mówią, że nie można polegać wyłącznie na rozróżnianiu wielkości liter do uściślania kolizji nazw, ponieważ niektóre języki klienta (na przykład Visual Basic) są nierozróżniające ze względu na wielkość 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żyj PascalCase dla ogólnych nazw parametrów
Należy użyć PascalCase dla ogólnych nazw parametrów w publicznych interfejsach API, w tym także w bibliotekach przeznaczonych dla F#. W szczególności należy używać nazw takich jak T, U, T1, T2 dla dowolnych parametrów ogólnych, a gdy określone nazwy mają sens, w przypadku bibliotek przeznaczonych dla F# należy używać nazw takich jak Key, Value, Arg (ale nie na przykład TKey).
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 mają być używane bez kwalifikacji (na przykład invalidArg) i dla "standardowych funkcji kolekcji" (na przykład 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ą rozciągać się na 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żyj metod i właściwości operacji typowych dla typów obiektów
Podczas pracy z obiektami najlepiej upewnić się, że funkcjonalność do wykorzystania jest implementowana 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ść funkcjonalności danego członka nie musi być koniecznie zaimplementowana w tym członku, ale element przystosowany do użycia tej funkcjonalności powinien 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 od innych opcji, takich jak krotki funkcji lub rekordy 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 dają funktory. Ponadto mogą być używane do kodowania typów egzystencjalnych w programie, czego rekordy funkcji nie są w stanie zrobić.
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 automatycznie otwieraną kolekcją 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 [<RequireQualifiedAccess>] do modułu wskazuje, że moduł może nie zostać otwarty i że odwołania do elementów modułu wymagają jawnego kwalifikowanego dostępu. Na przykład moduł Microsoft.FSharp.Collections.List 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.
Silnie zaleca się użycie atrybutu [<RequireQualifiedAccess>] w modułach niestandardowych, które rozszerzają te udostępniane przez FSharp.Core (takie jak Seq, List, Array), ponieważ te moduły są powszechnie używane w kodzie F# i mają zdefiniowane [<RequireQualifiedAccess>]; ogólnie rzecz biorąc, nie zaleca się definiowania modułów niestandardowych bez atrybutu, gdy zasłaniają lub rozszerzają inne moduły, które mają atrybut.
Dodanie atrybutu [<AutoOpen>] do modułu oznacza, że moduł zostanie otwarty po otwarciu zawierającej przestrzeni nazw. Atrybut [<AutoOpen>] można również zastosować do zestawu w celu wskazania modułu, 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 zawierające funkcje erf i erfc. Rozsądnie jest oznaczyć ten moduł jako [<AutoOpen>]. Oznacza to, że open MathsHeaven.Statistics otworzy również ten moduł i wprowadzi nazwy erf i erfc do zakresu. Innym dobrym zastosowaniem [<AutoOpen>] są moduły zawierające metody rozszerzające.
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>] może prowadzić do lepszej 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ć nazwę przyjazną dla platformy .NET, zrozumiałą dla użytkowników innych języków platformy .NET.
Czasami musicie nazwać coś w jednym stylu dla użytkowników języka F# (na przykład statyczny członek pisany małymi literami, tak aby wyglądało na funkcję powiązaną z modułem), ale użyć innego stylu nazwy, kiedy jest kompilowana do zestawu. Możesz użyć atrybutu [<CompiledName>], aby zapewnić inny styl dla kodu niebędącego językiem F#, który korzysta 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
Korzystając z [<CompiledName>], można stosować konwencje nazewnictwa .NET dla odbiorców zestawu, którzy nie używają F#.
Użyj przeciążania metod dla funkcji składowych, jeśli to pozwala na prostszy interfejs API.
Przeciążenie metody to potężne narzędzie do upraszczania interfejsu API, które może wymagać wykonywania 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 typów unii, jeśli projekt tych typów może ewoluować
Unikaj odsłaniania konkretnych reprezentacji obiektów. Na przykład zewnętrzny, publiczny interfejs API biblioteki .NET nie ujawnia konkretnej reprezentacji wartości DateTime. 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, które zawierają wiele składników, lub gdy składniki są powiązane z pojedynczą rozpoznawalną jednostką, rozważ użycie typu nazwanego zamiast krotki.
Używanie Async<T> do programowania asynchronicznego w granicach interfejsu API języka F#
Jeśli istnieje odpowiadająca operacja synchroniczna o nazwie Operation zwracająca T, operacja asynchroniczna powinna mieć nazwę AsyncOperation, jeśli zwraca Async<T> lub OperationAsync, jeśli zwraca Task<T>. W przypadku powszechnie używanych typów platformy .NET, które udostępniają metody Begin/End, rozważ użycie Async.FromBeginEnd do pisania metod rozszerzeń jako fasady, która zapewni model programowania asynchronicznego języka F# dla tych interfejsów 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() =
...
Wyjątki
Zobacz Zarządzanie błędami, aby dowiedzieć się więcej o odpowiednim użyciu wyjątków, wyników i opcji.
Członkowie rozszerzenia
Starannie używaj członków rozszerzeń F# w składnikach F#-to-F#.
Członkowie rozszerzeń F# powinni być na ogół używani tylko do operacji, które są określone przez wewnętrzne operacje związane z typem w większości jego sposobów użytkowania. Jednym z typowych zastosowań jest zapewnienie interfejsów API, które są bardziej idiomatyczne dla języka F# w przypadku 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
Zamiast hierarchii klas, używaj związków dyskryminowanych dla danych o strukturze 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 o strukturze drzewiastej połączeniami dyskryminującymi umożliwia korzystanie z wyczerpującego dopasowywania wzorców.
Użyj [<RequireQualifiedAccess>] w typach unii, których nazwy przypadków 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 w celu uniknięcia 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 unie bezmyślnie, może okazać się trudne wersjonowanie biblioteki bez przerywania kodu użytkownika. Zamiast tego rozważ ujawnienie co najmniej jednego aktywnego wzorca, aby umożliwić dopasowywanie wzorca do wartości twojego typu.
Aktywne wzorce stanowią alternatywny sposób oferowania użytkownikom języka F# dopasowywania wzorców, bez bezpośredniego ujawniania 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ównawcze w 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 duck typing
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 intensywne użycie ograniczeń członkowskich 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 środkami notacyjnymi w dużych zasobach kodu implementacyjnego. 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 mają trudności z wyszukiwaniem pomocy dotyczącej operatorów z powodu istniejących ograniczeń w środowiskach IDE i wyszukiwarkach.
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 typach jednostek miary są usuwane, gdy są przeglądane przez inne języki platformy .NET. Należy pamiętać, że składniki, narzędzia i mechanizm odbicia platformy .NET będą widzieć typy bez jednostek. 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ć opakowywanie skrótu w typie klasy lub unie rozróżnianą jednowartościową (lub, gdy wydajność jest niezbędna, rozważ użycie typu struktury do opakowywania 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, aby operator wyszukiwania map[key] zwrócił 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 bazowych bibliotek platformy .NET oznacza zapewnienie znanych i idiomatycznych interfejsów API spójnych z resztą platformy .NET Framework, minimalizując użycie konstrukcji specyficznych dla języka F# w publicznym interfejsie API. Reguły zostały wyjaśnione w poniższych sekcjach.
Przestrzeń nazw i projektowanie typów do bibliotek używanych z innych języków platformy .NET
Stosuj konwencje nazewnictwa platformy .NET do publicznego API twoich komponentó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 jako podstawowej struktury organizacyjnej składników
Wszystkie pliki zawierające funkcje publiczne powinny zaczynać się od deklaracji namespace, 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 atrybutów [<NoEquality>] i [<NoComparison>], aby pominąć automatyczne generowanie interfejsów. Należy również unikać używania pól modyfikowalnych rekordów w bazowych interfejsach API platformy .NET, ponieważ to umożliwia dostęp do pola publicznego. 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 związków języka F# w standardowych interfejsach API platformy .NET
Typy unii języka F# nie są często używane na granicach komponentó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 podstawowego interfejsu API platformy .NET rozważ ukrycie reprezentacji typu unii poprzez użycie deklaracji prywatnej lub pliku podpisu.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Można również rozszerzyć typy, które wewnętrznie używają reprezentacji unii, dodając członków, aby zapewnić pożądany interfejs API widoczny 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 znajdujące się w System.Collections.ObjectModel.
Projektowanie obiektów i składowych (dla bibliotek przeznaczonych do użycia z innych języków platformy .NET)
Używanie atrybutu CLIEvent do uwidaczniania zdarzeń platformy .NET
Skonstruuj DelegateEvent o określonym typie delegata .NET, który przyjmuje obiekt oraz EventArgs (zamiast Event, który domyślnie używa tylko typu FSharpHandler), aby zdarzenia były publikowane w dobrze znany sposób innym językom 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
Ujawniaj operacje asynchroniczne jako metody zwracające zadania .NET
Zadania są używane na platformie .NET do reprezentowania aktywnych obliczeń asynchronicznych. Zadania są ogólnie mniej złożone niż obiekty Async<T> języka F#, ponieważ reprezentują one zadania "już wykonywane" i nie mogą być komponowane 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żyj typów delegatów platformy .NET zamiast typów funkcji języka F#
W tym przypadku "typy funkcji F#" oznaczają typy "strzałki (arrow)", 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 w języku F# jest reprezentowany jako class FSharpFunc<T,U> w innych językach platformy .NET i jest mniej odpowiedni dla funkcji językowych oraz narzędzi, które rozumieją typy delegatów. Podczas tworzenia metody wyższego rzędu przeznaczonej dla platformy .NET Framework 3.5 lub nowszej System.Func i System.Action są właściwymi API do opublikowania, aby umożliwić deweloperom platformy .NET korzystanie z tych API w prosty sposób. (W przypadku kompilowania dla .NET Framework 2.0 typy delegatów zdefiniowane przez system są bardziej ograniczone; rozważ użycie predefiniowanych typów delegatów, takich jak System.Converter<T,U> lub zdefiniowanie konkretnego typu delegata).
Z drugiej strony delegaty .NET nie są naturalne dla bibliotek przeznaczonych do interakcji z językiem F# (zobacz następną sekcję o bibliotekach F#). W związku z tym powszechną strategią implementacji podczas opracowywania metod wyższej kolejności dla bibliotek platformy .NET jest opracowanie całej implementacji przy użyciu typów funkcji języka F#, a następnie stworzenie publicznego interfejsu API z wykorzystaniem delegatów jako cienkiej fasady na rzeczywistej implementacji języka F#.
Używaj wzorca TryGetValue zamiast zwracania wartości opcji F#, a przeciążanie metod preferuj zamiast przyjmowania wartości opcji F# jako argumentów.
Typowe wzorce użycia typu opción w F# w API są lepiej realizowane w standardowych API .NET za pomocą typowych technik projektowania platformy .NET. Zamiast zwracać wartość opcji języka F#, rozważ użycie typu zwracanego wartości logicznej oraz parametru out, jak we wzorcu "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> do parametrów i wartości zwracanych
Unikaj używania konkretnych typów kolekcji, takich jak tablice .NET T[], typy F# list<T>, Map<Key,Value> i Set<T>, oraz konkretnych typów kolekcji .NET, takich jak Dictionary<Key,Value>. Wskazówki dotyczące projektowania biblioteki .NET zawierają 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 pewnych okolicznościach ze względów wydajnościowych. Należy szczególnie pamiętać, że seq<T> jest jedynie aliasem F# dla IEnumerable<T>, i w związku z tym seq jest często odpowiednim typem dla podstawowego 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ę bezargumentową, lub jako jedynego typu zwracanego, aby zdefiniować metodę zwracającą void.
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 na 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 znacznie częściej używają null jako wartości. 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 użyć funkcji isNull lub wzorca pasującego do wzorca null.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
Począwszy od języka F# 9, możesz użyć nowej składni | null, aby kompilator wskazywał możliwe wartości null i gdzie wymagają obsługi:
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 ()
W języku F# 9 kompilator emituje ostrzeżenie, gdy wykryje, że możliwa wartość null nie jest obsługiwana:
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
Te ostrzeżenia powinny zostać uwzględnione przy użyciu wzorca null wartości F# podczas dopasowywania.
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
Unikaj używania krotek 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 struktur istnieją w .NET (w tym z obsługą języka C# dla krotek struktur), najczęściej nie zapewniają idealnego i oczekiwanego interfejsu API dla deweloperów .NET.
Nie stosuj techniki przekształcania funkcji z parametrami
Zamiast tego należy użyć konwencji wywoływania platformy .NET Method(arg1,arg2,…,argN).
member this.TupledArguments(str, num) = String.replicate num str
Porada: Jeśli projektujesz biblioteki do użycia z dowolnego języka .NET, nic nie zastąpi rzeczywistego przeprowadzenia eksperymentalnego programowania w językach C# i Visual Basic, aby upewnić się, że biblioteki dobrze „pasują” do tych języków. Możesz również użyć narzędzi, takich jak refleksora .NET i przeglądarki obiektów programu Visual Studio, aby upewnić się, że biblioteki i ich dokumentacja są wyświetlane zgodnie z oczekiwaniami dla deweloperów.
Aneks
Całościowy przykład projektowania kodu F# do wykorzystania przez inne języki .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 do zauważenia co do sposobu, w jaki język F# reprezentuje tutaj konstrukcje. 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 standardowej biblioteki .NET są następujące:
Kilka nazw zostało zmienionych:
Point1,n,lifzostały zmienione na odpowiednioRadialPoint,count,factoritransform.Zmieniono konstrukcję listy używając
seq<RadialPoint>na konstrukcję sekwencji przy użyciuRadialPoint list, stosując typ zwrotny[ ... ]zamiastIEnumerable<RadialPoint>.Użyto typu delegata platformy .NET
System.Funczamiast typu funkcji języka F#.
To sprawia, że kod w języku C# jest znacznie łatwiejszy i przyjemniejszy w użyciu.