Esercitazione: creazione di un provider di tipi (F#)
Il meccanismo del provider di tipi in F# 3.0 rappresenta una parte importante del supporto per la programmazione information rich. Questo tutorial mostra come creare i propri provider di tipi guidandoti attraverso lo sviluppo di diversi provider di tipi semplici per illustrare i concetti di base. Per ulteriori informazioni sul meccanismo del provider di tipi in F#, vedere Provider di tipi.
F# 3.0 contiene diversi provider di tipi incorporati comunemente utilizzati per servizi dati di Internet e di azienda. Questi provider di tipi forniscono l'accesso semplice e normale a database relazionali SQL e a servizi basati sulla rete di OData e di WSDL. Questi provider supportano inoltre l'utilizzo di query LINQ di F# rispetto alle origini di questi dati.
Dove necessario, è possibile creare provider di tipi personalizzati, oppure è possibile fare riferimento a provider di tipi che altri hanno creato. Ad esempio, un'organizzazione può avere un servizio dati che fornisce un grande e crescente numero d'identificativi per un set di dati, ognuno dei quali con il relativo schema stabile di dati. È possibile creare un provider di tipi che legga gli schemi e che presenti al programmatore i set di dati correnti in una modalità fortemente tipizzata.
Prima di iniziare
Il meccanismo del provider di tipi è principalmente progettato per introdurre spazi stabili di informazioni di dati e di servizi all'interno dell'esperienza di programmazione F#.
Questo meccanismo non è progettato per l'inserimento di spazi di informazioni il cui schema cambia durante l'esecuzione del programma in modo che questi siano pertinenti alla logica del programma. Inoltre, il meccanismo non è progettato per la meta-programmazione interna al linguaggio, anche se tale dominio contiene alcuni utilizzi validi. È possibile utilizzare questo meccanismo dove necessario e dove lo sviluppo di un provider di tipi è molto proficuo.
Si dovrebbe evitare di scrivere un provider di tipi dove non è disponibile uno schema. Inoltre, si dovrebbe evitare di scrivere un provider di tipi dove una normale (o persino esistente) libreria di .NET basterebbe.
Prima di iniziare, ci si dovrebbe porre le seguenti domande:
Si ha a disposizione uno schema per le sorgenti di informazione? In caso affermativo, qual è il mapping nel sistema di tipi di .NET e di F#?
È possibile utilizzare un API esistente (tipizzato dinamicamente) come punto di partenza per l'implementazione?
Tu e la tua organizzazione disponete di un numero sufficiente di utilizzi per il provider di tipi per rendere la sua scrittura proficua? Una normale libreria di .NET potrebbe soddisfare le vostre esigenze?
Quanto cambierà il vostro schema?
Verrà modificato durante la codifica?
Cambierà tra le sessioni di codifica?
Verrà modificato durante l'esecuzione del programma?
I provider di tipi risultano più adatti a situazioni in cui lo schema è stabile a runtime e durante il ciclo di vita del codice compilato.
Un semplice Provider di Tipi
Questo esempio è Samples.HelloWorldTypeProvider nella directory SampleProviders\Providers del Pacchetto di esempio di F# 3.0 sul sito web Codeplex. Il provider rende disponibile "uno spazio di tipi" che contiene 100 tipi cancellati, come illustrato nel seguente codice utilizzando la sintassi di F# e omettendo tutti i dettagli tranne quelli per Type1. Per ulteriori informazioni sui tipi cancellati, vedere Dettagli sui tipi forniti cancellati più avanti in questo topic.
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
/// This is an instance property.
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 di membri forniti è noto in modo statico. Questo esempio non sfrutta la possibilità dei provider di fornire dei tipi che dipendono da uno schema. L'implementazione del provider di tipi è descritta nel codice seguente e i dettagli vengono analizzati nelle sezioni successive di questo topic.
Avviso
È possibile che vi siano presenti alcune piccole differenze di denominazione tra questo codice e quello degli esempi online.
namespace Samples.FSharp.HelloWorldTypeProvider
open System
open System.Reflection
open Samples.FSharp.ProvidedTypes
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.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()
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 utilizzare questo provider, aprire un'istanza separata di Visual Studio 2012, creare uno script F# e quindi aggiungere un riferimento al provider dallo script utilizzando #r come mostra il 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
Individuare quindi i tipi nel namespace Samples.HelloWorldTypeProvider che il provider di tipi ha generato.
Prima di ricompilare il provider, assicurarsi di aver chiuso tutte le istanze di Visual Studio e di F# Interactive che utilizzano 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 tramite istruzioni di stampa, creare uno script che espone un problema con il provider e quindi utilizzare il codice seguente:
fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Per eseguire il debug di questo provider utilizzando Visual Studio, aprire il prompt dei comandi di Visual Studio con le credenziali di amministratore 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/Connetti a processo…**e collegarsi a un altro processo devenv in cui si desidera modificare lo script. Utilizzando questo metodo, è più facile individuare la logica specifica del provider di tipi in modo interattivo digitando le espressioni nella seconda istanza (con supporto completo di IntelliSense e altre funzionalità).
È possibile disabilitare il debug Solo codice utente per identificare meglio gli errori nel codice generato. Per informazioni su come abilitare o disabilitare questa funzionalità, vedere Restringere stepping a Just My Code. Inoltre, è possibile impostare l'intercettazione di eccezioni first-chance aprendo il menu Debug e scegliendo Eccezioni oppure premendo Ctrl+Alt+E per aprire la finestra di dialogo Eccezioni. In questa finestra di dialogo, in Eccezioni Common Language Runtime, selezionare la casella Generata.
Implementazione del Provider di Tipi
In questa sezione vengono illustrate le fasi principali dell'implementazione del provider di tipi. Innanzitutto, definire il tipo del 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 separato di F# fa riferimento all'assembly contenente il tipo. Il parametro config è facoltativo e, se presente, contiene le informazioni contestuali di configurazione per l'istanza del provider di tipi che il compilatore di F# ha creato.
Successivamente, si implementa l'interfaccia ITypeProvider. In questo caso, utilizzare il tipo TypeProviderForNamespaces dall'API ProvidedTypes come tipo di base. Questo tipo di supporto può fornire una raccolta limitata di namespace eagerly forniti, ognuno dei quali contiene direttamente un numero fisso di tipi eagerly forniti. In questo contesto, il provider eagerly genera dei tipi anche se non sono necessari o non sono utilizzati.
inherit TypeProviderForNamespaces()
A questo punto, definire dei valori privati locali che specificano il namespace per i tipi forniti e individuare l'assembly del provider di tipi. Questo assembly viene successivamente utilizzato come tipo padre logico dei tipi cancellati che sono forniti.
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
Successivamente, creare una funzione per fornire ciascuno dei tipi Type1…Type100. Questa funzione viene descritta più dettagliatamente più avanti in questo topic.
let makeOneProvidedType (n:int) = …
Dopo, generare i 100 tipi forniti:
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
Successivamente, aggiungere i tipi come un namespace fornito:
do this.AddNamespace(namespaceName, types)
Infine, aggiungere un attributo assembly che indica che si sta creando una DLL del provider di tipi:
[<assembly:TypeProviderAssembly>]
do()
Assegnare un tipo e i suoi membri
La funzione makeOneProvidedType svolge il lavoro effettivo di creazione di uno dei tipi.
let makeOneProvidedType (n:int) =
…
Questo passaggio illustra l'implementazione di questa funzione. Innanzitutto, creare il tipo fornito (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>)
È necessario tenere presente i seguenti punti:
Questo tipo fornito è 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 il namespace. Per i tipi cancellati, l'assembly deve essere l'assembly stesso del provider di tipi.
Successivamente, aggiungere la documentazione XML per il tipo. Questa documentazione è ritardata, ovvero, viene calcolata su richiesta se il compilatore dell'host la richiede.
t.AddXmlDocDelayed (fun () -> sprintf "This provided type %s" ("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!" @@>))
Recuperando questa proprietà sarà sempre valutata con la stringa “Hello!". Il GetterCode per la proprietà utilizza una quotation F#, che rappresenta il codice che il compilatore dell'host ha generato per ottenere la proprietà. Per ulteriori informazioni sulle quotation, vedere Quotation di codice (F#).
Aggiungere la documentazione XML nella proprietà.
staticProp.AddXmlDocDelayed(fun () -> "This is a static property")
Ora associare la proprietà fornita al tipo specificato. È necessario connettere un membro specificato a uno e a un solo tipo. In caso contrario, il membro non sarà mai accessibile.
t.AddMember staticProp
Creare ora un costruttore che non accetta parametri.
let ctor = ProvidedConstructor(parameters = [ ],
InvokeCode= (fun args -> <@@ "The object data" :> obj @@>))
L' InvokeCode per il costruttore restituisce una quotation F#, che rappresenta il codice che il compilatore dell'host ha generato quando il costruttore è stato chiamato. È possibile utilizzare ad esempio il seguente costruttore:
new Type10()
Un'istanza del tipo specificato verrà creata con i dati sottostanti “The object data". Il codice citato include una conversione a obj poiché questo tipo è la cancellazione di questo tipo fornito (come specificato quando è stato dichiarato il tipo fornito).
Aggiungere la documentazione XML al costruttore e aggiungere il costruttore fornito al tipo fornito:
ctor.AddXmlDocDelayed(fun () -> "This is a constructor")
t.AddMember ctor
Creare un secondo costruttore che accetta un parametro:
let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
InvokeCode= (fun args -> <@@ (%%(args.[0]) : string) :> obj @@>))
L' InvokeCode per il costruttore restituirà ancora una quotation F#, che rappresenta il codice che il compilatore dell'host ha generato per una chiamata al metodo. È possibile utilizzare ad esempio il seguente costruttore:
new Type10("ten")
Un'istanza del tipo specificato viene creata con i dati sottostanti "ten". Si dovrebbe aver già notato che la funzione InvokeCode restituisce una quotation. L'input a questa funzione è un elenco di espressioni, una per parametro del costruttore. In questo caso, un'espressione che rappresenta il valore del parametro è disponibile in args.[0]. Il codice per una chiamata al costruttore assegna il valore restituito al tipo cancellato obj. Dopo aver aggiunto il secondo costruttore al tipo, si crei una istanza della proprietà fornita:
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
Recuperando questa proprietà essa restituirà la lunghezza della stringa, che è la rappresentazione dell'oggetto. La proprietà GetterCode restituisce una quotation di F# che specifica il codice che il compilatore dell'host ha generato per ottenere la proprietà. Come InvokeCode, la funzione GetterCode restituisce una quotation. Il compilatore dell'host chiama questa funzione con una lista di argomenti. In questo caso, gli argomenti includono solo l'espressione che rappresenta l'istanza su cui il getter viene chiamato, accessibile tramite args.[0]. L'implementazione di GetterCode quindi congiunge le quotation restituite al tipo cancellato obj e un cast viene utilizzato per soddisfare il meccanismo del compilatore per controllare che i tipi di questo oggetto siano 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
Infine, si crea un tipo annidato contenente 100 proprietà annidate. La creazione di questo tipo annidato e delle relative proprietà è ritardata, ovvero, viene calcolata su richiesta.
t.AddMembersDelayed(fun () ->
let nestedType = ProvidedTypeDefinition("NestedType",
Some typeof<obj>
)
nestedType.AddMembersDelayed (fun () ->
let staticPropsInNestedType =
[ for i in 1 .. 100 do
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 () ->
sprintf "This is StaticProperty%d on NestedType" i)
yield p ]
staticPropsInNestedType)
[nestedType])
// The result of makeOneProvidedType is the type.
t
Dettagli sui Tipi Forniti Cancellati
L'esempio in questa sezione si occupa solo dei tipi forniti cancellati, particolarmente utili nelle situazioni seguenti:
Quando si scrive un provider per uno spazio di informazioni che contiene solo dati e metodi.
Quando si scrive un provider in cui una semantica accurata dei tipi runtime non è indispensabile per un utilizzo pratico dello spazio di informazioni.
Quando si scrive un provider per uno spazio di informazioni che è così grande ed interconnesso che non è tecnicamente possibile generare i tipi reali di .NET per lo spazio di informazioni.
In questo esempio, ogni tipo fornito viene cancellato al tipo obj e tutti gli utilizzi del tipo verranno visualizzati come tipo obj nel codice compilato. Infatti, gli oggetti sottostanti in questi esempi sono stringhe, ma il tipo verrà visualizzato come Object nel codice compilato di .NET. Come in tutti gli utilizzi della cancellazione di tipo, è possibile utilizzare il boxing, l'unboxing e il cast esplicito per sovvertire i tipi cancellati. In questo caso, può verificarsi un'eccezione di cast non valido quando l'oggetto viene utilizzato. Il runtime del provider può definire il proprio tipo privato rappresentativo per proteggersi dalle rappresentazioni false. Non è possibile definire tipi cancellati in F#. Solo i tipi forniti possono essere cancellati. È necessario comprendere le implicazioni, sia pratiche che semantiche, dell'utilizzo dei tipi cancellati per il provider di tipi o di un provider che fornisce tipi cancellati. Un tipo cancellato non ha un tipo .NET reale. Di conseguenza, non è possibile fare considerazioni accurate sul tipo e si potrebbero sovvertire i tipi cancellati se si utilizzano i cast runtime e altre tecniche che si basano sulla semantica dei tipi runtime. La sovversione dei tipi cancellati comporta spesso eccezioni di cast di tipo in fase di esecuzione.
Scegliere le Rappresentazioni per i Tipi Forniti Cancellati
Per alcuni utilizzi dei tipi forniti cancellati, nessuna rappresentazione è necessaria. Ad esempio, il tipo fornito cancellato potrebbe contenere solo proprietà e membri statici e non costruttori e nessun metodo o proprietà restituirebbe un istanza del tipo. Se è possibile ottenere istanze di un tipo fornito cancellato, è necessario considerare le seguenti domande:
Qual è la cancellazione di un tipo fornito?
La cancellazione di un tipo fornito è come il tipo che appare nel codice compilato di .NET.
La cancellazione di un tipo di una classe fornita cancellata è sempre il primo tipo di base non cancellato nella catena di ereditarietà del tipo.
La cancellazione di un tipo fornito cancellato di un'interfaccia è sempre Object.
Quali sono le rappresentazioni di un tipo fornito?
- Il set di oggetti possibili per un tipo fornito cancellato viene indicato come la sua rappresentazione. Nell'esempio di questo documento, le rappresentazioni di tutti i tipi forniti cancellati Type1..Type100 sono sempre oggetti di tipo stringa.
Tutte le rappresentazioni di un tipo fornito devono essere compatibili con la cancellazione del tipo fornito. (Altrimenti, o il compilatore di F# genera un errore per l'utilizzo del provider di tipi, oppure un'eccezione di codice non verificabile di .NET che non è valido verrà generata. Un provider di tipi non è valido se restituisce un codice che fornisce una rappresentazione non valida.)
È possibile scegliere una rappresentazione per gli oggetti forniti tramite uno dei seguenti approcci, entrambi sono molto comuni:
Se si immette semplicemente un wrapper fortemente tipizzato su un tipo .NET esistente, spesso è consigliabile che il tipo per cancellare questo tipo, utilizzi un'istanza di quel tipo come rappresentazione, oppure entrambe. Questo approccio è appropriato per la maggior parte dei metodi esistenti su quel tipo che hanno ancora senso quando si utilizza la versione fortemente tipizzata.
Se si desidera creare una API che differisce in modo significativo da qualsiasi altra API esistente di .NET, sarà opportuno creare dei tipi runtime che saranno il tipo della cancellazione e delle rappresentazioni per i tipi forniti.
L'esempio in questo documento utilizza delle stringhe come rappresentazioni per gli oggetti forniti. Spesso, può essere opportuno utilizzare degli altri oggetti per le rappresentazioni. Ad esempio, è possibile utilizzare un dizionario come un elenco di proprietà:
ProvidedConstructor(parameters = [],
InvokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))
In alternativa, è possibile definire un tipo nel provider di tipi che verrà utilizzato in fase di esecuzione per formare la rappresentazione, con uno o più operazioni runtime:
type DataObject() =
let data = Dictionary<string,obj>()
member x.RuntimeOperation() = data.Count
I membri forniti possono quindi creare delle istanze di questo tipo di oggetto:
ProvidedConstructor(parameters = [],
InvokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))
In questo caso, è possibile (eventualmente) utilizzare questo tipo come tipo della cancellazione specificando questo tipo come baseType quando si crea ProvidedTypeDefinition:
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
Lezioni principali
La sezione precedente ha illustrato come creare un semplice provider di tipi cancellati che fornisce un intervallo dei tipi, di proprietà e di metodi. Questa sezione ha illustrato il concetto di cancellazione di tipo, inclusi i vantaggi e gli svantaggi di fornire i tipi cancellati da un provider di tipi e ha trattato le rappresentazioni dei tipi cancellati.
Un Provider di Tipi che utilizza Parametri Statici
La possibilità di parametrizzare il provider di tipi con dati statici apre numerosi scenari interessanti, anche nei casi in cui il provider non necessita di accedere a nessun dato locale o remoto. In questa sezione, verranno illustrate alcune tecniche di base per mettere insieme un tale provider.
Provider Checked Regex di Tipi
Si supponga di voler implementare un provider di tipi per le espressioni regolari che eseguono il wrapping delle librerie .NET Regex in un'interfaccia che fornisce le seguenti garanzie in fase di compilazione:
Verificare se un'espressione regolare è valida.
Fornire delle proprietà denominate nelle corrispondenze basate su tutti i nomi del gruppo nell'espressione regolare.
In questa sezione viene illustrato come utilizzare i provider di tipi per creare un tipo RegExProviderType che il modello dell'espressione regolare parametrizza per fornire questi vantaggi. Il compilatore visualizzerà un errore se il modello specificato non è valido e il provider di tipi può estrarre i gruppi dal modello in modo che sia possibile accedervi utilizzando le proprietà denominate nelle corrispondenze. Quando si progetta un provider di tipi, è necessario considerare come l'API relativo dovrebbe apparire agli utenti finali e come questo progetto verrà tradotto in codice .NET. Il seguente esempio mostra come utilizzare tale API per ottenere i componenti del prefisso:
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"
Il seguente esempio illustra 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"
Osservare i punti seguenti:
Il tipo standard Regex rappresenta il tipo parametrizzato RegexTyped.
Il costruttore di RegexTyped comporta una chiamata al costruttore di Regex, passandogli l'argomento di tipo statico per il modello.
I risultati del metodo Match sono rappresentati dal tipo standard Match.
Ogni gruppo denominato risulta in una proprietà fornita e accedere alla proprietà genera un utilizzo di un indicizzatore sulla raccolta Groups di una corrispondenza.
Il codice seguente è il centro logico per implementare tale provider e in questo esempio viene omessa l'aggiunta di tutti i membri del tipo fornito. Per informazioni su ogni membro aggiunto, vedere la sezione appropriata più avanti in questo topic. Per il codice completo, scaricare l'esempio dal Pacchetto di esempio F# 3,0 sul sito Web Codeplex.
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 ()
Osservare i punti seguenti:
Il provider di tipi accetta due parametri statici: il pattern, che è obbligatorio e le options, che sono facoltative (poiché un valore predefinito viene fornito).
Dopo che gli argomenti statici sono stati forniti, creare un'istanza dell'espressione regolare. Questa istanza genera un'eccezione se il Regex ha un formato non valido e questo errore sarà riportato agli utenti.
All'interno della callback DefineStaticParameters, definire il tipo che verrà restituito dopo che gli argomenti sono stati forniti.
Questo codice imposta HideObjectMethods a true così l'utilizzo di IntelliSense resterà semplificato. Questo attributo fa sì che Equals, GetHashCode, Finalize e i membri di GetType siano eliminati dagli elenchi di IntelliSense per l'oggetto specificato.
Si è utilizzato obj come tipo di base del metodo, ma verrà utilizzato un oggetto Regex come rappresentazione runtime di questo tipo, come illustrato nell'esempio seguente.
La chiamata al costruttore di Regex genera una ArgumentException quando l'espressione regolare non è valida. Il compilatore rileva questa eccezione e segnala un messaggio di errore all'utente in fase di compilazione o all'interno dell'editor di Visual Studio. Questa eccezione consente alle espressioni regolari di essere convalidate senza eseguire un'applicazione.
Il tipo definito in precedenza non è ancora utile perché non contiene metodi o proprietà importanti. Innanzitutto, aggiungere un metodo statico IsMatch:
let isMatch = ProvidedMethod(
methodName = "IsMatch",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = typeof<bool>,
IsStaticMethod = 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 bool. L'unica parte difficile è l'utilizzo di argomenti args all'interno della definizione di InvokeCode. In questo esempio, args è una lista di quotation 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 semplicemente tutti gli argomenti espliciti al metodo. Si noti che il tipo del valore tra virgolette deve corrispondere al tipo restituito specificato (in questo caso, bool). Si noti inoltre che questo codice utilizza il metodo AddXmlDoc per assicurarsi che il metodo fornito abbia una documentazione utile, che può essere erogata da IntelliSense.
Successivamente, aggiungere un metodo Match di istanza. Tuttavia, questo metodo deve restituire un valore di un tipo specifico Match in modo che si possa accedere ai gruppi in modo fortemente tipizzato. Pertanto, prima è necessario dichiarare il tipo Match. Poiché questo tipo dipende dal modello fornito come argomento statico, questo tipo deve essere annidato all'interno della definizione del tipo parametrizzato:
let matchTy = ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
HideObjectMethods = true)
ty.AddMember matchTy
Quindi aggiungere una proprietà al tipo Match per ogni gruppo. In fase di esecuzione, un match viene rappresentato come un valore Match, pertanto la quotation che definisce la proprietà deve utilizzare la proprietà indicizzata Groups per ottenere il gruppo appropriato.
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
Nuovamente, notare che si sta aggiungendo la documentazione XML nella proprietà fornita. Inoltre si noti che una proprietà può essere letta se una funzione GetterCode viene fornita e la proprietà può essere scritta se una funzione SetterCode viene fornita, pertanto la proprietà risultante è di sola lettura.
Ora è possibile creare un metodo di istanza che restituisce un valore di questo tipo Match :
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 crea un metodo di istanza, args.[0] rappresenta l'istanza RegexTyped in cui il metodo viene chiamato e args.[1] è l'argomento di input.
Infine, fornire un costruttore in modo che queste istanze del tipo fornito possano essere create.
let ctor = ProvidedConstructor(
parameters = [],
InvokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)
ctor.AddXmlDoc("Initializes a regular expression instance.")
ty.AddMember ctor
Il costruttore semplicemente esegue la cancellazione alla creazione di una istanza standard di Regex di .NET, che è nuovamente sottoposta al boxing in un oggetto perché obj è la cancellazione del tipo specificato. Con tale modifica, l'utilizzo dell'API che è stato specificato precedentemente in questo topic lavora 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>,
IsStaticMethod = 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 occurence 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 principali
In questa sezione è stato illustrato come creare un provider di tipi che agisce sui parametri statici. Il provider controlla il parametro statico e fornisce operazioni basate sul suo valore.
Un Provider di Tipi supportato da Dati Locali
Frequentemente si vuole che il provider di tipi presenti delle API basate non solo sui parametri statici ma anche sulle informazioni da sistemi locali o remoti. In questa sezione vengono descritti i provider di tipi basati sui dati locali, come i file di dati locali.
Semplice Provider per File CSV
Come semplice esempio, si consideri un provider di tipi per accedere a dati scientifici col formato Comma Separated Value (CSV). In questa sezione si presuppone che i file CSV contengano una riga di intestazione seguita dai dati a virgola mobile, come illustrato nella tabella riportata di seguito:
Distanza (metro) |
Tempo (secondo) |
---|---|
50.0 |
3.7 |
100.0 |
5.2 |
150.0 |
6.4 |
In questa sezione viene illustrato come fornire un tipo che è possibile utilizzare per ottenere le righe con una proprietà Distance di tipo float<meter> e una proprietà Time di tipo float<second>. Per semplicità, vengono fatte le seguenti assunzioni:
I nomi di intestazione sono senza unità o presentano il formato "Nome (unità)" e non contengono virgole.
Le unità sono tutte unità del Sistema Internazionale (SI) come il modulo Modulo di itNames di Microsoft.FSharp.Data.UnitSystems.SI.Un (F#) le definisce.
Le unità sono tutte semplici (ad esempio metro) anziché composte (ad esempio metro/secondo).
Tutte le colonne contengono dati a virgola mobile.
Un provider più completo allenterebbe queste restrizioni.
Ancora una volta il primo passaggio è quello di considerare come l'API deve apparire. Dato un file info.csv con i contenuti della tabella precedente (nel formato separazione-con-virgola), gli utenti del provider possono scrivere codice simile al seguente esempio:
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn "%f" (float time)
In questo caso, il compilatore deve convertire queste chiamate in qualcosa di simile all'esempio seguente:
let info = new MiniCsvFile("info.csv")
for row in info.Data do
let (time:float) = row.[1]
printfn "%f" (float time)
La conversione ottimale richiede al provider di tipi di definire un tipo reale CsvFile nell'assembly del provider di tipi. I provider di tipi sono spesso basati su alcuni tipi e metodi di supporto per includere la logica di importanza. Poiché le misure verranno cancellate in fase di esecuzione, è possibile utilizzare float[] come tipo cancellato per una riga. Il compilatore considererà le colonne diverse come aventi differenti tipi di misura. Ad esempio, la prima colonna nell'esempio è di tipo float<meter> e la seconda è di tipo float<second>. Tuttavia, la rappresentazione cancellata rimane abbastanza semplice.
Nel codice riportato di seguito viene visualizzato il centro 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 do
yield line.Split(',') |> Array.map float }
|> Seq.cache
member __.Data = data
[<TypeProvider>]
type public MiniCsvProvider(cfg:TypeProviderConfig) as this =
inherit TypeProviderForNamespaces()
// 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 runtime.
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])
Tenere presente i seguenti punti sull'implementazione:
I costruttori sottoposti a overload consentono al file originale o a uno che ha uno schema identico di essere letto. Questo modello è comune quando si scrive un provider di tipi per dati con origine locale o remota e questo modello consente a un file locale di essere utilizzato come modello per i dati remoti.
È possibile utilizzare il valore TypeProviderConfig che viene passato al costruttore del provider di tipi per risolvere i nomi dei file relativi.
È possibile utilizzare il metodo AddDefinitionLocation per definire la posizione delle proprietà specificate. Pertanto, se si utilizza Vai a definizione su una proprietà fornita, il file CSV verrà aperto in Visual Studio.
È possibile utilizzare il tipo ProvidedMeasureBuilder per trovare le unità di misura del SI e per generare i relativi tipi float<_>.
Lezioni principali
In questa sezione è stato illustrato come creare un provider di tipi per un'origine dati locale con un semplice schema che è contenuto esso stesso nell'origine dati.
Approfondimenti
Nelle sezioni seguenti sono inclusi dei suggerimenti per ulteriori informazioni.
Uno sguardo al Codice Compilato per i Tipi Cancellati
Per fornire un'idea di utilizzo del provider di tipi corrispondente al codice generato, guardare la seguente funzione utilizzando HelloWorldTypeProvider usato precedentemente in questo topic.
let function1 () =
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
obj1.InstanceProperty
Di seguito vi è un'immagine del codice risultante decompilato mediante 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 soltanto le operazioni sui tipi runtime coinvolti.
Progettazione e Convenzioni di Denominazione per i Provider di Tipi
Osservare le seguenti convenzioni quando si creano i provider di tipi.
Provider per i Protocolli di Connettività
In genere i nomi della maggior parte delle DLL dei provider per i protocolli di connettività di dati e servizi, come OData o connessioni SQL, devono terminare in TypeProvider o in TypeProviders. Ad esempio, utilizzare un nome per la DLL simile alla stringa seguente:
Fabrikam.Management.BasicTypeProviders.dll
Assicurarsi che i tipi forniti siano membri del namespace corrispondente e indicare il protocollo di connettività che si è implementato:
Fabrikam.Management.BasicTypeProviders.WmiConnection<…> Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>
Provider di Utilità per Codifica Generale
Per un provider di tipi di utilità come quello per le espressioni regolari, il provider di tipi può essere incluso in una libreria di base, come illustrato nel seguente esempio:
#r "Fabrikam.Core.Text.Utilities.dll"
In questo caso, il tipo fornito apparirebbe in un punto appropriato in base alle convenzioni standard di progettazione di .NET:
open Fabrikam.Core.Text.RegexTyped let regex = new RegexTyped<"a+b+a+b+">()
Origini Dati Singleton
Alcuni provider di tipi si connettono a una singola origine dati dedicata e forniscono soltanto i dati. In questo caso, è necessario rilasciare il suffisso TypeProvider e utilizzare le normali convenzioni di denominazione di .NET:
#r "Fabrikam.Data.Freebase.dll" let data = Fabrikam.Data.Freebase.Astronomy.Asteroids
Per ulteriori informazioni, vedere la convenzione di progettazione GetConnection che viene descritta più avanti in questo topic.
Modelli di Progettazione per i Provider di Tipi
Nelle sezioni seguenti vengono descritti i modelli di progettazione che si possono utilizzare per la creazione di provider di tipi.
Il Modello di Progettazione GetConnection
La maggior parte dei provider di tipi devono essere scritti per utilizzare il modello GetConnection utilizzato dai provider di tipi in FSharp.Data.TypeProviders.dll, come illustrato nel seguente esempio:
#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 supportato da Dati Remoti e Servizi
Prima di creare un provider di tipi supportato dai dati remoti e dai servizi, è necessario considerare un intervallo di problemi che sono relativi alla programmazione connessa. Questi problemi includono le seguenti considerazioni:
mapping dello schema
liveness e invalidazione in presenza di modifiche dello schema
memorizzazione nella cache dello schema
implementazioni asincrone di operazioni con accesso ai dati
query di supporto, incluse le query LINQ
credenziali e autenticazione
Questo topic non esamina ulteriormente questi problemi.
Tecniche Aggiuntive di Creazione
Quando si scrivono i propri provider di tipi, è necessario utilizzare le seguenti tecniche aggiuntive.
Creazione dei Tipi e dei Membri su Richiesta
L'API ProvidedType dispone di una versione ritardata di AddMember.
type ProvidedType = member AddMemberDelayed : (unit -> MemberInfo) -> unit member AddMembersDelayed : (unit -> MemberInfo list) -> unit
Queste versioni vengono utilizzate per creare gli spazi dei tipi su richiesta.
Fornire tipi Matrice, ByRef e Puntatore
Si creano i membri forniti (le cui firme includono tipi matrice, tipi byref e istanze di tipi generici) utilizzando MakeArrayType, MakePointerType e MakeGenericType su qualsiasi istanza System.Type, inclusa ProvidedTypeDefinitions.
Fornire le Annotazioni delle Unità di Misura
L'API ProvidedTypes fornisce degli helper per fornire le annotazioni di misura. Ad esempio, per fornire il tipo float<kg>, utilizzare 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>>, utilizzare 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 |]
Accedere al Progetto Locale o allo Script di Risorse Locali
Ad ogni istanza di un provider di tipi può essere assegnato un valore TypeProviderConfig durante la costruzione. Questo valore contiene la "cartella di soluzione" per il provider (ovvero la cartella del progetto per la compilazione o la directory contenente uno script), la lista dell'assembly a cui si fa riferimento e altre informazioni.
Invalidazione
I provider possono generare segnali di invalidazione per notificare al servizio del linguaggio F# che le assunzioni dello schema potrebbero essere cambiate. Quando una invalidazione si verifica, un typecheck viene ripetuto se il provider è ospitato in Visual Studio. Questo segnale verrà ignorato quando il provider è ospitato in F# Interactive o dal compilatore di F# (fsc.exe).
Informazioni sul Caching dello Schema
I provider devono spesso accedere alla cache per le informazioni dello schema. I dati memorizzati nella cache devono essere archiviati utilizzando un nome di file che viene specificato come parametro statico o come dato dall'utente. Un esempio di schema di caching è il parametro LocalSchemaFile nei provider di tipi nell'assembly FSharp.Data.TypeProviders. Nell'implementazione di questi provider, questo parametro statico dirige il provider di tipi a utilizzare le informazioni dello schema nel file locale specificato anziché accedere all'origine dati attraverso la rete. Per utilizzare le informazioni dello schema memorizzate nella cache, è anche necessario impostare il parametro statico ForceUpdate a false. È possibile utilizzare una tecnica simile per consentire l'accesso ai dati online e offline.
Assembly di Supporto
Quando si compila un file .dll o un file .exe, il file .dll di supporto per i tipi generati viene collegato all'interno dell'assembly risultante. Questo collegamento viene creato copiando le definizioni del tipo di Intermediate Language (IL) e ogni risorsa gestita dall'assembly di supporto all'interno dell'assembly finale. Quando si utilizza F# Interactive, il file .dll di supporto non viene copiato ma viene caricato direttamente nel processo di F# Interactive.
Eccezioni e Diagnostica dei Provider di Tipi
Tutti gli utilizzi di tutti i membri dai tipi forniti possono generare eccezioni. In tutti i casi, se un provider di tipi genera un'eccezione, il compilatore dell'host attribuisce l'errore a un provider di tipi specifico.
Le eccezioni dei provider di tipi non devono mai risultare come errori interni del compilatore.
I provider di tipi non possono segnalare warning.
Quando un provider di tipi è ospitato nel compilatore di F#, in un ambiente di sviluppo di F#, o in F# Interactive, tutte le eccezioni del provider vengono rilevate. La proprietà del messaggio contiene sempre il testo dell'errore e non la traccia dello stack. Se si desidera generare un'eccezione, è possibile generare gli esempi seguenti:
Specificare dei Tipi Generati
Finora, questo documento ha illustrato come fornire i tipi cancellati. È inoltre possibile utilizzare il meccanismo dei provider di tipi in F# per fornire i tipi generati, che vengono aggiunti alle reali definizioni di tipo di .NET nel programma applicativo. È necessario fare riferimento ai tipi forniti generati tramite una definizione di tipo.
open Microsoft.FSharp.TypeProviders
type Service = ODataService<" http://services.odata.org/Northwind/Northwind.svc/">
Il codice di supporto ProvidedTypes-0.2 che fa parte della versione di F# 3.0 dispone di un supporto limitato per fornire i tipi generati. Le istruzioni seguenti devono essere vere per una definizione di tipi generati:
IsErased deve essere impostato a false.
Il provider deve disporre di un assembly con un file di supporto .dll di .NET corrispondente al file .dll sul disco.
È inoltre necessario chiamare ConvertToGenerated in un tipo radice fornito i cui tipi annidati formano un set chiuso di tipi generati. Questa chiamata genera la definizione di tipi forniti specificata e le relative definizioni dei tipi annidati in un assembly e modifica la proprietà Assembly di qualsiasi definizione di tipi forniti per restituire tale assembly. L'assembly viene generato solamente quando la proprietà dell'assembly nel tipo radice viene acceduta per la prima volta. Il compilatore dell'host di F# accede alla proprietà quando processa una dichiarazione di tipo generata per il tipo.
Regole e Limitazioni
Quando si scrivono i provider di tipi, tenere presente le seguenti regole e limitazioni.
I tipi forniti devono essere raggiungibili.
Tutti i tipi forniti devono essere raggiungibili da tipi non annidati. I tipi non annidati sono riportati nella chiamata al costruttore TypeProviderForNamespaces o in una chiamata ad AddNamespace. Ad esempio, se il provider fornisce un tipo StaticClass.P : T, è necessario assicurarsi che T sia un tipo non annidato o annidato sotto un altro.
Ad esempio, alcuni provider dispongono di una classe statica come DataTypes che contiene i tipi T1, T2, T3, .... In caso contrario, l'errore indica che un riferimento al tipo T nell'assembly A è stato trovato, ma il tipo non è stato trovato in tale assembly. Se viene visualizzato questo errore, verificare che tutti i sottotipi possano essere raggiunti dai provider di tipi. Nota: Questi tipi T1, T2, T3... vengono definiti come tipi on-the-fly. Ricordarsi di inserirli in un namespace accessibile o in un tipo padre.
Limitazioni del Meccanismo del Provider di Tipi
Il meccanismo del provider di tipi in F# dispone delle seguenti limitazioni:
L'infrastruttura sottostante per i provider di tipi in F# non supporta tipi generici forniti o metodi generici forniti.
Il meccanismo non supporta i tipi annidati con parametri statici.
Limitazioni del Codice di Supporto di ProvidedTypes
Il codice di supporto ProvidedTypes ha le seguenti regole e limitazioni:
Le proprietà fornite con getters e setters indicizzati non vengono implementate.
Gli eventi forniti non sono implementati.
I tipi forniti e gli oggetti di informazione devono essere utilizzati solo per il meccanismo del provider di tipi in F#. Questi non sono in generale utilizzabili come oggetti System.Type.
I costrutti utilizzabili nelle quotation che definiscono le implementazioni dei metodi presentano alcune limitazioni. È possibile fare riferimento al codice sorgente per ProvidedTypes-Version per individuare i costrutti supportati nelle citazioni.
I provider di tipo devono generare assembly di output che sono file DLL, non file EXE.
Suggerimenti per lo sviluppo
È possibile trovare i seguenti utili suggerimenti durante il processo di sviluppo.
Eseguire due istanze di Visual Studio. È possibile sviluppare il provider di tipi in un'istanza e testare il provider nell'altra perché il test IDE bloccherà il file .dll per impedire al provider di tipi di essere ricompilato. 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 che il provider è stato compilato.
Eseguire il debug del provider di tipi tramite chiamate a fsc.exe. È possibile richiamare il provider di tipi utilizzando gli strumenti seguenti:
fsc.exe (Il compilatore da linea di comando di F#)
fsi.exe (Il compilatore di F# Interactive)
devenv.exe (Visual Studio)
È possibile eseguire il debug del provider di tipi più facilmente utilizzando fsc.exe in un file di script di test (ad esempio script.fsx). È possibile avviare il debugger da un prompt dei comandi.
devenv /debugexe fsc.exe script.fsx
È possibile utilizzare la registrazione print-to-stdout.