Samouczek: tworzenie dostawcy typów
Mechanizm dostawcy typów w języku F# jest znaczącą częścią jego obsługi programowania rozbudowanego w zakresie informacji. W tym samouczku wyjaśniono, jak utworzyć własnych dostawców typów, przechodząc przez proces tworzenia kilku prostych dostawców typów w celu zilustrowania podstawowych pojęć. Aby uzyskać więcej informacji na temat mechanizmu dostawcy typów w języku F#, zobacz Dostawcy typów.
Ekosystem języka F# zawiera wielu dostawców typów dla powszechnie używanych usług danych internetowych i przedsiębiorstwa. Na przykład:
Plik FSharp.Data zawiera dostawców typów dla formatów dokumentów JSON, XML, CSV i HTML.
Dostawca SwaggerProvider obejmuje dwóch dostawców typów generowania, którzy generują model obiektów i klientów HTTP dla interfejsów API opisanych przez schematy OpenApi 3.0 i Swagger 2.0.
Obiekt FSharp.Data.SqlClient ma zestaw dostawców typów dla funkcji osadzania języka T-SQL w języku F#w czasie kompilacji.
Możesz utworzyć dostawców typów niestandardowych lub odwoływać się do dostawców typów utworzonych przez inne osoby. Na przykład organizacja może mieć usługę danych, która zapewnia dużą i rosnącą liczbę nazwanych zestawów danych, z których każda ma własny stabilny schemat danych. Możesz utworzyć dostawcę typów, który odczytuje schematy i przedstawia bieżące zestawy danych programistom w sposób silnie typizowane.
Przed rozpoczęciem
Mechanizm dostawcy typów jest przeznaczony przede wszystkim do wstrzykiwania stabilnych danych i przestrzeni informacyjnych usług do środowiska programowania języka F#.
Ten mechanizm nie jest przeznaczony do wstrzykiwania przestrzeni informacyjnych, których zmiany schematu podczas wykonywania programu są istotne dla logiki programu. Ponadto mechanizm nie jest przeznaczony do programowania metajęzycznego, mimo że ta domena zawiera pewne prawidłowe zastosowania. Należy użyć tego mechanizmu tylko wtedy, gdy jest to konieczne, a rozwój dostawcy typów daje bardzo wysoką wartość.
Należy unikać pisania dostawcy typów, w którym schemat jest niedostępny. Podobnie należy unikać pisania dostawcy typów, w którym wystarczy zwykła (a nawet istniejąca) biblioteka .NET.
Przed rozpoczęciem możesz zadać następujące pytania:
Czy masz schemat źródła informacji? Jeśli tak, jakie jest mapowanie w systemie typów F# i .NET?
Czy możesz użyć istniejącego (dynamicznie typizowanego) interfejsu API jako punktu wyjścia dla implementacji?
Czy ty i Twoja organizacja będą mieć wystarczająco dużo użycia dostawcy typów, aby zapisać go warto? Czy normalna biblioteka .NET spełnia Twoje potrzeby?
Ile zmieni schemat?
Czy zmieni się podczas kodowania?
Czy zmieni się między sesjami kodowania?
Czy zmieni się podczas wykonywania programu?
Dostawcy typów najlepiej nadają się do sytuacji, w których schemat jest stabilny w czasie wykonywania i w okresie istnienia skompilowanego kodu.
Dostawca prostego typu
Ten przykład to Samples.HelloWorldTypeProvider, podobnie jak przykłady w examples
katalogu zestawu SDK dostawcy typów języka F#. Dostawca udostępnia "przestrzeń typów", która zawiera 100 wymazanych typów, jak pokazano w poniższym kodzie, używając składni podpisu języka F# i pomijając szczegóły dla wszystkich z wyjątkiem Type1
. Aby uzyskać więcej informacji na temat wymazanych typów, zobacz Szczegóły dotyczące wymazanych typów podanych w dalszej części tego tematu.
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 =
…
Należy pamiętać, że zestaw podanych typów i elementów członkowskich jest statycznie znany. W tym przykładzie nie wykorzystuje się możliwości dostawców do udostępniania typów, które zależą od schematu. Implementacja dostawcy typów jest opisana w poniższym kodzie, a szczegółowe informacje zostały omówione w kolejnych sekcjach tego tematu.
Ostrzeżenie
Mogą istnieć różnice między tym kodem a przykładami 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()
Aby użyć tego dostawcy, otwórz oddzielne wystąpienie programu Visual Studio, utwórz skrypt języka F#, a następnie dodaj odwołanie do dostawcy ze skryptu przy użyciu #r jak pokazano w poniższym kodzie:
#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
Następnie poszukaj typów w przestrzeni nazw wygenerowanej Samples.HelloWorldTypeProvider
przez dostawcę typów.
Przed ponownym skompilowania dostawcy upewnij się, że wszystkie wystąpienia programu Visual Studio i F# Interactive, które korzystają z biblioteki DLL dostawcy. W przeciwnym razie wystąpi błąd kompilacji, ponieważ wyjściowa biblioteka DLL zostanie zablokowana.
Aby debugować tego dostawcę przy użyciu instrukcji drukowania, utwórz skrypt, który uwidacznia problem z dostawcą, a następnie użyj następującego kodu:
fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Aby debugować tego dostawcę przy użyciu programu Visual Studio, otwórz wiersz polecenia dla deweloperów dla programu Visual Studio z poświadczeniami administracyjnymi i uruchom następujące polecenie:
devenv.exe /debugexe fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
Alternatywnie otwórz program Visual Studio, otwórz menu Debuguj, wybierz pozycję Debug/Attach to process…
i dołącz do innego devenv
procesu, w którym edytujesz skrypt. Korzystając z tej metody, można łatwiej określić konkretną logikę w dostawcy typów, interakcyjnie wpisując wyrażenia w drugim wystąpieniu (z pełną funkcją IntelliSense i innymi funkcjami).
Możesz wyłączyć debugowanie Just My Code, aby lepiej zidentyfikować błędy w wygenerowanych kodzie. Aby uzyskać informacje na temat włączania lub wyłączania tej funkcji, zobacz Nawigowanie po kodzie za pomocą debugera. Ponadto można również ustawić przechwytywanie wyjątków pierwszej szansy, otwierając Debug
menu, a następnie Exceptions
wybierając lub wybierając klawisze Ctrl+Alt+E, aby otworzyć Exceptions
okno dialogowe. W tym oknie dialogowym w obszarze Common Language Runtime Exceptions
zaznacz Thrown
pole wyboru.
Implementacja dostawcy typów
W tej sekcji przedstawiono główne sekcje implementacji dostawcy typów. Najpierw należy zdefiniować typ dostawcy typów niestandardowych:
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
Ten typ musi być publiczny i należy oznaczyć go atrybutem TypeProvider , aby kompilator rozpoznał dostawcę typów, gdy oddzielny projekt F# odwołuje się do zestawu zawierającego typ. Parametr konfiguracji jest opcjonalny, a jeśli jest obecny, zawiera informacje o konfiguracji kontekstowej dla wystąpienia dostawcy typów tworzonego przez kompilator języka F#.
Następnie zaimplementujesz interfejs ITypeProvider . W takim przypadku jako typ podstawowy należy użyć TypeProviderForNamespaces
typu z interfejsu ProvidedTypes
API. Ten typ pomocnika może zapewnić skończoną kolekcję niecierpliwie udostępnianych przestrzeni nazw, z których każda bezpośrednio zawiera skończona liczbę stałych, chętnie dostarczonych typów. W tym kontekście dostawca chętnie generuje typy, nawet jeśli nie są potrzebne lub używane.
inherit TypeProviderForNamespaces(config)
Następnie zdefiniuj lokalne wartości prywatne, które określają przestrzeń nazw dla podanych typów, i znajdź sam zestaw dostawcy typów. Ten zestaw jest używany później jako logiczny typ nadrzędny wymazanych typów, które są dostarczane.
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
Następnie utwórz funkcję, aby podać każdy z typów Type1... Wpisz 100. Ta funkcja jest bardziej szczegółowo objaśniona w dalszej części tego tematu.
let makeOneProvidedType (n:int) = …
Następnie wygeneruj 100 dostarczonych typów:
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
Następnie dodaj typy jako przestrzeń nazw:
do this.AddNamespace(namespaceName, types)
Na koniec dodaj atrybut zestawu wskazujący, że tworzysz bibliotekę DLL dostawcy typów:
[<assembly:TypeProviderAssembly>]
do()
Podawanie jednego typu i jego składowych
Funkcja makeOneProvidedType
wykonuje rzeczywistą pracę nad zapewnieniem jednego z typów.
let makeOneProvidedType (n:int) =
…
W tym kroku wyjaśniono implementację tej funkcji. Najpierw utwórz podany typ (na przykład Typ1, gdy n = 1 lub Type57, gdy 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>)
Należy zwrócić uwagę na następujące kwestie:
Ten podany typ jest wymazany. Ponieważ wskazujesz, że typ podstawowy to
obj
, wystąpienia będą wyświetlane jako wartości typu obj w skompilowanym kodzie.Po określeniu typu niezagnieżdżonego należy określić zestaw i przestrzeń nazw. W przypadku wymazanych typów zestaw powinien być zestawem dostawcy typów.
Następnie dodaj dokumentację XML do typu . Ta dokumentacja jest opóźniona, czyli obliczana na żądanie, jeśli kompilator hosta go potrzebuje.
t.AddXmlDocDelayed (fun () -> $"""This provided type {"Type" + string n}""")
Następnie dodasz podaną właściwość statyczną do typu:
let staticProp = ProvidedProperty(propertyName = "StaticProperty",
propertyType = typeof<string>,
isStatic = true,
getterCode = (fun args -> <@@ "Hello!" @@>))
Pobranie tej właściwości zawsze będzie oceniać ciąg "Hello!". Właściwość GetterCode
dla właściwości używa cudzysłowu języka F#, który reprezentuje kod generowany przez kompilator hosta na potrzeby pobierania właściwości. Aby uzyskać więcej informacji na temat cudzysłowów, zobacz Cytaty kodu (F#).
Dodaj dokumentację XML do właściwości .
staticProp.AddXmlDocDelayed(fun () -> "This is a static property")
Teraz dołącz podaną właściwość do podanego typu. Musisz dołączyć podany element członkowski do jednego i tylko jednego typu. W przeciwnym razie członek nigdy nie będzie dostępny.
t.AddMember staticProp
Teraz utwórz podany konstruktor, który nie przyjmuje żadnych parametrów.
let ctor = ProvidedConstructor(parameters = [ ],
invokeCode = (fun args -> <@@ "The object data" :> obj @@>))
Dla konstruktora InvokeCode
zwraca cudzysłów języka F#, który reprezentuje kod generowany przez kompilator hosta podczas wywoływania konstruktora. Można na przykład użyć następującego konstruktora:
new Type10()
Wystąpienie podanego typu zostanie utworzone przy użyciu danych bazowych "Dane obiektu". Kod cytowany zawiera konwersję na obj , ponieważ ten typ jest wymazywaniem tego typu podanego typu (jak określono podczas deklarowanego podanego typu).
Dodaj dokumentację XML do konstruktora i dodaj podany konstruktor do podanego typu:
ctor.AddXmlDocDelayed(fun () -> "This is a constructor")
t.AddMember ctor
Utwórz drugi podany konstruktor, który przyjmuje jeden parametr:
let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
invokeCode = (fun args -> <@@ (%%(args[0]) : string) :> obj @@>))
Dla InvokeCode
konstruktora ponownie zwraca cudzysłów języka F#, który reprezentuje kod wygenerowany przez kompilator hosta dla wywołania metody . Można na przykład użyć następującego konstruktora:
new Type10("ten")
Wystąpienie podanego typu jest tworzone z danymi bazowymi "dziesięć". Być może już zauważono, że InvokeCode
funkcja zwraca cudzysłów. Dane wejściowe tej funkcji to lista wyrażeń, jedna na parametr konstruktora. W tym przypadku wyrażenie reprezentujące wartość pojedynczego parametru jest dostępne w pliku args[0]
. Kod wywołania konstruktora przekształca wartość zwracaną do wymazanego typu obj
. Po dodaniu drugiego dostarczonego konstruktora do typu należy utworzyć podaną właściwość wystąpienia:
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
Pobranie tej właściwości spowoduje zwrócenie długości ciągu, który jest obiektem reprezentacji. Właściwość GetterCode
zwraca cudzysłów języka F#, który określa kod generowany przez kompilator hosta w celu pobrania właściwości. GetterCode
Na przykład InvokeCode
funkcja zwraca cudzysłów. Kompilator hosta wywołuje tę funkcję z listą argumentów. W takim przypadku argumenty obejmują tylko jedno wyrażenie, które reprezentuje wystąpienie, na którym jest wywoływany element getter, do którego można uzyskać dostęp przy użyciu polecenia args[0]
. Implementacja GetterCode
następnie splices do cudzysłowu wyniku w wymazanym typie obj
, a rzutowanie jest używane do zaspokojenia mechanizmu kompilatora do sprawdzania typów, które obiekt jest ciągiem. Następna część zawiera makeOneProvidedType
metodę wystąpienia z jednym parametrem.
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
Na koniec utwórz zagnieżdżony typ zawierający 100 zagnieżdżonych właściwości. Tworzenie tego typu zagnieżdżonego i jego właściwości jest opóźnione, czyli obliczane na żądanie.
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])
Szczegółowe informacje o wymazanych typach udostępnionych
Przykład w tej sekcji zawiera tylko usunięte typy, które są szczególnie przydatne w następujących sytuacjach:
Podczas pisania dostawcy dla przestrzeni informacyjnej zawierającej tylko dane i metody.
Podczas pisania dostawcy, w którym dokładne semantyka typu środowiska uruchomieniowego nie ma krytycznego dla praktycznego wykorzystania przestrzeni informacyjnej.
Podczas pisania dostawcy dla przestrzeni informacyjnej, która jest tak duża i połączona, że technicznie nie jest możliwe generowanie rzeczywistych typów platformy .NET dla przestrzeni informacyjnej.
W tym przykładzie każdy podany typ jest usuwany do typu obj
, a wszystkie zastosowania typu będą wyświetlane jako typ obj
w skompilowanym kodzie. W rzeczywistości obiekty bazowe w tych przykładach są ciągami, ale typ będzie wyświetlany tak jak System.Object
w skompilowanym kodzie platformy .NET. Podobnie jak w przypadku wszystkich zastosowań wymazywania typów, można użyć jawnego boksu, rozpasywania i rzutowania do odwróć wymazane typy. W takim przypadku wyjątek rzutu, który nie jest prawidłowy, może spowodować, że zostanie użyty obiekt. Środowisko uruchomieniowe dostawcy może zdefiniować własny prywatny typ reprezentacji, aby chronić przed fałszywymi reprezentacjami. W języku F# nie można definiować wymazanych typów. Tylko podane typy mogą być wymazane. Należy zrozumieć konsekwencje, zarówno praktyczne, jak i semantyczne, przy użyciu wymazanych typów dla dostawcy typów lub dostawcy, który udostępnia wymazane typy. Wymazany typ nie ma rzeczywistego typu .NET. W związku z tym nie można wykonać dokładnego odbicia na typie i można odwrócić wymazane typy, jeśli używasz rzutów środowiska uruchomieniowego i innych technik, które opierają się na dokładnych semantyce typów środowiska uruchomieniowego. Podwersja wymazanych typów często powoduje rzutowanie wyjątków typu w czasie wykonywania.
Wybieranie reprezentacji dla wymazanych typów
W przypadku niektórych zastosowań wymazanych typów nie jest wymagana żadna reprezentacja. Na przykład wymazany podany typ może zawierać tylko właściwości statyczne i składowe i bez konstruktorów, a żadne metody lub właściwości nie zwracają wystąpienia typu. Jeśli możesz uzyskać dostęp do wystąpień wymazanego typu, należy wziąć pod uwagę następujące pytania:
Co to jest wymazywanie podanego typu?
Wymazywanie podanego typu to sposób wyświetlania typu w skompilowanym kodzie platformy .NET.
Wymazywanie podanego wymazanego typu klasy jest zawsze pierwszym nienamazanym typem podstawowym w łańcuchu dziedziczenia typu.
Wymazywanie podanego wymazanego typu interfejsu jest zawsze
System.Object
.
Jakie są reprezentacje podanego typu?
- Zestaw możliwych obiektów dla wymazanego typu jest nazywany jego reprezentacjami. W przykładzie w tym dokumencie reprezentacje wszystkich wymazanych typów
Type1..Type100
są zawsze obiektami ciągów.
Wszystkie reprezentacje podanego typu muszą być zgodne z wymazywaniem podanego typu. (W przeciwnym razie kompilator języka F# zwróci błąd użycia dostawcy typów lub nieweryfikowalny kod platformy .NET, który nie jest prawidłowy, zostanie wygenerowany. Dostawca typów nie jest prawidłowy, jeśli zwraca kod, który daje reprezentację, która nie jest prawidłowa.
Możesz wybrać reprezentację dla udostępnionych obiektów przy użyciu jednej z następujących metod, które są bardzo typowe:
Jeśli po prostu udostępniasz silnie typową otokę dla istniejącego typu platformy .NET, często warto wymazać typ z tym typem, użyć wystąpień tego typu jako reprezentacji lub obu tych typów. Takie podejście jest odpowiednie, gdy większość istniejących metod w tym typie nadal ma sens w przypadku korzystania z silnie typizowanej wersji.
Jeśli chcesz utworzyć interfejs API, który znacznie różni się od istniejącego interfejsu API platformy .NET, warto utworzyć typy środowiska uruchomieniowego, które będą wymazywaniem typów i reprezentacjami dla podanych typów.
W przykładzie w tym dokumencie są używane ciągi jako reprezentacje podanych obiektów. Często może być konieczne użycie innych obiektów do reprezentowania. Na przykład możesz użyć słownika jako torby właściwości:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))
Alternatywnie można zdefiniować typ dostawcy typów, który będzie używany w czasie wykonywania w celu utworzenia reprezentacji, wraz z co najmniej jedną operacją środowiska uruchomieniowego:
type DataObject() =
let data = Dictionary<string,obj>()
member x.RuntimeOperation() = data.Count
Udostępnione elementy członkowskie mogą następnie konstruować wystąpienia tego typu obiektu:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))
W takim przypadku można (opcjonalnie) użyć tego typu jako wymazywania typu, określając ten typ jako baseType
typ podczas konstruowania ProvidedTypeDefinition
elementu :
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
Kluczowe lekcje
W poprzedniej sekcji wyjaśniono, jak utworzyć prostego dostawcę typu wymazywania, który udostępnia szereg typów, właściwości i metod. W tej sekcji wyjaśniono również koncepcję wymazywania typów, w tym niektóre zalety i wady dostarczania wymazanych typów od dostawcy typów oraz omówiono reprezentacje wymazanych typów.
Dostawca typu korzystający z parametrów statycznych
Możliwość sparametryzowania dostawców typów przez dane statyczne umożliwia korzystanie z wielu interesujących scenariuszy, nawet w przypadkach, gdy dostawca nie musi uzyskiwać dostępu do żadnych danych lokalnych ani zdalnych. W tej sekcji poznasz kilka podstawowych technik łączenia takiego dostawcy.
Typ sprawdzony dostawca wyrażeń regularnych
Załóżmy, że chcesz zaimplementować dostawcę typów dla wyrażeń regularnych, które opakowują biblioteki .NET Regex w interfejsie, który zapewnia następujące gwarancje czasu kompilacji:
Sprawdzanie, czy wyrażenie regularne jest prawidłowe.
Podawanie nazwanych właściwości na dopasowaniach opartych na nazwach grup w wyrażeniu regularnym.
W tej sekcji pokazano, jak za pomocą dostawców typów utworzyć typ, który wzorzec wyrażenia regularnego RegexTyped
sparametryzuje w celu zapewnienia tych korzyści. Kompilator zgłosi błąd, jeśli podany wzorzec jest nieprawidłowy, a dostawca typów może wyodrębnić grupy ze wzorca, aby można było uzyskać do nich dostęp przy użyciu nazwanych właściwości na dopasowaniach. Podczas projektowania dostawcy typów należy rozważyć, w jaki sposób jego uwidoczniony interfejs API powinien wyglądać dla użytkowników końcowych i jak ten projekt będzie tłumaczony na kod platformy .NET. W poniższym przykładzie pokazano, jak za pomocą takiego interfejsu API pobrać składniki kodu obszaru:
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"
W poniższym przykładzie pokazano, jak dostawca typów tłumaczy te wywołania:
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"
Należy uwzględnić następujące informacje:
Standardowy typ wyrażenia regularnego reprezentuje typ sparametryzowany
RegexTyped
.Konstruktor
RegexTyped
powoduje wywołanie konstruktora wyrażeń regularnych, przekazując argument typu statycznego dla wzorca.Wyniki
Match
metody są reprezentowane przez typ standardowy Match .Każda nazwana grupa powoduje wyświetlenie podanej właściwości i uzyskanie dostępu do właściwości powoduje użycie indeksatora
Groups
w kolekcji dopasowania.
Poniższy kod jest podstawą logiki do zaimplementowania takiego dostawcy, a w tym przykładzie pominięto dodanie wszystkich elementów członkowskich do podanego typu. Aby uzyskać informacje o każdym dodanym elemencie członkowskim, zobacz odpowiednią sekcję w dalszej części tego tematu.
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 ()
Należy uwzględnić następujące informacje:
Dostawca typów przyjmuje dwa parametry statyczne:
pattern
parametr , który jest obowiązkowy, ioptions
, które są opcjonalne (ponieważ podano wartość domyślną).Po podaniu argumentów statycznych należy utworzyć wystąpienie wyrażenia regularnego. To wystąpienie zgłosi wyjątek, jeśli wyrażenie regularne jest źle sformułowane, a ten błąd zostanie zgłoszony użytkownikom.
W wywołaniu zwrotnym
DefineStaticParameters
zdefiniujesz typ, który zostanie zwrócony po podaniu argumentów.Ten kod ustawia
HideObjectMethods
wartość true, aby środowisko IntelliSense pozostało usprawnione. Ten atrybut powodujeEquals
pomijanie elementów członkowskich ,GetHashCode
,Finalize
iGetType
z list funkcji IntelliSense dla podanego obiektu.obj
Użyjesz jako podstawowego typu metody, ale użyjeszRegex
obiektu jako reprezentacji środowiska uruchomieniowego tego typu, jak pokazano w następnym przykładzie.Wywołanie konstruktora
Regex
zgłasza ArgumentException błąd, gdy wyrażenie regularne nie jest prawidłowe. Kompilator przechwytuje ten wyjątek i zgłasza użytkownikowi komunikat o błędzie w czasie kompilacji lub w edytorze programu Visual Studio. Ten wyjątek umożliwia weryfikowanie wyrażeń regularnych bez uruchamiania aplikacji.
Typ zdefiniowany powyżej nie jest jeszcze przydatny, ponieważ nie zawiera żadnych znaczących metod ani właściwości. Najpierw dodaj metodę statyczną 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
Poprzedni kod definiuje metodę IsMatch
, która przyjmuje ciąg jako dane wejściowe i zwraca wartość bool
. Jedyną trudną częścią jest użycie argumentu args
w InvokeCode
definicji. W tym przykładzie args
jest to lista cudzysłowów reprezentujących argumenty tej metody. Jeśli metoda jest metodą wystąpienia, pierwszy argument reprezentuje this
argument. Jednak w przypadku metody statycznej argumenty są tylko jawnymi argumentami metody . Należy pamiętać, że typ cytowanej wartości powinien być zgodny z określonym typem zwracanym (w tym przypadku bool
). Należy również pamiętać, że ten kod używa AddXmlDoc
metody , aby upewnić się, że podana metoda ma również przydatną dokumentację, którą można dostarczyć za pomocą funkcji IntelliSense.
Następnie dodaj metodę Match wystąpienia. Jednak ta metoda powinna zwrócić wartość podanego Match
typu, aby można było uzyskać dostęp do grup w sposób silnie typizowane. W związku z tym należy najpierw zadeklarować Match
typ. Ponieważ ten typ zależy od wzorca dostarczonego jako argument statyczny, ten typ musi być zagnieżdżony w sparametryzowanej definicji typu:
let matchTy =
ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
hideObjectMethods = true)
ty.AddMember matchTy
Następnie należy dodać jedną właściwość do typu Dopasowania dla każdej grupy. W czasie wykonywania dopasowanie jest reprezentowane jako Match wartość, więc cudzysłów definiujący właściwość musi używać Groups właściwości indeksowanej, aby uzyskać odpowiednią grupę.
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
Ponownie należy pamiętać, że do podanej właściwości dodajesz dokumentację XML. Należy również pamiętać, że właściwość można odczytać, jeśli GetterCode
podano funkcję, a właściwość może być zapisywana w przypadku SetterCode
podania funkcji, więc wynikowa właściwość jest tylko do odczytu.
Teraz możesz utworzyć metodę wystąpienia zwracającą wartość tego Match
typu:
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
Ponieważ tworzysz metodę wystąpienia, reprezentuje RegexTyped
wystąpienie, args[0]
na którym jest wywoływana metoda, i args[1]
jest argumentem wejściowym.
Na koniec podaj konstruktor, aby można było utworzyć wystąpienia podanego typu.
let ctor =
ProvidedConstructor(
parameters = [],
invokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)
ctor.AddXmlDoc("Initializes a regular expression instance.")
ty.AddMember ctor
Konstruktor usuwa jedynie utworzenie standardowego wystąpienia wyrażeń regularnych platformy .NET, które jest ponownie wymazane do obiektu, ponieważ obj
jest to wymazywanie podanego typu. Dzięki tej zmianie przykładowe użycie interfejsu API określone wcześniej w temacie działa zgodnie z oczekiwaniami. Poniższy kod jest kompletny i końcowy:
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 ()
Kluczowe lekcje
W tej sekcji wyjaśniono, jak utworzyć dostawcę typów, który działa na jego parametrach statycznych. Dostawca sprawdza parametr statyczny i udostępnia operacje na podstawie jego wartości.
Dostawca typów, który jest wspierany przez dane lokalne
Często dostawcy typów mogą prezentować interfejsy API na podstawie nie tylko parametrów statycznych, ale także informacji z systemów lokalnych lub zdalnych. W tej sekcji omówiono dostawców typów opartych na danych lokalnych, takich jak pliki danych lokalnych.
Prosty dostawca plików CSV
W prostym przykładzie rozważ dostawcę typów na potrzeby uzyskiwania dostępu do danych naukowych w formacie wartości rozdzielanej przecinkami (CSV). W tej sekcji założono, że pliki CSV zawierają wiersz nagłówka, po którym znajdują się dane zmiennoprzecinkowe, jak pokazano w poniższej tabeli:
Odległość (miernik) | Czas (sekunda) |
---|---|
50,0 | 3.7 |
100,0 | 5.2 |
150.0 | 6.4 |
W tej sekcji pokazano, jak podać typ, którego można użyć do pobierania wierszy z właściwością Distance
typu float<meter>
i Time
właściwością typu float<second>
. Dla uproszczenia przyjmuje się następujące założenia:
Nazwy nagłówków są bez jednostki lub mają postać "Nazwa (jednostka)" i nie zawierają przecinków.
Jednostki to wszystkie jednostki System International (SI), ponieważ definiuje moduł FSharp.Data.UnitSystems.SI.UnitNames Module (F#).
Jednostki są proste (na przykład miernik), a nie złożone (na przykład miernik/sekunda).
Wszystkie kolumny zawierają dane zmiennoprzecinkowe.
Bardziej kompletny dostawca poluzowałby te ograniczenia.
Ponownie pierwszym krokiem jest rozważenie wyglądu interfejsu API. info.csv
Biorąc pod uwagę plik z zawartością poprzedniej tabeli (w formacie rozdzielanym przecinkami), użytkownicy dostawcy powinni mieć możliwość pisania kodu przypominającego następujący przykład:
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn $"{float time}"
W takim przypadku kompilator powinien przekonwertować te wywołania na coś podobnego do następującego przykładu:
let info = new CsvFile("info.csv")
for row in info.Data do
let (time:float) = row[1]
printfn $"%f{float time}"
Optymalne tłumaczenie będzie wymagać od dostawcy typów definiowania rzeczywistego CsvFile
typu w zestawie dostawcy typów. Dostawcy typów często polegają na kilku typach i metodach pomocnika, aby opakowować ważną logikę. Ponieważ miary są usuwane w czasie wykonywania, można użyć float[]
jako typu wymazanego dla wiersza. Kompilator będzie traktować różne kolumny jako o różnych typach miar. Na przykład pierwsza kolumna w naszym przykładzie ma typ float<meter>
, a druga ma wartość float<second>
. Jednak wymazana reprezentacja może pozostać dość prosta.
Poniższy kod przedstawia rdzeń implementacji.
// 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])
Zwróć uwagę na następujące kwestie dotyczące implementacji:
Przeciążone konstruktory zezwalają na odczyt oryginalnego pliku lub taki, który ma identyczny schemat. Ten wzorzec jest typowy podczas pisania dostawcy typów dla lokalnych lub zdalnych źródeł danych, a ten wzorzec umożliwia użycie pliku lokalnego jako szablonu dla danych zdalnych.
Aby rozpoznać względne nazwy plików, możesz użyć wartości TypeProviderConfig przekazanej do konstruktora dostawcy typów.
Za pomocą
AddDefinitionLocation
metody można zdefiniować lokalizację podanych właściwości. W związku z tym, jeśli używaszGo To Definition
w podanej właściwości, plik CSV zostanie otwarty w programie Visual Studio.Możesz użyć
ProvidedMeasureBuilder
typu , aby wyszukać jednostki SI i wygenerować odpowiedniefloat<_>
typy.
Kluczowe lekcje
W tej sekcji wyjaśniono, jak utworzyć dostawcę typów dla lokalnego źródła danych przy użyciu prostego schematu zawartego w samym źródle danych.
Dalsze przechodzenie
Poniższe sekcje zawierają sugestie dotyczące dalszej analizy.
Spojrzenie na skompilowany kod dla wymazanych typów
Aby podać pewne pojęcie o tym, jak użycie dostawcy typów odpowiada kodowi emitowanemu, zapoznaj się z następującą funkcją przy użyciu używanego HelloWorldTypeProvider
wcześniej w tym temacie.
let function1 () =
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
obj1.InstanceProperty
Oto obraz wynikowego kodu dekompilowanego przy użyciu 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
Jak pokazano w przykładzie, wszystkie wzmianki o typie Type1
i InstanceProperty
właściwości zostały wymazane, pozostawiając tylko operacje na typach środowiska uruchomieniowego.
Konwencje projektowania i nazewnictwa dla dostawców typów
Podczas tworzenia dostawców typów należy przestrzegać następujących konwencji.
Dostawcy protokołów Połączenie ivity Ogólnie nazwy większości bibliotek DLL dostawców dla protokołów łączności danych i usług, takich jak połączenia OData lub SQL, powinny kończyć TypeProvider
się wartościami lub TypeProviders
. Na przykład użyj nazwy DLL podobnej do następującego ciągu:
Fabrikam.Management.BasicTypeProviders.dll
Upewnij się, że podane typy są elementami członkowskimi odpowiedniej przestrzeni nazw i wskaż zaimplementowany protokół łączności:
Fabrikam.Management.BasicTypeProviders.WmiConnection<…>
Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>
Dostawcy narzędzi do ogólnego kodowania. W przypadku dostawcy typów narzędzi, takiego jak dla wyrażeń regularnych, dostawca typów może być częścią biblioteki podstawowej, jak pokazano w poniższym przykładzie:
#r "Fabrikam.Core.Text.Utilities.dll"
W takim przypadku podany typ będzie wyświetlany w odpowiednim momencie zgodnie z normalnymi konwencjami projektowania platformy .NET:
open Fabrikam.Core.Text.RegexTyped
let regex = new RegexTyped<"a+b+a+b+">()
Pojedyncze źródła danych. Niektórzy dostawcy typów łączą się z jednym dedykowanym źródłem danych i udostępniają tylko dane. W takim przypadku należy usunąć TypeProvider
sufiks i użyć normalnych konwencji nazewnictwa platformy .NET:
#r "Fabrikam.Data.Freebase.dll"
let data = Fabrikam.Data.Freebase.Astronomy.Asteroids
Aby uzyskać więcej informacji, zobacz konwencję GetConnection
projektowania opisaną w dalszej części tego tematu.
Wzorce projektowe dla dostawców typów
W poniższych sekcjach opisano wzorce projektowe, których można używać podczas tworzenia dostawców typów.
Wzorzec projektu Get Połączenie ion
Większość dostawców typów należy napisać, aby używać GetConnection
wzorca używanego przez dostawców typów w FSharp.Data.TypeProviders.dll, jak pokazano w poniższym przykładzie:
#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
Dostawcy typów wspierani przez zdalne dane i usługi
Przed utworzeniem dostawcy typu, który jest wspierany przez zdalne dane i usługi, należy wziąć pod uwagę szereg problemów, które są związane z połączonym programowaniem. Te problemy obejmują następujące zagadnienia:
mapowanie schematu
żywość i unieważnienie w obecności zmiany schematu
buforowanie schematu
asynchroniczne implementacje operacji dostępu do danych
obsługa zapytań, w tym zapytań LINQ
poświadczenia i uwierzytelnianie
Ten temat nie zawiera dalszych informacji na temat tych problemów.
Dodatkowe techniki tworzenia
Podczas pisania własnych dostawców typów warto użyć następujących dodatkowych technik.
Tworzenie typów i elementów członkowskich na żądanie
Interfejs API ProvidedType opóźnił wersje polecenia AddMember.
type ProvidedType =
member AddMemberDelayed : (unit -> MemberInfo) -> unit
member AddMembersDelayed : (unit -> MemberInfo list) -> unit
Te wersje służą do tworzenia przestrzeni na żądanie typów.
Dostarczanie typów tablic i wystąpień typów ogólnych
Udostępniasz elementy członkowskie (których podpisy obejmują typy tablic, typy byref i wystąpienia typów ogólnych) przy użyciu normalnego MakeArrayType
elementu , MakePointerType
i MakeGenericType
w dowolnym wystąpieniu Typeelementu , w tym ProvidedTypeDefinitions
.
Uwaga
W niektórych przypadkach może być konieczne użycie pomocnika w programie ProvidedTypeBuilder.MakeGenericType
. Aby uzyskać więcej informacji, zobacz dokumentację zestawu SDK dostawcy typów.
Zapewnianie jednostki adnotacji miary
Interfejs API ProvidedTypes udostępnia pomocników do udostępniania adnotacji miar. Aby na przykład podać typ float<kg>
, użyj następującego kodu:
let measures = ProvidedMeasureBuilder.Default
let kg = measures.SI "kilogram"
let m = measures.SI "meter"
let float_kg = measures.AnnotateType(typeof<float>,[kg])
Aby podać typ Nullable<decimal<kg/m^2>>
, użyj następującego kodu:
let kgpm2 = measures.Ratio(kg, measures.Square m)
let dkgpm2 = measures.AnnotateType(typeof<decimal>,[kgpm2])
let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]
Uzyskiwanie dostępu do zasobów lokalnych lub skryptów projektu
Każde wystąpienie dostawcy typów może otrzymać TypeProviderConfig
wartość podczas budowy. Ta wartość zawiera "folder rozdzielczości" dostawcy (czyli folder projektu kompilacji lub katalogu zawierającego skrypt), listę zestawów, do których odwołuje się odwołanie i inne informacje.
Unieważnienie
Dostawcy mogą zgłaszać sygnały unieważnienia, aby powiadomić usługę języka F# o zmianie założeń schematu. W przypadku wystąpienia unieważnienia sprawdzanie typów jest redone, jeśli dostawca jest hostowany w programie Visual Studio. Ten sygnał zostanie zignorowany, gdy dostawca jest hostowany w języku F# Interactive lub przez kompilator języka F# (fsc.exe).
informacje o schemacie Buforowanie
Dostawcy muszą często buforować dostęp do informacji o schemacie. Buforowane dane powinny być przechowywane przy użyciu nazwy pliku podanej jako parametr statyczny lub jako dane użytkownika. Przykładem buforowania schematu jest LocalSchemaFile
parametr dostawcy typów w FSharp.Data.TypeProviders
zestawie. W implementacji tych dostawców ten parametr statyczny kieruje dostawcę typów do używania informacji o schemacie w określonym pliku lokalnym zamiast uzyskiwania dostępu do źródła danych za pośrednictwem sieci. Aby użyć buforowanych informacji o schemacie, należy również ustawić parametr ForceUpdate
statyczny na false
wartość . Podobną technikę można użyć, aby włączyć dostęp do danych w trybie online i offline.
Zestaw zapasowy
Podczas kompilowania .dll
pliku lub .exe
plik .dll kopii zapasowej dla wygenerowanych typów jest statycznie połączony z wynikowym zestawem. Ten link jest tworzony przez skopiowanie definicji typów języka pośredniego (IL) i wszystkich zarządzanych zasobów z zestawu zapasowego do zestawu końcowego. W przypadku korzystania z języka F# Interactive plik .dll kopii zapasowej nie jest kopiowany i zamiast tego jest ładowany bezpośrednio do procesu interaktywnego języka F#.
Wyjątki i diagnostyka od dostawców typów
Wszystkie zastosowania wszystkich elementów członkowskich z podanych typów mogą zgłaszać wyjątki. We wszystkich przypadkach, jeśli dostawca typów zgłasza wyjątek, kompilator hosta przypisuje błąd określonemu dostawcy typów.
Wyjątki dostawcy typów nigdy nie powinny powodować błędów wewnętrznych kompilatora.
Dostawcy typów nie mogą zgłaszać ostrzeżeń.
Gdy dostawca typów jest hostowany w kompilatorze języka F#, środowisku projektowym F# lub F# Interactive, wszystkie wyjątki od tego dostawcy są przechwytywane. Właściwość Message jest zawsze tekstem błędu i nie jest wyświetlany żaden ślad stosu. Jeśli zgłosisz wyjątek, możesz zgłosić następujące przykłady:
System.NotSupportedException
, ,System.IO.IOException
System.Exception
.
Dostarczanie wygenerowanych typów
Do tej pory w tym dokumencie wyjaśniono, jak dostarczać wymazane typy. Można również użyć mechanizmu dostawcy typów w języku F#, aby udostępnić wygenerowane typy, które są dodawane jako rzeczywiste definicje typów platformy .NET do programu użytkowników. Należy odwołać się do wygenerowanych typów przy użyciu definicji typu.
open Microsoft.FSharp.TypeProviders
type Service = ODataService<"http://services.odata.org/Northwind/Northwind.svc/">
Kod pomocnika ProvidedTypes-0.2, który jest częścią wydania języka F# 3.0, ma ograniczoną obsługę dostarczania wygenerowanych typów. Następujące instrukcje muszą mieć wartość true dla wygenerowanej definicji typu:
isErased
musi być ustawiona nafalse
.Wygenerowany typ musi zostać dodany do nowo skonstruowanego
ProvidedAssembly()
elementu , który reprezentuje kontener dla wygenerowanych fragmentów kodu.Dostawca musi mieć zestaw zawierający rzeczywisty plik .dll platformy .NET z pasującym plikiem .dll na dysku.
Reguły i ograniczenia
Podczas pisania dostawców typów należy pamiętać o następujących regułach i ograniczeniach.
Podane typy muszą być osiągalne
Wszystkie podane typy powinny być dostępne z typów niezagnieżdżonych. Typy niezagnieżdżone są podane w wywołaniu konstruktora TypeProviderForNamespaces
lub wywołania metody AddNamespace
. Jeśli na przykład dostawca udostępnia typ StaticClass.P : T
, należy upewnić się, że T jest typem niezagnieżdżonym lub zagnieżdżonym pod jednym.
Na przykład niektórzy dostawcy mają klasę statyczną, taką jak DataTypes
te T1, T2, T3, ...
typy. W przeciwnym razie błąd informuje, że znaleziono odwołanie do typu T w zestawie A, ale nie można odnaleźć typu w tym zestawie. Jeśli zostanie wyświetlony ten błąd, sprawdź, czy wszystkie podtypy można uzyskać z typów dostawców. Uwaga: Te T1, T2, T3...
typy są określane jako typy na bieżąco . Pamiętaj, aby umieścić je w dostępnej przestrzeni nazw lub typie nadrzędnym.
Ograniczenia mechanizmu dostawcy typów
Mechanizm dostawcy typów w języku F# ma następujące ograniczenia:
Podstawowa infrastruktura dla dostawców typów w języku F# nie obsługuje typów ogólnych ani nie zapewnia metod ogólnych.
Mechanizm nie obsługuje typów zagnieżdżonych z parametrami statycznymi.
Porady dotyczące projektowania
Podczas procesu programowania mogą znajdować się następujące wskazówki:
Uruchamianie dwóch wystąpień programu Visual Studio
Możesz opracować dostawcę typów w jednym wystąpieniu i przetestować dostawcę w drugim, ponieważ testowe środowisko IDE zablokuje plik .dll, który uniemożliwia ponowne skompilowania dostawcy typów. W związku z tym należy zamknąć drugie wystąpienie programu Visual Studio, gdy dostawca jest wbudowany w pierwszym wystąpieniu, a następnie należy ponownie otworzyć drugie wystąpienie po utworzeniu dostawcy.
Debugowanie dostawców typów przy użyciu wywołań fsc.exe
Dostawcy typów można wywoływać przy użyciu następujących narzędzi:
fsc.exe (kompilator wiersza polecenia języka F#)
fsi.exe (kompilator interaktywny języka F#)
devenv.exe (Visual Studio)
Dostawcy typów często można debugować, korzystając z fsc.exe w pliku skryptu testowego (na przykład script.fsx). Debuger można uruchomić z poziomu wiersza polecenia.
devenv /debugexe fsc.exe script.fsx
Możesz użyć rejestrowania print-to-stdout.