Tutorial: criar um provedor de tipos
O mecanismo de provedor de tipos em F# é uma parte significativa do suporte dessa plataforma para programação avançada em informações. Este tutorial explica como criar provedores de tipos próprios orientando você no desenvolvimento de vários provedores de tipos simples a fim de ilustrar os conceitos básicos. Para obter mais informações sobre o mecanismo de provedor de tipos em F#, confira Provedores de Tipos.
O ecossistema F# contém uma série de provedores de tipos para serviços de dados corporativos e de Internet usados com frequência. Por exemplo:
O FSharp.Data inclui provedores de tipos para formatos de documento JSON, XML, CSV e HTML.
O SwaggerProvider inclui dois provedores de tipos generativos que geram o modelo de objeto e os clientes HTTP para APIs descritos pelos esquemas OpenApi 3.0 e Swagger 2.0.
O FSharp.Data.SqlClient tem um conjunto de provedores de tipos para a inserção marcada em tempo de compilação do T-SQL em F#.
Você pode criar provedores de tipo personalizados ou pode fazer referência a provedores de tipo criados por outros. Por exemplo, suponha que sua organização tenha um serviço de dados que forneça um crescente número de conjuntos de dados nomeados, cada um com um esquema próprio de dados estáveis. Você pode criar um provedor de tipos que leia os esquemas e apresente os conjuntos de dados atuais para o programador de uma forma fortemente tipada.
Antes de iniciar
O mecanismo de provedor de tipos foi projetado principalmente para injetar espaços de informações de serviço e dados estáveis na experiência de programação do F#.
Esse mecanismo não foi projetado para injetar espaços de informações cujo esquema é alterado durante a execução do programa de maneiras relevantes para a lógica do programa. Além disso, o mecanismo não foi projetado para meta-programação dentro da linguagem, embora esse domínio contenha alguns usos válidos. Você deve usar esse mecanismo somente quando necessário e onde o desenvolvimento de um provedor de tipos gera um valor muito alto.
Você deve evitar escrever um provedor de tipos em que um esquema não esteja disponível. Da mesma forma, você deve evitar escrever um provedor de tipos em que uma biblioteca .NET comum (ou até mesmo existente) seria suficiente.
Antes de começar, você pode fazer as seguintes perguntas:
Você tem um esquema para sua fonte de informações? Nesse caso, qual é o mapeamento para o sistema de tipos F# e .NET?
Você pode usar uma API existente (tipada dinamicamente) como ponto de partida para sua implementação?
Você e sua organização terão usos suficientes do provedor de tipos para fazer a gravação valer a pena? Uma biblioteca .NET normal atenderia às suas necessidades?
Quanto seu esquema será alterado?
Ele será alterado durante a codificação?
Ele será alterado entre as sessões de codificação?
Ele será alterado durante a execução do programa?
Os provedores de tipos são mais adequados para situações em que o esquema é estável em tempo de execução e durante o tempo de vida do código compilado.
Um provedor de tipos simples
Este exemplo do Samples.HelloWorldTypeProvider, é semelhante aos exemplos no diretório examples
do SDK do Provedor de Tipos F#. O provedor disponibiliza um "espaço de tipos" que contém 100 tipos apagados, como mostra o código a seguir usando a sintaxe de assinatura F# e omitindo os detalhes de todos, exceto Type1
. Para obter mais informações sobre tipos apagados, confira Detalhes sobre tipos fornecidos apagados mais adiante neste tópico.
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 =
…
Observe que o conjunto de tipos e membros fornecidos é estaticamente conhecido. Este exemplo não aproveita a capacidade dos provedores de fornecer tipos que dependem de um esquema. A implementação do provedor de tipos é descrita no código a seguir e os detalhes são abordados em seções posteriores deste tópico.
Aviso
Pode haver diferenças entre esse código e os exemplos 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()
Para usar esse provedor, abra uma instância separada do Visual Studio, crie um script F# e adicione uma referência ao provedor do script usando #r, como mostra o seguinte código:
#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
Em seguida, procure os tipos no namespace Samples.HelloWorldTypeProvider
que o provedor de tipos gerou.
Antes de recompilar o provedor, verifique se você fechou todas as instâncias do Visual Studio e do F# Interativo que estão usando a DLL do provedor. Caso contrário, ocorrerá um erro de compilação porque a DLL de saída será bloqueada.
Para depurar esse provedor usando instruções de impressão, faça um script que exponha um problema com o provedor e use o seguinte código:
fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Para depurar esse provedor usando o Visual Studio, abra o Prompt de Comando do Desenvolvedor para Visual Studio com credenciais administrativas e execute o seguinte comando:
devenv.exe /debugexe fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Como alternativa, abra o Visual Studio, acesse o menu Depurar, escolha Debug/Attach to process…
e anexe a outro processo devenv
em que você está editando seu script. Usando esse método, você pode direcionar mais facilmente uma lógica específica no provedor de tipos digitando interativamente expressões na segunda instância (com IntelliSense completo e outros recursos).
Você pode desabilitar a depuração Apenas Meu Código para identificar melhor os erros no código gerado. Para obter informações sobre como habilitar ou desabilitar esse recurso, confira Navegar pelo Código com o Depurador. Além disso, você também pode definir a captura de exceção de primeira chance abrindo o menu Debug
e escolhendo Exceptions
ou selecionando as teclas Ctrl+Alt+E para abrir a caixa de diálogo Exceptions
. Nessa caixa de diálogo, em Common Language Runtime Exceptions
, marque a caixa de seleção Thrown
.
Implementação do Provedor de Tipos
Esta seção orienta você nas seções principais da implementação do provedor de tipo. Primeiro, você define o tipo do próprio provedor de tipos personalizado:
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
Esse tipo deve ser público e você deve marcá-lo com o atributo TypeProvider para que o compilador reconheça o provedor de tipos quando outro projeto F# tentar referenciar o assembly que contém o tipo. O parâmetro config é opcional e, se presente, contém informações de configuração contextuais para a instância do provedor de tipos que o compilador F# cria.
Em seguida, você implementa a interface ITypeProvider. Nesse caso, você usa o tipo TypeProviderForNamespaces
da API ProvidedTypes
como um tipo base. Esse tipo auxiliar pode fornecer uma coleção finita de namespaces fornecidos antecipadamente, cada um dos quais contém diretamente um número finito de tipos fixos fornecidos antecipadamente. Nesse contexto, o provedor gera tipos antecipadamente, mesmo que não sejam necessários ou usados.
inherit TypeProviderForNamespaces(config)
Em seguida, defina valores privados locais que especificam o namespace para os tipos fornecidos e localize o próprio assembly do provedor de tipos. Esse assembly é usado posteriormente como o tipo pai lógico dos tipos fornecidos apagados.
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
Em seguida, crie uma função para fornecer cada um dos tipos Type1... Tipo100. Essa função é explicada mais detalhadamente mais adiante neste tópico.
let makeOneProvidedType (n:int) = …
Em seguida, gere os 100 tipos fornecidos:
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
Então, adicione os tipos como um namespace fornecido:
do this.AddNamespace(namespaceName, types)
Por fim, adicione um atributo de assembly que indica que você está criando uma DLL do provedor de tipos:
[<assembly:TypeProviderAssembly>]
do()
Fornecendo um tipo e seus membros
A função makeOneProvidedType
faz o trabalho real de fornecer um dos tipos.
let makeOneProvidedType (n:int) =
…
Esta etapa explica a implementação dessa função. Primeiro, crie o tipo fornecido (por exemplo, Type1, quando n = 1 ou 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>)
Você deve observar os seguintes pontos:
Esse tipo fornecido é apagado. Como você indica que o tipo base é
obj
, as instâncias aparecerão como valores do tipo obj no código compilado.Ao especificar um tipo não aninhado, você deve especificar o assembly e o namespace. Para tipos apagados, o assembly deve ser o próprio assembly do provedor de tipos.
Em seguida, adicione a documentação XML ao tipo. Essa documentação é atrasada, ou seja, calculada sob demanda se o compilador de host precisar dela.
t.AddXmlDocDelayed (fun () -> $"""This provided type {"Type" + string n}""")
Em seguida, adicione uma propriedade estática fornecida ao tipo:
let staticProp = ProvidedProperty(propertyName = "StaticProperty",
propertyType = typeof<string>,
isStatic = true,
getterCode = (fun args -> <@@ "Hello!" @@>))
Obter essa propriedade sempre resultará na cadeia de caracteres "Olá!". A GetterCode
da propriedade usa uma citação da F#, que representa o código que o compilador de host gera para obter a propriedade. Para obter mais informações sobre citações, confira Citações de Código (F#).
Adicione a documentação XML à propriedade.
staticProp.AddXmlDocDelayed(fun () -> "This is a static property")
Agora, anexe a propriedade fornecida ao tipo fornecido. Você deve anexar um membro fornecido a apenas um tipo. Caso contrário, o membro nunca estará acessível.
t.AddMember staticProp
Agora, crie um construtor fornecido que não usa parâmetros.
let ctor = ProvidedConstructor(parameters = [ ],
invokeCode = (fun args -> <@@ "The object data" :> obj @@>))
O InvokeCode
do construtor retorna uma citação do F#, que representa o código que o compilador de host gera quando o construtor é chamado. Por exemplo, você pode usar o seguinte construtor:
new Type10()
Uma instância do tipo fornecido será criada com os dados subjacentes "Os dados do objeto". O código entre aspas inclui uma conversão para obj porque esse tipo é a eliminação desse tipo fornecido (como você especificou quando declarou o tipo fornecido).
Adicione a documentação XML ao construtor e adicione o construtor fornecido ao tipo fornecido:
ctor.AddXmlDocDelayed(fun () -> "This is a constructor")
t.AddMember ctor
Crie um segundo construtor fornecido que usa um parâmetro:
let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
invokeCode = (fun args -> <@@ (%%(args[0]) : string) :> obj @@>))
O construtor InvokeCode
retorna novamente uma citação do F#, que representa o código que o compilador de host gerou para uma chamada ao método. Por exemplo, você pode usar o seguinte construtor:
new Type10("ten")
Uma instância do tipo fornecido é criada com os dados subjacentes "dez". Talvez você já tenha notado que a função InvokeCode
retorna uma citação. A entrada para essa função é uma lista de expressões, uma por parâmetro do construtor. Nesse caso, uma expressão que representa o valor de parâmetro único está disponível em args[0]
. O código de uma chamada ao construtor força o valor de retorno para o tipo obj
apagado. Depois de adicionar o segundo construtor fornecido ao tipo, você cria uma propriedade de instância fornecida:
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
Obter essa propriedade retornará o comprimento da cadeia de caracteres, que é o objeto de representação. A propriedade GetterCode
retorna uma citação do F# que especifica o código que o compilador de host gera para obter a propriedade. Assim como InvokeCode
, a função GetterCode
retorna uma citação. O compilador de host chama essa função com uma lista de argumentos. Nesse caso, os argumentos incluem apenas a única expressão que representa a instância na qual o getter está sendo chamado, que você pode acessar usando args[0]
. A implementação de GetterCode
então se junta à citação resultante no tipo apagado obj
e uma conversão é usada para atender ao mecanismo do compilador para verificar os tipos de que o objeto é uma cadeia de caracteres. A próxima parte de makeOneProvidedType
fornece um método de instância com um parâmetro.
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
Por fim, crie um tipo aninhado que contenha 100 propriedades aninhadas. A criação desse tipo aninhado e das respectivas propriedades dele é atrasada, ou seja, computada sob demanda.
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])
Detalhes sobre tipos fornecidos apagados
O exemplo nesta seção fornece apenas tipos fornecidos apagados, que são particularmente úteis nas seguintes situações:
Quando você está escrevendo um provedor para um espaço de informações que contém apenas dados e métodos.
Quando você está escrevendo um provedor em que a semântica precisa do tipo runtime não é essencial para o uso prático do espaço de informações.
Quando você está escrevendo um provedor para um espaço de informações tão grande e interconectado que tecnicamente não é viável gerar tipos reais do .NET para o espaço de informações.
Neste exemplo, cada tipo fornecido é apagado para o tipo obj
e todos os usos do tipo aparecerão como tipo obj
no código compilado. Na verdade, os objetos subjacentes nesses exemplos são cadeias de caracteres, mas o tipo aparecerá como System.Object
no código compilado do .NET. Assim como acontece com todos os usos de apagamento de tipo, você pode usar a conversão boxing, conversão unboxing e conversão regular explícitas para subverter os tipos apagados. Nesse caso, pode ocorrer uma exceção de conversão inválida quando o objeto é usado. Um runtime de provedor pode definir um tipo próprio de representação privada para ajudar na proteção contra falsas representações. Você não pode definir tipos apagados no próprio F#. Somente tipos fornecidos podem ser apagados. Você deve entender as ramificações, práticas e semânticas, de usar tipos apagados para seu provedor de tipos ou um provedor que fornece tipos apagados. Um tipo apagado não tem um tipo .NET real. Portanto, você não pode fazer uma reflexão precisa sobre o tipo e pode subverter tipos apagados se usar conversões de runtime e outras técnicas que dependem da semântica exata do tipo de runtime. A subversão de tipos apagados frequentemente resulta em exceções de conversão de tipo em tempo de execução.
Como escolher representações para tipos fornecidos apagados
Para alguns usos de tipos fornecidos apagados, nenhuma representação é necessária. Por exemplo, o tipo fornecido apagado pode conter apenas propriedades estáticas e membros e nenhum construtor, e nenhum método ou propriedades retornaria uma instância do tipo. Se você puder acessar instâncias de um tipo fornecido apagado, considere as seguintes perguntas:
O que é o apagamento de um tipo fornecido?
O apagamento de um tipo fornecido é como o tipo aparece no código .NET compilado.
O apagamento de um tipo de classe apagado fornecido é sempre o primeiro tipo base não apagado na cadeia de herança do tipo.
O apagamento de um tipo de interface apagado fornecido é sempre
System.Object
.
Quais são as representações de um tipo fornecido?
- O conjunto de objetos possíveis para um tipo fornecido apagado é chamado de representações do tipo. No exemplo neste documento, as representações de todos os tipos fornecidos apagados
Type1..Type100
são sempre objetos de cadeia de caracteres.
Todas as representações de um tipo fornecido devem ser compatíveis com o apagamento do tipo fornecido. (Caso contrário, o compilador F# vai gerar um erro de uso do provedor de tipos ou será gerado um código .NET não verificável inválido. Um provedor de tipo não será válido se retornar o código que fornece uma representação que não seja válida.)
Você pode escolher uma representação para objetos fornecidos usando qualquer uma das seguintes abordagens, ambas muito comuns:
Se você estiver simplesmente fornecendo um wrapper fortemente tipado sobre um tipo .NET existente, geralmente faz sentido que seu tipo se apague para esse tipo, use instâncias desse tipo como representações ou ambos. Essa abordagem é apropriada quando a maioria dos métodos existentes nesse tipo ainda faz sentido ao usar a versão fortemente tipada.
Se você quiser criar uma API que difere significativamente de qualquer API .NET existente, faz sentido criar tipos de runtime que serão a eliminação de tipo e representações para os tipos fornecidos.
O exemplo neste documento usa cadeias de caracteres como representações de objetos fornecidos. Com frequência, pode ser apropriado usar outros objetos para representações. Por exemplo, você pode usar um dicionário como um recipiente de propriedades:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))
Como alternativa, você pode definir um tipo no provedor de tipos que será usado em tempo de execução para formar a representação, juntamente com uma ou mais operações de runtime:
type DataObject() =
let data = Dictionary<string,obj>()
member x.RuntimeOperation() = data.Count
Os membros fornecidos podem, então, construir instâncias desse tipo de objeto:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))
Nesse caso, você pode (opcionalmente) usar esse tipo como o apagamento de tipo especificando esse tipo como o baseType
ao construir ProvidedTypeDefinition
:
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
Principais lições
A seção anterior explicava como criar um provedor de tipo de apagamento simples que fornece um intervalo de tipos, propriedades e métodos. Esta seção também explicou o conceito de apagamento de tipo, incluindo algumas das vantagens e desvantagens de fornecer tipos apagados de um provedor de tipos e discutiu representações de tipos apagados.
Um provedor de tipo que usa parâmetros estáticos
A capacidade de parametrizar provedores de tipo por dados estáticos permite muitos cenários interessantes, mesmo nos casos em que o provedor não precisa acessar dados locais ou remotos. Nesta seção, você aprenderá algumas técnicas básicas para montar esse provedor.
Tipo de provedor Regex verificado
Imagine que você deseja implementar um provedor de tipos para expressões regulares que encapsula as bibliotecas .NET Regex em uma interface que fornece as seguintes garantias de tempo de compilação:
Como verificar se uma expressão regular é válida.
Fornecendo propriedades nomeadas em correspondências baseadas em nomes de grupo na expressão regular.
Esta seção mostra como usar provedores de tipos para criar um tipo RegexTyped
que o padrão de expressão regular parametriza a fim de fornecer esses benefícios. O compilador relatará um erro se o padrão fornecido não for válido e o provedor de tipos poderá extrair os grupos do padrão para que você possa acessá-los usando propriedades nomeadas em correspondências. Ao criar um provedor de tipo, você deve considerar como a API exposta deve ser para os usuários finais e como esse design será traduzido para o código .NET. O seguinte exemplo mostra como usar essa API para obter os componentes do código de área:
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"
O seguinte exemplo mostra como o provedor de tipos converte essas chamadas:
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"
Observe os seguintes pontos:
O tipo Regex padrão representa o tipo parametrizado
RegexTyped
.O construtor
RegexTyped
resulta em uma chamada para o construtor Regex, passando o argumento de tipo estático para o padrão.Os resultados do método
Match
são representados pelo tipo padrão Match.Cada grupo nomeado resulta em uma propriedade fornecida e acessar a propriedade resulta em um uso de um indexador na coleção de uma correspondência
Groups
.
O código a seguir é o núcleo da lógica para implementar esse provedor, e este exemplo omite a adição de todos os membros ao tipo fornecido. Para obter informações sobre cada membro adicionado, confira a seção apropriada mais adiante neste tópico.
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 ()
Observe os seguintes pontos:
O provedor de tipo usa dois parâmetros estáticos:
pattern
, que é obrigatório, eoptions
, que são opcionais (porque um valor padrão é fornecido).Depois que os argumentos estáticos forem fornecidos, você criará uma instância da expressão regular. Essa instância vai gerar uma exceção se o Regex estiver malformado e esse erro for relatado aos usuários.
No retorno de chamada
DefineStaticParameters
, você define o tipo que será retornado depois que os argumentos forem fornecidos.Esse código se define
HideObjectMethods
como true para que a experiência do IntelliSense permaneça simplificada. Esse atributo faz com que os membrosEquals
,GetHashCode
,Finalize
eGetType
sejam suprimidos das listas do IntelliSense de um objeto fornecido.Você usa
obj
como o tipo base do método, mas usará um objetoRegex
como a representação de runtime desse tipo, como mostra o próximo exemplo.A chamada para o construtor
Regex
gera um ArgumentException quando a expressão regular não é válida. O compilador captura essa exceção e relata uma mensagem de erro para o usuário no momento da compilação ou no editor do Visual Studio. Essa exceção permite que expressões regulares sejam validadas sem executar um aplicativo.
O tipo definido acima ainda não é útil porque não contém métodos ou propriedades significativas. Primeiro, adicione um método estático 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
O código anterior define um método IsMatch
, que usa uma cadeia de caracteres como entrada e retorna uma bool
. A única parte complicada é o uso do argumento args
dentro da definição InvokeCode
. Neste exemplo, args
é uma lista de citações que representa os argumentos para esse método. Se o método for um método de instância, o primeiro argumento representará o argumento this
. No entanto, para um método estático, os argumentos são apenas os argumentos explícitos do método. Observe que o tipo do valor citado deve corresponder ao tipo de retorno especificado (nesse caso, bool
). Observe também que esse código usa o método AddXmlDoc
para garantir que o método fornecido também tenha uma documentação útil, que você pode fornecer por meio do IntelliSense.
Em seguida, adicione um método de correspondência de instância. No entanto, esse método deve retornar um valor de um tipo fornecido Match
para que os grupos possam ser acessados de maneira fortemente tipada. Assim, primeiro você declara o tipo Match
. Como esse tipo depende do padrão fornecido como um argumento estático, ele deve ser aninhado dentro da definição de tipo parametrizado:
let matchTy =
ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
hideObjectMethods = true)
ty.AddMember matchTy
Em seguida, você adiciona uma propriedade ao tipo de correspondência de cada grupo. Em tempo de execução, uma correspondência é representada como um valor Match, portanto, a citação que define a propriedade deve usar a propriedade indexada Groups para obter o grupo relevante.
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
Novamente, observe que você está adicionando a documentação XML à propriedade fornecida. Observe também que uma propriedade pode ser lida se uma função GetterCode
for fornecida e a propriedade poderá ser gravada se uma função SetterCode
for fornecida, portanto, a propriedade resultante será somente leitura.
Agora você pode criar um método de instância que retorna um valor desse 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
Como você está criando um método de instância, args[0]
representa a instância RegexTyped
na qual o método está sendo chamado e args[1]
é o argumento de entrada.
Por fim, forneça um construtor para que as instâncias do tipo fornecido possam ser criadas.
let ctor =
ProvidedConstructor(
parameters = [],
invokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)
ctor.AddXmlDoc("Initializes a regular expression instance.")
ty.AddMember ctor
O construtor simplesmente apaga a criação de uma instância padrão do Regex do .NET, que é novamente demarcada em um objeto porque obj
é o apagamento do tipo fornecido. Com essa alteração, o uso de API de exemplo especificado anteriormente no tópico funciona conforme o esperado. O seguinte código é completo e final:
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 ()
Principais lições
Esta seção explicou como criar um provedor de tipos que opera sobre os parâmetros estáticos dele. O provedor verifica o parâmetro estático e fornece operações com base no valor dele.
Um provedor de tipos que é apoiado por dados locais
Frequentemente, você pode querer que os provedores de tipos apresentem APIs com base não apenas em parâmetros estáticos, mas também em informações de sistemas locais ou remotos. Esta seção discute provedores de tipos baseados em dados locais, como arquivos de dados locais.
Provedor de arquivos CSV simples
Como um exemplo simples, considere um provedor de tipos para acessar dados científicos no formato CSV (Valor Separado por Vírgulas). Esta seção pressupõe que os arquivos CSV contenham uma linha de cabeçalho seguida de dados de ponto flutuante, como ilustra a seguinte tabela:
Distância (metro) | Hora (segundo) |
---|---|
50,0 | 3.7 |
100.0 | 5.2 |
150.0 | 6.4 |
Esta seção mostra como fornecer um tipo que você pode usar para obter linhas com uma propriedade Distance
do tipo float<meter>
e uma propriedade Time
do tipo float<second>
. Para simplificar, as seguintes suposições são feitas:
Os nomes de cabeçalho são sem unidade ou têm o formulário "Nome (unidade)" e não contêm vírgulas.
As unidades são todas unidades do System International (SI), como define o módulo Data.UnitSystems.SI.UnitNames (F#).
As unidades são todas simples (por exemplo, metro) em vez de compostas (por exemplo, metro/segundo).
Todas as colunas contêm dados de ponto flutuante.
Um provedor mais completo atenuaria essas restrições.
Novamente, a primeira etapa é considerar a aparência da API. Dado um arquivo info.csv
com o conteúdo da tabela anterior (em formato separado por vírgulas), os usuários do provedor devem ser capazes de escrever código semelhante ao seguinte exemplo:
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn $"{float time}"
Nesse caso, o compilador deve converter essas chamadas em algo semelhante ao seguinte exemplo:
let info = new CsvFile("info.csv")
for row in info.Data do
let (time:float) = row[1]
printfn $"%f{float time}"
A tradução ideal exigirá que o provedor de tipos defina um tipo real CsvFile
no assembly do provedor de tipos. Os provedores de tipos geralmente dependem de alguns tipos auxiliares e métodos para encapsular uma lógica importante. Como as medidas são apagadas em tempo de execução, você pode usar um float[]
como o tipo apagado de uma linha. O compilador tratará diferentes colunas como tendo tipos de medida diferentes. Por exemplo, a primeira coluna em nosso exemplo tem tipo float<meter>
e a segunda tem tipo float<second>
. No entanto, a representação apagada pode permanecer bastante simples.
O seguinte código mostra a implementação dessa classe.
// 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])
Observe os seguintes pontos sobre a implementação:
Construtores sobrecarregados permitem que o arquivo original ou aquele que tem um esquema idêntico seja lido. Esse padrão é comum quando você escreve um provedor de tipo para fontes de dados locais ou remotas, e esse padrão permite que um arquivo local seja usado como modelo para dados remotos.
Você pode usar o valor TypeProviderConfig que é passado para o construtor do provedor de tipo para resolver nomes de arquivo relativos.
Você pode usar o método
AddDefinitionLocation
para definir o local das propriedades fornecidas. Portanto, se você usarGo To Definition
em uma propriedade fornecida, o arquivo CSV será aberto no Visual Studio.Você pode usar o tipo
ProvidedMeasureBuilder
para pesquisar as unidades do SI e gerar os tipos relevantesfloat<_>
.
Principais lições
Esta seção explicou como criar um provedor de tipos para uma fonte de dados local com um esquema simples contido na própria fonte de dados.
Aprofundamento
As seções a seguir incluem sugestões para mais estudos.
Uma olhada no código compilado para tipos apagados
Para dar uma ideia de como o uso do provedor de tipos corresponde ao código emitido, examine a função a seguir usando o HelloWorldTypeProvider
empregado anteriormente neste tópico.
let function1 () =
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
obj1.InstanceProperty
Aqui está uma imagem do código resultante descompilado 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
Como mostra o exemplo, todas as menções do tipo Type1
e da propriedade InstanceProperty
foram apagadas, deixando apenas operações nos tipos de runtime envolvidos.
Convenções de design e nomenclatura para provedores de tipo
Observe as convenções a seguir ao criar provedores de tipo.
Provedores de Protocolos de Conectividade Em geral, os nomes da maioria das DLLs do provedor para protocolos de conectividade de dados e serviços, como conexões OData ou SQL, devem terminar em TypeProvider
ou TypeProviders
. Por exemplo, use um nome de DLL semelhante à seguinte cadeia de caracteres:
Fabrikam.Management.BasicTypeProviders.dll
Verifique se os tipos fornecidos são membros do namespace correspondente e indique o protocolo de conectividade implementado:
Fabrikam.Management.BasicTypeProviders.WmiConnection<…>
Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>
Provedores de utilitários para codificação geral. Para um provedor de tipos de utilitário como esse para expressões regulares, o provedor de tipos pode fazer parte de uma biblioteca base, como mostra o seguinte exemplo:
#r "Fabrikam.Core.Text.Utilities.dll"
Nesse caso, o tipo fornecido aparecerá em um ponto apropriado de acordo com convenções normais de design do .NET:
open Fabrikam.Core.Text.RegexTyped
let regex = new RegexTyped<"a+b+a+b+">()
Fontes de dados Singleton. Alguns provedores de tipos se conectam a apenas uma fonte de dados dedicada e fornecem apenas dados. Nesse caso, você deve descartar o sufixo TypeProvider
e usar convenções normais para nomenclatura do .NET:
#r "Fabrikam.Data.Freebase.dll"
let data = Fabrikam.Data.Freebase.Astronomy.Asteroids
Para obter mais informações, confira a convenção de design GetConnection
descrita posteriormente neste tópico.
Padrões de design para provedores de tipo
As seções a seguir descrevem padrões de design que você pode usar ao criar provedores de tipos.
O padrão de design GetConnection
A maioria dos provedores de tipos deve ser gravada para usar o padrão GetConnection
, usado pelos provedores de tipos na FSharp.Data.TypeProviders.dll, como mostra o seguinte exemplo:
#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
Provedores de tipos apoiados por dados e serviços remotos
Antes de criar um provedor de tipos com suporte de dados e serviços remotos, você deve considerar uma série de problemas inerentes à programação conectada. Esses problemas incluem as seguintes considerações:
Mapeamento de esquema
Dinamismo e invalidação na presença de alterações no esquema
Armazenamento em cache do esquema
Implementações assíncronas de operações de acesso a dados
Consultas de apoio, incluindo consultas LINQ
Credenciais e autenticação
Este tópico não explora mais esses problemas.
Técnicas de criação adicionais
Ao escrever seus próprios provedores de tipos, convém usar as seguintes técnicas adicionais.
Criando tipos e membros sob demanda
A API ProvidedType atrasou as versões do AddMember.
type ProvidedType =
member AddMemberDelayed : (unit -> MemberInfo) -> unit
member AddMembersDelayed : (unit -> MemberInfo list) -> unit
Essas versões são usadas para criar espaços de tipos sob demanda.
Fornecendo tipos de matriz e instanciações de tipo genérico
Você faz membros fornecidos (cujas assinaturas incluem tipos de matriz, tipos byref e instanciações de tipos genéricos) usando o normal MakeArrayType
, MakePointerType
e MakeGenericType
em qualquer instância de Type, incluindo ProvidedTypeDefinitions
.
Observação
Em alguns casos, talvez seja necessário usar o auxiliar em ProvidedTypeBuilder.MakeGenericType
. Confira a documentação do SDK do Provedor de Tipos para obter mais detalhes.
Fornecendo uma unidade de anotações de medida
A API ProvidedTypes fornece auxiliares para fornecer anotações de medida. Por exemplo, para fornecer o tipo float<kg>
, use o seguinte código:
let measures = ProvidedMeasureBuilder.Default
let kg = measures.SI "kilogram"
let m = measures.SI "meter"
let float_kg = measures.AnnotateType(typeof<float>,[kg])
Para fornecer o tipo Nullable<decimal<kg/m^2>>
, use o seguinte código:
let kgpm2 = measures.Ratio(kg, measures.Square m)
let dkgpm2 = measures.AnnotateType(typeof<decimal>,[kgpm2])
let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]
Acessando recursos de Project-Local ou Script-Local
Cada instância de um provedor de tipo pode receber um valor TypeProviderConfig
durante a construção. Esse valor contém a "pasta de resolução" do provedor (ou seja, a pasta do projeto para a compilação ou o diretório que contém um script), a lista de assemblies referenciados e outras informações.
Invalidação
Os provedores podem gerar sinais de invalidação para notificar o serviço da linguagem F# de que as suposições de esquema podem ter sido alteradas. Quando ocorre a invalidação, a verificação de tipo é refeita se o provedor está sendo hospedado no Visual Studio. Esse sinal será ignorado quando o provedor estiver hospedado no F# Interativo ou pelo Compilador F# (fsc.exe).
Informações do esquema de cache
Os provedores geralmente devem armazenar em cache o acesso às informações do esquema. Os dados armazenados em cache devem ser armazenados usando um nome de arquivo fornecido como um parâmetro estático ou como dados do usuário. Um exemplo de cache de esquema é o parâmetro LocalSchemaFile
nos provedores de tipos do assembly FSharp.Data.TypeProviders
. Na implementação desses provedores, esse parâmetro estático orienta o provedor de tipos a usar as informações de esquema no arquivo local especificado em vez de acessar a fonte de dados pela rede. Para usar informações de esquema em cache, você também deve definir o parâmetro estático ForceUpdate
como false
. Você pode usar uma técnica semelhante para habilitar o acesso a dados online e offline.
Assembly de suporte
Quando você compila um arquivo .dll
ou .exe
, o arquivo de .dll de suporte dos tipos gerados é vinculado estaticamente ao assembly resultante. Esse link é criado copiando as definições de tipo IL (Linguagem Intermediária) e todos os recursos gerenciados do assembly de suporte para o assembly final. Quando você usa o F# Interativo, o arquivo de .dll de suporte não é copiado se, em vez disso, é carregado diretamente no processo do F# Interativo.
Exceções e diagnósticos de provedores de tipos
Todos os usos de todos os membros de tipos fornecidos podem gerar exceções. Em todos os casos, se um provedor de tipos gerar uma exceção, o compilador de host atribui o erro a um provedor de tipos específico.
As exceções do provedor de tipos nunca devem resultar em erros internos do compilador.
Os provedores de tipos não podem relatar avisos.
Quando um provedor de tipo é hospedado no compilador F#, em um ambiente de desenvolvimento F# ou no F# Interativo, todas as exceções desse provedor são capturadas. A propriedade Message é sempre o texto de erro e nenhum rastreamento de pilha é exibido. Se você vai gerar uma exceção, é possível gerar os seguintes exemplos:
System.NotSupportedException
,System.IO.IOException
eSystem.Exception
.
Fornecendo tipos gerados
Até agora, este documento explicou como fornecer tipos apagados. Você também pode usar o mecanismo de provedor de tipos em F# para fornecer tipos gerados, que são adicionados como definições reais de tipo .NET no programa dos usuários. Você deve se referir aos tipos fornecidos gerados usando uma definição de tipo.
open Microsoft.FSharp.TypeProviders
type Service = ODataService<"http://services.odata.org/Northwind/Northwind.svc/">
O código auxiliar ProvidedTypes-0.2 que faz parte da versão F# 3.0 tem suporte limitado apenas para fornecer tipos gerados. As seguintes instruções devem ser verdadeiras para uma definição de tipo gerada:
isErased
deve ser definido comofalse
.O tipo gerado deve ser adicionado a um
ProvidedAssembly()
recém-construído, que representa um contêiner para fragmentos de código gerados.O provedor deve ter um assembly que tenha um arquivo .NET .dll de suporte real com um arquivo de .dll correspondente no disco.
Regras e limitações
Ao escrever provedores de tipos, tenha em mente as regras e limitações a seguir.
Os tipos fornecidos devem ser acessíveis
Todos os tipos fornecidos devem ser acessíveis por meio dos tipos não aninhados. Os tipos não aninhados são dados na chamada do construtor TypeProviderForNamespaces
ou na chamada de AddNamespace
. Por exemplo, se o provedor fornecer um tipo StaticClass.P : T
, você deverá garantir que T seja um tipo não aninhado ou aninhado em apenas um.
Por exemplo, alguns provedores têm uma classe estática, como DataTypes
, a que contém esses tipos T1, T2, T3, ...
. Caso contrário, o erro diz que uma referência ao tipo T no assembly A foi encontrada, mas o tipo não pôde ser encontrado nesse assembly. Se esse erro for exibido, verifique se todos os seus subtipos podem ser acessados por meio dos tipos do provedor. Observação: esses tipos T1, T2, T3...
são chamados de tipos on-the-fly. Lembre-se de colocá-los em um namespace acessível ou em um tipo pai.
Limitações do mecanismo de provedor de tipos
O mecanismo de provedor de tipos em F# tem as seguintes limitações:
A infraestrutura subjacente para provedores de tipo no F# não dá suporte a tipos genéricos fornecidos ou métodos genéricos fornecidos.
O mecanismo não dá suporte a tipos aninhados com parâmetros estáticos.
Dicas de desenvolvimento
Você pode achar as seguintes dicas úteis durante o processo de desenvolvimento:
Executar duas instâncias do Visual Studio
Você pode desenvolver o provedor de tipos em uma instância e testar o provedor na outra porque o IDE de teste terá um bloqueio no arquivo .dll que impede que o provedor de tipos seja recriado. Portanto, você deve fechar a segunda instância do Visual Studio enquanto o provedor é criado na primeira instância e, em seguida, você deve reabrir a segunda instância após a criação do provedor.
Depurar provedores de tipos usando invocações de fsc.exe
Você pode invocar provedores de tipos usando as seguintes ferramentas:
fsc.exe (compilador de linha de comando do F#)
fsi.exe (compilador do F# Interativo)
devenv.exe (Visual Studio)
Geralmente, você pode depurar provedores de tipo mais facilmente usando fsc.exe em um arquivo de script de teste (por exemplo, script.fsx). Você pode iniciar um depurador por meio de um prompt de comando.
devenv /debugexe fsc.exe script.fsx
Você pode usar o registro em log de impressão para stdout.