教學課程:建立型別提供者
F# 中的型別提供者機制是其支援資訊豐富程式設計的重要部分。 本教學課程將逐步引導您開發數個簡單型別提供者以釐清基本概念,藉以說明如何建立您自己的型別提供者。 若要深入了解 F# 中的型別提供者機制,請參閱型別提供者。
F# 生態系統包含多種型別提供者,可供常用的網際網路和企業資料服務使用。 例如:
FSharp.Data 包含 JSON、XML、CSV 和 HTML 文件格式的型別提供者。
SwaggerProvider 包含兩種產生性型別提供者,會為 OpenApi 3.0 和 Swagger 2.0 結構描述所說明的 API 產生物件模型和 HTTP 用戶端。
FSharp.Data.SqlClient 有一組型別提供者,用於在 F# 中檢查 T-SQL 的編譯時間內嵌。
您可以建立自訂型別提供者,也可以參考其他人建立的型別提供者。 例如,您的組織可能會有資料服務提供大量且不斷增加的具名資料集,且各資料集都有其本身的穩定資料結構描述。 您可以建立一個型別提供者,並使其以強型別方式讀取結構描述,並且對程式設計人員呈現目前的資料集。
開始之前
型別提供者機制主要用來將穩定資料和服務資訊空間導入 F# 程式設計體驗中。
此機制的用途並非是要使用與程式邏輯有關的方式,導入結構描述在程式執行期間有所變更的資訊空間。 此外,此機制也不是為了語言內部中繼程式設計而設計的;即使該領域確有某些有效的用途。 只有在需要時,以及型別提供者的開發有非常高的價值時,才適合使用此機制。
您應避免在結構描述無法使用的情況下撰寫型別提供者。 同樣地,若一般 (甚或現有的) .NET 程式庫已堪用,則應避免撰寫型別提供者。
開始之前,您可以提出下列問題:
您的資訊來源是否有結構描述? 如果有,在 F# 和 .NET 型別系統中的對應為何?
您可以使用現有 (動態型別) API 作為實作的起點嗎?
您和組織是否會充分使用型別提供者,使其值得撰寫? 一般 .NET 程式庫是否符合您的需求?
您的結構描述會有多少變更?
在編碼期間是否會變更?
在編碼工作階段之間是否會變更?
在程式執行期間是否會變更?
結構描述在執行階段和已編譯程式碼的存留期內都很穩定,是型別提供者最適用的狀態。
簡單型別提供者
此範例是 Samples.HelloWorldTypeProvider,類似於 F# 型別提供者 SDK 的 examples
目錄中的範例。 此提供者會提供包含 100 個清除型別的「型別空間」,如下列程式碼所示,其間使用 F# 簽章語法,並省略了 Type1
以外所有項目的詳細資料。 如需清除型別的詳細資訊,請參閱本主題稍後的關於清除已提供型別的詳細資料。
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 =
…
請注意,提供的型別和成員集合是靜態已知的。 此範例不會利用提供者的功能提供相依於結構描述的型別。 型別提供者的實作概述於下列程式碼中,本主題的後續章節將提供詳細資料。
警告
此程式碼與線上範例可能有所差異。
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()
若要使用此提供者,請開啟個別的 Visual Studio 執行個體、建立 F# 指令碼,然後使用 #r 從您的指令碼新增對提供者的參考,如下列程式碼所示:
#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
然後,在型別提供者產生的 Samples.HelloWorldTypeProvider
命名空間底下尋找型別。
重新編譯提供者之前,請確定您已關閉所有正在使用提供者 DLL 的 Visual Studio 和 F# 互動執行個體。 否則將會發生建置錯誤,因為輸出 DLL 會遭到鎖定。
若要使用 print 陳述式對此提供者進行偵錯,請建立指令碼將提供者的問題公開,然後使用下列程式碼:
fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
若要使用 Visual Studio 對此提供者進行偵錯,請使用系統管理認證開啟 Visual Studio 的開發人員命令提示字元,然後執行下列命令:
devenv.exe /debugexe fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx
或者,開啟 Visual Studio、開啟 [偵錯] 功能表、選擇 Debug/Attach to process…
,然後附加至另一個 devenv
程序以在該處編輯指令碼。 藉由使用此方法,您可透過互動方式將運算式輸入第二個執行個體 (具有完整的 IntelliSense 和其他功能) 中,而更輕鬆地將型別提供者中的特定邏輯設為目標。
您可以停用 Just My Code 偵錯,以在產生的程式碼中更清楚地識別錯誤。 如需如何啟用或停用此功能的資訊,請參閱使用偵錯工具瀏覽程式碼。 此外,您也可以開啟 Debug
功能表,然後選擇 Exceptions
或選擇 Ctrl+Alt+E 鍵將 Exceptions
對話方塊開啟,以設定成要攔截第一個可能發生的例外狀況。 在該對話方塊的 Common Language Runtime Exceptions
底下,選取 Thrown
核取方塊。
型別提供者的實作
本節將逐步引導您完成型別提供者實作的主體區段。 首先,您會定義自訂型別提供者本身的型別:
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
此型別必須是公用的,且您必須使用 TypeProvider 屬性加以標記,如此,當個別的 F# 專案參考包含型別的組件時,編譯器將可辨識型別提供者。 config 參數是選用的,如果存在,會包含 F# 編譯器所建立的型別提供者執行個體的內容組態資訊。
接著,您會實作 ITypeProvider 介面。 在此案例中,您會使用 ProvidedTypes
API 中的 TypeProviderForNamespaces
型別作為基底型別。 此協助程式型別可提供一組有限而積極提供的命名空間,每個命名空間都直接包含有限的固定數目、積極提供的型別。 在此內容中,提供者會積極產生型別,即使不需要或未使用型別亦然。
inherit TypeProviderForNamespaces(config)
接下來,定義本機私人值為提供的型別指定命名空間,並尋找型別提供者組件本身。 此組件後續將作為清除已提供型別的邏輯父型別。
let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()
接著,建立函式以提供各個型別:Type1…Type100。 本主題稍後將進一步詳細說明此函式。
let makeOneProvidedType (n:int) = …
然後,產生 100 個提供的型別:
let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]
接下來,將型別新增為提供的命名空間:
do this.AddNamespace(namespaceName, types)
最後,新增組件屬性,以指出您要建立型別提供者 DLL:
[<assembly:TypeProviderAssembly>]
do()
提供一個型別及其成員
makeOneProvidedType
函式會執行提供其中一種型別的實際工作。
let makeOneProvidedType (n:int) =
…
下列步驟說明此函式的實作。 首先,建立提供的型別 (例如,n = 1 時為 Type1、n = 57 時為 Type57)。
// 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>)
您應注意以下幾點:
這個提供的型別會清除。 由於您指出基底型別是
obj
,因此執行個體會在已編譯的程式碼中顯示為 obj 型別的值。指定了非巢狀型別時,您必須指定組件和命名空間。 如果是清除型別,則組件應為型別提供者組件本身。
接著,將 XML 文件新增至型別。 此文件是延遲的,也就是說,主機編譯器需要時就會隨需計算。
t.AddXmlDocDelayed (fun () -> $"""This provided type {"Type" + string n}""")
接下來,您會將提供的靜態屬性新增至型別:
let staticProp = ProvidedProperty(propertyName = "StaticProperty",
propertyType = typeof<string>,
isStatic = true,
getterCode = (fun args -> <@@ "Hello!" @@>))
取得此屬性一律會評估為字串 "Hello!"。 屬性的 GetterCode
會使用 F# 引用,代表主機編譯器為了取得屬性而產生的程式碼。 如需引用的詳細資訊,請參閱程式碼引用 (F#)。
將 XML 文件新增至屬性。
staticProp.AddXmlDocDelayed(fun () -> "This is a static property")
現在,將提供的屬性附加至提供的型別。 您必須將提供的成員附加至單一型別。 否則將無法存取成員。
t.AddMember staticProp
現在,建立提供的建構函式,且不採用任何參數。
let ctor = ProvidedConstructor(parameters = [ ],
invokeCode = (fun args -> <@@ "The object data" :> obj @@>))
建構函式的 InvokeCode
會傳回 F# 引用,代表主機編譯器在呼叫建構函式時產生的程式碼。 例如,您可以使用下列建構函式:
new Type10()
已提供型別的執行個體將會使用基礎資料「物件資料」來建立。 引用的程式碼會包含 obj 的轉換,因為該型別是將這個提供的型別清除 (如您在宣告提供的型別時所指定)。
將 XML 文件新增至建構函式,並將提供的建構函式新增至提供的型別:
ctor.AddXmlDocDelayed(fun () -> "This is a constructor")
t.AddMember ctor
建立第二個提供的建構函式,並使其採用一個參數:
let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
invokeCode = (fun args -> <@@ (%%(args[0]) : string) :> obj @@>))
建構函式的 InvokeCode
會再次傳回 F# 引用,代表主機編譯器為了呼叫方法而產生的程式碼。 例如,您可以使用下列建構函式:
new Type10("ten")
已提供型別的執行個體會使用基礎資料 "ten" 來建立。 您可能已注意到 InvokeCode
函式會傳回引用。 此函式的輸入是運算式清單,每個建構函式參數各有一個。 在此案例中,可在 args[0]
中使用代表單一參數值的運算式。 呼叫建構函式的程式碼會將傳回值強制轉型為清除型別 obj
。 將第二個提供的建構函式新增至型別之後,您會建立提供的執行個體屬性:
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
取得此屬性將會傳回字串的長度,此為表示法物件。 GetterCode
屬性會傳回 F# 引用,指定主機編譯器產生用以取得屬性的程式碼。 如同 InvokeCode
,GetterCode
函式會傳回引用。 主機編譯器會使用引數清單呼叫此函式。 在此案例中,引數僅包含單一運算式,代表您要對其呼叫 getter 的執行個體,可以使用 args[0]
來存取。 接著,GetterCode
的實作會接合到清除型別 obj
的結果引用中,並使用轉換來因應編譯器中用來檢查型別的物件是否為字串的機制。 makeOneProvidedType
的下一個部分提供具有一個參數的執行個體方法。
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
最後,建立包含 100 個巢狀屬性的巢狀型別。 此巢狀型別及其屬性會延遲建立,也就是進行隨需計算。
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])
清除已提供型別的詳細資料
本節中的範例僅提供清除已提供型別,這在下列情況下特別有用:
當您為僅包含資料和方法的資訊空間撰寫提供者時。
您在撰寫提供者,而正確的執行階段型別語意對於資訊空間的實際使用並不重要。
您在撰寫資訊空間的提供者,而空間龐大且互連,因此在技術上無法為資訊空間產生實際的 .NET 型別。
在此範例中,每個提供的型別都會清除為型別 obj
,而且所有的型別使用在已編譯的程式碼中都會顯示為型別 obj
。 事實上,這些範例中的基礎物件是字串,但型別在 .NET 編譯的程式碼中會顯示為 System.Object
。 如同型別清除的各種用法,您可以使用明確的 boxing、unboxing 和轉換來推翻清除型別。 在此情況下,使用物件時可能會產生無效的轉換例外狀況。 提供者執行階段可定義本身的私人表示型別,以利防範錯誤的表示法。 您無法在 F# 本身定義清除型別。 只有已提供的型別才可清除。 您必須了解對型別提供者或提供清除型別的提供者使用清除型別,實際上和語意上會有何後果。 清除型別沒有實際的 .NET 型別。 因此,您無法精確反映型別,且如果使用執行階段轉換和其他依賴確切執行階段型別語意的技術,您可能會推翻清除型別。 清除型別的推翻常會導致執行階段的型別轉換例外狀況。
選擇清除已提供型別的表示法
清除已提供型別的某些用法並不需要表示法。 例如,清除已提供型別可能僅包含靜態屬性和成員而沒有建構函式,而且沒有方法或屬性會傳回型別的執行個體。 如果您可以連線到清除已提供型別的執行個體,則必須考量下列問題:
清除已提供的型別是何意?
清除已提供的型別,型別才會出現在已編譯的 .NET 程式碼中。
清除已提供的清除類別型別,一律是型別繼承鏈結中的第一個非清除基底型別。
清除已提供的清除介面型別一律為
System.Object
。
已提供型別的表示法是何意?
- 清除已提供型別可能的物件集,稱為其表示法。 在本文件的範例中,所有清除已提供型別
Type1..Type100
的表示法一律為字串物件。
提供的型別所有的表示法都必須與已提供型別的清除相容。 (否則,F# 編譯器會針對型別提供者的使用提供錯誤,或會產生無效的無法驗證的 .NET 程序碼。如果型別提供者傳回的程式碼提供了無效的表示法,則該型別提供者無效。)
您可以使用下列兩種方法之一來選擇已提供物件的表示法;兩種方法都很常見:
如果您只是對現有的 .NET 型別提供強型別包裝函式,則讓您的型別清除為該型別,並且 (或) 使用該型別的執行個體作為表示法,應該會有好處。 如果該型別大部分的現有方法在使用強型別版本時仍然有利,此方法就很合用。
如果您想要建立與任何現有 .NET API 明顯不同的 API,可以建立執行階段型別,並且使其成為已提供型別的型別清除和表示法。
本文件中的範例會使用字串作為已提供物件的表示法。 表示法往往適合使用其他物件。 例如,您可以使用字典作為屬性包:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))
或者,您也可以在型別提供者中定義型別,以在執行階段連同一或多個執行階段作業一起用來形成表示法:
type DataObject() =
let data = Dictionary<string,obj>()
member x.RuntimeOperation() = data.Count
提供的成員隨後可以建構此物件型別的執行個體:
ProvidedConstructor(parameters = [],
invokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))
在此情況下,您可以 (選擇性地) 在建構 ProvidedTypeDefinition
時將此型別指定為 baseType
,以使用此型別作為型別清除:
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
主要課題
上一節說明了如何建立會提供各種型別、屬性和方法的簡單清除型別提供者。 本節也說明了型別清除的概念,包括從型別提供者提供清除型別的一些優缺點,並討論了清除型別的表示法。
使用靜態參數的型別提供者
以靜態資料將型別提供者參數化的能力可實現許多有趣的案例,即使在提供者不需要存取任何本機或遠端資料的情況下,也是如此。 在本節中,您將了解整合這類提供者的一些基本技術。
型別檢查 Regex 提供者
假設您想要為規則運算式實作型別提供者,將 .NET Regex 程式庫包裝在提供下列編譯時期保證的介面中:
驗證規則運算式是否有效。
對以規則運算式中的任何群組名稱為基礎的相符項目提供具名屬性。
本節說明如何使用型別提供者建立 RegexTyped
型別,讓規則運算式模式加以參數化而提供這些效益。 如果提供的模式無效,編譯器將會報告錯誤,且型別提供者可從模式中擷取群組,以便您可以使用相符項目的具名屬性加以存取。 在設計型別提供者時,您應該考量其公開 API 對終端使用者呈現的外觀,以及此設計如何轉譯為 .NET 程式碼。 下列範例說明如何使用這類 API 取得區碼的元件:
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"
下列範例說明型別提供者如何轉譯這些呼叫:
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"
請注意下列幾點:
標準 Regex 型別代表參數化
RegexTyped
型別。RegexTyped
建構函式會產生對 Regex 建構函式的呼叫,並傳入模式的靜態型別引數。Match
方法的結果以標準 Match 型別表示。每個具名群組都會產生提供的屬性,且存取屬性會使相符項目的
Groups
集合使用索引子。
下列程式碼是實作這類提供者的邏輯核心;此範例並未示範如何將所有成員新增至提供的型別。 如需每個新增成員的相關資訊,請參閱本主題稍後的適當章節。
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 ()
請注意下列幾點:
型別提供者會採用兩個靜態參數:必要的
pattern
和選用的options
(因為提供了預設值)。提供靜態引數之後,您會建立規則運算式的執行個體。 如果 Regex 格式不正確,此執行個體會擲回例外狀況,而此錯誤將會回報給使用者。
在
DefineStaticParameters
回呼內,您可以定義將在提供引數後傳回的型別。此程式碼會將
HideObjectMethods
設定為 true,讓 IntelliSense 體驗保持精簡。 此屬性會使已提供物件的 IntelliSense 清單隱藏Equals
、GetHashCode
、Finalize
和GetType
成員。您使用
obj
作為方法的基底型別,但您將使用Regex
物件作為此型別的執行階段表示法,如下一個範例所示。當規則運算式無效時,呼叫
Regex
建構函式將會擲回 ArgumentException。 編譯器會攔截此例外狀況,並在編譯時期或在 Visual Studio 編輯器中向使用者報告錯誤訊息。 有了此例外狀況,就可直接驗證規則運算式而無須執行應用程式。
以上定義的型別未包含任何有意義的方法或屬性,因此尚無效用。 請先新增靜態 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
先前的程式碼定義了方法 IsMatch
,此方法用字串作為輸入,並且傳回 bool
。 唯一棘手的部分,是在 InvokeCode
定義中使用 args
引數。 在此範例中,args
是代表此方法之引數的引用清單。 如果方法是執行個體方法,則第一個引數代表 this
引數。 但對靜態方法而言,引數只是方法的明確引數。 請注意,引用值的型別應符合指定的傳回型別 (在此案例中為 bool
)。 另請注意,此程式碼會使用 AddXmlDoc
方法來確定提供的方法也有實用的文件 (可以透過 IntelliSense 提供)。
接著,新增執行個體 Match 方法。 不過,此方法應傳回提供的 Match
型別值,以便用強型別的方式存取群組。 因此,您會先宣告 Match
型別。 由於此型別相依於提供作為靜態引數的模式,此型別必須內嵌在參數化型別定義內:
let matchTy =
ProvidedTypeDefinition(
"MatchType",
baseType = Some baseTy,
hideObjectMethods = true)
ty.AddMember matchTy
接著,對每個群組的 Match 型別新增一個屬性。 在執行階段,相符項目會以 Match 值表示,因此定義屬性的引用必須使用 Groups 索引屬性取得相關群組。
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
同樣請注意,您會將 XML 文件新增至提供的屬性。 另請注意,如果提供了 GetterCode
函式,則可以讀取屬性,若提供了 SetterCode
函式則可寫入屬性,因此產生的屬性是唯讀的。
現在,您可以建立會傳回此 Match
型別之值的執行個體方法:
let matchMethod =
ProvidedMethod(
methodName = "Match",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = matchTy,
invokeCode = fun args -> <@@ ((%%args[0]:obj) :?> Regex).Match(%%args[1]) :> obj @@>)
matchMeth.AddXmlDoc "Searches the specified input string for the first occurrence of this regular expression"
ty.AddMember matchMeth
由於您要建立執行個體方法,因此 args[0]
代表您要對其呼叫方法的 RegexTyped
執行個體,而 args[1]
是輸入引數。
最後,提供建構函式,以便建立已提供型別的執行個體。
let ctor =
ProvidedConstructor(
parameters = [],
invokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)
ctor.AddXmlDoc("Initializes a regular expression instance.")
ty.AddMember ctor
建構函式只會清除並建立標準 .NET Regex 執行個體,而此執行個體會再次 Boxed 到某個物件,因為 obj
是已提供型別的清除。 透過該變更,本主題先前指定的範例 API 使用方式即可如預期運作。 以下是完整的最終程式碼:
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 ()
主要課題
本節說明如何建立對本身的靜態參數運作的型別提供者。 提供者會檢查靜態參數,並根據其值提供作業。
受本機資料支援的型別提供者
您很可能會希望型別提供者不僅根據靜態參數來呈現 API,也會依據來自本機或遠端系統的資訊。 本節討論以本機資料 (例如本機資料檔案) 為基礎的型別提供者。
簡單 CSV 檔案提供者
在簡單的範例中,我們假設有一個型別提供者用來存取逗號分隔值 (CSV) 格式的科學資料。 本節假設 CSV 檔案包含一個尾隨浮點數資料的標頭資料列,如下表所示:
距離 (公尺) | 時間 (秒) |
---|---|
50.0 | 3.7 |
100.0 | 5.2 |
150.0 | 6.4 |
本節說明如何提供型別,讓您用來取得具有型別 Distance
之 float<meter>
屬性的資料列,以及具有型別 float<second>
之 Time
屬性的資料列。 為了簡單起見,我們做了下列假設:
標頭名稱沒有單位,或採用「名稱 (單位)」的格式,且不含逗號。
單位全都是 System International (SI) 單位,如 FSharp.Data.UnitSystems.SI.UnitNames Module (F#) 模組所定義。
單位都是簡單的 (例如公尺),而不是複合的 (例如公尺/秒)。
所有資料行都包含浮點數資料。
更完整的提供者會放寬這些限制。
同樣地,第一個步驟是考量 API 的外觀。 假設有一個包含先前資料表內容的 info.csv
檔案 (採用逗號分隔的格式),則提供者的使用者應該可以編寫類似下列範例的程式碼:
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn $"{float time}"
在此案例中,編譯器應將這些呼叫轉換成類似下列範例的內容:
let info = new CsvFile("info.csv")
for row in info.Data do
let (time:float) = row[1]
printfn $"%f{float time}"
要做出最佳轉譯,型別提供者必須在型別提供者的組件中定義實際的 CsvFile
型別。 型別提供者常會依賴數個協助程式型別和方法來包裝重要邏輯。 由於量值會在執行階段清除,您可以使用 float[]
作為資料列的清除型別。 編譯器會將不同的資料行視為具有不同的量值型別。 例如,範例中的第一個資料行具有型別 float<meter>
,第二個則具有 float<second>
。 不過,清除表示法仍可相當簡單。
下列程式碼顯示實作的核心。
// 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])
請注意下列實作相關要點:
多載的建構函式允許讀取原始檔案或具有相同結構描述的檔案。 此模式在您撰寫本機或遠端資料來源的型別提供者時很常見,可讓本機檔案作為遠端資料的範本。
您可以使用傳至型別提供者建構函式的 TypeProviderConfig 值來解析相對檔案名稱。
您可以使用
AddDefinitionLocation
方法來定義提供的屬性所在的位置。 因此,如果您對提供的屬性使用Go To Definition
,CSV 檔案將會在 Visual Studio 中開啟。您可以使用
ProvidedMeasureBuilder
型別來查閱 SI 單位,並產生相關的float<_>
型別。
主要課題
本節說明如何使用包含在資料來源本身的簡單結構描述,為本機資料來源建立型別提供者。
更進一步
以下幾節包含進一步研究的建議。
了解清除型別的已編譯程式碼
為了讓您了解型別提供者的用法與發出的程式碼之間的對應情形,請使用本主題中先前使用的 HelloWorldTypeProvider
查看下列函式。
let function1 () =
let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
obj1.InstanceProperty
下圖是使用 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
如範例所示,所有對型別 Type1
與 InstanceProperty
屬性的引述均已清除,僅保留相關執行階段型別的作業。
型別提供者的設計和命名慣例
撰寫型別提供者時請觀察下列慣例。
連線通訊協定的提供者 一般而言,資料與服務連線通訊協定 (例如 OData 或 SQL 連線) 的多數提供者 DLL 的名稱都應以 TypeProvider
或 TypeProviders
結尾。 例如,請使用類似下列字串的 DLL 名稱:
Fabrikam.Management.BasicTypeProviders.dll
請確定您提供的型別是對應命名空間的成員,並指出您實作的連線通訊協定:
Fabrikam.Management.BasicTypeProviders.WmiConnection<…>
Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>
一般編碼的公用程式提供者。 對於公用程式型別提供者 (例如規則運算式的型別提供者),型別提供者可以是基底程式庫的一部分,如下列範例所示:
#r "Fabrikam.Core.Text.Utilities.dll"
在此情況下,提供的型別會根據一般 .NET 設計慣例出現在適當的點:
open Fabrikam.Core.Text.RegexTyped
let regex = new RegexTyped<"a+b+a+b+">()
單一資料來源。 某些型別提供者會連線至單一專用資料來源,並且僅提供資料。 在此情況下,您應卸除 TypeProvider
尾碼,並使用一般慣例進行 .NET 命名:
#r "Fabrikam.Data.Freebase.dll"
let data = Fabrikam.Data.Freebase.Astronomy.Asteroids
如需詳細資訊,請參閱本主題稍後說明的 GetConnection
設計慣例。
型別提供者的設計模式
以下幾節說明您在撰寫型別提供者時可使用的設計模式。
GetConnection 設計模式
大部分型別提供者都應撰寫為使用 FSharp.Data.TypeProviders.dll 中的型別提供者所使用的 GetConnection
模式,如下列範例所示:
#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
由遠端資料和服務支援的型別提供者
建立由遠端資料和服務支援的型別提供者之前,您必須先考量連線程式設計固有的一系列問題。 這些問題包含下列考量:
結構描述對應
結構描述有所變更時的活躍度和失效
結構描述快取
資料存取作業的非同步實作
支援查詢,包括 LINQ 查詢
認證和驗證
本主題不會進一步探索這些問題。
其他撰寫技術
您在撰寫自己的型別提供者時可以使用下列其他技術。
隨需建立型別和成員
ProvidedType API 具有延遲版的 AddMember。
type ProvidedType =
member AddMemberDelayed : (unit -> MemberInfo) -> unit
member AddMembersDelayed : (unit -> MemberInfo list) -> unit
這些版本可用來建立型別的隨需空間。
提供陣列型別和泛型型別具現化
您對 Type 的任何執行個體 (包括 ProvidedTypeDefinitions
) 使用一般 MakeArrayType
、MakePointerType
和 MakeGenericType
,以建立提供的成員 (其簽章包括陣列型別、byref 型別,以及泛型型別的具現化)。
注意
在某些情況下,您可能必須在 ProvidedTypeBuilder.MakeGenericType
中使用協助程式。 如需詳細資訊,請參閱型別提供者 SDK 文件。
提供測量單位註釋
ProvidedTypes API 具有提供量值註釋的協助程式。 例如,若要提供型別 float<kg>
,請使用下列程式碼:
let measures = ProvidedMeasureBuilder.Default
let kg = measures.SI "kilogram"
let m = measures.SI "meter"
let float_kg = measures.AnnotateType(typeof<float>,[kg])
若要提供型別 Nullable<decimal<kg/m^2>>
,請使用下列程式碼:
let kgpm2 = measures.Ratio(kg, measures.Square m)
let dkgpm2 = measures.AnnotateType(typeof<decimal>,[kgpm2])
let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]
存取專案本機或指令碼本機資源
型別提供者的每個執行個體在建構期間都可獲得一個 TypeProviderConfig
值。 此值包含提供者的「解析資料夾」(也就是編譯的專案資料夾或包含指令碼的目錄)、參考的組件清單,以及其他資訊。
失效
提供者可以引發無效信號,以通知 F# 語言服務結構描述假設可能已變更。 無效發生時,如果提供者裝載於 Visual Studio 中,就會重新執行型別檢查。 提供者裝載於 F# 互動或 F# 編譯器 (fsc.exe) 時,將會忽略此信號。
快取結構描述資訊
提供者常須快取結構描述資訊的存取。 快取的資料應使用指定為靜態參數或使用者資料的檔案名稱來儲存。 舉例來說,FSharp.Data.TypeProviders
組件中型別提供者的 LocalSchemaFile
參數,即為結構描述快取。 在這些提供者的實作中,此靜態參數會指示型別提供者使用指定本機檔案中的結構描述資訊,而不是透過網路存取資料來源。 若要使用快取的結構描述資訊,您也必須將靜態參數 ForceUpdate
設定為 false
。 您可以使用類似的技術來啟用線上和離線資料存取。
支援組件
當您編譯 .dll
或 .exe
檔案時,產生的型別適用的支援 .dll 檔案會靜態連結至產生的組件中。 此連結的建立方式,是將中繼語言 (IL) 型別定義和任何受控資源從支援組件複製到最終組件中。 當您使用 F# 互動時,支援 .dll 檔案不會複製,而是直接載入 F# 互動程序中。
型別提供者的例外狀況和診斷
以任何方式使用已提供型別的任何成員,都有可能擲回例外狀況。 在任何情況下,當型別提供者擲回例外狀況時,主機編譯器就會將錯誤歸因於特定型別提供者。
型別提供者例外狀況絕不會產生內部編譯器錯誤。
型別提供者無法報告警告。
當型別提供者裝載於 F# 編譯器、F# 開發環境或 F# 互動時,將會攔截該提供者的所有例外狀況。 Message 屬性一律為錯誤文字,且不會顯示堆疊追蹤。 如果您要擲回例外狀況,可以擲回下列範例:
System.NotSupportedException
、System.IO.IOException
、System.Exception
。
提供產生的型別
到目前為止,本文件說明了如何提供清除型別。 您也可以使用 F# 中的型別提供者機制來提供產生的型別,這些型別會在使用者的程式中新增為實際的 .NET 型別定義。 您必須使用型別定義來參考產生的已提供型別。
open Microsoft.FSharp.TypeProviders
type Service = ODataService<"http://services.odata.org/Northwind/Northwind.svc/">
F# 3.0 版包含的 ProvidedTypes-0.2 協助程式程式碼只能有限度地支援提供產生的型別。 對於產生的型別定義,下列陳述必須屬實:
isErased
必須設定為false
。產生的型別必須新增至新建構的
ProvidedAssembly()
,這代表產生的程式碼片段的容器。提供者必須有一個組件,具有與磁碟上的 .dll 檔案相符的實際支援 .NET .dll 檔案。
規則和限制
在撰寫型別提供者時,請留意下列規則和限制。
提供的型別必須是可連線的
所有提供的型別都應可從非巢狀型別連線。 在呼叫 TypeProviderForNamespaces
建構函式或呼叫 AddNamespace
時,會提供非巢狀型別。 例如,如果提供者提供了型別 StaticClass.P : T
,您必須確定 T 是非巢狀型別還是內嵌於其下的型別。
例如,某些提供者具有靜態類別,例如包含 T1, T2, T3, ...
等型別的 DataTypes
。 否則,會有錯誤指出在組件 A 中可找到型別 T 的參考,但該組件中找不到該型別。 如果出現此錯誤,請確認您可以從提供者型別連線到所有子型別。 注意:這些 T1, T2, T3...
型別稱為即時型別。 請記得將其放在可存取的命名空間或父型別中。
型別提供者機制的限制
F# 中的型別提供者機制有下列限制:
F# 中型別提供者的基礎結構不支援提供的泛型型別或提供的泛型方法。
此機制不支援具有靜態參數的巢狀型別。
開發秘訣
在開發過程中,下列秘訣可能會有所幫助:
執行兩個 Visual Studio 執行個體
您可以在一個執行個體中開發型別提供者,並在另一個執行個體中測試提供者,因為測試 IDE 會鎖定 .dll 檔案,以防止重建型別提供者。 因此,在 Visual Studio 的第一個執行個體中建置提供者時,您必須關閉第二個執行個體,然後必須在提供者建置後重新開啟第二個執行個體。
使用 fsc.exe 的叫用對型別提供者進行偵錯
您可以使用下列工具來叫用型別提供者:
fsc.exe (F# 命令列編譯器)
fsi.exe (F# 互動編譯器)
devenv.exe (Visual Studio)
對測試指令碼檔案 (例如 script.fsx) 使用 fsc.exe,通常最容易對型別提供者進行偵錯。 您可以從命令提示字元啟動偵錯工具。
devenv /debugexe fsc.exe script.fsx
您可以使用「列印到 stdout」記錄。