Udostępnij za pośrednictwem


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, ValueArg (ale nie na przykład TKey). T1UT

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 invalidArgList.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ż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. , ArrayList), 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 erferfc. 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 TAsync<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, lni f stały się RadialPointodpowiednio , count, factori transform.

  • Użyto typu zwracanego seq<RadialPoint> zamiast RadialPoint list przez zmianę konstrukcji listy na [ ... ] konstrukcję sekwencji przy użyciu polecenia IEnumerable<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#.