Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 różnorodnych dostawców typów dla powszechnie używanych usług danych internetowych i korporacyjnych. Przykład:
Plik FSharp.Data zawiera dostawców typów dla formatów dokumentów JSON, XML, CSV i HTML.
SwaggerProvider obejmuje dwóch dostawców typów generatywnych, którzy generują model obiektów i klientów HTTP dla interfejsów API opisanych przez schematy OpenApi 3.0 i Swagger 2.0.
FSharp.Data.SqlClient ma zestaw dostawców typów, które umożliwiają weryfikację poprawności osadzania zapytań T-SQL w F# na etapie 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 programiście w sposób silnie typizowany.
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ć tworzenia dostawcy typów, jeśli schemat nie jest dostę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ędziecie mieć wystarczająco dużo zastosowań dostawcy typów, aby warto było go napisać? Czy normalna biblioteka .NET spełnia Twoje potrzeby?
Jak bardzo zmieni się 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, podobny do przykładów w folderze examples
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 znanych typów i członków 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 są omawiane 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 Samples.HelloWorldTypeProvider
wygenerowanej przez dostawcę typów.
Przed ponownym skompilowaniem dostawcy upewnij się, że wszystkie wystąpienia programu Visual Studio i F# Interactive, które korzystają z pliku DLL dostawcy, zostały zamknięte. W przeciwnym razie wystąpi błąd kompilacji, ponieważ wyjściowa biblioteka DLL zostanie zablokowana.
Aby debugować tego dostawcę przy użyciu instrukcji print, utwórz skrypt, który wykrywa 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 procesu devenv
, 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 ustawić przechwytywanie wyjątków pierwszej szansy, otwierając menu Debug
, a następnie wybierając Exceptions
lub przez naciśnięcie klawiszy Ctrl+Alt+E, aby otworzyć okno dialogowe Exceptions
. W tym oknie dialogowym, w obszarze Common Language Runtime Exceptions
, zaznacz pole wyboru Thrown
.
Implementacja dostawcy typów danych
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 tym przypadku używasz typu TypeProviderForNamespaces
z interfejsu API ProvidedTypes
jako typu podstawowego. 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 Typ1...Typ100. 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 do określonej przestrzeni 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ę, zapewniając jeden z rodzajó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.Gdy określasz typ niezagnieżdżony, musisz także podać zestaw (assembly) i przestrzeń nazw (namespace). 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 miało wartość "Hello!". Element GetterCode
dla właściwości używa cytatu F#, który reprezentuje kod generowany przez kompilator hosta do pobierania wartości właściwości. Aby uzyskać więcej informacji na temat cytató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ć podanego członka 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 @@>))
InvokeCode
dla konstruktora zwraca wypowiedź F#, która reprezentuje kod generowany przez kompilator hosta podczas wywoływania konstruktora. Można na przykład użyć następującego konstruktora:
new Type10()
Instancja podanego typu zostanie utworzona przy użyciu danych podstawowych "Dane obiektu". Kod cytowany zawiera konwersję na obj, ponieważ ten typ jest wymazaniem tego podanego typu (jak określono, kiedy zadeklarowano podany typ).
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
konstruktor ponownie zwraca cytat 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 bazowymi danymi "ten". Być może już zauważyłeś, że funkcja InvokeCode
zwraca cytat. 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 cytat języka F#, który określa kod, który kompilator hosta generuje, aby uzyskać właściwość. Podobnie jak InvokeCode
, funkcja GetterCode
zwraca wypowiedź. 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 włącza się do wynikowego wyrażenia w usuniętym typie obj
, a rzutowanie jest używane, aby spełnić wymogi mechanizmu kompilatora kontrolującego typy, aby obiekt był ciągiem znaków. Następna część makeOneProvidedType
zawiera metodę instancyjną 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 usuniętych typach dostarczonych
Przykład w tej sekcji zawiera tylko usunięte typy, które są szczególnie przydatne w następujących sytuacjach:
Podczas tworzenia dostawcy dla przestrzeni informacyjnej zawierającej tylko dane i metody.
Podczas tworzeniu dostawcy, w którym dokładna semantyka typów w środowisku uruchomieniowym nie jest krytyczna 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 pakowania, rozpakowywania i rzutowania, aby obejść 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ć praktyczne i semantyczne konsekwencje korzystania z wymazywanych typów w dostawcach typów lub z dostawcy, który udostępnia wymazywane 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 dostarczonych typów
W przypadku niektórych zastosowań wymazanych typów nie jest wymagana żadna reprezentacja. Na przykład typ o wymazanej informacji może zawierać tylko właściwości statyczne i składowe, bez konstruktorów, a także brak jest metod lub właściwości, które zwracają wystąpienie 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 wykreślenie zadanego 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 usunięciem informacji o podanym typie. (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 typowany opakowanie dla istniejącego typu .NET, często warto, aby Twój typ był zgodny z tym typem, używał instancji tego typu jako reprezentacji lub obu. 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 zbioru 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
Dostępni członkowie 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 ukrycia typu, określając go jako baseType
podczas konstruowania ProvidedTypeDefinition
.
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 elementarnych technik tworzenia takiego dostawcy usług.
Dostawca wyrażeń regularnych z kontrolą typów
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.
Nadawanie nazw właściwości dopasowaniom opartym na dowolnych 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"
Poniższy przykład pokazuje, jak dostawca typów interpretuje 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 Regex reprezentuje typ sparametryzowany
RegexTyped
.Konstruktor
RegexTyped
skutkuje wywołaniem konstruktora Regex, przekazując argument typu statycznego dotyczący wzorca.Wyniki
Match
metody są reprezentowane przez typ standardowy Match .Każda nazwana grupa daje wynik w postaci podanej właściwości, a dostęp do właściwości skutkuje użyciem indeksatora w kolekcji dopasowania
Groups
.
Poniższy kod jest podstawą logiki do zaimplementowania takiego dostawcy, a w tym przykładzie pominięto dodanie wszystkich członków do udostępnionego typu. Aby uzyskać informacje o każdym dodanym członku, zobacz odpowiednią sekcję później w tym temacie.
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
, który jest obowiązkowy, orazoptions
, który jest opcjonalny (ponieważ podano wartość domyślną).Po podaniu argumentów statycznych należy utworzyć wystąpienie wyrażenia regularnego. Ten obiekt zgłosi wyjątek, jeśli wyrażenie regularne jest źle sformułowane, a błąd ten będzie zgłoszony użytkownikom.
W wywołaniu zwrotnym
DefineStaticParameters
zdefiniujesz typ, który zostanie zwrócony po podaniu argumentów.Ten kod ustawia
HideObjectMethods
na wartość true, aby funkcjonalność IntelliSense pozostała bardziej funkcjonalna. Ten atrybut powoduje, że elementy członkowskieEquals
,GetHashCode
,Finalize
iGetType
są pomijane na listach IntelliSense dla podanego obiektu.Używasz
obj
jako podstawowego typu metody, ale wykorzystasz obiektRegex
jako reprezentację środowiska uruchomieniowego tego typu, jak pokazano w poniższym przykładzie.Wywołanie konstruktora
Regex
zgłasza ArgumentException, 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 listą cytatów, które stanowią 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 instancyjną metodę Match. Jednak ta metoda powinna zwrócić wartość podanego Match
typu, aby można było uzyskać dostęp do grup w sposób silnie typizowany. Tak więc 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 dodajesz jedną właściwość do typu Match dla każdej grupy. Podczas wykonywania, dopasowanie jest reprezentowane jako Match wartość, więc wyrażenie definiujące właściwość musi używać Groups właściwości indeksowanej, by uzyskać właściwą 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 podano funkcję GetterCode
, a zapisać, jeśli podano funkcję SetterCode
, więc właściwość może być 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, args[0]
reprezentuje wystąpienie RegexTyped
, na którym jest wywoływana metoda, a 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 jedynie przeprowadza wymazanie do utworzenia standardowej instancji wyrażeń regularnych platformy .NET, która jest ponownie opakowana jako obiekt, ponieważ obj
jest wymazaniem 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 oparty na danych lokalnych
Często możesz chcieć, aby dostawcy typów prezentowali 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ść (metr) | 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.
Wszystkie jednostki są jednostkami Systemu Międzynarodowego (SI), ponieważ moduł FSharp.Data.UnitSystems.SI.UnitNames Module (F#) je definiuje.
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}"
Optymalny przekład będzie wymagać od dostawcy typów zdefiniowania rzeczywistego typu CsvFile
w zestawie dostawcy typów. Dostawcy typów często polegają na kilku pomocniczych typach i metodach, aby opakować ważną logikę. Ponieważ mierniki są usunięte podczas wykonywania procesu, można użyć float[]
jako wymazanego typu 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 umożliwiają odczytanie oryginalnego pliku lub pliku, 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.
Idąc dalej
Poniższe sekcje zawierają sugestie dotyczące dalszej analizy.
Spojrzenie na skompilowany kod dla wymazanych typów
Aby zilustrować, jak użycie dostawcy typów odpowiada emitowanemu kodowi, spójrz na następującą funkcję przy użyciu HelloWorldTypeProvider
, który został użyty 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 odwołania do typu Type1
oraz właściwości InstanceProperty
zostały wymazane, pozostawiając jedynie operacje dotyczące typów czasu wykonywania.
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 łączności Ogólnie rzecz biorąc, 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ć się znacznikami TypeProvider
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+">()
Źródła danych typu singleton. Niektórzy z dostawców połączeń łączą się z jednym dedykowanym źródłem danych i udostępniają wyłącznie 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 GetConnection
Większość dostawców typów powinna być napisana tak, aby używała 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
żywotność i unieważnienie w przypadku 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 członów na żądanie
Interfejs API ProvidedType ma opóźnione wersje AddMember.
type ProvidedType =
member AddMemberDelayed : (unit -> MemberInfo) -> unit
member AddMembersDelayed : (unit -> MemberInfo list) -> unit
Te wersje służą do tworzenia przestrzeni na żądanie różnych 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) używając standardowych elementów MakeArrayType
, MakePointerType
i MakeGenericType
dla dowolnego wystąpienia elementu Type, w tym ProvidedTypeDefinitions
.
Uwaga / Notatka
W niektórych przypadkach może być konieczne użycie pomocnika w programie ProvidedTypeBuilder.MakeGenericType
. Zobacz dokumentację zestawu SDK dostawcy typów aby uzyskać więcej informacji.
Tworzenie adnotacji jednostek 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 Project-Local lub Script-Local
Każde wystąpienie dostawcy typów może otrzymać wartość TypeProviderConfig
podczas utworzenia. 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 ponownie wykonane, 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 buforowania
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 parametr LocalSchemaFile
w dostawcach typów w bibliotece FSharp.Data.TypeProviders
. 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ć statyczny parametr ForceUpdate
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 pliku .dll
lub pliku .exe
, plik zapasowy .dll dla wygenerowanych typów jest statycznie dołączany do wynikowego zestawienia. 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 użycia członków podanych typów mogą powodować 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 żaden ślad stosu nie jest wyświetlany. 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 dokumencie wyjaśniono, jak udostępniać 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 stwierdzenia muszą być prawdziwe 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ć osiągalne z typów niezagnieżdżonych. Typy niezagnieżdżone są przekazywane w wywołaniu konstruktora TypeProviderForNamespaces
lub w wywołaniu 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 w ramach takiego.
Na przykład niektórzy dostawcy mają klasę statyczną DataTypes
, która zawiera te typy T1, T2, T3, ...
. 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 są osiągalne z typów udostępnianych przez dostawcę. Uwaga: Te T1, T2, T3...
typy są określane jako typy dynamiczne. Pamiętaj, aby umieścić je w dostępnej przestrzeni nazw lub typie nadrzędnym.
Ograniczenia mechanizmu dostarczania 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 programowania
Podczas procesu rozwoju pomocne mogą się okazać następujące wskazówki:
Uruchom dwa wystąpienia programu Visual Studio
Można opracować dostawcę typów w jednym wystąpieniu i przetestować go w drugim, ponieważ testowe środowisko IDE zablokuje plik .dll, co uniemożliwia ponowne skompilowanie dostawcy typów. W związku z tym należy zamknąć drugie wystąpienie programu Visual Studio, gdy dostawca jest tworzony w pierwszym wystąpieniu, a następnie 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.