Linee guida per la progettazione dei componenti F#

Questo documento è un set di linee guida per la progettazione dei componenti per la programmazione F#, basate sulle linee guida per la progettazione dei componenti F#, v14, Microsoft Research e una versione originariamente curata e gestita da F# Software Foundation.

Questo documento presuppone che si abbia familiarità con la programmazione F#. Molti grazie alla community di F# per i loro contributi e feedback utili su varie versioni di questa guida.

Panoramica

Questo documento esamina alcuni dei problemi relativi alla progettazione e alla codifica dei componenti F#. Un componente può indicare uno dei seguenti elementi:

  • Livello nel progetto F# con consumer esterni all'interno di tale progetto.
  • Libreria destinata all'utilizzo da parte del codice F# oltre i limiti dell'assembly.
  • Libreria destinata all'utilizzo da parte di qualsiasi linguaggio .NET oltre i limiti dell'assembly.
  • Libreria destinata alla distribuzione tramite un repository di pacchetti, ad esempio NuGet.

Le tecniche descritte in questo articolo seguono i cinque principi del codice F# valido e quindi usano sia la programmazione funzionale che quella degli oggetti in base alle esigenze.

Indipendentemente dalla metodologia, il progettista di componenti e librerie affronta diversi problemi pratici e prosaici quando si tenta di creare un'API che è più facilmente utilizzabile dagli sviluppatori. L'applicazione coscienziosa delle linee guida per la progettazione della libreria .NET consente di creare un set coerente di API che sono piacevoli da usare.

Linee guida generali

Esistono alcune linee guida universali applicabili alle librerie F#, indipendentemente dal gruppo di destinatari previsto per la libreria.

Informazioni sulle linee guida per la progettazione della libreria .NET

Indipendentemente dal tipo di codifica F# in corso, è utile avere una conoscenza approfondita delle linee guida per la progettazione della libreria .NET. La maggior parte degli altri programmatori F# e .NET avrà familiarità con queste linee guida e prevede che il codice .NET sia conforme.

Le linee guida per la progettazione della libreria .NET forniscono indicazioni generali sulla denominazione, la progettazione di classi e interfacce, la progettazione dei membri (proprietà, metodi, eventi e così via) e altro ancora e sono un punto di riferimento utile per un'ampia gamma di linee guida di progettazione.

Aggiungere commenti alla documentazione XML al codice

La documentazione XML sulle API pubbliche garantisce che gli utenti possano ottenere informazioni dettagliate su IntelliSense e Quickinfo quando si usano questi tipi e membri e abilitare la compilazione di file di documentazione per la libreria. Vedere la documentazione XML sui vari tag XML che possono essere usati per markup aggiuntivi all'interno dei commenti xmldoc.

/// A class for representing (x,y) coordinates
type Point =

    /// Computes the distance between this point and another
    member DistanceTo: otherPoint:Point -> float

È possibile utilizzare i commenti XML in formato breve (/// comment) o i commenti XML standard (///<summary>comment</summary>).

Prendere in considerazione l'uso di file di firma espliciti (fsi) per le API della libreria e dei componenti stabili

L'uso di file di firme esplicite in una libreria F# fornisce un riepilogo conciso dell'API pubblica, che consente di conoscere la superficie pubblica completa della libreria e fornisce una netta separazione tra la documentazione pubblica e i dettagli interni dell'implementazione. I file di firma aggiungono attrito alla modifica dell'API pubblica, richiedendo modifiche da apportare nei file di implementazione e firma. Di conseguenza, i file di firma devono essere in genere introdotti solo quando un'API è diventata solidificata e non è più prevista una modifica significativa.

Seguire le procedure consigliate per l'uso di stringhe in .NET

Seguire le procedure consigliate per l'uso di stringhe nelle linee guida di .NET quando l'ambito del progetto lo garantisce. In particolare, specificando in modo esplicito la finalità culturale nella conversione e nel confronto delle stringhe (ove applicabile).

Linee guida per le librerie F#-facing

Questa sezione presenta raccomandazioni per lo sviluppo di librerie F#rivolte al pubblico; ovvero librerie che espongono API pubbliche destinate a essere utilizzate dagli sviluppatori F#. Esistono diversi consigli per la progettazione di librerie applicabili in modo specifico a F#. In assenza di raccomandazioni specifiche che seguono, le linee guida per la progettazione della libreria .NET sono le linee guida per il fallback.

Convenzioni di denominazione

Usare convenzioni di denominazione e maiuscole di .NET

La tabella seguente segue le convenzioni di denominazione e maiuscole di .NET. Sono disponibili piccole aggiunte per includere anche costrutti F#. Queste raccomandazioni sono progettate soprattutto per le API che superano i limiti F#-to-F#, adattandosi ai linguaggi di .NET BCL e alla maggior parte delle librerie.

Costrutto Case In parte Esempi Note
Tipi concreti PascalCase Sostantivo/aggettivo List, Double, Complex I tipi concreti sono struct, classi, enumerazioni, delegati, record e unioni. Anche se i nomi dei tipi sono tradizionalmente minuscoli in OCaml, F# ha adottato lo schema di denominazione .NET per i tipi.
DLL PascalCase Fabrikam.Core.dll
Tag unione PascalCase Sostantivo Alcuni, Aggiungi, Operazione riuscita Non usare un prefisso nelle API pubbliche. Facoltativamente, usare un prefisso quando è interno, ad esempio "type Teams = TAlpha | TBeta | TDelta".
Event PascalCase Verbo ValueChanged/ValueChanging
Eccezioni PascalCase WebException Il nome deve terminare con "Eccezione".
Campo PascalCase Sostantivo CurrentName
Tipi interfaccia PascalCase Sostantivo/aggettivo IDisposable Il nome deve iniziare con "I".
metodo PascalCase Verbo ToString
Spazio dei nomi PascalCase Microsoft.FSharp.Core In genere usare <Organization>.<Technology>[.<Subnamespace>], anche se eliminare l'organizzazione se la tecnologia è indipendente dall'organizzazione.
Parametri camelCase Sostantivo typeName, transform, range
valori let (interno) camelCase o PascalCase Sostantivo/verbo getValue, myTable
valori let (esterni) camelCase o PascalCase Sostantivo/verbo List.map, Dates.Today I valori con associazione let sono spesso pubblici quando si seguono i modelli di progettazione funzionale tradizionali. Tuttavia, in genere usare PascalCase quando l'identificatore può essere usato da altri linguaggi .NET.
Proprietà PascalCase Sostantivo/aggettivo IsEndOfFile, BackColor Le proprietà booleane in genere usano Is e Can e devono essere affermative, come in IsEndOfFile, non IsNotEndOfFile.

Evitare abbreviazioni

Le linee guida di .NET sconsigliano l'uso delle abbreviazioni (ad esempio, "usare OnButtonClick anziché OnBtnClick"). Le abbreviazioni comuni, ad esempio Async per "Asincrona", sono tollerate. Questa linea guida viene talvolta ignorata per la programmazione funzionale; Ad esempio, List.iter usa un'abbreviazione per "iterazione". Per questo motivo, l'uso delle abbreviazioni tende a essere tollerato in modo più elevato nella programmazione F#-to-F#, ma deve comunque essere generalmente evitato nella progettazione di componenti pubblici.

Evitare conflitti di maiuscole e minuscole nei nomi

Le linee guida .NET dicono che la combinazione di maiuscole e minuscole non può essere usata per evitare ambiguità tra conflitti di nomi, perché alcuni linguaggi client (ad esempio, Visual Basic) non fanno distinzione tra maiuscole e minuscole.

Usare gli acronimi, se appropriato

Gli acronimi come XML non sono abbreviazioni e sono ampiamente usati nelle librerie .NET in formato non capitalizzato (Xml). È consigliabile usare solo acronimi ben noti e ampiamente riconosciuti.

Usare PascalCase per i nomi di parametro generici

Usare PascalCase per i nomi di parametri generici nelle API pubbliche, tra cui per le librerie F#. In particolare, usare nomi come , , T1per parametri generici arbitrari e quando i nomi specifici hanno senso, quindi per le librerie F#- usano nomi come TKey, Value, Arg (ma non ad esempio, TKey). T2U

Usare PascalCase o camelCase per funzioni e valori pubblici nei moduli F#

camelCase viene usato per le funzioni pubbliche progettate per essere usate non qualificate (ad esempio, invalidArg) e per le "funzioni di raccolta standard", ad esempio List.map. In entrambi questi casi, i nomi delle funzioni agiscono in modo molto simile alle parole chiave nel linguaggio.

Progettazione di oggetti, tipi e moduli

Usare spazi dei nomi o moduli per contenere i tipi e i moduli

Ogni file F# in un componente deve iniziare con una dichiarazione dello spazio dei nomi o una dichiarazione di modulo.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

or

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

Le differenze tra l'uso di moduli e spazi dei nomi per organizzare il codice al livello superiore sono le seguenti:

  • Gli spazi dei nomi possono estendersi su più file
  • Gli spazi dei nomi non possono contenere funzioni F# a meno che non si trovino all'interno di un modulo interno
  • Il codice per qualsiasi modulo specificato deve essere contenuto all'interno di un singolo file
  • I moduli di primo livello possono contenere funzioni F# senza la necessità di un modulo interno

La scelta tra uno spazio dei nomi o un modulo di primo livello influisce sulla forma compilata del codice e quindi influirà sulla visualizzazione di altri linguaggi .NET se l'API verrà usata all'esterno del codice F#.

Usare metodi e proprietà per le operazioni intrinseche ai tipi di oggetto

Quando si utilizzano oggetti, è consigliabile assicurarsi che le funzionalità di consumo vengano implementate come metodi e proprietà su tale tipo.

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) = ...

La maggior parte delle funzionalità per un determinato membro non deve necessariamente essere implementata in tale membro, ma la parte di consumo di tale funzionalità deve essere.

Usare le classi per incapsulare lo stato modificabile

In F# questa operazione deve essere eseguita solo in cui tale stato non è già incapsulato da un altro costrutto di linguaggio, ad esempio una chiusura, un'espressione di sequenza o un calcolo asincrono.

type Counter() =
    // let-bound values are private in classes.
    let mutable count = 0

    member this.Next() =
        count <- count + 1
        count

Usare i tipi di interfaccia per rappresentare un set di operazioni. Questa opzione è preferibile ad altre opzioni, ad esempio tuple di funzioni o record di funzioni.

type Serializer =
    abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
    abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T

In preferenza a:

type Serializer<'T> = {
    Serialize: bool -> 'T -> string
    Deserialize: bool -> string -> 'T
}

Le interfacce sono concetti di prima classe in .NET, che è possibile usare per ottenere ciò che i Functor di solito offrono. Inoltre, possono essere usati per codificare i tipi esistenziali nel programma, quali record di funzioni non possono.

Usare un modulo per raggruppare le funzioni che agiscono sulle raccolte

Quando si definisce un tipo di raccolta, è consigliabile fornire un set standard di operazioni come CollectionType.map e CollectionType.iter) per i nuovi tipi di raccolta.

module CollectionType =
    let map f c =
        ...
    let iter f c =
        ...

Se si include un modulo di questo tipo, seguire le convenzioni di denominazione standard per le funzioni disponibili in FSharp.Core.

Usare un modulo per raggruppare le funzioni per funzioni comuni canoniche, in particolare nelle librerie matematiche e DSL

Ad esempio, Microsoft.FSharp.Core.Operators è una raccolta aperta automaticamente di funzioni di primo livello (ad abs esempio e sin) fornite da FSharp.Core.dll.

Analogamente, una libreria di statistiche può includere un modulo con funzioni erf e erfc, in cui questo modulo è progettato per essere aperto in modo esplicito o automatico.

Prendere in considerazione l'uso di RequireQualifiedAccess e applicare attentamente gli attributi AutoOpen

L'aggiunta dell'attributo [<RequireQualifiedAccess>] a un modulo indica che il modulo potrebbe non essere aperto e che i riferimenti agli elementi del modulo richiedono l'accesso completo esplicito. Ad esempio, il Microsoft.FSharp.Collections.List modulo ha questo attributo.

Ciò è utile quando le funzioni e i valori nel modulo hanno nomi che potrebbero essere in conflitto con i nomi in altri moduli. La richiesta di accesso qualificato può aumentare notevolmente la gestibilità a lungo termine e l'elulubilità di una libreria.

È consigliabile avere l'attributo [<RequireQualifiedAccess>] per i moduli personalizzati che estendono quelli forniti da FSharp.Core (ad esempio Seq, List, Array), poiché questi moduli vengono usati principalmente nel codice F# e ne sono [<RequireQualifiedAccess>] stati definiti; in genere, è sconsigliato definire moduli personalizzati privi dell'attributo, quando tali shadow del modulo o estendono altri moduli con l'attributo .

L'aggiunta dell'attributo [<AutoOpen>] a un modulo indica che il modulo verrà aperto all'apertura dello spazio dei nomi contenitore. L'attributo [<AutoOpen>] può anche essere applicato a un assembly per indicare un modulo che viene aperto automaticamente quando viene fatto riferimento all'assembly.

Ad esempio, una libreria di statistiche MathsHeaven.Statistics può contenere funzioni module MathsHeaven.Statistics.Operators contenenti funzioni erf e erfc. È ragionevole contrassegnare questo modulo come [<AutoOpen>]. Questo significa che open MathsHeaven.Statistics aprirà anche questo modulo e porterà i nomi erf e erfc l'ambito. Un altro buon uso di [<AutoOpen>] è per i moduli contenenti metodi di estensione.

L'uso eccessivo di [<AutoOpen>] porta a spazi dei nomi inquinanti e l'attributo deve essere usato con attenzione. Per librerie specifiche in domini specifici, l'uso succoso di [<AutoOpen>] può portare a una migliore usabilità.

Valutare la possibilità di definire i membri dell'operatore nelle classi in cui l'uso di operatori noti è appropriato

A volte le classi vengono usate per modellare costrutti matematici, ad esempio Vettori. Quando il dominio modellato ha operatori noti, è utile definirli come membri intrinseci alla classe.

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

Queste linee guida corrispondono a linee guida .NET generali per questi tipi. Tuttavia, può essere importante anche nella codifica F# perché consente l'uso di questi tipi in combinazione con funzioni e metodi F# con vincoli membro, ad esempio List.sumBy.

Prendere in considerazione l'uso di CompiledName per fornire un oggetto . Nome descrittivo di NET per altri consumer di linguaggi .NET

A volte è possibile assegnare un nome in uno stile per i consumer F# (ad esempio un membro statico in lettere minuscole in modo che venga visualizzato come se fosse una funzione associata a un modulo), ma avere uno stile diverso per il nome quando viene compilato in un assembly. È possibile usare l'attributo [<CompiledName>] per fornire uno stile diverso per il codice non F# che utilizza l'assembly.

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

[<CompiledName>]Usando , è possibile usare le convenzioni di denominazione .NET per i consumer non F# dell'assembly.

Usare l'overload dei metodi per le funzioni membro, in tal caso fornisce un'API più semplice

L'overload dei metodi è uno strumento potente per semplificare un'API che potrebbe dover eseguire funzionalità simili, ma con opzioni o argomenti diversi.

type Logger() =

    member this.Log(message) =
        ...
    member this.Log(message, retryPolicy) =
        ...

In F# è più comune eseguire l'overload in base al numero di argomenti anziché ai tipi di argomenti.

Nascondere le rappresentazioni dei tipi di record e unione se è probabile che la progettazione di questi tipi si evolva

Evitare di rivelare rappresentazioni concrete di oggetti. Ad esempio, la rappresentazione concreta dei DateTime valori non viene rilevata dall'API pubblica esterna della progettazione della libreria .NET. In fase di esecuzione, Common Language Runtime conosce l'implementazione di cui è stato eseguito il commit che verrà usato durante l'esecuzione. Tuttavia, il codice compilato non rileva le dipendenze dalla rappresentazione concreta.

Evitare l'uso dell'ereditarietà dell'implementazione per l'estendibilità

In F# l'ereditarietà dell'implementazione viene usata raramente. Inoltre, le gerarchie di ereditarietà sono spesso complesse e difficili da modificare quando arrivano nuovi requisiti. L'implementazione dell'ereditarietà esiste ancora in F# per compatibilità e rari casi in cui si tratta della soluzione migliore a un problema, ma è consigliabile cercare tecniche alternative nei programmi F# durante la progettazione per il polimorfismo, ad esempio l'implementazione dell'interfaccia.

Firme di funzioni e membri

Usare le tuple per i valori restituiti quando si restituisce un numero ridotto di più valori non correlati

Di seguito è riportato un buon esempio dell'uso di una tupla in un tipo restituito:

val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger

Per i tipi restituiti contenenti molti componenti o in cui i componenti sono correlati a una singola entità identificabile, è consigliabile usare un tipo denominato anziché una tupla.

Usare Async<T> per la programmazione asincrona nei limiti dell'API F#

Se è presente un'operazione sincrona corrispondente denominata Operation che restituisce un Toggetto , l'operazione asincrona deve essere denominata AsyncOperation se restituisce Async<T> o OperationAsync se restituisce Task<T>. Per i tipi .NET comunemente usati che espongono metodi Begin/End, è consigliabile usare Async.FromBeginEnd per scrivere metodi di estensione come facciata per fornire il modello di programmazione asincrona F# a tali 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() =
        ...

Eccezioni

Vedere Gestione errori per informazioni sull'uso appropriato di eccezioni, risultati e opzioni.

Membri dell'estensione

Applicare con attenzione i membri dell'estensione F# nei componenti F#-to-F#

I membri dell'estensione F# devono in genere essere usati solo per le operazioni che si trovano nella chiusura di operazioni intrinseche associate a un tipo nella maggior parte delle modalità di utilizzo. Un uso comune consiste nel fornire API che sono più idiotiche per F# per vari tipi .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

Tipi di unione

Usare unioni discriminate anziché gerarchie di classi per i dati strutturati ad albero

Le strutture simili ad albero vengono definite in modo ricorsivo. Questo è imbarazzante con ereditarietà, ma elegante con unioni discriminate.

type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T>

La rappresentazione di dati simili ad albero con unioni discriminate consente anche di trarre vantaggio dall'esaustività dei criteri di ricerca.

Uso [<RequireQualifiedAccess>] su tipi di unione i cui nomi di maiuscole e minuscole non sono sufficientemente univoci

Ci si può trovare in un dominio in cui lo stesso nome è il nome migliore per cose diverse, ad esempio casi di unione discriminata. È possibile usare [<RequireQualifiedAccess>] per evitare ambiguità tra nomi di maiuscole e minuscole per evitare di generare errori di confusione a causa dell'ombreggiatura dipendente dall'ordinamento delle open istruzioni

Nascondere le rappresentazioni delle unioni discriminate per le API compatibili con binarie se la progettazione di questi tipi è probabile che si evolva

I tipi di unioni si basano su moduli di criteri di ricerca F# per un modello di programmazione conciso. Come accennato in precedenza, è consigliabile evitare di rivelare rappresentazioni di dati concrete se è probabile che la progettazione di questi tipi si evolva.

Ad esempio, la rappresentazione di un'unione discriminata può essere nascosta usando una dichiarazione privata o interna o usando un file di firma.

type Union =
    private
    | CaseA of int
    | CaseB of string

Se si rivelano unioni discriminate in modo indiscriminato, potrebbe risultare difficile eseguire la versione della libreria senza interrompere il codice utente. È invece consigliabile rivelare uno o più modelli attivi per consentire la corrispondenza dei criteri rispetto ai valori del tipo.

I modelli attivi offrono un modo alternativo per fornire ai consumer F# criteri di ricerca, evitando di esporre direttamente i tipi di unione F#.

Funzioni inline e vincoli membro

Definire algoritmi numerici generici usando funzioni inline con vincoli membro impliciti e tipi generici risolti staticamente

I vincoli dei membri aritmetici e i vincoli di confronto F# sono uno standard per la programmazione F#. Si consideri il codice di esempio seguente:

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

Il tipo di questa funzione è il seguente:

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

Si tratta di una funzione adatta per un'API pubblica in una libreria matematica.

Evitare di usare vincoli membro per simulare classi di tipi e tipi di anatra

È possibile simulare la "digitazione anatra" usando i vincoli dei membri F#. Tuttavia, i membri che fanno uso di questo non devono essere in generale usati nelle progettazioni della libreria F#-to-F#. Ciò è dovuto al fatto che le progettazioni di librerie basate su vincoli impliciti non familiari o non standard tendono a rendere il codice utente inflessibile e legato a un modello di framework specifico.

Inoltre, esiste una buona probabilità che l'uso elevato dei vincoli membro in questo modo possa comportare tempi di compilazione molto lunghi.

Definizioni di operatore

Evitare di definire operatori simbolici personalizzati

Gli operatori personalizzati sono essenziali in alcune situazioni e sono dispositivi notazione estremamente utili all'interno di un ampio corpo di codice di implementazione. Per i nuovi utenti di una libreria, le funzioni denominate sono spesso più facili da usare. Inoltre, gli operatori simbolici personalizzati possono essere difficili da documentare e gli utenti trovano più difficile cercare aiuto sugli operatori, a causa di limitazioni esistenti nell'IDE e nei motori di ricerca.

Di conseguenza, è consigliabile pubblicare le funzionalità come funzioni e membri denominati ed esporre anche gli operatori per questa funzionalità solo se i vantaggi della notazione superano la documentazione e il costo cognitivo della loro presenza.

Unità di misura

Usare attentamente le unità di misura per l'aggiunta della sicurezza dei tipi nel codice F#

Le informazioni aggiuntive sulla digitazione per le unità di misura vengono cancellate quando vengono visualizzate da altri linguaggi .NET. Tenere presente che i componenti, gli strumenti e la reflection .NET vedranno types-sans-units. Ad esempio, i consumer C# vedranno float anziché float<kg>.

Abbreviazioni dei tipi

Usare attentamente le abbreviazioni dei tipi per semplificare il codice F#

I componenti, gli strumenti e la reflection .NET non vedranno nomi abbreviati per i tipi. Un uso significativo delle abbreviazioni dei tipi può anche rendere un dominio più complesso di quanto effettivamente sia, che potrebbe confondere i consumer.

Evitare abbreviazioni di tipo per i tipi pubblici i cui membri e proprietà devono essere intrinsecamente diversi da quelli disponibili nel tipo abbreviato

In questo caso, il tipo abbreviato rivela troppe informazioni sulla rappresentazione del tipo effettivo definito. Prendere invece in considerazione la possibilità di eseguire il wrapping dell'abbreviazione in un tipo di classe o di un'unione discriminata a maiuscole e minuscole oppure, quando le prestazioni sono essenziali, è consigliabile usare un tipo di struct per eseguire il wrapping dell'abbreviazione.

Ad esempio, è possibile definire una mappa multipla come caso speciale di una mappa F#, ad esempio:

type MultiMap<'Key,'Value> = Map<'Key,'Value list>

Tuttavia, le operazioni logiche con notazione punto su questo tipo non sono uguali alle operazioni su una mappa, ad esempio è ragionevole che l'operatore map[key] di ricerca restituisca l'elenco vuoto se la chiave non è presente nel dizionario, anziché generare un'eccezione.

Linee guida per le librerie per l'uso da altri linguaggi .NET

Quando si progettano librerie per l'uso da altri linguaggi .NET, è importante rispettare le linee guida per la progettazione di librerie .NET. In questo documento queste librerie vengono etichettate come librerie .NET di vaniglia, anziché librerie F#che usano costrutti F# senza restrizioni. La progettazione di librerie .NET di vaniglia significa fornire API familiari e idiotiche coerenti con il resto di .NET Framework riducendo al minimo l'uso di costrutti specifici di F#nell'API pubblica. Le regole sono illustrate nelle sezioni seguenti.

Progettazione dello spazio dei nomi e dei tipi (per le librerie da usare da altri linguaggi .NET)

Applicare le convenzioni di denominazione .NET all'API pubblica dei componenti

Prestare particolare attenzione all'uso dei nomi abbreviati e delle linee guida per le maiuscole .NET.

type pCoord = ...
    member this.theta = ...

type PolarCoordinate = ...
    member this.Theta = ...

Usare spazi dei nomi, tipi e membri come struttura organizzativa principale per i componenti

Tutti i file contenenti funzionalità pubbliche devono iniziare con una namespace dichiarazione e le uniche entità pubbliche negli spazi dei nomi devono essere tipi. Non usare i moduli F#.

Usare moduli non pubblici per contenere codice di implementazione, tipi di utilità e funzioni di utilità.

I tipi statici devono essere preferiti rispetto ai moduli, in quanto consentono un'evoluzione futura dell'API per usare l'overload e altri concetti di progettazione api .NET che potrebbero non essere usati all'interno dei moduli F#.

Ad esempio, al posto dell'API pubblica seguente:

module Fabrikam

module Utilities =
    let Name = "Bob"
    let Add2 x y = x + y
    let Add3 x y z = x + y + z

Prendere invece in considerazione:

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

Usare i tipi di record F# nelle API .NET vanilla se la progettazione dei tipi non si evolverà

I tipi di record F# vengono compilati in una semplice classe .NET. Questi sono adatti per alcuni tipi semplici e stabili nelle API. È consigliabile usare gli [<NoEquality>] attributi e [<NoComparison>] per eliminare la generazione automatica delle interfacce. Evitare inoltre di usare campi di record modificabili nelle API .NET di vaniglia, perché espongono un campo pubblico. Valutare sempre se una classe offrirà un'opzione più flessibile per l'evoluzione futura dell'API.

Ad esempio, il codice F# seguente espone l'API pubblica a un consumer 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; }
}

Nascondere la rappresentazione dei tipi di unione F# nelle API .NET di vaniglia

I tipi di unione F# non vengono comunemente usati attraverso i limiti dei componenti, anche per la codifica F#-to-F#. Sono un dispositivo di implementazione eccellente quando usato internamente all'interno di componenti e librerie.

Quando si progetta un'API .NET di vaniglia, è consigliabile nascondere la rappresentazione di un tipo di unione usando una dichiarazione privata o un file di firma.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

È anche possibile aumentare i tipi che usano una rappresentazione di unione internamente con i membri per fornire un oggetto desiderato. API con connessione 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)

Progettare l'interfaccia utente grafica e altri componenti usando i modelli di progettazione del framework

In .NET sono disponibili molti framework diversi, ad esempio WinForms, WPF e ASP.NET. Se si progettano componenti da usare in questi framework, è consigliabile usare le convenzioni di denominazione e progettazione per ognuno di essi. Ad esempio, per la programmazione WPF, adottare modelli di progettazione WPF per le classi che si stanno progettando. Per i modelli nella programmazione dell'interfaccia utente, usare modelli di progettazione come eventi e raccolte basate su notifica, ad esempio quelle disponibili in System.Collections.ObjectModel.

Progettazione di oggetti e membri (per librerie da usare da altri linguaggi .NET)

Usare l'attributo CLIEvent per esporre gli eventi .NET

Costruire un DelegateEvent oggetto con un tipo di delegato .NET specifico che accetta un oggetto e EventArgs (anziché un Event, che usa semplicemente il FSharpHandler tipo per impostazione predefinita) in modo che gli eventi vengano pubblicati nel modo familiare in altri linguaggi .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

Esporre le operazioni asincrone come metodi che restituiscono attività .NET

Le attività vengono usate in .NET per rappresentare calcoli asincroni attivi. Le attività sono in genere meno compositionali rispetto agli oggetti F# Async<T> , poiché rappresentano attività "già in esecuzione" e non possono essere composte insieme in modi che eseguono la composizione parallela o che nascondono la propagazione dei segnali di annullamento e altri parametri contestuali.

Tuttavia, nonostante questo, i metodi che restituiscono Attività sono la rappresentazione standard della programmazione asincrona in .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

Spesso si vuole anche accettare un token di annullamento esplicito:

/// 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)

Usare tipi delegati .NET anziché tipi di funzione F#

Qui "Tipi di funzione F#" significano tipi "freccia" come int -> int.

Invece di questo:

member this.Transform(f: int->int) =
    ...

Eseguire questa operazione:

member this.Transform(f: Func<int,int>) =
    ...

Il tipo di funzione F# viene visualizzato come class FSharpFunc<T,U> in altri linguaggi .NET ed è meno adatto per le funzionalità del linguaggio e gli strumenti che comprendono i tipi delegati. Quando si crea un metodo di ordine superiore destinato a .NET Framework 3.5 o versione successiva, i System.Func delegati e System.Action sono le API appropriate per la pubblicazione per consentire agli sviluppatori .NET di usare queste API in modo a basso attrito. Quando la destinazione è .NET Framework 2.0, i tipi delegati definiti dal sistema sono più limitati. Prendere in considerazione l'uso di tipi delegati predefiniti, ad System.Converter<T,U> esempio o la definizione di un tipo delegato specifico.

Sul lato capovolto, i delegati .NET non sono naturali per le librerie F#-facing (vedere la sezione successiva sulle librerie F#-facing). Di conseguenza, una strategia di implementazione comune quando si sviluppano metodi di ordine superiore per le librerie .NET di vanilla consiste nell'creare tutte le implementazioni usando i tipi di funzione F# e quindi creare l'API pubblica usando delegati come facciata sottile in cima all'implementazione effettiva di F#.

Usare il modello TryGetValue invece di restituire i valori delle opzioni F# e preferire l'overload del metodo per accettare i valori delle opzioni F# come argomenti

I modelli comuni d'uso per il tipo di opzione F# nelle API sono implementati meglio nelle API .NET di vaniglia usando tecniche di progettazione .NET standard. Anziché restituire un valore di opzione F#, provare a usare il tipo restituito bool più un parametro out come nel modello "TryGetValue". Invece di accettare i valori delle opzioni F# come parametri, è consigliabile usare l'overload del metodo o gli argomenti facoltativi.

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

Usare i tipi di interfaccia di raccolta .NET IEnumerable<T> e IDictionary<Key, Value> per i parametri e i valori restituiti

Evitare l'uso di tipi di raccolta concreti, ad esempio matrici T[].NET, tipi list<T>Map<Key,Value> F# e Set<T>e tipi di raccolta concreta .NET, ad esempio Dictionary<Key,Value>. Le linee guida per la progettazione della libreria .NET sono utili consigli su quando usare vari tipi di raccolta, ad esempio IEnumerable<T>. Alcuni usi di matrici (T[]) sono accettabili in alcune circostanze, in base alle prestazioni. Si noti in particolare che seq<T> è solo l'alias F# per IEnumerable<T>e quindi seq è spesso un tipo appropriato per un'API .NET di vaniglia.

Anziché elenchi F#:

member this.PrintNames(names: string list) =
    ...

Usare le sequenze F#:

member this.PrintNames(names: seq<string>) =
    ...

Usare il tipo di unità come unico tipo di input di un metodo per definire un metodo zero-argument o come unico tipo restituito per definire un metodo che restituisce void

Evitare altri usi del tipo di unità. Questi sono buoni:

✔ member this.NoArguments() = 3

✔ member this.ReturnVoid(x: int) = ()

Si tratta di un problema:

member this.WrongUnit( x: unit, z: int) = ((), ())

Verificare la presenza di valori Null nei limiti dell'API .NET vanilla

Il codice di implementazione F# tende ad avere meno valori Null, a causa di modelli di progettazione non modificabili e restrizioni sull'uso di valori letterali Null per i tipi F#. Altri linguaggi .NET spesso usano null come valore molto più frequentemente. Per questo motivo, il codice F# che espone un'API .NET vanilla deve controllare i parametri per null al limite dell'API e impedire che questi valori vengano trasmessi più in profondità nel codice di implementazione F#. È possibile usare la isNull funzione o la corrispondenza dei criteri nel null modello.

let checkNonNull argName (arg: obj) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull` argName (arg: obj) =
    if isNull arg then nullArg argName
    else ()

Evitare di usare le tuple come valori restituiti

Preferire invece la restituzione di un tipo denominato contenente i dati aggregati o l'uso di parametri out per restituire più valori. Sebbene le tuple e le tuple degli struct esistano in .NET (incluso il supporto del linguaggio C# per le tuple struct), spesso non forniscono l'API ideale e prevista per gli sviluppatori .NET.

Evitare l'uso di currying di parametri

Usare invece le convenzioni di Method(arg1,arg2,…,argN)chiamata .NET.

member this.TupledArguments(str, num) = String.replicate num str

Suggerimento: se si progettano librerie per l'uso da qualsiasi linguaggio .NET, non esiste un sostituto per eseguire effettivamente alcune operazioni sperimentali di programmazione C# e Visual Basic per assicurarsi che le librerie siano corrette da questi linguaggi. È anche possibile usare strumenti come .NET Reflector e Visualizzatore oggetti di Visual Studio per assicurarsi che le librerie e la relativa documentazione vengano visualizzate come previsto per gli sviluppatori.

Appendice

Esempio end-to-end di progettazione di codice F# per l'uso da parte di altri linguaggi .NET

Si consideri la classe seguente:

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) ]

Il tipo F# dedotto di questa classe è il seguente:

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

Di seguito viene illustrato come appare questo tipo F# a un programmatore usando un altro linguaggio .NET. Ad esempio, la "firma" approssimativa di C# è la seguente:

// 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; }
}

Esistono alcuni punti importanti da notare sul modo in cui F# rappresenta i costrutti qui. Ad esempio:

  • I metadati, ad esempio i nomi degli argomenti, sono stati mantenuti.

  • I metodi F# che accettano due argomenti diventano metodi C# che accettano due argomenti.

  • Funzioni ed elenchi diventano riferimenti ai tipi corrispondenti nella libreria F#.

Il codice seguente illustra come modificare questo codice in modo da tenere conto di questi elementi.

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) }

Il tipo F# dedotto del codice è il seguente:

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

La firma C# è ora la seguente:

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; }
}

Le correzioni apportate per preparare questo tipo per l'uso come parte di una libreria .NET di vaniglia sono le seguenti:

  • Sono stati modificati diversi nomi: Point1, nl, e f sono diventati RadialPointrispettivamente , , countfactore transform.

  • Utilizzato un tipo restituito anziché seq<RadialPoint> modificando una costruzione di RadialPoint list elenco utilizzando una costruzione di sequenza utilizzando [ ... ]IEnumerable<RadialPoint>.

  • Usato il tipo System.Func delegato .NET anziché un tipo di funzione F#.

In questo modo è molto più bello usare nel codice C#.