Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Il meccanismo del provider di tipi è una parte significativa del supporto in F# per la programmazione ad informazioni ricche. Questa esercitazione illustra come creare provider di tipi personalizzati illustrando lo sviluppo di diversi provider di tipi semplici per illustrare i concetti di base. Per ulteriori informazioni sul meccanismo dei Type Providers in F#, vedere Type Providers.
L'ecosistema F# contiene una gamma di provider di tipi per i servizi di dati Internet e aziendali comunemente usati. Per esempio:
FSharp.Data include provider di tipi per formati di documento JSON, XML, CSV e HTML.
SwaggerProvider include due provider di tipi generativi che generano modelli a oggetti e client HTTP per le API descritte dagli schemi OpenApi 3.0 e Swagger 2.0.
FSharp.Data.SqlClient include un set di provider di tipi per l'incorporamento controllato in fase di compilazione di T-SQL in F#.
È possibile creare provider di tipi personalizzati oppure fare riferimento a provider di tipi creati da altri utenti. Ad esempio, l'organizzazione potrebbe avere un servizio dati che fornisce un numero elevato e crescente di set di dati denominati, ognuno con il proprio schema di dati stabile. È possibile creare un provider di tipi che legge gli schemi e presenta i set di dati correnti al programmatore in modo fortemente tipizzato.
Prima di iniziare
Il meccanismo del provider di tipi è progettato principalmente per l'inserimento di spazi stabili di dati e informazioni sui servizi nell'esperienza di programmazione in F#.
Questo meccanismo non è progettato per l'inserimento di spazi informativi le cui modifiche allo schema durante l'esecuzione del programma sono rilevanti per la logica del programma. Inoltre, il meccanismo non è progettato per la meta-programmazione all'interno del linguaggio, anche se tale dominio contiene alcuni usi validi. È consigliabile usare questo meccanismo solo se necessario e quando lo sviluppo di un provider di tipi genera un valore molto elevato.
È consigliabile evitare di scrivere un provider di tipi in cui uno schema non è disponibile. Analogamente, è consigliabile evitare di scrivere un provider di tipi in cui sarebbe sufficiente una libreria .NET normale (o anche esistente).
Prima di iniziare, è possibile porre le domande seguenti:
Hai uno schema per la tua fonte di informazioni? In tal caso, qual è la mappatura nel sistema di tipi F# e .NET?
È possibile usare un'API esistente (tipizzata in modo dinamico) come punto di partenza per l'implementazione?
L'utente e l'organizzazione avranno un numero sufficiente di usi del provider di tipi per renderlo utile per la scrittura? Una normale libreria .NET soddisfa le proprie esigenze?
Quanto cambierà lo schema?
Cambierà durante la codifica?
Cambierà tra le sessioni di codifica?
Cambierà durante l'esecuzione del programma?
I provider di tipi sono più adatti alle situazioni in cui lo schema è stabile in fase di esecuzione e durante la durata del codice compilato.
Provider di tipo semplice
Questo esempio è Samples.HelloWorldTypeProvider, simile agli esempi nella examples directory di F# Type Provider SDK. Il provider rende disponibile uno "spazio dei tipi" che contiene 100 tipi cancellati, come illustrato nel codice seguente usando la sintassi della firma F# e omettendo i dettagli per tutti tranne Type1. Per altre informazioni sui tipi cancellati, vedere Dettagli sui tipi specificati cancellati più avanti in questo argomento.
namespace Samples.HelloWorldTypeProvider
type Type1 =
/// This is a static property.
static member StaticProperty : string
/// This constructor takes no arguments.
new : unit -> Type1
/// This constructor takes one argument.
new : data:string -> Type1
/// This is an instance property.
member InstanceProperty : int
/// This is an instance method.
member InstanceMethod : x:int -> char
nested type NestedType =
/// This is StaticProperty1 on NestedType.
static member StaticProperty1 : string
…
/// This is StaticProperty100 on NestedType.
static member StaticProperty100 : string
type Type2 =
…
…
type Type100 =
…
Si noti che il set di tipi e membri forniti è noto in modo statico. Questo esempio non sfrutta la capacità dei provider di fornire tipi che dipendono da uno schema. L'implementazione del provider di tipi è descritta nel codice seguente e i dettagli sono trattati nelle sezioni successive di questo argomento.
Avvertimento
Potrebbero esserci differenze tra questo codice e gli esempi online.
namespace Samples.FSharp.HelloWorldTypeProvider
open System
open System.Reflection
open ProviderImplementation.ProvidedTypes
open FSharp.Core.CompilerServices
open FSharp.Quotations
// This type defines the type provider. When compiled to a DLL, it can be added
// as a reference to an F# command-line compilation, script, or project.
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
// Inheriting from this type provides implementations of ITypeProvider
// in terms of the provided types below.
inherit TypeProviderForNamespaces(config)
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
// Make one provided type, called TypeN.
let makeOneProvidedType (n:int) =
…
// Now generate 100 types
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
// And add them to the namespace
do this.AddNamespace(namespaceName, types)
[<assembly:TypeProviderAssembly>]
do()
Per usare questo provider, aprire un'istanza separata di Visual Studio, creare uno script F# e quindi aggiungere un riferimento al provider dallo script usando #r come illustrato nel codice seguente:
#r @".\bin\Debug\Samples.HelloWorldTypeProvider.dll"
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
let obj2 = Samples.HelloWorldTypeProvider.Type1("some other data")
obj1.InstanceProperty
obj2.InstanceProperty
[ for index in 0 .. obj1.InstanceProperty-1 -> obj1.InstanceMethod(index) ]
[ for index in 0 .. obj2.InstanceProperty-1 -> obj2.InstanceMethod(index) ]
let data1 = Samples.HelloWorldTypeProvider.Type1.NestedType.StaticProperty35
Quindi, cercate i tipi nello spazio dei nomi Samples.HelloWorldTypeProvider generato dal fornitore di tipi.
Prima di ricompilare il provider, assicurarsi di aver chiuso tutte le istanze di Visual Studio e F# Interactive che usano la DLL del provider. In caso contrario, si verificherà un errore di compilazione perché la DLL di output verrà bloccata.
Per eseguire il debug di questo provider usando istruzioni print, creare uno script che espone un problema con il provider e quindi usare il codice seguente:
fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Per eseguire il debug di questo provider usando Visual Studio, aprire il prompt dei comandi per gli sviluppatori per Visual Studio con credenziali amministrative ed eseguire il comando seguente:
devenv.exe /debugexe fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
In alternativa, aprire Visual Studio, aprire il menu Debug, scegliere Debug/Attach to process…e collegarsi a un altro devenv processo in cui si sta modificando lo script. Usando questo metodo, è possibile specificare più facilmente la logica specifica nel provider di tipi digitando in modo interattivo le espressioni nella seconda istanza (con IntelliSense completo e altre funzionalità).
È possibile disabilitare il debug Just My Code per identificare meglio gli errori nel codice generato. Per informazioni su come abilitare o disabilitare questa funzionalità, vedere Esplorazione del codice con il debugger. Inoltre, è possibile impostare la gestione delle eccezioni first-chance aprendo il menu Debug e quindi selezionando Exceptions o premendo i tasti CTRL+ALT+E per aprire la finestra di dialogo Exceptions. In tale finestra di dialogo, in Common Language Runtime Exceptions, selezionare la Thrown casella di controllo.
Implementazione del provider di tipi
Questa sezione illustra le sezioni principali dell'implementazione del provider di tipi. Prima di tutto, si definisce il tipo per il provider di tipi personalizzato:
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
Questo tipo deve essere pubblico ed è necessario contrassegnarlo con l'attributo TypeProvider in modo che il compilatore riconosca il provider di tipi quando un progetto F# separato fa riferimento all'assembly che contiene il tipo. Il parametro config è facoltativo e, se presente, contiene informazioni di configurazione contestuali per l'istanza del provider di tipi creata dal compilatore F#.
Successivamente, si implementa l'interfaccia ITypeProvider . In questo caso, si usa il TypeProviderForNamespaces tipo dell'API ProvidedTypes come tipo di base. Questo tipo di helper può fornire una raccolta finita di spazi dei nomi forniti immediatamente, ognuno dei quali contiene direttamente un numero finito di tipi fissi e forniti immediatamente. In questo contesto, il provider genera con zelo i tipi anche se non sono necessari o usati.
inherit TypeProviderForNamespaces(config)
Successivamente, definire i valori privati locali che specificano lo spazio dei nomi per i tipi forniti e individuare l'assembly del fornitore di tipi. Questo assembly viene utilizzato successivamente come tipo di base logico dei tipi eliminati forniti.
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
Creare quindi una funzione per fornire ognuno dei tipi Type1... Tipo100. Questa funzione viene illustrata in modo più dettagliato più avanti in questo argomento.
let makeOneProvidedType (n:int) = …
Generare quindi i 100 tipi forniti:
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
Successivamente, aggiungi i tipi come namespace fornito:
do this.AddNamespace(namespaceName, types)
Aggiungere infine un attributo assembly che indica che si sta creando una DLL del provider di tipi:
[<assembly:TypeProviderAssembly>]
do()
Fornitura di un tipo e dei suoi membri
La makeOneProvidedType funzione esegue il lavoro reale di fornire uno dei tipi .
let makeOneProvidedType (n:int) =
…
Questo passaggio illustra l'implementazione di questa funzione. Prima di tutto, creare il tipo specificato (ad esempio, Type1, quando n = 1 o Type57, quando n = 57).
// This is the provided type. It is an erased provided type and, in compiled code,
// will appear as type 'obj'.
let t = ProvidedTypeDefinition(thisAssembly, namespaceName,
"Type" + string n,
baseType = Some typeof<obj>)
Si notino i punti seguenti:
Questo tipo specificato viene cancellato. Poiché si indica che il tipo di base è
obj, le istanze verranno visualizzate come valori di tipo obj nel codice compilato.Quando si specifica un tipo non annidato, è necessario specificare l'assembly e lo spazio dei nomi. Per i tipi cancellati, l'assembly deve essere l'assembly del provider di tipi stesso.
Aggiungere quindi la documentazione XML al tipo . Questa documentazione è ritardata, ovvero calcolata su richiesta se il compilatore host lo richiede.
t.AddXmlDocDelayed (fun () -> $"""This provided type {"Type" + string n}""")
Aggiungere quindi una proprietà statica fornita al tipo:
let staticProp = ProvidedProperty(propertyName = "StaticProperty",
propertyType = typeof<string>,
isStatic = true,
getterCode = (fun args -> <@@ "Hello!" @@>))
Ottenendo questa proprietà otterrà sempre la stringa "Hello!". Per GetterCode di una proprietà viene utilizzata una citazione F#, che rappresenta il codice generato dal compilatore host per ottenere la proprietà. Per ulteriori informazioni sulle citazioni, vedere Citazioni di codice (F#).
Aggiungere la documentazione XML alla proprietà .
staticProp.AddXmlDocDelayed(fun () -> "This is a static property")
Associare ora la proprietà fornita al tipo specificato. È necessario associare un membro fornito a uno e a un solo tipo. In caso contrario, il membro non sarà mai accessibile.
t.AddMember staticProp
Creare ora un costruttore fornito che non accetta parametri.
let ctor = ProvidedConstructor(parameters = [ ],
invokeCode = (fun args -> <@@ "The object data" :> obj @@>))
Per il costruttore, InvokeCode restituisce un'espressione F#, che rappresenta il codice generato dal compilatore host quando il costruttore viene chiamato. Ad esempio, è possibile usare il costruttore seguente:
new Type10()
Verrà creata un'istanza del tipo fornito con i dati sottostanti "I dati oggetto". Il codice tra virgolette include una conversione in obj perché tale tipo è la cancellazione di questo tipo specificato (come specificato quando è stato dichiarato il tipo fornito).
Aggiungere la documentazione XML al costruttore e aggiungere il costruttore fornito al tipo specificato:
ctor.AddXmlDocDelayed(fun () -> "This is a constructor")
t.AddMember ctor
Creare un secondo costruttore fornito che accetta un parametro:
let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
invokeCode = (fun args -> <@@ (%%(args[0]) : string) :> obj @@>))
Il InvokeCode per il costruttore restituisce di nuovo un'espressione F#, che rappresenta il codice generato dal compilatore host per una chiamata al metodo. Ad esempio, è possibile usare il costruttore seguente:
new Type10("ten")
Viene creata un'istanza del tipo fornito con i dati sottostanti "ten". È possibile che si sia già notato che la InvokeCode funzione restituisce un'offerta. L'input di questa funzione è un elenco di espressioni, una per parametro del costruttore. In questo caso, un'espressione che rappresenta il valore del singolo parametro è disponibile in args[0]. Il codice per una chiamata al costruttore forza il valore restituito ad essere del tipo obj cancellato. Dopo aver aggiunto il secondo costruttore fornito al tipo, si crea una proprietà di istanza specificata:
let instanceProp =
ProvidedProperty(propertyName = "InstanceProperty",
propertyType = typeof<int>,
getterCode= (fun args ->
<@@ ((%%(args[0]) : obj) :?> string).Length @@>))
instanceProp.AddXmlDocDelayed(fun () -> "This is an instance property")
t.AddMember instanceProp
Ottenere questa proprietà restituirà la lunghezza della stringa, ovvero l'oggetto rappresentazione. La GetterCode proprietà restituisce un'offerta F# che specifica il codice generato dal compilatore host per ottenere la proprietà . Come InvokeCode, la GetterCode funzione restituisce un'offerta. Il compilatore host chiama questa funzione con un elenco di argomenti. In questo caso, gli argomenti includono solo l'unica espressione che rappresenta l'istanza su cui viene chiamato il getter, a cui è possibile accedere usando args[0]. L'implementazione di GetterCode viene quindi inserita nella quotazione risultante al tipo obj cancellato, e viene utilizzato un cast per soddisfare il meccanismo del compilatore che controlla che l'oggetto sia una stringa. La parte successiva di makeOneProvidedType fornisce un metodo di istanza con un parametro.
let instanceMeth =
ProvidedMethod(methodName = "InstanceMethod",
parameters = [ProvidedParameter("x",typeof<int>)],
returnType = typeof<char>,
invokeCode = (fun args ->
<@@ ((%%(args[0]) : obj) :?> string).Chars(%%(args[1]) : int) @@>))
instanceMeth.AddXmlDocDelayed(fun () -> "This is an instance method")
// Add the instance method to the type.
t.AddMember instanceMeth
Creare infine un tipo annidato contenente 100 proprietà annidate. La creazione di questo tipo annidato e le relative proprietà vengono ritardate, ovvero calcolate su richiesta.
t.AddMembersDelayed(fun () ->
let nestedType = ProvidedTypeDefinition("NestedType", Some typeof<obj>)
nestedType.AddMembersDelayed (fun () ->
let staticPropsInNestedType =
[
for i in 1 .. 100 ->
let valueOfTheProperty = "I am string " + string i
let p =
ProvidedProperty(propertyName = "StaticProperty" + string i,
propertyType = typeof<string>,
isStatic = true,
getterCode= (fun args -> <@@ valueOfTheProperty @@>))
p.AddXmlDocDelayed(fun () ->
$"This is StaticProperty{i} on NestedType")
p
]
staticPropsInNestedType)
[nestedType])
Dettagli sui tipi forniti cancellati
L'esempio in questa sezione fornisce solo tipi specificati cancellati, particolarmente utili nelle situazioni seguenti:
Quando si scrive un provider per uno spazio informazioni che contiene solo dati e metodi.
Quando si scrive un provider in cui la semantica di tipo runtime accurata non è fondamentale per l'uso pratico dello spazio informativo.
Quando si scrive un provider per uno spazio di informazioni così grande e interconnesso che non è tecnicamente fattibile generare tipi .NET reali per lo spazio delle informazioni.
In questo esempio, ogni tipo fornito viene cancellato per il tipo obje tutti gli usi del tipo verranno visualizzati come tipo obj nel codice compilato. In effetti, gli oggetti sottostanti in questi esempi sono stringhe, ma il tipo verrà visualizzato come System.Object nel codice compilato .NET. Come per tutti gli usi di eliminazione dei tipi, si può utilizzare boxing esplicito, unboxing e casting per i tipi eliminati. In questo caso, un'eccezione di cast che non è valida può risultare quando l'oggetto viene usato. Un runtime del provider può definire il proprio tipo di rappresentazione privata per proteggere da false rappresentazioni. Non è possibile definire tipi cancellati in F#. È possibile cancellare solo i tipi specificati. È necessario comprendere le ramificazioni, sia pratiche che semantiche, dell'uso di tipi cancellati per il provider di tipi o di un provider che fornisce tipi cancellati. Un tipo cancellato dal sistema non ha un vero tipo .NET. Pertanto, non è possibile eseguire una reflection accurata sul tipo e potresti compromettere i tipi cancellati se si usano cast di runtime e altre tecniche che si basano sulla semantica esatta dei tipi di runtime. La subversione dei tipi cancellati comporta spesso eccezioni di cast dei tipi in fase di esecuzione.
Scelta delle rappresentazioni per i tipi specificati cancellati
Per alcuni usi di tipi specificati cancellati, non è necessaria alcuna rappresentazione. Ad esempio, il tipo cancellato specificato potrebbe contenere solo proprietà e membri statici, nessun costruttore, e nessun metodo o proprietà restituirebbe un'istanza del tipo. Se è possibile raggiungere istanze di un tipo fornito cancellato, è necessario considerare le domande seguenti:
Qual è la cancellazione di un tipo specificato?
La cancellazione di un tipo specificato è la modalità di visualizzazione del tipo nel codice .NET compilato.
La cancellazione di un tipo di classe già cancellato è sempre il primo tipo di base non cancellato nella gerarchia di ereditarietà del tipo.
La cancellazione di un tipo di interfaccia cancellato fornito è sempre
System.Object.
Quali sono le rappresentazioni di un tipo specificato?
- L'insieme di oggetti possibili per un tipo fornito cancellato è denominato le sue rappresentazioni. Nell'esempio di questo documento, le rappresentazioni di tutti i tipi
Type1..Type100specificati cancellati sono sempre oggetti stringa.
Tutte le rappresentazioni di un tipo specificato devono essere compatibili con la cancellazione del tipo specificato. In caso contrario, il compilatore F# restituirà un errore per l'uso del provider di tipi o il codice .NET non verificabile che non è valido verrà generato. Un provider di tipi non è valido se restituisce codice che fornisce una rappresentazione non valida.
È possibile scegliere una rappresentazione per gli oggetti forniti usando uno degli approcci seguenti, entrambi molto comuni:
Se si sta semplicemente fornendo un wrapper fortemente tipizzato su un tipo .NET esistente, spesso ha senso effettuare l'erasure su quel tipo, utilizzare istanze di quel tipo come rappresentazioni, o entrambi. Questo approccio è appropriato quando la maggior parte dei metodi esistenti su quel tipo ha ancora senso quando si usa la versione fortemente tipizzata.
Se si vuole creare un'API che differisce in modo significativo da qualsiasi API .NET esistente, è opportuno creare tipi di runtime che saranno la cancellazione dei tipi e le rappresentazioni per i tipi forniti.
Nell'esempio di questo documento vengono utilizzate stringhe come rappresentazioni di oggetti forniti. Spesso può essere opportuno usare altri oggetti per le rappresentazioni. Ad esempio, è possibile usare un dizionario come sacchetto di proprietà.
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))
In alternativa, è possibile definire un tipo nel provider di tipi che verrà usato in fase di esecuzione per formare la rappresentazione, insieme a una o più operazioni di runtime:
type DataObject() =
let data = Dictionary<string,obj>()
member x.RuntimeOperation() = data.Count
I membri forniti possono quindi costruire istanze di questo tipo di oggetto:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))
In questo caso, puoi (facoltativamente) usare questo tipo come eliminazione del tipo specificando questo tipo come baseType quando si costruisce ilProvidedTypeDefinition.
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
Lezioni chiave
Nella sezione precedente è stato illustrato come creare un semplice provider di tipi di cancellazione che fornisce un intervallo di tipi, proprietà e metodi. In questa sezione è stato illustrato anche il concetto di cancellazione dei tipi, inclusi alcuni dei vantaggi e degli svantaggi della fornitura di tipi cancellati da un provider di tipi e sono state illustrate le rappresentazioni dei tipi cancellati.
Provider di tipi che usa parametri statici
La possibilità di parametrizzare i provider di tipi in base ai dati statici consente molti scenari interessanti, anche nei casi in cui il provider non deve accedere a dati locali o remoti. In questa sezione verranno illustrate alcune tecniche di base per mettere insieme un provider di questo tipo.
Fornitore di regex con controllo del tipo
Immagina di voler implementare un provider di tipi per le espressioni regolari che avvolge le librerie .NET Regex in un'interfaccia che fornisce le seguenti garanzie di compilazione:
Verifica della validità di un'espressione regolare.
Specifica di proprietà denominate sulle corrispondenze basate su qualsiasi nome di gruppo nell'espressione regolare.
In questa sezione viene illustrato come usare i provider di tipi per creare un tipo RegexTyped che il criterio di espressione regolare parametrizza per fornire questi vantaggi. Il compilatore riporterà un errore se il modello fornito non è valido e il provider di tipi può estrarre i gruppi dal modello, consentendo di accedervi usando le proprietà denominate sulle corrispondenze. Quando si progetta un provider di tipi, è consigliabile considerare il modo in cui l'API esposta deve apparire agli utenti finali e come questa progettazione verrà convertita in codice .NET. L'esempio seguente illustra come usare tale API per ottenere i componenti del codice dell'area:
type T = RegexTyped< @"(?<AreaCode>^\d{3})-(?<PhoneNumber>\d{3}-\d{4}$)">
let reg = T()
let result = T.IsMatch("425-555-2345")
let r = reg.Match("425-555-2345").Group_AreaCode.Value //r equals "425"
Nell'esempio seguente viene illustrato come il provider di tipi converte queste chiamate:
let reg = new Regex(@"(?<AreaCode>^\d{3})-(?<PhoneNumber>\d{3}-\d{4}$)")
let result = reg.IsMatch("425-123-2345")
let r = reg.Match("425-123-2345").Groups["AreaCode"].Value //r equals "425"
Si notino i punti seguenti:
Il tipo Regex standard rappresenta il tipo con
RegexTypedparametri.Il
RegexTypedcostruttore genera una chiamata al costruttore Regex, passando come argomento un tipo statico per il pattern.I risultati del
Matchmetodo sono rappresentati dal tipo standard Match .Ogni gruppo denominato restituisce una proprietà specificata e l'accesso alla proprietà comporta l'uso di un indicizzatore nella raccolta di
Groupsuna corrispondenza.
Il codice seguente è il nucleo della logica per implementare tale provider e questo esempio omette l'aggiunta di tutti i membri al tipo specificato. Per informazioni su ogni membro aggiunto, vedere la sezione appropriata più avanti in questo argomento.
namespace Samples.FSharp.RegexTypeProvider
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.Text.RegularExpressions
[<TypeProvider>]
type public CheckedRegexProvider() as this =
inherit TypeProviderForNamespaces()
// Get the assembly and namespace used to house the provided types
let thisAssembly = Assembly.GetExecutingAssembly()
let rootNamespace = "Samples.FSharp.RegexTypeProvider"
let baseTy = typeof<obj>
let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]
let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "RegexTyped", Some baseTy)
do regexTy.DefineStaticParameters(
parameters=staticParams,
instantiationFunction=(fun typeName parameterValues ->
match parameterValues with
| [| :? string as pattern|] ->
// Create an instance of the regular expression.
//
// This will fail with System.ArgumentException if the regular expression is not valid.
// The exception will escape the type provider and be reported in client code.
let r = System.Text.RegularExpressions.Regex(pattern)
// Declare the typed regex provided type.
// The type erasure of this type is 'obj', even though the representation will always be a Regex
// This, combined with hiding the object methods, makes the IntelliSense experience simpler.
let ty =
ProvidedTypeDefinition(
thisAssembly,
rootNamespace,
typeName,
baseType = Some baseTy)
...
ty
| _ -> failwith "unexpected parameter values"))
do this.AddNamespace(rootNamespace, [regexTy])
[<TypeProviderAssembly>]
do ()
Si notino i punti seguenti:
Il provider di tipi accetta due parametri statici:
pattern, che è obbligatorio eoptions, che sono facoltativi (perché viene fornito un valore predefinito).Dopo aver fornito gli argomenti statici, si crea un'istanza dell'espressione regolare. Questa istanza genererà un'eccezione se il Regex non è valido e questo errore verrà segnalato agli utenti.
All'interno del
DefineStaticParameterscallback si definisce il tipo che verrà restituito dopo l'invio degli argomenti.Questo codice imposta
HideObjectMethodssu true in modo che l'esperienza IntelliSense rimanga semplificata. Questo attributo fa sì che iEqualsmembri ,GetHashCodeFinalize, eGetTypevengano eliminati dagli elenchi IntelliSense per un oggetto specificato.Si usa
objcome tipo di base del metodo, ma si userà unRegexoggetto come rappresentazione di runtime di questo tipo, come illustrato nell'esempio seguente.La chiamata al
Regexcostruttore genera un'eccezione ArgumentException quando un'espressione regolare non è valida. Il compilatore rileva questa eccezione e segnala un messaggio di errore all'utente in fase di compilazione o nell'editor di Visual Studio. Questa eccezione consente di convalidare le espressioni regolari senza eseguire un'applicazione.
Il tipo definito in precedenza non è ancora utile perché non contiene metodi o proprietà significativi. Aggiungere prima di tutto un metodo statico IsMatch :
let isMatch =
ProvidedMethod(
methodName = "IsMatch",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = typeof<bool>,
isStatic = true,
invokeCode = fun args -> <@@ Regex.IsMatch(%%args[0], pattern) @@>)
isMatch.AddXmlDoc "Indicates whether the regular expression finds a match in the specified input string."
ty.AddMember isMatch
Il codice precedente definisce un metodo IsMatch, che accetta una stringa come input e restituisce un oggetto bool. L'unica parte complessa è l'uso dell'argomento args all'interno della InvokeCode definizione. In questo esempio, args è un elenco di citazioni che rappresentano gli argomenti di questo metodo. Se il metodo è un metodo di istanza, il primo argomento rappresenta l'argomento this . Tuttavia, per un metodo statico, gli argomenti sono tutti solo gli argomenti espliciti per il metodo . Si noti che il tipo del valore tra virgolette deve corrispondere al tipo restituito specificato (in questo caso , bool). Si noti anche che questo codice usa il AddXmlDoc metodo per assicurarsi che il metodo fornito abbia anche una documentazione utile, che è possibile fornire tramite IntelliSense.
Aggiungere quindi un metodo Match di istanza. Tuttavia, questo metodo deve restituire un valore di un tipo fornito Match in modo che i gruppi possano essere accessibili in modo fortemente tipizzato. Pertanto, si dichiara prima il tipo Match. Poiché questo tipo dipende dal modello fornito come argomento statico, questo tipo deve essere annidato all'interno della definizione del tipo con parametri:
let matchTy =
ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
hideObjectMethods = true)
ty.AddMember matchTy
Aggiungi quindi una proprietà al tipo di corrispondenza per ogni gruppo. In fase di esecuzione, una corrispondenza viene rappresentata come un valore Match, pertanto l'espressione che definisce la proprietà deve utilizzare Groups proprietà indicizzata per ottenere il gruppo pertinente.
for group in r.GetGroupNames() do
// Ignore the group named 0, which represents all input.
if group <> "0" then
let prop =
ProvidedProperty(
propertyName = group,
propertyType = typeof<Group>,
getterCode = fun args -> <@@ ((%%args[0]:obj) :?> Match).Groups[group] @@>)
prop.AddXmlDoc($"""Gets the ""{group}"" group from this match""")
matchTy.AddMember prop
Anche in questo caso, si noti che si aggiunge la documentazione XML alla proprietà specificata. Si noti anche che una proprietà può essere letta se viene fornita una GetterCode funzione e la proprietà può essere scritta se viene fornita una SetterCode funzione, quindi la proprietà risultante è di sola lettura.
È ora possibile creare un metodo di istanza che restituisce un valore di questo Match tipo:
let matchMethod =
ProvidedMethod(
methodName = "Match",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = matchTy,
invokeCode = fun args -> <@@ ((%%args[0]:obj) :?> Regex).Match(%%args[1]) :> obj @@>)
matchMeth.AddXmlDoc "Searches the specified input string for the first occurrence of this regular expression"
ty.AddMember matchMeth
Poiché si sta creando un metodo di istanza, args[0] rappresenta l'istanza RegexTyped in cui viene chiamato il metodo ed args[1] è l'argomento di input.
Specificare infine un costruttore in modo che sia possibile creare istanze del tipo fornito.
let ctor =
ProvidedConstructor(
parameters = [],
invokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)
ctor.AddXmlDoc("Initializes a regular expression instance.")
ty.AddMember ctor
Il costruttore porta semplicemente alla creazione di un'istanza standard di regex .NET, che viene di nuovo confezionata in un oggetto perché obj rappresenta l'eliminazione del tipo specificato. Con questa modifica, l'utilizzo dell'API di esempio specificato in precedenza nell'argomento funziona come previsto. Il codice seguente è completo e finale:
namespace Samples.FSharp.RegexTypeProvider
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.Text.RegularExpressions
[<TypeProvider>]
type public CheckedRegexProvider() as this =
inherit TypeProviderForNamespaces()
// Get the assembly and namespace used to house the provided types.
let thisAssembly = Assembly.GetExecutingAssembly()
let rootNamespace = "Samples.FSharp.RegexTypeProvider"
let baseTy = typeof<obj>
let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]
let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "RegexTyped", Some baseTy)
do regexTy.DefineStaticParameters(
parameters=staticParams,
instantiationFunction=(fun typeName parameterValues ->
match parameterValues with
| [| :? string as pattern|] ->
// Create an instance of the regular expression.
let r = System.Text.RegularExpressions.Regex(pattern)
// Declare the typed regex provided type.
let ty =
ProvidedTypeDefinition(
thisAssembly,
rootNamespace,
typeName,
baseType = Some baseTy)
ty.AddXmlDoc "A strongly typed interface to the regular expression '%s'"
// Provide strongly typed version of Regex.IsMatch static method.
let isMatch =
ProvidedMethod(
methodName = "IsMatch",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = typeof<bool>,
isStatic = true,
invokeCode = fun args -> <@@ Regex.IsMatch(%%args[0], pattern) @@>)
isMatch.AddXmlDoc "Indicates whether the regular expression finds a match in the specified input string"
ty.AddMember isMatch
// Provided type for matches
// Again, erase to obj even though the representation will always be a Match
let matchTy =
ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
hideObjectMethods = true)
// Nest the match type within parameterized Regex type.
ty.AddMember matchTy
// Add group properties to match type
for group in r.GetGroupNames() do
// Ignore the group named 0, which represents all input.
if group <> "0" then
let prop =
ProvidedProperty(
propertyName = group,
propertyType = typeof<Group>,
getterCode = fun args -> <@@ ((%%args[0]:obj) :?> Match).Groups[group] @@>)
prop.AddXmlDoc(sprintf @"Gets the ""%s"" group from this match" group)
matchTy.AddMember(prop)
// Provide strongly typed version of Regex.Match instance method.
let matchMeth =
ProvidedMethod(
methodName = "Match",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = matchTy,
invokeCode = fun args -> <@@ ((%%args[0]:obj) :?> Regex).Match(%%args[1]) :> obj @@>)
matchMeth.AddXmlDoc "Searches the specified input string for the first occurrence of this regular expression"
ty.AddMember matchMeth
// Declare a constructor.
let ctor =
ProvidedConstructor(
parameters = [],
invokeCode = fun args -> <@@ Regex(pattern) :> obj @@>)
// Add documentation to the constructor.
ctor.AddXmlDoc "Initializes a regular expression instance"
ty.AddMember ctor
ty
| _ -> failwith "unexpected parameter values"))
do this.AddNamespace(rootNamespace, [regexTy])
[<TypeProviderAssembly>]
do ()
Lezioni chiave
In questa sezione è stato illustrato come creare un provider di tipi che opera sui relativi parametri statici. Il provider controlla il parametro statico e fornisce operazioni in base al relativo valore.
Provider di tipi supportato da dati locali
Spesso è consigliabile che i provider di tipi presentino API in base non solo ai parametri statici, ma anche alle informazioni provenienti da sistemi locali o remoti. In questa sezione vengono illustrati i provider di tipi basati su dati locali, ad esempio i file di dati locali.
Provider di file CSV semplice
Come esempio semplice, si consideri un provider di tipi per l'accesso ai dati scientifici in formato CSV (Comma Separated Value). Questa sezione presuppone che i file CSV contengano una riga di intestazione seguita da dati a virgola mobile, come illustrato nella tabella seguente:
| Distanza (metro) | Tempo (secondo) |
|---|---|
| 50,0 | 3.7 |
| 100.0 | 5.2 |
| 150.0 | 6.4 |
Questa sezione illustra come fornire un tipo che è possibile usare per ottenere righe con una proprietà di tipo Distance e una proprietà di tipo float<meter>. Per semplicità, vengono effettuati i presupposti seguenti:
I nomi di intestazione sono privi di unità o hanno la forma "Nome (unità)" e non contengono virgole.
Le unità sono tutte unità System International (SI) definite dal modulo FSharp.Data.UnitSystems.SI.UnitNames Module (F#).
Le unità sono tutte semplici (ad esempio, contatore) anziché composte (ad esempio, contatore/secondo).
Tutte le colonne contengono dati a virgola mobile.
Un provider più completo potrebbe allentare queste restrizioni.
Anche in questo caso, il primo passaggio consiste nel considerare l'aspetto dell'API. Dato un info.csv file con il contenuto della tabella precedente (in formato delimitato da virgole), gli utenti del provider devono poter scrivere codice simile all'esempio seguente:
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn $"{float time}"
In questo caso, il compilatore deve convertire queste chiamate in un modo simile all'esempio seguente:
let info = new CsvFile("info.csv")
for row in info.Data do
let (time:float) = row[1]
printfn $"%f{float time}"
La conversione ottimale richiederà al provider di tipi di definire un tipo reale CsvFile nell'assembly del provider di tipi. I provider di tipi spesso si basano su alcuni tipi e metodi helper per eseguire il wrapping di una logica importante. Poiché le misure vengono cancellate in fase di esecuzione, è possibile utilizzare float[] come tipo cancellato per una riga. Il compilatore considererà colonne diverse come tipi di misura diversi. Ad esempio, la prima colonna nell'esempio ha il tipo float<meter>e la seconda ha float<second>. Tuttavia, la rappresentazione cancellata può rimanere piuttosto semplice.
Il codice seguente illustra il nucleo dell'implementazione.
// Simple type wrapping CSV data
type CsvFile(filename) =
// Cache the sequence of all data lines (all lines but the first)
let data =
seq {
for line in File.ReadAllLines(filename) |> Seq.skip 1 ->
line.Split(',') |> Array.map float
}
|> Seq.cache
member _.Data = data
[<TypeProvider>]
type public MiniCsvProvider(cfg:TypeProviderConfig) as this =
inherit TypeProviderForNamespaces(cfg)
// Get the assembly and namespace used to house the provided types.
let asm = System.Reflection.Assembly.GetExecutingAssembly()
let ns = "Samples.FSharp.MiniCsvProvider"
// Create the main provided type.
let csvTy = ProvidedTypeDefinition(asm, ns, "MiniCsv", Some(typeof<obj>))
// Parameterize the type by the file to use as a template.
let filename = ProvidedStaticParameter("filename", typeof<string>)
do csvTy.DefineStaticParameters([filename], fun tyName [| :? string as filename |] ->
// Resolve the filename relative to the resolution folder.
let resolvedFilename = Path.Combine(cfg.ResolutionFolder, filename)
// Get the first line from the file.
let headerLine = File.ReadLines(resolvedFilename) |> Seq.head
// Define a provided type for each row, erasing to a float[].
let rowTy = ProvidedTypeDefinition("Row", Some(typeof<float[]>))
// Extract header names from the file, splitting on commas.
// use Regex matching to get the position in the row at which the field occurs
let headers = Regex.Matches(headerLine, "[^,]+")
// Add one property per CSV field.
for i in 0 .. headers.Count - 1 do
let headerText = headers[i].Value
// Try to decompose this header into a name and unit.
let fieldName, fieldTy =
let m = Regex.Match(headerText, @"(?<field>.+) \((?<unit>.+)\)")
if m.Success then
let unitName = m.Groups["unit"].Value
let units = ProvidedMeasureBuilder.Default.SI unitName
m.Groups["field"].Value, ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>,[units])
else
// no units, just treat it as a normal float
headerText, typeof<float>
let prop =
ProvidedProperty(fieldName, fieldTy,
getterCode = fun [row] -> <@@ (%%row:float[])[i] @@>)
// Add metadata that defines the property's location in the referenced file.
prop.AddDefinitionLocation(1, headers[i].Index + 1, filename)
rowTy.AddMember(prop)
// Define the provided type, erasing to CsvFile.
let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<CsvFile>))
// Add a parameterless constructor that loads the file that was used to define the schema.
let ctor0 =
ProvidedConstructor([],
invokeCode = fun [] -> <@@ CsvFile(resolvedFilename) @@>)
ty.AddMember ctor0
// Add a constructor that takes the file name to load.
let ctor1 = ProvidedConstructor([ProvidedParameter("filename", typeof<string>)],
invokeCode = fun [filename] -> <@@ CsvFile(%%filename) @@>)
ty.AddMember ctor1
// Add a more strongly typed Data property, which uses the existing property at run time.
let prop =
ProvidedProperty("Data", typedefof<seq<_>>.MakeGenericType(rowTy),
getterCode = fun [csvFile] -> <@@ (%%csvFile:CsvFile).Data @@>)
ty.AddMember prop
// Add the row type as a nested type.
ty.AddMember rowTy
ty)
// Add the type to the namespace.
do this.AddNamespace(ns, [csvTy])
Si notino i punti seguenti sull'implementazione:
I costruttori sovraccaricati consentono di leggere sia il file originale sia uno con uno schema identico. Questo modello è comune quando si scrive un provider di tipi per origini dati locali o remote e questo modello consente l'uso di un file locale come modello per i dati remoti.
È possibile usare il valore TypeProviderConfig passato al costruttore del provider di tipi per risolvere i nomi di file relativi.
È possibile utilizzare il
AddDefinitionLocationmetodo per definire la posizione delle proprietà specificate. Pertanto, se si usaGo To Definitionin una proprietà specificata, il file CSV verrà aperto in Visual Studio.È possibile usare il
ProvidedMeasureBuildertipo per cercare le unità SI e generare i tipi pertinentifloat<_>.
Lezioni chiave
In questa sezione è stato illustrato come creare un provider di tipi per un'origine dati locale con uno schema semplice contenuto nell'origine dati stessa.
Andare oltre
Le sezioni seguenti includono suggerimenti per ulteriori studi.
Esaminare il codice compilato per i tipi cancellati
Per dare un'idea di come l'uso del provider di tipi corrisponda al codice generato, esamina la funzione seguente usando l'oggetto HelloWorldTypeProvider usato in precedenza in questo argomento.
let function1 () =
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
obj1.InstanceProperty
Ecco un'immagine del codice risultante decompilato usando ildasm.exe:
.class public abstract auto ansi sealed Module1
extends [mscorlib]System.Object
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAtt
ribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags)
= ( 01 00 07 00 00 00 00 00 )
.method public static int32 function1() cil managed
{
// Code size 24 (0x18)
.maxstack 3
.locals init ([0] object obj1)
IL_0000: nop
IL_0001: ldstr "some data"
IL_0006: unbox.any [mscorlib]System.Object
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: call !!0 [FSharp.Core_2]Microsoft.FSharp.Core.LanguagePrimit
ives/IntrinsicFunctions::UnboxGeneric<string>(object)
IL_0012: callvirt instance int32 [mscorlib_3]System.String::get_Length()
IL_0017: ret
} // end of method Module1::function1
} // end of class Module1
Come illustrato nell'esempio, tutte le menzioni del tipo Type1 e la proprietà InstanceProperty sono state cancellate, lasciando solo le operazioni sui tipi di runtime coinvolti.
Progettazione e convenzioni di denominazione per i tipi fornitori
Quando si creano provider di tipi, osservare le convenzioni seguenti.
Provider per protocolli di connettività In generale, i nomi della maggior parte delle DLL del provider per i protocolli di connettività dei dati e dei servizi, ad esempio le connessioni OData o SQL, devono terminare in TypeProvider o TypeProviders. Ad esempio, usare un nome DLL simile alla stringa seguente:
Fabrikam.Management.BasicTypeProviders.dll
Assicurarsi che i tipi forniti siano membri dello spazio dei nomi corrispondente e indicare il protocollo di connettività implementato:
Fabrikam.Management.BasicTypeProviders.WmiConnection<…>
Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>
Provider di utilità per la codifica generale. Per un provider di tipi di utilità, ad esempio per le espressioni regolari, il provider di tipi può far parte di una libreria di base, come illustrato nell'esempio seguente:
#r "Fabrikam.Core.Text.Utilities.dll"
In questo caso, il tipo fornito viene visualizzato in un punto appropriato in base alle normali convenzioni di progettazione .NET:
open Fabrikam.Core.Text.RegexTyped
let regex = new RegexTyped<"a+b+a+b+">()
Origini dati Singleton. Alcuni fornitori di tipi si connettono a un'unica origine dati dedicata e forniscono solo dati. In questo caso, è necessario eliminare il TypeProvider suffisso e usare le convenzioni normali per la denominazione .NET:
#r "Fabrikam.Data.Freebase.dll"
let data = Fabrikam.Data.Freebase.Astronomy.Asteroids
Per altre informazioni, vedere la GetConnection convenzione di progettazione descritta più avanti in questo argomento.
Modelli di progettazione per fornitori di tipi
Le sezioni seguenti descrivono i modelli di progettazione che è possibile usare per la creazione di provider di tipi.
Modello di progettazione GetConnection
La maggior parte dei provider di tipi dovrebbe essere scritta per utilizzare il modello GetConnection usato dai provider di tipi in FSharp.Data.TypeProviders.dll, come illustrato nell'esempio seguente.
#r "Fabrikam.Data.WebDataStore.dll"
type Service = Fabrikam.Data.WebDataStore<…static connection parameters…>
let connection = Service.GetConnection(…dynamic connection parameters…)
let data = connection.Astronomy.Asteroids
Provider di tipi supportati da dati e servizi remoti
Prima di creare un provider di tipi supportato da dati e servizi remoti, è necessario considerare una serie di problemi intrinseci nella programmazione connessa. Questi problemi includono le considerazioni seguenti:
mapping dello schema
liveness e invalidazione in presenza di modifiche dello schema
memorizzazione nella cache dello schema
implementazioni asincrone delle operazioni di accesso ai dati
supporto di query, incluse le query LINQ
credenziali e autenticazione
Questo argomento non esplora ulteriormente questi problemi.
Tecniche di creazione aggiuntive
Quando scrivi provider di tipi personalizzati, potresti voler usare le seguenti tecniche aggiuntive.
Creazione di tipi e membri su richiesta
L'API ProvidedType ha ritardato le versioni di AddMember.
type ProvidedType =
member AddMemberDelayed : (unit -> MemberInfo) -> unit
member AddMembersDelayed : (unit -> MemberInfo list) -> unit
Queste versioni vengono usate per creare spazi di diversi tipi a richiesta.
Fornitura di tipi di array e istanze di tipi generici
Tu fornisci i membri (le cui firme includono tipi di matrice, tipi byref e istanze di tipi generici) usando i normali MakeArrayType, MakePointerType e MakeGenericType su qualsiasi istanza di Type, incluso ProvidedTypeDefinitions.
Annotazioni
In alcuni casi potrebbe essere necessario usare l'assistente in ProvidedTypeBuilder.MakeGenericType. Per altri dettagli, vedere la documentazione di Type Provider SDK .
Fornitura di annotazioni delle unità di misura
L'API ProvidedTypes fornisce helper per fornire annotazioni di misura. Ad esempio, per fornire il tipo float<kg>, usare il codice seguente:
let measures = ProvidedMeasureBuilder.Default
let kg = measures.SI "kilogram"
let m = measures.SI "meter"
let float_kg = measures.AnnotateType(typeof<float>,[kg])
Per specificare il tipo Nullable<decimal<kg/m^2>>, usare il codice seguente:
let kgpm2 = measures.Ratio(kg, measures.Square m)
let dkgpm2 = measures.AnnotateType(typeof<decimal>,[kgpm2])
let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]
Accesso alle risorse di Project-Local o di Script-Local
A ogni istanza di un provider di tipi può essere assegnato un TypeProviderConfig valore durante la costruzione. Questo valore contiene la "cartella di risoluzione" per il provider, ovvero la cartella del progetto per la compilazione o la directory che contiene uno script, l'elenco di assembly a cui si fa riferimento e altre informazioni.
Invalidazione
I provider possono generare segnali di invalidazione per notificare al servizio linguaggio F# che i presupposti dello schema potrebbero essere stati modificati. Quando si verifica l'invalidazione, un controllo del tipo viene eseguito di nuovo se il provider è ospitato in Visual Studio. Questo segnale verrà ignorato quando il provider è ospitato in F# Interactive o dal compilatore F# (fsc.exe).
Memorizzazione nella cache delle informazioni sullo schema
I provider devono spesso memorizzare nella cache l'accesso alle informazioni sullo schema. I dati memorizzati nella cache devono essere archiviati usando un nome file specificato come parametro statico o come dati utente. Un esempio di memorizzazione nella cache dello schema è il LocalSchemaFile parametro nei tipi di provider nell'assembly FSharp.Data.TypeProviders. Nell'implementazione di questi provider, questo parametro statico indirizza il provider di tipi a usare le informazioni sullo schema nel file locale specificato anziché accedere all'origine dati in rete. Per usare le informazioni sullo schema memorizzate nella cache, è necessario impostare anche il parametro ForceUpdate statico su false. È possibile usare una tecnica simile per abilitare l'accesso ai dati online e offline.
Gruppo di supporto
Quando si compila un .dll file o .exe , il file di backup .dll per i tipi generati viene collegato in modo statico all'assembly risultante. Questo collegamento viene creato copiando le definizioni dei tipi del linguaggio intermedio (IL) e qualsiasi risorsa gestita dall'assembly di supporto nell'assembly finale. Quando si usa F# Interactive, il file di backup .dll non viene copiato e viene invece caricato direttamente nel processo F# Interactive.
Eccezioni e diagnostica dei provider di tipi
Tutti gli usi di tutti i membri dei tipi forniti possono generare eccezioni. In tutti i casi, se un provider di tipi genera un'eccezione, il compilatore host assegna l'errore a un provider di tipi specifico.
Le eccezioni del provider di tipi non devono mai generare errori interni del compilatore.
I provider di tipi non possono segnalare avvisi.
Quando un provider di tipi è ospitato nel compilatore F#, un ambiente di sviluppo F# o F# Interactive, vengono rilevate tutte le eccezioni di tale provider. La proprietà Message è sempre il testo dell'errore e non viene visualizzata alcuna traccia dello stack. Se si intende generare un'eccezione, è possibile generare gli esempi seguenti:
System.NotSupportedException,System.IO.IOException,System.Exception.
Fornitura di tipi generati
Finora, questo documento ha spiegato come fornire tipi cancellati. È anche possibile usare il meccanismo del provider di tipi in F# per fornire tipi generati, che vengono aggiunti come definizioni di tipi .NET reali nel programma degli utenti. È necessario fare riferimento ai tipi forniti generati usando una definizione di tipo.
open Microsoft.FSharp.TypeProviders
type Service = ODataService<"http://services.odata.org/Northwind/Northwind.svc/">
Il codice helper ProvidedTypes-0.2 che fa parte della versione F# 3.0 ha solo un supporto limitato per fornire tipi generati. Le affermazioni seguenti devono essere vere per una definizione di tipo generato.
isErasedDeve essere impostato sufalse.Il tipo generato deve essere aggiunto a un oggetto appena costruito
ProvidedAssembly(), che rappresenta un contenitore per frammenti di codice generati.Il provider deve avere un assembly con un file di backup effettivo di .NET .dll con un file .dll corrispondente su disco.
Regole e limitazioni
Quando si scrivono i tipi di provider, tenere presenti le regole e le limitazioni seguenti.
I tipi forniti devono essere raggiungibili
Tutti i tipi forniti devono essere raggiungibili dai tipi non annidati. I tipi non annidati vengono specificati nella chiamata al costruttore TypeProviderForNamespaces o a AddNamespace. Ad esempio, se il provider fornisce un tipo StaticClass.P : T, è necessario assicurarsi che T sia un tipo non annidato o annidato all'interno di un altro.
Ad esempio, alcuni provider hanno una classe statica, DataTypes ad esempio che contiene questi T1, T2, T3, ... tipi. In caso contrario, l'errore indica che è stato trovato un riferimento al tipo T nell'assembly A, ma non è stato possibile trovare il tipo in tale assembly. Se viene visualizzato questo errore, verificare che tutti i sottotipi possano essere raggiunti dai tipi di provider. Nota: questi T1, T2, T3... tipi sono chiamati tipi on-the-fly. Ricorda di inserirli in uno spazio dei nomi accessibile o in un tipo padre.
Limitazioni del meccanismo dei provider di tipi
Il meccanismo del provider di tipi in F# presenta le limitazioni seguenti:
L'infrastruttura sottostante per i provider di tipi in F# non supporta i tipi generici forniti o i metodi generici forniti.
Il meccanismo non supporta i tipi annidati con parametri statici.
Suggerimenti per lo sviluppo
Durante il processo di sviluppo potrebbero essere utili i suggerimenti seguenti:
Eseguire due istanze di Visual Studio
È possibile sviluppare il provider di tipi in un'istanza e testare il provider nell'altro perché l'IDE di test eseguirà un blocco sul file .dll che impedisce la ricompilazione del provider di tipi. Pertanto, è necessario chiudere la seconda istanza di Visual Studio mentre il provider viene compilato nella prima istanza e quindi è necessario riaprire la seconda istanza dopo la compilazione del provider.
Eseguire il debug dei provider di tipi usando invocazioni di fsc.exe.
È possibile richiamare provider di tipi usando gli strumenti seguenti:
fsc.exe (compilatore della riga di comando F#)
fsi.exe (compilatore F# Interactive)
devenv.exe (Visual Studio)
È spesso possibile eseguire il debug dei provider di tipi più facilmente usando fsc.exe in un file di script di test, ad esempio script.fsx. È possibile avviare un debugger da un prompt dei comandi.
devenv /debugexe fsc.exe script.fsx
È possibile usare la registrazione con print su stdout.