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:
- Un livello nel tuo progetto F# che ha consumatori 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 di codice F# validoe 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 ti guiderà verso la creazione di 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 di librerie .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.
Aggiungi commenti di documentazione XML al codice
La documentazione XML sulle API pubbliche garantisce che gli utenti possano ottenere eccellenti informazioni con IntelliSense e Quickinfo quando usano questi tipi e membri, e consente di 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 usare 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
Segui le migliori pratiche per l'uso di stringhe in .NET, come indicato nella guida , quando l'ambito del progetto lo richiede. In particolare, dichiarando esplicitamente finalità culturale nella conversione e nel confronto delle stringhe (ove applicabile).
Linee guida per le librerie destinate a F#
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 delle raccomandazioni specifiche che seguono, si faccia riferimento alle linee guida per la progettazione delle librerie .NET come riferimento di riserva.
Convenzioni di denominazione
Seguire le convenzioni di denominazione e di maiuscole di .NET
La tabella seguente segue le convenzioni di denominazione e maiuscole di .NET. Ci sono piccole modifiche per includere anche i 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.
Costruire | Caso | Parte | Esempi | Note |
---|---|---|---|---|
Tipi concreti | PascalCase | Sostantivo/aggettivo | Lista, Doppio, Complesso | I tipi concreti sono strutture, 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 (librerie di collegamento dinamico) | PascalCase | Fabrikam.Core.dll | ||
Tag delle unioni | PascalCase | Sostantivo | Alcuni, Aggiungi, Successo | Non usare un prefisso nelle API pubbliche. Opzionalmente, usare un prefisso quando si tratta di un'implementazione interna, ad esempio "type Teams = TAlpha | TBeta | TDelta". |
Evento | PascalCase | Verbo | ValoreCambiato/ValoreInCambiamento | |
Eccezioni | PascalCase | WebException | Il nome deve terminare con "Eccezione". | |
Campo | PascalCase | Sostantivo | NomeAttuale | |
Tipi di interfaccia | PascalCase | Sostantivo/aggettivo | IDisposable | Il nome deve iniziare con "I". |
Metodo | PascalCase | Verbo | ToString | |
Namespace | PascalCase | Microsoft.FSharp.Core | In genere, utilizzare <Organization>.<Technology>[.<Subnamespace>] , ma eliminare l'organizzazione se la tecnologia è indipendente dall'organizzazione. |
|
Parametri | camelCase | Sostantivo | nomeTipo, trasformazione, intervallo | |
valori let (interno) | camelCase o PascalCase | Sostantivo/verbo | getValue, myTable | |
valori let (esterni) | camelCase o PascalCase | Sostantivo/verbo | List.map, Dates.Today | I valori legati con let sono spesso pubblici quando si seguono i modelli tradizionali di progettazione funzionale. 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 .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 nei nomi dovuti all'uso delle maiuscole e minuscole.
Le linee guida di .NET dicono che la differenza tra maiuscole e minuscole non può essere usata per risolvere collisioni di nomi, poiché alcuni linguaggi client (ad esempio, Visual Basic) sono insensibili alla differenza 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, utilizzare nomi come T
, U
, T1
, T2
per parametri generici arbitrari e, quando ha senso usare nomi specifici, per le librerie rivolte a F# utilizzare nomi come Key
, Value
, Arg
(ma non, ad esempio, TKey
).
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 namespace 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 =
...
o
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 necessita necessariamente di essere implementata in quel membro, ma la parte utilizzabile di tale funzionalità dovrebbe esserlo.
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 le interfacce per raggruppare le operazioni correlate
Usare i tipi di interfaccia per rappresentare un set di operazioni. Questa soluzione è preferita rispetto ad altre opzioni, come le tuple di funzioni o i 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 tuo programma, cosa che i record di funzioni non possono fare.
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 esempio abs
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 modulo Microsoft.FSharp.Collections.List
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 hanno [<RequireQualifiedAccess>]
definiti su di essi; più in generale, è sconsigliato definire moduli personalizzati privi dell'attributo, quando tali ombreggiature di 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ò essere applicato anche a un assembly per indicare un modulo aperto automaticamente quando si fa riferimento all'assembly.
Ad esempio, una libreria di statistiche MathsHeaven.Statistics potrebbe contenere un module MathsHeaven.Statistics.Operators
contenente funzioni erf
e erfc
. È ragionevole contrassegnare questo modulo come [<AutoOpen>]
. Ciò significa che open MathsHeaven.Statistics
aprirà anche questo modulo e porterà i nomi erf
e erfc
nell'ambito. Un altro buon uso di [<AutoOpen>]
è per i moduli contenenti metodi di estensione.
L'uso eccessivo di [<AutoOpen>]
porta a namespace inquinati e l'attributo deve essere utilizzato con cura. 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.
Si consiglia di usare CompiledName per fornire un nome compatibile con .NET per i consumatori di altri 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
Usando [<CompiledName>]
è possibile usare le convenzioni di denominazione .NET per i consumatori 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.
Nascondi le rappresentazioni dei tipi di record e unioni se è probabile che la loro progettazione evolva.
Evitare di rivelare rappresentazioni concrete di oggetti. Ad esempio, la rappresentazione concreta dei valori di DateTime non viene rilevata dall'API pubblica esterna della progettazione della libreria .NET. Durante l'esecuzione, il Common Language Runtime conosce l'implementazione confermata che verrà utilizzata nel corso dell'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.
Signature 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 T
, 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
Fare attenzione nell'applicare 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 nel contesto di operazioni intrinseche associate a un tipo nella maggior parte dei modi in cui viene utilizzato. Un uso comune consiste nel fornire API che sono più idiomatiche in 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 l'ereditarietà, ma elegante con le 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.
Usare [<RequireQualifiedAccess>]
sui tipi di unione i cui nomi dei casi 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 disambiguare i nomi dei casi al fine di evitare di generare errori confusi a causa del mascheramento dipendente dall'ordine delle istruzioni open
.
Nascondere le rappresentazioni delle unioni discriminate per le API compatibili con binario, se è probabile che la progettazione di questi tipi si evolva.
I tipi di unioni sfruttano le forme di pattern matching 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 il pattern matching sui valori del tuo 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 sui membri
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 ad esempio il codice 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 sui membri per simulare classi di tipi e duck typing
È possibile simulare "duck typing" usando vincoli di membri F#. Tuttavia, i membri che utilizzano questa funzione non dovrebbero essere generalmente usati nei progetti di 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 di notazione estremamente utili all'interno di un vasto corpus 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 la tua funzionalità come funzioni e membri con nome ed esporre anche gli operatori per questa funzionalità solo se i vantaggi notazionali superano il costo della documentazione e il costo cognitivo della loro presenza.
Unità di misura
Utilizzare con attenzione le unità di misura per migliorare la 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 riflessione .NET vedranno i tipi senza unità. 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 .NET, gli strumenti e la riflessione non vedranno nomi abbreviati per i tipi. Un uso significativo delle abbreviazioni dei tipi può anche rendere un dominio più complesso di quanto non lo sia realmente, il che potrebbe confondere i consumatori.
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 racchiudere l'abbreviazione in un tipo di classe o in un'unione discriminata con un solo caso (oppure, quando le prestazioni sono essenziali, è consigliabile usare un tipo di struct per racchiudere l'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 di ricerca map[key]
restituisca la lista vuota se la chiave non è presente nel dizionario, anziché sollevare un'eccezione.
Linee guida per l'uso delle librerie da parte di 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 standard .NET, in contrapposizione alle librerie orientate a F# che usano liberamente i costrutti F# senza restrizioni. La progettazione di librerie .NET standard significa fornire API familiari e idiomatiche coerenti con il resto di .NET Framework, minimizzando l'uso di costrutti specifici di F# nell'API pubblica. Le regole sono illustrate nelle sezioni seguenti.
Progettazione di Namespace e tipi (per librerie utilizzabili 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 = ...
Usa namespace, tipi e membri come struttura organizzativa principale per i tuoi componenti
Tutti i file contenenti funzionalità pubbliche devono iniziare con una dichiarazione di namespace
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 attributi [<NoEquality>]
e [<NoComparison>]
per eliminare la generazione automatica delle interfacce. Evitare inoltre di usare campi di record modificabili nelle API .NET standard, 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 standard
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 arricchire i tipi che usano internamente una rappresentazione di unione con membri per fornire un'API .NET desiderata.
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 presenti 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
Costruisci un DelegateEvent
con un tipo delegato .NET specifico che accetta un oggetto e EventArgs
(anziché un Event
, che usa solo il tipo FSharpHandler
per impostazione predefinita) in modo che gli eventi vengano pubblicati in 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 i tipi di funzione F# indicano tipi "freccia", ad esempio 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# appare come class FSharpFunc<T,U>
agli altri linguaggi .NET ed è meno adatto alle funzionalità del linguaggio e agli strumenti che supportano i tipi delegati. Quando si crea un metodo di ordine superiore destinato a .NET Framework 3.5 o versione successiva, i delegati System.Func
e System.Action
sono le API corrette da pubblicare 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 esempio System.Converter<T,U>
o la definizione di un tipo delegato specifico.
D'altro canto, i delegati .NET non sono naturali per le librerie rivolte a F# (vedere la sezione successiva sulle librerie rivolte a F#). 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 di opzione F# e preferire il sovraccarico dei metodi anziché accettare valori di opzione F# come argomenti.
I modelli comuni d'uso per l'option type F# nelle API sono meglio implementati nelle API standard di .NET usando tecniche di progettazione .NET standard. Anziché restituire un valore di opzione F#, si consideri di usare il tipo di ritorno bool più un parametro out come nel modello "TryGetValue". Invece di accettare i valori delle opzioni F# come parametri, è consigliabile usare il sovraccarico 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 della 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 .NET T[]
, tipi F# list<T>
, Map<Key,Value>
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>
è soltanto l'alias F# di IEnumerable<T>
e pertanto seq è spesso un tipo appropriato per un'API .NET standard.
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 senza argomenti o come unico tipo restituito per definire un metodo che non restituisce nulla.
Evitare altri usi del tipo di unità. Questi sono buoni:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
Questo è brutto:
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 utilizzare la funzione isNull
o la corrispondenza del modello su null
.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
A partire da F# 9, è possibile sfruttare la nuova sintassi | null
per indicare i possibili valori null e dove debbano essere gestiti.
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 ()
In F# 9 il compilatore genera un avviso quando rileva che un possibile valore Null non viene gestito:
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
Questi avvisi devono essere risolti usando F# modello Null nella corrispondenza:
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
Evitate di usare tuple come valori di ritorno
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 struct tuple esistano in .NET (incluso il supporto del linguaggio C# per le struct tuple), spesso non forniscono l'API ideale e prevista per gli sviluppatori .NET.
Evitare l'uso del "currying" dei parametri
Usare invece convenzioni di chiamata .NET Method(arg1,arg2,…,argN)
.
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. Per 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 standard sono le seguenti:
Sono stati modificati diversi nomi:
Point1
,n
,l
ef
sono diventati rispettivamenteRadialPoint
,count
,factor
etransform
.Utilizzato un tipo restituito di
seq<RadialPoint>
anzichéRadialPoint list
modificando una costruzione di elenco da[ ... ]
a una costruzione di sequenza usandoIEnumerable<RadialPoint>
.Usato il tipo delegato .NET
System.Func
anziché un tipo di funzione F#.
In questo modo è molto più piacevole utilizzare il codice C#.