Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de changer d’annuaire.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer d’annuaire.
Le mécanisme de fournisseur de types en F# fait partie intégrante de sa prise en charge de la programmation riche en informations. Ce tutoriel explique comment créer vos propres fournisseurs de types en vous aidant au développement de plusieurs fournisseurs de types simples pour illustrer les concepts de base. Pour plus d’informations sur le mécanisme de fournisseur de types en F#, consultez Fournisseurs de types.
L’écosystème F# contient une gamme de fournisseurs de types pour les services de données Internet et d’entreprise couramment utilisés. Par exemple:
FSharp.Data inclut des fournisseurs de types pour les formats de documents JSON, XML, CSV et HTML.
SwaggerProvider inclut deux fournisseurs de types de génération qui génèrent un modèle objet et des clients HTTP pour les API décrites par les schémas OpenApi 3.0 et Swagger 2.0.
FSharp.Data.SqlClient a un ensemble de fournisseurs de types pour l’incorporation vérifiée au moment de la compilation de T-SQL dans F#.
Vous pouvez créer des fournisseurs de types personnalisés ou référencer des fournisseurs de types que d’autres ont créés. Par exemple, votre organisation peut avoir un service de données qui fournit un grand nombre et croissant de jeux de données nommés, chacun avec son propre schéma de données stable. Vous pouvez créer un fournisseur de types qui lit les schémas et présente les jeux de données actuels au programmeur de manière fortement typée.
Avant de commencer
Le mécanisme de fournisseur de types est principalement conçu pour injecter des espaces d’informations de service et de données stables dans l’expérience de programmation F#.
Ce mécanisme n’est pas conçu pour injecter des espaces d’information dont le schéma change pendant l’exécution du programme de manière appropriée à la logique du programme. En outre, le mécanisme n’est pas conçu pour la méta-programmation intra-langage, même si ce domaine contient certaines utilisations valides. Vous devez utiliser ce mécanisme uniquement lorsqu’il nécessaire et lorsque le développement d’un fournisseur de type génère une valeur très élevée.
Vous devez éviter d’écrire un fournisseur de types où un schéma n’est pas disponible. De même, vous devez éviter d’écrire un fournisseur de types où une bibliothèque .NET ordinaire (ou même existante) suffirait.
Avant de commencer, vous pouvez poser les questions suivantes :
Avez-vous un schéma pour votre source d’informations ? Si c’est le cas, quel est le mappage dans le système de type F# et .NET ?
Pouvez-vous utiliser une API existante (typée dynamiquement) comme point de départ pour votre implémentation ?
Est-ce que vous et votre organisation aurez suffisamment d’utilisations du fournisseur de type pour que l’écriture en vaut la peine ? Une bibliothèque .NET normale répond-elle à vos besoins ?
Combien votre schéma change-t-il ?
Va-t-il changer pendant le codage ?
Va-t-il changer entre les sessions de codage ?
Va-t-il changer pendant l’exécution du programme ?
Les fournisseurs de types conviennent mieux aux situations où le schéma est stable au moment de l’exécution et pendant la durée de vie du code compilé.
Un fournisseur de type simple
Cet exemple est Samples.HelloWorldTypeProvider, similaire aux exemples dans le examples répertoire du Kit de développement logiciel (SDK) du fournisseur de types F#. Le fournisseur met à disposition un « espace de type » qui contient 100 types effacés, comme le montre le code suivant à l’aide de la syntaxe de signature F# et omettant les détails pour tous à l’exception Type1. Pour plus d’informations sur les types supprimés, consultez Détails sur les types fournis supprimés plus tard dans ce sujet.
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 =
…
Notez que l’ensemble de types et de membres fournis est connu statiquement. Cet exemple ne tire pas parti de la capacité des fournisseurs à fournir des types qui dépendent d’un schéma. L’implémentation du fournisseur de types est décrite dans le code suivant et les détails sont abordés dans les sections ultérieures de cette rubrique.
Avertissement
Il peut y avoir des différences entre ce code et les exemples en ligne.
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()
Pour utiliser ce fournisseur, ouvrez une instance distincte de Visual Studio, créez un script F#, puis ajoutez une référence au fournisseur à partir de votre script à l’aide de #r comme le montre le code suivant :
#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
Recherchez ensuite les types sous l’espace de noms Samples.HelloWorldTypeProvider que le fournisseur de type a générés.
Avant de recompiler le fournisseur, vérifiez que vous avez fermé toutes les instances de Visual Studio et F# Interactive qui utilisent la DLL du fournisseur. Sinon, une erreur de génération se produit, car la DLL de sortie est verrouillée.
Pour déboguer ce fournisseur à l’aide d’instructions d’impression, créez un script qui expose un problème avec le fournisseur, puis utilisez le code suivant :
fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Pour déboguer ce fournisseur à l’aide de Visual Studio, ouvrez l’invite de commandes développeur pour Visual Studio avec des informations d’identification d’administration, puis exécutez la commande suivante :
devenv.exe /debugexe fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
En guise d’alternative, ouvrez Visual Studio, ouvrez le menu Débogage, choisissez Debug/Attach to process…et joignez-vous à un autre devenv processus dans lequel vous modifiez votre script. À l’aide de cette méthode, vous pouvez cibler plus facilement une logique particulière dans le fournisseur de types en tapant de manière interactive des expressions dans la deuxième instance (avec IntelliSense complet et d’autres fonctionnalités).
Vous pouvez désactiver le débogage Just My Code pour mieux identifier les erreurs dans le code généré. Pour plus d’informations sur l’activation ou la désactivation de cette fonctionnalité, consultez Navigation dans le code avec le débogueur. En outre, vous pouvez également définir l’interception des exceptions de première chance en ouvrant le menu Debug, puis en choisissant Exceptions, ou en choisissant les touches Ctrl+Alt+E pour ouvrir la boîte de dialogue Exceptions. Dans cette boîte de dialogue, sous Common Language Runtime Exceptions, activez la Thrown case à cocher.
Implémentation du fournisseur de type
Cette section vous guide tout au long des sections principales de l’implémentation du fournisseur de type. Tout d’abord, vous définissez le type pour le fournisseur de types personnalisé lui-même :
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
Ce type doit être public et vous devez le marquer avec l’attribut TypeProvider afin que le compilateur reconnaisse le fournisseur de types lorsqu’un projet F# distinct fait référence à l’assembly qui contient le type. Le paramètre de configuration est facultatif et, s’il est présent, contient des informations de configuration contextuelles pour l’instance de fournisseur de types que le compilateur F# crée.
Ensuite, vous implémentez l’interface ITypeProvider . Dans ce cas, vous utilisez le TypeProviderForNamespaces type de l’API ProvidedTypes comme type de base. Ce type auxiliaire peut fournir une collection finie d’espaces de noms rapidement disponibles, chacun contenant directement un nombre fini de types fixes, eux aussi disponibles rapidement. Dans ce contexte, le fournisseur génère avec impatience des types même s’ils ne sont pas nécessaires ou utilisés.
inherit TypeProviderForNamespaces(config)
Ensuite, définissez des valeurs privées locales qui spécifient l’espace de noms pour les types fournis et recherchez l’assembly du fournisseur de types lui-même. Cet assemblage est utilisé ultérieurement comme type parent logique des types supprimés fournis.
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
Ensuite, créez une fonction pour fournir chacun des types Type1... Type100. Cette fonction est expliquée plus en détail plus loin dans cette rubrique.
let makeOneProvidedType (n:int) = …
Ensuite, générez les 100 types fournis :
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
Puis, ajoutez les types en tant qu’espace de noms fourni :
do this.AddNamespace(namespaceName, types)
Enfin, ajoutez un attribut d’assemblage qui indique que vous créez une DLL pour fournisseur de type :
[<assembly:TypeProviderAssembly>]
do()
Fournir un type et ses membres
La makeOneProvidedType fonction effectue le travail réel de fournir l’un des types.
let makeOneProvidedType (n:int) =
…
Cette étape explique l’implémentation de cette fonction. Tout d’abord, créez le type fourni (par exemple, Type1, quand n = 1 ou Type57, quand 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>)
Notez les points suivants :
Ce type fourni est effacé. Étant donné que vous indiquez que le type de base est
obj, les instances apparaissent sous forme de valeurs de type obj dans le code compilé.Lorsque vous spécifiez un type non imbriqué, vous devez spécifier l’assembly et l’espace de noms. Pour les types effacés, l’assembly doit être l’assembly du fournisseur de type lui-même.
Ensuite, ajoutez la documentation XML au type. Cette documentation est retardée, c’est-à-dire calculée à la demande si le compilateur hôte en a besoin.
t.AddXmlDocDelayed (fun () -> $"""This provided type {"Type" + string n}""")
Ensuite, vous ajoutez une propriété statique fournie au type :
let staticProp = ProvidedProperty(propertyName = "StaticProperty",
propertyType = typeof<string>,
isStatic = true,
getterCode = (fun args -> <@@ "Hello!" @@>))
L’obtention de cette propriété correspond toujours à la chaîne « Hello! ». La GetterCode propriété utilise un guillemet F#, qui représente le code généré par le compilateur hôte pour obtenir la propriété. Pour plus d’informations sur les quotations, consultez Quotations de code (F#).
Ajoutez la documentation XML à la propriété.
staticProp.AddXmlDocDelayed(fun () -> "This is a static property")
Joignez maintenant la propriété fournie au type fourni. Vous devez joindre un membre fourni à un seul type uniquement. Sinon, le membre ne sera jamais accessible.
t.AddMember staticProp
Créez maintenant un constructeur fourni qui ne prend aucun paramètre.
let ctor = ProvidedConstructor(parameters = [ ],
invokeCode = (fun args -> <@@ "The object data" :> obj @@>))
Le InvokeCode pour le constructeur retourne une quotation F#, qui représente le code que le compilateur hôte génère lorsque le constructeur est appelé. Par exemple, vous pouvez utiliser le constructeur suivant :
new Type10()
Une instance du type fourni est créée avec des données sous-jacentes « Données d’objet ». Le code entre guillemets inclut une conversion en obj , car ce type est l’effacement de ce type fourni (comme vous l’avez spécifié lorsque vous avez déclaré le type fourni).
Ajoutez la documentation XML au constructeur et ajoutez le constructeur fourni au type fourni :
ctor.AddXmlDocDelayed(fun () -> "This is a constructor")
t.AddMember ctor
Créez un deuxième constructeur fourni qui accepte un paramètre :
let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
invokeCode = (fun args -> <@@ (%%(args[0]) : string) :> obj @@>))
Le InvokeCode du constructeur renvoie de nouveau une citation F#, qui représente le code généré par le compilateur hôte pour un appel à la méthode. Par exemple, vous pouvez utiliser le constructeur suivant :
new Type10("ten")
Une instance du type fourni est créée avec les données sous-jacentes « ten ». Vous avez peut-être déjà remarqué que la InvokeCode fonction retourne un devis. L’entrée de cette fonction est une liste d’expressions, une par paramètre de constructeur. Dans ce cas, une expression qui représente la valeur de paramètre unique est disponible dans args[0]. Le code d’un appel au constructeur force la valeur renvoyée au type objeffacé. Après avoir ajouté le deuxième constructeur fourni au type, vous créez une propriété d’instance fournie :
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
L’obtention de cette propriété retourne la longueur de la chaîne, qui est l’objet de représentation. La propriété GetterCode retourne une expression F# qui spécifie le code généré par le compilateur hôte pour obtenir la propriété. Comme InvokeCode, la fonction GetterCode retourne une citation. Le compilateur hôte appelle cette fonction avec une liste d’arguments. Dans ce cas, les arguments incluent uniquement l'expression unique qui représente l'instance sur laquelle le getter est appelé, à laquelle vous pouvez accéder à l'aide de args[0]. L’implémentation de GetterCode se joint ensuite à la quotation de résultat au niveau du type objeffacé et un cast est utilisé pour satisfaire le mécanisme du compilateur pour vérifier les types dont l’objet est une chaîne. La partie suivante de makeOneProvidedType fournit une méthode d’instance avec un paramètre.
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
Enfin, créez un type imbriqué qui contient 100 propriétés imbriquées. La création de ce type imbriqué et ses propriétés sont retardées, c’est-à-dire calculées à la demande.
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])
Détails sur les types fournis effacés
L’exemple de cette section fournit uniquement les types fournis effacés, qui sont particulièrement utiles dans les situations suivantes :
Lorsque vous écrivez un fournisseur pour un espace d’informations qui contient uniquement des données et des méthodes.
Lorsque vous écrivez un fournisseur où une sémantique de type runtime précise n’est pas critique pour une utilisation pratique de l’espace d’informations.
Lorsque vous écrivez un fournisseur pour un espace d’information si volumineux et interconnecté qu’il n’est pas techniquement possible de générer des types .NET réels pour l’espace d’informations.
Dans cet exemple, chaque type fourni est effacé au type obj, et toutes les utilisations du type s’affichent comme type obj dans le code compilé. En fait, les objets sous-jacents de ces exemples sont des chaînes, mais le type apparaît comme System.Object dans le code compilé .NET. Comme pour toutes les utilisations de l’effacement de type, vous pouvez utiliser le boxing, l’unboxing et le cast explicites pour subvertir les types effacés. Dans ce cas, une exception cast qui n’est pas valide peut se produire lorsque l’objet est utilisé. Un runtime de fournisseur peut définir son propre type de représentation privée pour vous protéger contre les fausses représentations. Vous ne pouvez pas définir de types effacés en F# lui-même. Seuls les types fournis peuvent être effacés. Vous devez comprendre les ramifications, pratiques et sémantiques, de l’utilisation de types effacés pour votre fournisseur de types ou d’un fournisseur qui fournit des types effacés. Un type effacé n’a pas de type .NET réel. Par conséquent, vous ne pouvez pas effectuer une réflexion précise sur le type, et vous pouvez subvertir les types effacés si vous utilisez des casts runtime et d’autres techniques qui s’appuient sur la sémantique exacte du type d’exécution. La subversion des types effacés entraîne fréquemment des exceptions de type cast au moment de l’exécution.
Choix des représentations pour les types fournis effacés
Pour certaines utilisations des types fournis effacés, aucune représentation n’est requise. Par exemple, le type fourni effacé peut contenir uniquement des propriétés statiques et des membres et aucun constructeur, et aucune méthode ou propriété ne retournerait une instance du type. Si vous pouvez atteindre des instances d’un type fourni effacé, vous devez prendre en compte les questions suivantes :
Qu’est-ce que l’effacement d’un type fourni ?
L’effacement d’un type fourni est la façon dont le type apparaît dans le code .NET compilé.
L’effacement d’un type de classe effacé fourni est toujours le premier type de base non effacé dans la chaîne d’héritage du type.
L’effacement d’un type d’interface effacé fourni est toujours
System.Object.
Quelles sont les représentations d’un type fourni ?
- L’ensemble d’objets possibles pour un type fourni effacé est appelé ses représentations. Dans l’exemple de ce document, les représentations de tous les types fournis effacés
Type1..Type100sont toujours des objets de chaîne.
Toutes les représentations d’un type fourni doivent être compatibles avec l’effacement du type fourni. (Sinon, le compilateur F# génèrera une erreur lors de l'utilisation d'un fournisseur de types, ou un code .NET non vérifiable sera généré. Un fournisseur de types n'est pas valide s'il retourne du code qui fournit une représentation invalide.)
Vous pouvez choisir une représentation pour les objets fournis à l’aide de l’une des approches suivantes, qui sont toutes deux très courantes :
Si vous fournissez simplement un wrapper fortement typé sur un type .NET existant, il est souvent plus judicieux pour votre type d’effacer ce type, d’utiliser des instances de ce type en tant que représentations, ou les deux. Cette approche est appropriée lorsque la plupart des méthodes existantes sur ce type ont toujours un sens lors de l’utilisation de la version fortement typée.
Si vous souhaitez créer une API qui diffère considérablement de n’importe quelle API .NET existante, il est judicieux de créer des types de runtime qui seront l’effacement du type et les représentations pour les types fournis.
L’exemple de ce document utilise des chaînes comme représentations d’objets fournis. Souvent, il peut être approprié d’utiliser d’autres objets pour les représentations. Par exemple, vous pouvez utiliser un dictionnaire comme un sac de propriétés :
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))
En guise d’alternative, vous pouvez définir un type dans votre fournisseur de types qui sera utilisé au moment de l’exécution pour former la représentation, ainsi qu’une ou plusieurs opérations d’exécution :
type DataObject() =
let data = Dictionary<string,obj>()
member x.RuntimeOperation() = data.Count
Les membres fournis peuvent ensuite construire des instances de ce type d’objet :
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))
Dans ce cas, vous pouvez (éventuellement) utiliser ce type comme effacement de type en spécifiant ce type comme baseType lors de la construction du ProvidedTypeDefinition:
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
Leçons clés
La section précédente a expliqué comment créer un fournisseur de types d’effacement simple qui fournit une plage de types, de propriétés et de méthodes. Cette section a également expliqué le concept d’effacement de type, y compris certains des avantages et inconvénients liés à l'offre de types effacés par un fournisseur de types, et a discuté des représentations des types effacés.
Fournisseur de type qui utilise des paramètres statiques
La possibilité de paramétrer des fournisseurs de types par des données statiques permet de nombreux scénarios intéressants, même si le fournisseur n’a pas besoin d’accéder à des données locales ou distantes. Dans cette section, vous allez apprendre quelques techniques de base pour la mise en place d’un tel fournisseur.
Fournisseur de type Regex vérifié
Imaginez que vous souhaitez implémenter un fournisseur de types pour les expressions régulières qui encapsulent les bibliothèques .NET Regex dans une interface qui fournit les garanties de compilation suivantes :
Vérifiez si une expression régulière est valide.
Fourniture de propriétés nommées sur des correspondances basées sur des noms de groupe dans l’expression régulière.
Cette section vous montre comment utiliser des fournisseurs de types pour créer un RegexTyped type paramétré par le modèle d’expression régulière pour fournir ces avantages. Le compilateur signale une erreur si le modèle fourni n’est pas valide et le fournisseur de types peut extraire les groupes du modèle afin que vous puissiez y accéder à l’aide de propriétés nommées sur les correspondances. Lorsque vous concevez un fournisseur de types, vous devez tenir compte de la façon dont son API exposée doit ressembler aux utilisateurs finaux et de la façon dont cette conception se traduit par du code .NET. L’exemple suivant montre comment utiliser une telle API pour obtenir les composants du code de zone :
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"
L’exemple suivant montre comment le fournisseur de type traduit ces appels :
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"
Notez les points suivants :
Le type Regex standard représente le type paramétrable
RegexTyped.Le
RegexTypedconstructeur entraîne un appel au constructeur Regex, en transmettant l'argument de type statique relatif au modèle.Les résultats de la
Matchméthode sont représentés par le type standard Match .Chaque groupe nommé génère une propriété fournie et l’accès à la propriété entraîne l’utilisation d’un indexeur sur la collection d’une correspondance
Groups.
Le code suivant est le cœur de la logique à implémenter un tel fournisseur, et cet exemple omet l’ajout de tous les membres au type fourni. Pour plus d’informations sur chaque membre ajouté, consultez la section appropriée plus loin dans cette rubrique.
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 ()
Notez les points suivants :
Le fournisseur de types accepte deux paramètres statiques : le
pattern, qui est obligatoire et leoptions, qui sont facultatifs (car une valeur par défaut est fournie).Une fois les arguments statiques fournis, vous créez une instance de l’expression régulière. Cette instance lève une exception si l’expression regex est malformée et cette erreur est signalée aux utilisateurs.
Dans le
DefineStaticParametersrappel, vous définissez le type qui sera retourné une fois les arguments fournis.Ce code définit
HideObjectMethodsla valeur true afin que l’expérience IntelliSense reste simplifiée. Cet attribut supprime les membresEquals,GetHashCode,FinalizeetGetTypedes listes IntelliSense pour un objet fourni.Vous utilisez
objcomme type de base de la méthode, mais vous utiliserez unRegexobjet comme représentation runtime de ce type, comme l’illustre l’exemple suivant.L’appel au constructeur
Regexlève un ArgumentException quand une expression régulière n’est pas valide. Le compilateur intercepte cette exception et signale un message d’erreur à l’utilisateur au moment de la compilation ou dans l’éditeur Visual Studio. Cette exception permet aux expressions régulières d’être validées sans exécuter d’application.
Le type défini ci-dessus n’est pas encore utile, car il ne contient pas de méthodes ou de propriétés significatives. Tout d’abord, ajoutez une méthode statique 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
Le code précédent définit une méthode IsMatch, qui prend une chaîne comme entrée et retourne un bool. La seule partie délicate est l’utilisation de l’argument args dans la InvokeCode définition. Dans cet exemple, args est une liste de guillemets qui représente les arguments de cette méthode. Si la méthode est une méthode d’instance, le premier argument représente l’argument this . Toutefois, pour une méthode statique, les arguments ne sont que les arguments explicites de la méthode. Notez que le type de la valeur entre guillemets doit correspondre au type de retour spécifié (dans ce cas, bool). Notez également que ce code utilise la AddXmlDoc méthode pour vous assurer que la méthode fournie dispose également d’une documentation utile, que vous pouvez fournir via IntelliSense.
Ensuite, ajoutez une méthode d'instance Match. Cependant, cette méthode doit retourner une valeur d’un type Match fourni afin que les groupes soient accessibles de manière fortement typée. Ainsi, vous déclarez d’abord le Match type. Étant donné que ce type dépend du modèle fourni en tant qu’argument statique, ce type doit être imbriqué dans la définition de type paramétrable :
let matchTy =
ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
hideObjectMethods = true)
ty.AddMember matchTy
Vous ajoutez ensuite une propriété au type Match pour chaque groupe. Au moment de l’exécution, une correspondance est représentée sous forme de valeur Match. Par conséquent, l'expression qui définit la propriété doit utiliser la propriété indexée Groups pour obtenir le groupe approprié.
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
Là encore, notez que vous ajoutez de la documentation XML à la propriété fournie. Notez également qu’une propriété peut être lue si une GetterCode fonction est fournie et que la propriété peut être écrite si une SetterCode fonction est fournie, de sorte que la propriété résultante est en lecture seule.
Vous pouvez maintenant créer une méthode d’instance qui retourne une valeur de ce Match type :
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
Étant donné que vous créez une méthode d’instance, args[0] représente l’instance RegexTyped sur laquelle la méthode est appelée et args[1] est l’argument d’entrée.
Enfin, fournissez un constructeur afin que les instances du type fourni puissent être créées.
let ctor =
ProvidedConstructor(
parameters = [],
invokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)
ctor.AddXmlDoc("Initializes a regular expression instance.")
ty.AddMember ctor
Le constructeur efface simplement la création d’une instance Regex .NET standard, qui est à nouveau boxée dans un objet, car obj est l’effacement du type fourni. Avec cette modification, l’exemple d’utilisation de l’API spécifié précédemment dans la rubrique fonctionne comme prévu. Le code suivant est complet et 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 ()
Leçons clés
Cette section a expliqué comment créer un fournisseur de types qui fonctionne sur ses paramètres statiques. Le fournisseur vérifie le paramètre statique et fournit des opérations en fonction de sa valeur.
Fournisseur de type qui est soutenu par des données locales
Souvent, vous souhaiterez peut-être que les fournisseurs de types présentent des API basées non seulement sur des paramètres statiques, mais également des informations provenant de systèmes locaux ou distants. Cette section décrit les fournisseurs de types basés sur des données locales, telles que des fichiers de données locaux.
Fournisseur de fichiers CSV simple
En guise d’exemple simple, considérez un fournisseur de types pour accéder aux données scientifiques au format CSV (Valeur séparée par des virgules). Cette section suppose que les fichiers CSV contiennent une ligne d’en-tête suivie de données à virgule flottante, comme le montre le tableau suivant :
| Distance (mètre) | Temps (seconde) |
|---|---|
| 50,0 | 3.7 |
| 100.0 | 5.2 |
| 150.0 | 6.4 |
Cette section montre comment fournir un type que vous pouvez utiliser pour obtenir des lignes avec une Distance propriété de type float<meter> et une Time propriété de type float<second>. Par souci de simplicité, les hypothèses suivantes sont faites :
Les noms d'en-tête sont sans unité ou ont le format « Nom (unité) » et ne contiennent pas de virgules.
Les unités sont toutes les unités System International (SI) que le module FSharp.Data.UnitSystems.SI.UnitNames Module (F#) définit.
Les unités sont toutes simples (par exemple, compteur) plutôt que composées (par exemple, compteur/seconde).
Toutes les colonnes contiennent des données à virgule flottante.
Un fournisseur plus complet relâche ces restrictions.
Là encore, la première étape consiste à prendre en compte l’apparence de l’API. Étant donné un info.csv fichier contenant le contenu du tableau précédent (au format séparé par des virgules), les utilisateurs du fournisseur doivent pouvoir écrire du code semblable à l’exemple suivant :
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn $"{float time}"
Dans ce cas, le compilateur doit convertir ces appels en quelque chose comme l’exemple suivant :
let info = new CsvFile("info.csv")
for row in info.Data do
let (time:float) = row[1]
printfn $"%f{float time}"
La traduction optimale nécessite que le fournisseur de types définisse un type réel CsvFile dans l’assembly du fournisseur de types. Les fournisseurs de types s’appuient souvent sur quelques types d’assistance et méthodes pour encapsuler une logique importante. Étant donné que les mesures sont supprimées au moment de l'exécution, vous pouvez utiliser un float[] comme type supprimé pour une ligne. Le compilateur traite différentes colonnes comme ayant des types de mesures différents. Par exemple, la première colonne de notre exemple a le type float<meter>et la seconde a float<second>. Toutefois, la représentation effacée peut rester assez simple.
Le code suivant montre le cœur de l’implémentation.
// 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])
Notez les points suivants concernant l’implémentation :
Les constructeurs surchargés permettent de lire soit le fichier d'origine, soit un fichier dont le schéma est identique. Ce modèle est courant lorsque vous écrivez un fournisseur de types pour des sources de données locales ou distantes, et ce modèle permet à un fichier local d’être utilisé comme modèle pour les données distantes.
Vous pouvez utiliser la valeur TypeProviderConfig transmise au constructeur de fournisseur de types pour résoudre les noms de fichiers relatifs.
Vous pouvez utiliser la
AddDefinitionLocationméthode pour définir l’emplacement des propriétés fournies. Par conséquent, si vous utilisezGo To Definitionsur une propriété fournie, le fichier CSV s’ouvre dans Visual Studio.Vous pouvez utiliser le
ProvidedMeasureBuildertype pour rechercher les unités SI et générer les types appropriésfloat<_>.
Leçons clés
Cette section a expliqué comment créer un fournisseur de types pour une source de données locale avec un schéma simple contenu dans la source de données elle-même.
Aller plus loin
Les sections suivantes incluent des suggestions pour une étude plus approfondie.
Regardez le code compilé pour les types effacés
Pour vous donner une idée de la façon dont l’utilisation du fournisseur de types correspond au code émis, examinez la fonction suivante à l’aide de celle HelloWorldTypeProvider utilisée précédemment dans cette rubrique.
let function1 () =
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
obj1.InstanceProperty
Voici une image du code résultant décompilé à l’aide de 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
Comme l’illustre l’exemple, toutes les mentions du type Type1 et de la InstanceProperty propriété ont été effacées, laissant uniquement des opérations sur les types d’exécution impliqués.
Conception et conventions d’affectation de noms pour les fournisseurs de type
Observez les conventions suivantes lors de la création de fournisseurs de types.
Fournisseurs pour les protocoles de connectivité En général, les noms de la plupart des DLL de fournisseur pour les protocoles de connectivité de données et de service, tels que les connexions OData ou SQL, doivent se terminer par TypeProvider ou TypeProviders. Par exemple, utilisez un nom DLL qui ressemble à la chaîne suivante :
Fabrikam.Management.BasicTypeProviders.dll
Vérifiez que vos types fournis sont membres de l’espace de noms correspondant et indiquez le protocole de connectivité que vous avez implémenté :
Fabrikam.Management.BasicTypeProviders.WmiConnection<…>
Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>
Fournisseurs d’utilitaires pour le codage général. Pour un fournisseur de type utilitaire tel que celui des expressions régulières, le fournisseur de types peut faire partie d’une bibliothèque de base, comme l’illustre l’exemple suivant :
#r "Fabrikam.Core.Text.Utilities.dll"
Dans ce cas, le type fourni apparaît à un point approprié en fonction des conventions de conception .NET normales :
open Fabrikam.Core.Text.RegexTyped
let regex = new RegexTyped<"a+b+a+b+">()
Sources de données Singleton. Certains fournisseurs de type se connectent à une seule source de données dédiée et fournissent uniquement des données. Dans ce cas, vous devez supprimer le TypeProvider suffixe et utiliser des conventions normales pour le nommage .NET :
#r "Fabrikam.Data.Freebase.dll"
let data = Fabrikam.Data.Freebase.Astronomy.Asteroids
Pour plus d’informations, consultez la GetConnection convention de conception décrite plus loin dans cette rubrique.
Modèles de conception pour les fournisseurs de type
Les sections suivantes décrivent les modèles de conception que vous pouvez utiliser lors de la création de fournisseurs de types.
Modèle de conception GetConnection
La plupart des fournisseurs de types doivent être écrits pour utiliser le GetConnection modèle utilisé par les fournisseurs de types dans FSharp.Data.TypeProviders.dll, comme l’illustre l’exemple suivant :
#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
Fournisseurs de type soutenus par des données et services distants
Avant de créer un fournisseur de types soutenu par des données et des services distants, vous devez prendre en compte un éventail de problèmes inhérents à la programmation connectée. Ces problèmes incluent les considérations suivantes :
mappage de schéma
liveness et invalidation en présence d’une modification de schéma
mise en cache de schéma
implémentations asynchrones des opérations d’accès aux données
prise en charge des requêtes, y compris les requêtes LINQ
informations d’identification et authentification
Cette rubrique n’explore pas ces problèmes plus loin.
Techniques de création supplémentaires
Lorsque vous écrivez vos propres fournisseurs de types, vous pouvez utiliser les techniques supplémentaires suivantes.
Création de types et de membres à la demande
L’API ProvidedType a retardé les versions de AddMember.
type ProvidedType =
member AddMemberDelayed : (unit -> MemberInfo) -> unit
member AddMembersDelayed : (unit -> MemberInfo list) -> unit
Ces versions sont utilisées pour créer des espaces à la demande de types.
Fourniture de types de tableaux et d’instanciations de type générique
Vous créez des membres fournis (dont les signatures incluent des types de tableau, des types byref et des instanciations de types génériques) en utilisant la valeur normale MakeArrayType, MakePointerTypeet MakeGenericType sur n’importe quelle instance de Type, y compris ProvidedTypeDefinitions.
Remarque
Dans certains cas, vous devrez peut-être recourir à l’aide dans ProvidedTypeBuilder.MakeGenericType. Pour plus d’informations, consultez la documentation du kit de développement logiciel (SDK) du fournisseur de type.
Fourniture d'annotations pour les unités de mesure
L’API ProvidedTypes fournit des assistances pour fournir des annotations de mesure. Par exemple, pour fournir le type float<kg>, utilisez le code suivant :
let measures = ProvidedMeasureBuilder.Default
let kg = measures.SI "kilogram"
let m = measures.SI "meter"
let float_kg = measures.AnnotateType(typeof<float>,[kg])
Pour fournir le type Nullable<decimal<kg/m^2>>, utilisez le code suivant :
let kgpm2 = measures.Ratio(kg, measures.Square m)
let dkgpm2 = measures.AnnotateType(typeof<decimal>,[kgpm2])
let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]
Accès aux ressources Project-Local ou Script-Local
Chaque instance d’un fournisseur de types peut avoir une TypeProviderConfig valeur pendant la construction. Cette valeur contient le « dossier de résolution » du fournisseur (c’est-à-dire le dossier de projet pour la compilation ou le répertoire qui contient un script), la liste des assemblys référencés et d’autres informations.
Invalidité
Les fournisseurs peuvent déclencher des signaux d’invalidation pour informer le service de langage F# que les hypothèses de schéma peuvent avoir changé. Lorsque l’invalidation se produit, une vérification de type est refaite si le fournisseur est hébergé dans Visual Studio. Ce signal est ignoré lorsque le fournisseur est hébergé dans F# Interactive ou par le compilateur F# (fsc.exe).
Informations de schéma de mise en cache
Les fournisseurs doivent souvent mettre en cache l’accès aux informations de schéma. Les données mises en cache doivent être stockées à l’aide d’un nom de fichier donné en tant que paramètre statique ou en tant que données utilisateur. Un exemple de mise en cache de schéma est le paramètre LocalSchemaFile dans les fournisseurs de types de l'assembly FSharp.Data.TypeProviders. Dans l’implémentation de ces fournisseurs, ce paramètre statique dirige le fournisseur de types pour utiliser les informations de schéma dans le fichier local spécifié au lieu d’accéder à la source de données sur le réseau. Pour utiliser des informations de schéma mises en cache, vous devez également définir le paramètre ForceUpdate statique sur false. Vous pouvez utiliser une technique similaire pour activer l’accès aux données en ligne et hors connexion.
Assembly de stockage
Lorsque vous compilez un fichier .dll ou .exe, le fichier support .dll pour les types générés est statiquement lié à l’assembly résultant. Ce lien est créé en copiant les définitions de type IL (Intermediate Language) et toutes les ressources gérées de l’assembly source dans l’assembly final. Lorsque vous utilisez F# Interactive, le fichier de stockage .dll n’est pas copié et est chargé directement dans le processus interactif F#.
Exceptions et diagnostics des fournisseurs de type
Toutes les utilisations de tous les membres des types fournis peuvent lever des exceptions. Dans tous les cas, si un fournisseur de types lève une exception, le compilateur hôte attribue l’erreur à un fournisseur de type spécifique.
Les exceptions de fournisseur de types ne doivent jamais entraîner d’erreurs internes du compilateur.
Les fournisseurs de type ne peuvent pas signaler d’avertissements.
Lorsqu’un fournisseur de types est hébergé dans le compilateur F#, un environnement de développement F# ou F# Interactive, toutes les exceptions de ce fournisseur sont interceptées. La propriété Message est toujours le texte d’erreur et aucun rapport des appels de procédure n’apparaît. Si vous allez lever une exception, vous pouvez lever les exemples suivants :
System.NotSupportedException,System.IO.IOException,System.Exception.
Fourniture de types générés
Jusqu’à présent, ce document a expliqué comment fournir des types effacés. Vous pouvez également utiliser le mécanisme de fournisseur de types en F# pour fournir des types générés, qui sont ajoutés en tant que définitions de type .NET réelles dans le programme des utilisateurs. Vous devez faire référence aux types fournis générés à l’aide d’une définition de type.
open Microsoft.FSharp.TypeProviders
type Service = ODataService<"http://services.odata.org/Northwind/Northwind.svc/">
Le code d’assistance ProvidedTypes-0.2 qui fait partie de la version F# 3.0 n’a qu’une prise en charge limitée de la fourniture de types générés. Les instructions suivantes doivent être vraies pour une définition de type générée :
isEraseddoit être défini surfalse.Le type généré doit être ajouté à un
ProvidedAssembly()nouvellement construit, qui représente un conteneur pour les fragments de code générés.Le fournisseur doit avoir un assembly qui a un fichier de .dll .NET de stockage réel avec un fichier de .dll correspondant sur le disque.
Règles et limitations
Lorsque vous écrivez des fournisseurs de types, gardez à l’esprit les règles et limitations suivantes.
Les types fournis doivent être accessibles
Tous les types fournis doivent être accessibles à partir des types non imbriqués. Les types non imbriqués sont donnés dans l’appel au constructeur TypeProviderForNamespaces ou dans un appel à AddNamespace. Par exemple, si le fournisseur fournit un type StaticClass.P : T, vous devez vous assurer que T est un type non imbriqué ou qu’il est imbriqué sous un.
Par exemple, certains fournisseurs ont une classe statique telle que DataTypes celle qui contient ces T1, T2, T3, ... types. Sinon, l’erreur indique qu’une référence au type T dans l’assembly A a été trouvée, mais que le type n’a pas pu être trouvé dans cet assembly. Si cette erreur s’affiche, vérifiez que tous vos sous-types peuvent être atteints à partir des types de fournisseurs. Remarque : les types T1, T2, T3... sont appelés types à la volée. N’oubliez pas de les placer dans un espace de noms accessible ou dans un type parent.
Limitations du mécanisme de fournisseur de type
Le mécanisme de fournisseur de types en F# présente les limitations suivantes :
L’infrastructure sous-jacente pour les fournisseurs de types en F# ne prend pas en charge les types génériques fournis ou les méthodes génériques fournies.
Le mécanisme ne prend pas en charge les types imbriqués avec des paramètres statiques.
Conseils de développement
Vous trouverez peut-être les conseils suivants utiles pendant le processus de développement :
Exécuter deux instances de Visual Studio
Vous pouvez développer le fournisseur de types dans une instance et tester le fournisseur dans l’autre, car l’IDE de test prend un verrou sur le fichier .dll qui empêche le fournisseur de type d’être reconstruit. Par conséquent, vous devez fermer la deuxième instance de Visual Studio pendant que le fournisseur est intégré à la première instance, puis rouvrir la deuxième instance une fois le fournisseur généré.
Déboguer des fournisseurs de type à l’aide d’appels de fsc.exe
Vous pouvez appeler des fournisseurs de types à l’aide des outils suivants :
fsc.exe (compilateur de ligne de commande F#)
fsi.exe (compilateur interactif F#)
devenv.exe (Visual Studio)
Vous pouvez souvent déboguer des fournisseurs de type plus facilement en utilisant fsc.exe sur un fichier de script de test (par exemple, script.fsx). Vous pouvez lancer un débogueur à partir d’une invite de commandes.
devenv /debugexe fsc.exe script.fsx
Vous pouvez utiliser la journalisation print-to-stdout.