教學課程:建立型別提供者 (F#)
這種提供者機制在 F# 3.0 為其支援資訊豐富程式設計的重要部分。本教學課程說明如何建立您自己的型別提供者,並通過開發數個簡單型別提供者說明基本概念。如需這種提供者機制的詳細資訊,請參閱 F# 型別提供者。
F# 3.0 包含許多常用的網際網路和商務資料服務的內建型別提供者。這些型別提供者允許以簡單且一般方式存取 SQL 關聯式資料庫和網路架構 OData 和 WSDL 服務。這些提供者也支援使用 F# LINQ 查詢這些資料來源。
在需要時,您可以建立自訂型別提供者,也可以參考其他人所建立的型別提供者。例如,組織可能會提供名為的資料集當中和加入的資料服務,每一個與其穩定的資料結構描述。您可以建立讀取結構描述,並使用強型別的方式顯示目前資料集給程式設計人員的型別提供者。
在開始之前
型別提供者機制以插入穩定的資料和服務資訊空間作為 F# 程式設計經驗的主要設計。
這個機制不是設計成插入在程式執行期間會變更,而且與程式邏輯相關資訊的結構描述的方式的空間設計。此外,機制未提供程式設計階段的內部語言設計,即使該網域包含一些有效的用法。您應該只在需要時且型別提供者的開發產生非常高的價值才使用這種機制。
您應該避免撰寫無法使用結構描述的型別提供者。同樣地,您應該避免撰寫泛型型別 (甚至是提供者存在) .NET 程式庫就夠用了。
在您開始之前,可能會發生下列問題:
您是否擁有資訊來源的結構描述?如果是,什麼對應至 F# 與 .NET 型別系統?
您可以使用現有的 (動態型別) API 開始著手實作?
您與您的組織是否可以充分利用型別提供者來使得開發它有價值?一般 .NET 程式庫是否符合需求?
結構描述會變更有多少?
在編碼期間,它是否會變更?
它是否會在編碼工作階段之間變更?
它是否會在程式執行期間變更?
型別提供者最適用於在執行階段和在存留期中編譯程式碼時皆穩定的結構描述。
簡單型別提供者
這個範例會開啟 F# 3.0 範例套件 Codeplex 網站的 SampleProviders\Providers 目錄的 Samples.HelloWorldTypeProvider。提供者會提供包含 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
/// This is an instance property.
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 Samples.FSharp.ProvidedTypes
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.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()
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 2012另一個執行個體,建立 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, 開啟 [偵錯] 功能表,選擇 偵錯/附加至 process…,並將其附加到另一個可以直接編輯您的指令碼的 devenv 處理程序。您可以使用這個方法,以互動方式輸入運算式至第二個執行個體 (有完整的 IntelliSense 和其他功能),目標可以更輕鬆地設定成提供者的特定邏輯。
您可以停用 Just My Code 偵錯更快找出在產生程式碼中的錯誤。如需如何啟用或停用這個功能的詳細資訊,請參閱 HOW TO:逐步執行 Just My Code。此外,您也可以設定第一個可能藉由開啟攔截的例外狀況偵錯 功能表,然後選擇 例外狀況或選擇 Ctrl+Alt+E 機碼開啟例外狀況 對話方塊。在對話方塊中,在通用語言執行階段例外狀況,請選取 擲回 核取方塊。
型別提供者實作
本節將為您解說型別提供者實作的主剖面。首先,您要定義自訂型別的類型提供者:
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =
這個型別必須是公用的,因此,您必須將它標記為 TypeProvider 屬性,讓另一個 F# 專案參考包含此型別的組件時,編譯器會辨識這個型別提供者。config 參數是選擇性的。因此,如果有的話,則會包含 F# 編譯器所建立的型別提供者的執行個體相關組態資訊。
接下來,您會實作 ITypeProvider 介面。在這個案例中,您是使用 ProvidedTypes API 的 TypeProviderForNamespaces 型別做為基底型別。這個 Helper 型別立即提供的命名空間的有限集合,其中每一個直接包含一個有限數目的固定、立即提供的型別。在此內容中,提供者 立即 自動產生型別,即使不是必要項或需使用。
inherit TypeProviderForNamespaces()
然後,定義指定命名空間提供型別的私用區域值,並找出型別的提供者組件。這個組件未來當做提供要清除之型別的邏輯父項目型別。
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的組件屬性 (Attribute) :
[<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,執行個體會出現,因為型別 物件 的值在已編譯程式碼。
當您指定一個非巢狀型別時,必須指定組件和命名空間。對於要清除之型別,組件應該是型別提供者的組件。
接下來,將 XML 檔加入至這個型別。本文件會延遲,也就是說,如果主編譯器需要它才會計算。
t.AddXmlDocDelayed (fun () -> sprintf "This provided type %s" ("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()
提供型別的執行個體會包含基礎資料「物件資料」。以引號括住的程式碼加入至 物件 的轉換,因為該型別是這個抹除的提供型別 (指定成宣告所提供型別)。
將 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 函式傳回引號。主編譯器以引數清單呼叫函式。在這種狀況下,引數包括使用 args.[0],表示執行個體 getter 呼叫並可以存取的單一運算式。 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 do
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 () ->
sprintf "This is StaticProperty%d on NestedType" i)
yield p ]
staticPropsInNestedType)
[nestedType])
// The result of makeOneProvidedType is the type.
t
如需清除所提供之型別的詳細資訊
本節中的範例只提供 要清除之提供的型別,在下列情況中將特別有用:
當您針對只包含資料和方法的資訊空間撰寫提供者。
當您正在撰寫對部分資訊空間,執行階段型別語意不重要的提供者。
當您寫入很大空間資訊的提供者和在產生實際技術上不是可行的 .NET 用於訊息空間輸入的互動連接。
在此範例中,每個都提供了型別清除輸入, obj,而且此型別的所有使用在已編譯程式碼都以 obj 型別出現。實際上,在這些範例中的基礎物件是字串,不過在.NET 編譯程式碼中,型別都會顯示為 Object 。使用所有型別的抹除,您可以使用明確的 Boxing、Unboxing 和轉換推翻清除的型別。在這種情況下,當使用物件時,無效的轉換例外狀況可能發生。提供者執行階段可以定義自己的私用表示型別以助於防止錯誤的表示。您不能在F#中定義清除輸入。只有提供的型別可清除。您必須了解實際和語意的細節,包含會清除型別的型別提供者或提供提供者要清除之型別。已清除的型別沒有實際的 .NET 型別。因此,您無法正確反映這個型別。如果您使用依賴確切的執行階段型別語意的執行階段轉換和其他技術,您或許推翻清除的型別。清除型別的子版本經常會在執行階段造成型別轉型例外狀況。
選取要清除提供型態的表示
對於一些清除提供的型別則不需要表示。例如,會清除所提供的型別可能只包含靜態屬性和成員,並且建構函式、方法或屬性將不會傳回這個型別的執行個體。如果您到達已清除所提供之型別的執行個體,您必須考慮下列問題:
什麼是所提供之型別的抹除?
所提供之型別的抹除是型別如何出現在已編譯的 .NET 程式碼。
所提供的清除類別型別的抹除一定是在型別的繼承鏈結中,第一個非清除的基底型別。
提供的清除的介面型別的抹除一定是 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 @@>))
在這種情況下,您可以 (選擇性) 藉由指定型別 baseType 做為型別抹除,以建構 ProvidedTypeDefinition:
ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)
前置類別
上一節說明如何建立簡單的清除提供者型別,提供範圍的型別、屬性和方法。這個區段也說明型別抹除的概念,包含提供型別的提供者要清除之類型的優點和缺點,並會討論清除的型別。
使用靜態參數型別提供者
即使在提供者不需要存取任何本機或遠端資料時,可以由靜態資料參數化型別的提供者啟用許多有趣的案例。在本節中,您將學習集合這種提供者的基本技巧。
型別已核取 Regex 提供者
假設您想要實作包裝介面的 .NET Regex 程式庫提供下列編譯時期確保規則運算式的一種提供者:
驗證規則運算式是否有效。
提供根據規則運算式相符的所有群組名稱的具名屬性。
本節顯示如何使用型別的提供者建立規則運算式模式參數化的 RegExProviderType 型別以提供這些優點。如果所提供的模式比對是無效的,編譯器會報告錯誤。因此,這種提供者可以擷取模式的群組,讓您能夠存取符合項目的具名屬性,。當您設計一種提供者時,應該考慮其公開的 API 應該如何顯示給使用者,而設計方式會如何轉譯為 .NET 程式碼。下列範例顯示如何使用這類應用程式開發介面取得區碼的元件:
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 集合的索引子。
下列程式碼會實作這種提供者的邏輯核心,因此這個範例省略了所有成員加入至所提供的型別。如需每個加入的成員的詳細資訊,請參閱本主題稍後適當的一節。如需完整程式碼,請從 F# 3.0 範例套件 Codeplex 網站的範例。
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 體驗會繼續進行最佳化。這個屬性會導致 Equals、 GetHashCode、 Finalize和從提供物件的 IntelliSense 清單會隱藏的 GetType 成員。
您可以使用 obj 當做方法的基底型別,不過在下一個範例,您將使用 Regex 物件做為型別的執行階段表示。
當規則運算式是無效時, Regex 建構函式的呼叫便會擲回 ArgumentException 。編譯器會在編譯時期攔截此例外狀況並將錯誤訊息向使用者報告或顯示在Visual Studio 編輯器。這個例外狀況可讓規則運算式進行驗證,而不執行應用程式。
因為不包含任何有意義的方法或屬性,這個定義的型別還不是很有用。首先,請加入靜態 IsMatch 方法:
let isMatch = ProvidedMethod(
methodName = "IsMatch",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = typeof<bool>,
IsStaticMethod = 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 型別。因為這個型別視所提供靜態 (Static) 引數的模式的格式,這個型別必須為參數型型別定義的巢狀型別:
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(sprintf @"Gets the ""%s"" group from this match" group)
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
因為 obj 是所提供型別的抹除,建構函式只能清除標準 .NET Regex 建立的建構函式執行個體,其為重新載入對話方塊的物件。變更後,本主題先前指定的範例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>,
IsStaticMethod = 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 occurence 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 |
本節說明如何提供可用來取得具有型別 float<meter>Distance 屬性和型別 float<second>Time 屬性之資料列的型別。為了簡化,進行下列假設:
標頭名稱為單位或具有「名稱 (單位)」和且不包含逗號。
如同 Microsoft.FSharp.Data.UnitSystems.SI.Un itNames 模組 (F#) 模組定義,單位為所有 Systeme 國際 (SI) 單位。
單位都是簡單 (例如,公尺) 而非複合 (例如,公尺/秒)。
所有資料行包含浮點資料。
更完整的提供者將寬鬆度這些限制。
再次第一個步驟是考量 API 應該如何查看。根據指定的 info.csv 檔案以從上一個資料表的內容 (以逗號分隔的格式),提供者的使用者應該可以編寫類似下列範例的程式碼:
let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn "%f" (float time)
在這種情況下,編譯器應該將這些呼叫轉換類似下列範例:
let info = new MiniCsvFile("info.csv")
for row in info.Data do
let (time:float) = row.[1]
printfn "%f" (float time)
最佳化的轉譯需要這種提供者定義的實際 CsvFile 輸入型別提供者的組件。型別的提供者通常會仰賴一些 Helper 型別和方法包裝重要邏輯。由於測量在執行階段清除,您可以使用 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 do
yield line.Split(',') |> Array.map float }
|> Seq.cache
member __.Data = data
[<TypeProvider>]
type public MiniCsvProvider(cfg:TypeProviderConfig) as this =
inherit TypeProviderForNamespaces()
// 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 runtime.
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 方法定義所提供屬性的位置。因此,如果您使用移至定義上提供的屬性,則 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
這些版本是用來建立視需要空間的型別。
提供陣列、ByRef 和指標型別
使用一般 MakeArrayType、 MakePointerType和 MakeGenericType System.Type 在所有執行個體,包含 ProvidedTypeDefinitions所提供的成員(簽章包括陣列型別、指標型別或 byref 泛型型別和執行個體化)。
提供測量附註的單位
ProvidedTypes API 提供測量附註的 Helper。例如,如要提供型別 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 所裝載, typecheck 會重新執行。如果提供者裝載在 F# Interactive 或由 F# 編譯器 (fsc.exe),這個信號會忽略。
快取結構描述資訊。
提供者必須經常快取到結構描述資訊的存取。應該使用指定做為靜態 (Static) 參數或做為使用者資料的檔案名稱儲存快取資料。結構描述的快取範例位於型別提供者的 LocalSchemaFile 參數在 FSharp.Data.TypeProviders 組件。這些提供者的實作中,這個靜態參數指定這種提供者使用在本機檔案的結構描述資訊而不是在網路上存取的資料來源。若要使用快取的結構描述資訊,您也必須設定靜態參數 ForceUpdate 至 false。您可以使用類似的技術啟用線上和離線資料存取。
支援組件
當您編譯 .dll 或 .exe 檔時,會產生型別的支援 .dll 檔案以靜態方式連結至產生的組件。這個連結是藉由複製已支援組件的中繼語言 (Intermediate Language (IL) 建立的型別定義和所有 Managed 資源載入的組件。當您使用 F# Interactive 時,支援 .dll 檔不會複製而是直接載入至 F# 互動式處理序。
從型別提供者的例外狀況和診斷
從提供的型別的所有成員都可能會擲回例外狀況。在所有情況下,如果型別提供者擲回例外狀況,主編譯器屬性化這個錯誤至特定型別的提供者。
型別提供者的例外狀況不會導致編譯器內部錯誤。
輸入提供者無法報告警告。
當型別的提供者裝載在 F# 編譯器、F# 開發環境或F# Interactive 時,從該提供者的任何例外狀況都會遭到攔截。訊息屬性一定是錯誤文字,而且堆疊追蹤不會出現。如果您確實擲回例外狀況,則可以擲回下列範例:
提供產生的型別
到目前為止,這份文件說明了如何提供清除的型別。您可以在 F# 中使用此種提供者機制提供產生的型別,並可當做使用者程式真正的 .NET 型別定義。您可以使用型別定義參考所產生的所提供的型別。
open Microsoft.FSharp.TypeProviders
type Service = ODataService<" http://services.odata.org/Northwind/Northwind.svc/">
部分屬於 F# 3.0 發行的 ProvidedTypes-0.2 helper的程式碼對於提供產生的型別有限制的支援下列適用於產生型別定義的陳述式必須 true:
IsErased 必須設定至 false。
提供者必須在磁碟上具有相符的 .dll 檔並為實際支援的 .NET .dll 檔案以供組件的參考建立。
您也必須從產生型別的封閉型中,呼叫巢狀型別會提供的型別 ConvertToGenerated 。這個呼叫發出指定提供的型別定義與其巢狀型別定義,並調整所有提供型別定義 Assembly 的屬性傳回該組件。只有在根型別的組件屬性第一次存取時,組件才會發出。當F# 主機編譯器處理這個型別產生的型別宣告時, 它才會存取這個屬性。
規則和限制
當您撰寫型別提供者時,請記住下列規則和限制。
提供的型別必須可以取得。
所有提供的型別必須是從非巢狀型別可到達。非巢狀型別是在呼叫 TypeProviderForNamespaces 建構函式或呼叫 AddNamespace 中指定。例如,如果提供者提供 StaticClass.P : T 型別,您必須確定 T 是一個非巢狀型別或在其中一個巢狀。
例如,某些提供者有靜態類別,例如 T1, T2, T3, ... 型別包含 DataTypes 類別。否則,會發生在組件 A 的 T 找到型別參考這個錯誤,不過,這個型別在該組件中找不到。如果發生這個錯誤,請確認所有可以從提供者類型抵達的子型別。注意:這些 T1, T2, T3... 型別稱為 作業中的 型別。請記得將它們放入可存取命名空間或父型別。
型別的提供者機制的限制
這種提供者機制在 F# 中具有下列限制:
在 F# 的型別基礎提供者的基礎結構不支援提供的泛型型別或泛型方法。
這個機制不支援靜態參數的巢狀型別。
ProvidedTypes 支援程式碼的限制
ProvidedTypes 支援程式碼有下列規則和限制:
不會實作使用索引屬性的 getter 和 setter。
未實作提供的事件。
只有這種在 F# 的提供者機制才使用提供的型別資訊和物件。它們通常並不能當做 System.Type 物件使用。
您可以在引號內使用的定義方法實作的建構具有多種限制。您可以參考 ProvidedTypes-的版本 以查看原始程式碼引號所支援的建構。
型別提供者必須產生 .dll 檔案的輸出組件,而不是 .exe 檔案。
開發秘訣
於開發過程中,您可能會發現下列提示有幫助。
**執行 Visual Studio 的兩個執行個體。**因為該測試 IDE 將採取這種避免提供者重建 .dll 檔案上的鎖定,所以您可以開發一個執行個體的型別提供者並在其他的提供者做測試。因此,您必須關閉 Visual Studio 的第二個執行個體以完成第一個執行個體的建置;並在第一個執行個體的建置之後,重新開啟第二個執行個體。
您可以使用 fsc.exe 的引動過程來偵錯型別的提供者 。您可以使用下列工具叫用型別的提供者:
fsc.exe (F# 命令列編譯器)。
fsi.exe (F# Interactive 編譯器)。
devenv.exe (Visual Studio)
您通常最容易可以在測試指令碼檔(例如,script.fsx)的fsc.exe 來偵錯類型提供者。您可以從命令提示字元啟動偵錯工具。
devenv /debugexe fsc.exe script.fsx
您可以使用 print-to-stdout 記錄。