教程:创建类型提供程序 (F#)

该类型提供程序机制在 F# 3.0 是支持丰富信息编程的重要部分。 本教程阐释如何通过遍历创建自己的类型提供程序通过开发一些简单类型提供程序说明基本概念。 有关类型提供程序机制的更多信息,请参见 类型提供程序

F# 3.0 包含若干个常用的 Internet 和企业数据服务的内置类型提供程序。 这些类型提供程序提供简单和常规访问 SQL 关系数据库和基于网络的 OData 和 WSDL 服务。 这些提供程序也支持 F# LINQ 查询针对这些数据源的使用。

根据需要,您可以创建自定义类型提供程序,也可以引用其他创建的类型提供程序。 例如,您的组织可能具有某种数据服务,可提供且越来越多的海量数据集,这些数据集都有其自己的稳定数据架构。 可以创建读取架构的类型提供程序并以强类型的方法显示设置为程序员的当前数据。

安装前

类型提供程序机制主要是为在 F# 编程体验中插入稳定的数据和服务信息空间而设计的。

此机制没有设计架构更改程序执行期间会与程序逻辑相关的信息空间。 此外,该机制不是设计用于语言内元数据编程,即使该域包含一些有效用法。 您仅在需要以及类型提供程序的开发产生很高的值时使用此机制。

如果架构不可用,则应避免写入类型提供程序。 同样,应避免编写普通类型提供程序(或者存在).NET 库将足够了。

在开始之前,可询问以下问题:

  • 您是否具有信息源的架构? 如果是这样,映射到 F# 和 .NET 类型系统的是何内容?

  • 能否使用现有(动态类型化)API 作为您的实现的起始点?

  • 您和您的组织是否充分使用类型代码以使对其编写有价值? 常规 .NET 库是否满足您的需要?

  • 您的架构将发生多少变化?

  • 在编码过程中是否发生改变?

  • 这是否将会使编码会话之间发生改变?

  • 在编程执行期间是否会发生改变?

类型提供程序最适合于在运行时以及编译代码的生命周期内架构保持静态的情况。

简单的类型提供程序

此示例在 SampleProviders\Providers 打开 F#示例pack 3.0 Codeplex 网站的内容的 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# Interactive 的所有实例。 否则会出现生成错误,因为将锁定输出 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,打开“调试”菜单,选择**“调试/附加到进程…”**,并附加到另一个正在编辑脚本处的 devenv 进程。 通过使用此方法,您可以通过在第二个实例(具有完全的 IntelliSense 和其他功能)中以交互方式键入表达式,更轻松地以类型提供程序中的特定逻辑为目标。

可以禁用“仅我的代码”调试,以便更好地确定生成代码中的错误。 有关如何启用或禁用此功能的信息,请参见 如何:Step Into Just My Code。 此外,还可以通过打开**“调试”菜单并选择“异常”或通过选择 Ctrl+Alt+E 键打开“异常”对话框设置首次异常捕获。 在该对话框的“公共语言运行时异常”中,选择“引发”**复选框。

Hh361034.collapse_all(zh-cn,VS.110).gif类型提供程序的实现

本节将通过类型提供程序实现通过主体部分。 首先,您定义自定义类型提供程序本身的类型:

[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =

此类型必须是公共的,因此,您必须将其标记为与 TypeProvider 特性,以便编译器将识别该类型提供程序,在一个单独的 F# 项目引用包含该类型的程序集时。 config 参数是可选的,并且包含 F# 编译器创建的类型提供程序实例的上下文配置信息(如果存在)。

接下来,实现 ITypeProvider 接口。 这种情况下,从 ProvidedTypes API 将 TypeProviderForNamespaces 类型用作基类型。 此帮助器类型可以提供有限的预先提供命名空间的集合,每个包含有限的固定数量,预先提供类型。 在此上下文中,即使不需要或使用类型,提供程序“积极地”也会生成这些类型。

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 的程序集特性:

[<assembly:TypeProviderAssembly>] 
do()

Hh361034.collapse_all(zh-cn,VS.110).gif提供一种类型及其成员

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 () -> 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()

提供的类型的实例将由基础数据“对象数据”创建。 引号代码将 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")

提供的类型的实例由基础数据“10”创建。 您可能已经注意到 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 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

Hh361034.collapse_all(zh-cn,VS.110).gif有关擦除的所提供类型的详细信息

本节中的示例仅提供 清除的提供的类型,在以下情况下很有用:

  • 在写入仅包含数据和方法的信息控件的提供程序时。

  • 在您写入正确的运行时语句对信息空间实际使用不重要的提供程序时。

  • 在写入信息控件的提供程序时,信息空间过大且相互连接,因此在技术上无法生成信息空间的真实 .NET 类型。

在此示例中,各个提供类型将清除为类型 obj,并且对该类型的所有使用将在编译代码中显示为类型 obj。 实际上,这些示例中的基础对象均是字符串,但在 .NET 编译的代码中,该类型将显示为 Object。 与类型抹除的所有用法一样,可以使用显式装箱、取消装箱以及强制转换为破坏擦除的类型。 在这种情况下,无效强制可能对您使用对象会导致影响。 提供程序运行时可以定义其自己的私有表示类型,以帮助防止错误表示。 您不能定义 F# 本身中的清除类型。 仅可清除提供的类型。 您必须了解将任一清除类型用于您的类型提供程序或提供清除类型的提供程序的实际和语法的后果。 擦除的类型没有实际 .NET 类型。 因此,您不能对该类型准确反射,并且,可能推翻清除的类型,如果使用依赖于确切的运行时类型语义的运行时转换和其他技术。 颠覆清除的类型通常会导致运行时的类型转换异常。

Hh361034.collapse_all(zh-cn,VS.110).gif选择擦除的提供的类型的表示形式

对某些已清除的提供类型的用户,不需要进行表示。 例如,已清除的提供的类型可能只包含静态属性和成员而不包含构造函数,因此方法或属性不会返回该类型的实例。 如果可以访问已清除的提供类型的实例,您必须考虑以下问题:

  • 所提供的类型的抹除是什么?

    • 清除提供的类型是类型如何在已编译 .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 @@>))

在这种情况下,通过在构造 ProvidedTypeDefinition 构造时将此类型指定为 baseType,您可以将此类型用作类型抹除(选择性地):

   ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
   …
   ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)

密钥课程

上一节解释的如何创建提供类型的范围、属性和方法的简单清除的类型提供程序。 本节还阐释类型抹除的概念,包括某些提供从类型提供程序的清除的类型的优点和缺点和清除的类型的讨论表示。

使用静态参数的类型提供程序

通过静态数据参数化类型提供程序的能力使许多有趣的方案成为可能,甚至是在提供程序不需要访问任何本地或远程数据的情况下。 在本节中,您将了解收集此类提供程序的一些基本技术。

Hh361034.collapse_all(zh-cn,VS.110).gif键入选中的 Regex 提供程序

假设想对包装接口中提供以下编译时保证的 .NET Regex 库的正则表达式实现类型提供程序:

  • 验证正则表达式是否有效。

  • 提供基于正则表达式中任何组名的匹配上的命名属性。

本节显示如何使用类型提供程序创建正则表达式模式参数化提供这些优势的 RegExProviderType 类型。 如果所提供的模式无效,编译器将报告错误,类型提供程序可以从模式提取组,以便您可以通过使用匹配的命名属性访问它们。 当设计类型提供程序时,应考虑其公开的 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 集合上使用索引器。

下列代码是实现此类提供程序的逻辑内核,因此,此示例将向所提供的类型省略增加所有成员。 有关各个已添加成员的更多信息,请参见本主题后面的相应部分。 有关完整的代码,请从 Codeplex 网站的 F# 示例 pack 3.0 中下载示例。

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 体验将保持简化。 此特性会导致 EqualsGetHashCodeFinalize,因此 GetType 成员被取消从提供对象的 IntelliSense 列表中。

  • 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 类型。 由于此类型取决于提供为静态参数的模式,因此必须在参数化类型定义中嵌套此类型:

let matchTy = ProvidedTypeDefinition(
                "MatchType", 
                baseType = Some baseTy, 
                HideObjectMethods = true)

ty.AddMember matchTy

然后将一个属性添加到各组的匹配类型。 在运行时,匹配表示为 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

构造函数仅仅擦除标准 .NET Regex 实例的创建,再将其箱装到对象,因为 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>, 
                            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。 本节讨论基于本地数据的类型提供程序,例如本地数据文件。

Hh361034.collapse_all(zh-cn,VS.110).gif简单的 CSV 文件提供程序

作为简单示例,请考虑访问逗号分隔值 (CSV) 格式的科学数据的类型提供程序。 本节假定,CSV 文件包含浮点数据执行的标头行,如下表阐释:

距离(米)

时间(秒)

50.0

3.7

100.0

5.2

150.0

6.4

本节演示如何提供可用于类型 float<meter> 的 Distance 属性和类型 float<second> 的 Time 属性中获取行的类型。 为简单起见,将对以下做出假设:

  • 标题名称中不能有单位,其格式为“Name (unit)”且不包含逗号。

  • Microsoft.FSharp.Data.UnitSystems.SI.Un itNames Module (F#) 模块定义,单位均为 Systeme International (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 类型在提供程序的程序集中。 类型提供程序通常依赖于一些帮助器类型和方法来包装重要的逻辑。 由于度量在运行时擦除,所以可以使用 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<_> 类型。

密钥课程

解释的此部分如何包含用数据源本身简单架构创建一个本地数据源的类型提供程序。

继续

以下各节包括进行进一步研究的建议。

Hh361034.collapse_all(zh-cn,VS.110).gif查看擦除的类型的已编译代码

若要提供有关如何适用类型提供程序来对应已发出的代码,请使用本主题中之前使用的 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 属性的所有叙述,仅保留涉及运行时类型的操作。

Hh361034.collapse_all(zh-cn,VS.110).gif设计和命名类型提供程序的约定

编写类型提供程序时,请遵循下面的约定。

  • 连接协议的提供程序

    通常,大多针对数据和服务连接性协议的大多数提供程序 DLL 的名称,如 OData 或 SQL 连接,都应以 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 设计约定。

Hh361034.collapse_all(zh-cn,VS.110).gif类型提供程序的设计模式

以下各节描述在创作类型提供程序时可使用的设计模式。

Hh361034.collapse_all(zh-cn,VS.110).gifGetConnection 设计模式

编写大多数类型提供程序应使用在 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

Hh361034.collapse_all(zh-cn,VS.110).gif远程数据和服务备份的类型提供程序

在创建远程数据和服务支持的类型提供程序之前,必须考虑连接的编程中固有的问题的范围。 这些问题包括以下注意事项:

  • 架构映射

  • 实时和无效在架构更改时

  • 架构缓存

  • 数据访问操作的异步实现

  • 支持查询,包括 LINQ 查询

  • 凭据和身份验证

本主题不进一步浏览这些问题。

Hh361034.collapse_all(zh-cn,VS.110).gif其他创作技术

当您编写类型提供程序,您可能希望使用以下附加技术。

  • 按需创建类型和成员

    ProvidedType API 已延迟 AddMember 的版本。

    type ProvidedType =
        member AddMemberDelayed  : (unit -> MemberInfo)      -> unit
        member AddMembersDelayed : (unit -> MemberInfo list) -> unit
    

    这些版本用于创建类型的按需空间。

  • 提供数组、ByRef 和指针类型

    通过使用 System.Type 的任何实例(包括 ProvidedTypeDefinitions)上的常规 MakeArrayType、 MakePointerType 和 MakeGenericType 提供成员(其签名包括数组类型、byref 类型 和泛型类型的实例)。

  • 提供批注度量单位

    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 中承载提供程序,则重做 typecheck。 信号将被忽略,当提供程序承载在 F# Interactive 中或由 F# 编译器 (fsc.exe)。

  • 缓存架构信息

    提供程序通常必须缓存对架构信息的访问。 缓存数据应通过使用作为静态参数或用户数据的文件名存储。 架构缓存的示例是 FSharp.Data.TypeProviders 程序集中的类型提供程序中的 LocalSchemaFile 参数。 在这些提供程序的实现中,此静态参数指引类型提供程序使用指定本地文件中的架构信息而不是在网络上访问数据源。 若要使用缓存的架构信息,还必须将静态参数 ForceUpdate 设置为 false。 可以使用类似的技术来启用联机和脱机数据访问。

  • 支持程序集

    当您编译 .dll或 .exe 文件时,将生成文件的 .dll 文件以静态方式链接到结果程序集。 此链接是通过最后程序集中的后备程序集来创建后备中间语言 (IL) 类型定义和任何托管资源。 在使用 F# Interactive 时,不会复制后备 .dll 文件,而是将其直接加载到 F# Interactive 过程中。

  • 来自类型提供程序的异常和诊断

    对于从提供的类型的所有成员的所有使用都可能会引发异常。 在所有情况中,如果类型提供程序引发异常,则宿主编译器会将错误归属于特定类型的提供程序。

    • 类型提供程序异常不应引起内部编译器错误。

    • 类型提供程序无法报告警告。

    • 如果在 F# 编译器、F# 开发环境或 F# Interactive 内承载类型提供程序,则为从提供程序中捕获的所有异常。 消息属性始终为错误文本,并且未出现堆栈跟踪。 如果将要引发异常,则可以引发以下示例:

Hh361034.collapse_all(zh-cn,VS.110).gif提供生成的类型

到目前为止,本文档介绍了如何提供清除的类型。 还可以使用 F# 中的类型提供程序机制以提供生成的类型(作为真实的 .NET 类型定义添加到用户的程序中)。 您必须通过使用类型定义来引用已生成的提供类型。

open Microsoft.FSharp.TypeProviders 

type Service = ODataService<" http://services.odata.org/Northwind/Northwind.svc/">

是 F# 3.0 版本一部分的 ProvidedTypes-0.2 帮助器代码只限制支持提供的生成类型。 下面的语句必须适用于生成的类型定义:

  • 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 支持代码具有以下规则和限制:

    1. 假定不实现带索引的 getter 和setter 的属性。

    2. 假定不实现事件。

    3. 提供类型和信息对象应只使用类型提供程序机制在 F# 中。 它们不是通常可用作系统类型对象。

    4. 在定义方法实现的引用中可使用的构造有几种限制。 您可以参阅 ProvidedTypes-版本 的源代码以查看哪些构造在引用中受支持。

  • 类型提供程序必须生成是 .dll 文件的输出程序集,而不是 .exe 文件。

开发提示

您可能在开发过程中发现以下提示很有用。

  • **运行 Visual Studio 的两个实例。**您可以在一个实例中开发类型提供程序和在另一个实例中测试提供程序,因为测试 IDE 阻止重新生成类型提供程序的 .dll 文件上的锁定。 因此,必须关闭 Visual Studio 的第二个实例,尽管在第一个实例中生成提供程序,在提供程序生成之后,然后必须重新打开第二个实例中。

  • **使用 fsc.exe 的调用调试类型提供程序。**通过使用以下工具,可以调用类型提供程序:

    • fsc.exe(F# 命令行编译器)

    • fsi.exe(F# 交互式编译器)

    • devenv.exe (Visual Studio)

    通常,您可以通过使用测试脚本文件上的 fsc.exe(例如,script.fsx),以便更易于体哦啊是类型提供程序。 可以从命令提示符中生成调试器。

    devenv /debugexe fsc.exe script.fsx
    

    可以使用打印到 stdout 记录。

请参见

其他资源

类型提供程序