Udostępnij przez


Wskazówki dotyczące projektowania składników języka F#

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ż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, li f zostały zmienione na odpowiednio RadialPoint, count, factori transform.

  • Zmieniono konstrukcję listy używając seq<RadialPoint> na konstrukcję sekwencji przy użyciu RadialPoint list, stosując typ zwrotny [ ... ] zamiast IEnumerable<RadialPoint>.

  • Użyto typu delegata platformy .NET System.Func zamiast typu funkcji języka F#.

To sprawia, że kod w języku C# jest znacznie łatwiejszy i przyjemniejszy w użyciu.