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 Exceptionszaznacz 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 InvokeCodefunkcja 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 ProvidedTypeDefinitionelementu :

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: patternparametr , który jest obowiązkowy, i options, 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 powoduje Equalspomijanie elementów członkowskich , GetHashCode, Finalizei GetType z list funkcji IntelliSense dla podanego obiektu.

  • obj Użyjesz jako podstawowego typu metody, ale użyjesz Regex 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żywasz Go 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ć odpowiednie float<_> 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 MakeArrayTypeelementu , MakePointerTypei 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 falsewartość . 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.IOExceptionSystem.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 na false.

  • 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.

Zobacz też