教程:创建类型提供程序

F# 中的类型提供程序机制是它对信息丰富编程的支持的重要组成部分。 本教程介绍如何通过指导你开发几个简单类型提供程序来说明基本概念,从而创建自己的类型提供程序。 有关 F# 中的类型提供程序机制的详细信息,请参阅 类型提供程序

F# 生态系统包含用于常用 Internet 和企业数据服务的一系列类型提供程序。 例如:

  • 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,类似于 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# 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,打开“调试”菜单,选择 Debug/Attach to process…,然后附加到在其中编辑脚本的另一个 devenv 进程。 通过使用此方法,可以通过以交互方式将表达式键入到第二个实例(具有完整的 IntelliSense 和其他功能)来更轻松地定位类型提供程序中的特定逻辑。

可以禁用“仅我的代码”调试,以便更好地识别生成的代码中的错误。 有关如何启用或禁用此功能的信息,请参阅 使用调试器浏览代码。 此外,还可以通过打开 Debug 菜单并选择 Exceptions,或使用 Ctrl+Alt+E 键打开 Exceptions 对话框来设置第一次机会异常捕获。 在此对话框中的 Common Language Runtime Exceptions 下,选中 Thrown 复选框。

类型提供程序的实现

本部分会引导你完成类型提供程序实现的主体部分。 首先,定义自定义类型提供程序本身的类型:

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

此类型必须是公共的,并且必须使用 TypeProvider 属性标记它,以便当单独的 F# 项目引用包含该类型的程序集时,编译器将识别类型提供程序。 配置参数是可选的,如果存在,则包含 F# 编译器创建的类型提供程序实例的上下文配置信息。

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

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) =
…

此步骤说明此函数的实现。 首先,创建提供的类型(例如,Type1、n = 1 或 Type57 时为 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>)

应注意以下几点:

  • 此提供的类型将被擦除。 由于指示基类型为 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 。 事实上,这些示例中的基础对象是字符串,但该类型将显示在 System.Object .NET 编译的代码中。 与类型擦除的所有用法一样,可以使用显式装箱、取消装箱和强制转换来颠覆擦除类型。 在这种情况下,使用对象时可能会导致无效的强制转换异常。 提供程序运行时可以定义自己的专用表示类型,以帮助防止虚假表示形式。 不能在 F# 本身中定义擦除的类型。 只能擦除所提供类型。 必须了解对类型提供程序使用擦除类型或是使用提供擦除类型的提供程序的实际和语义影响。 擦除类型没有真正的 .NET 类型。 因此,您无法精确地反映类型,如果使用运行时类型转换和其他依赖于确切运行时类型语义的技术,可能会破坏已经被擦除的类型。 擦除类型的颠覆经常导致运行时出现类型强制转换异常。

为擦除所提供类型选择表示形式

对于擦除所提供类型的某些使用,不需要任何表示形式。 例如,擦除提供的类型可能仅包含静态属性和成员,并且没有构造函数,也没有方法或属性将返回该类型的实例。 如果可以访问擦除所提供类型的实例,则必须考虑以下问题:

什么是所提供类型的擦除?

  • 所提供类型的擦除是类型在已编译 .NET 代码中的显示方式。

  • 所提供擦除类类型的擦除始终是类型继承链中的第一个非擦除基类型。

  • 所提供擦除接口类型的擦除始终为 System.Object

提供类型的表示形式是什么?

  • 擦除所提供类型的可能对象集称为其表示形式。 在本文档中的示例中,所有擦除的类型 Type1..Type100 表示形式始终是字符串对象。

所提供类型的所有表示形式必须与所提供类型的擦除兼容。 (否则,F# 编译器会为使用类型提供程序提供错误,或者将生成无效的不可验证的 .NET 代码。如果类型提供程序返回提供无效表示形式的代码,则类型提供程序无效。

可以使用以下任一方法为提供的对象选择表示形式,这两种方法都是非常常见的:

  • 如果只是对现有 .NET 类型提供强类型包装器,则擦除到该类型和/或使用该类型的实例作为表示形式通常对于你的类型十分有意义。 使用强类型版本时,当该类型的大多数现有方法仍有意义时,此方法十分合适。

  • 如果您希望创建的 API 与任何现有的 .NET 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() @@>), …)

关键课程

上一部分介绍了如何创建一个简单的擦除类型提供程序,该提供程序提供一系列类型、属性和方法。 本部分还介绍了类型擦除的概念,包括从类型提供程序提供擦除类型的一些优点和缺点,并讨论了擦除类型的表示形式。

使用静态参数的类型提供器

通过静态数据参数化类型提供程序的功能可实现许多有趣的方案,即使在提供程序不需要访问任何本地或远程数据的情况下也是如此。 在本部分中,你将了解一些用于组合此类提供程序的基本技术。

类型检查正则表达式提供程序

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

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

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

本部分介绍如何使用类型提供程序创建一个类型,该类型通过正则表达式模式参数化来提供这些优势。 如果提供的模式无效,编译器将报告错误,类型提供程序可以从模式中提取组,以便可以使用匹配项上的命名属性来访问它们。 设计类型提供程序时,应考虑其公开的 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"

请注意以下几点:

  • 标准正则表达式类型表示参数化 RegexTyped 类型。

  • RegexTyped 构造函数导致调用正则表达式构造函数,从而传入模式的静态类型参数。

  • 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 体验保持简化。 此属性会导致EqualsGetHashCodeFinalizeGetType成员在提供的对象的 IntelliSense 列表中被隐藏。

  • 您使用 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。 唯一棘手的部分是在 args 定义中使用 InvokeCode 参数。 在此示例中,是一个引号列表, 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 正则表达式实例的创建,这会再次装箱到对象,因为 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

本部分介绍如何提供一种类型,该类型可用于获取具有类型Distancefloat<meter>属性和类型Timefloat<second>属性的行。 为简单起见,将做出以下假设:

  • 标题名称没有单位或格式为“名称(单位)”,并且不包含逗号。

  • 单元都是 FSharp.Data.UnitSystems.SI.UnitNames 模块(F#)模块 定义的所有系统国际(SI)单元。

  • 单位都是简单单位(例如米),而不是复合单位(例如米/秒)。

  • 所有列都包含浮点数据。

更全面的服务提供商会放宽这些限制。

第一步是考虑 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

如本示例所示,类型Type1InstanceProperty属性的所有提及已被清除,只留下对涉及的运行时类型的操作。

类型提供程序的设计和命名约定

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

连接协议的提供程序通常,数据和服务连接协议(如 OData 或 SQL 连接)的大多数提供程序 DLL 的名称应以 TypeProviderTypeProviders 结尾。 例如,使用类似于以下字符串的 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 设计模式

大多数类型提供器应该编写成使用GetConnection模式,即FSharp.Data.TypeProviders.dll中的类型提供器使用的模式,如以下示例所示:

#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

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

提供数组类型和泛型类型实例化

通过对 MakeArrayType 的任何实例使用正常 MakePointerTypeMakeGenericTypeType(包括 ProvidedTypeDefinitions),来创建所提供成员(其签名包括数组类型、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 |]

访问 Project-Local 或 Script-Local 资源

在构造过程中,可以为类型提供程序的每个实例指定一个 TypeProviderConfig 值。 此值包含提供程序的“解析文件夹”(即编译的项目文件夹或包含脚本的目录)、引用的程序集列表和其他信息。

失效

提供程序可以引发无效信号,以通知 F# 语言服务架构假设可能已更改。 当发生失效时,如果提供程序在 Visual Studio 中托管,则重新执行类型检查。 当提供程序托管在 F# Interactive 或 F# 编译器(fsc.exe)中时,将忽略此信号。

缓存模式信息

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

支持程序集

编译 .dll.exe 文件时,所生成类型的支持 .dll 文件会静态链接到结果程序集。 此链接是通过将中间语言(IL)类型定义和任何托管资源从后盾程序集复制到最终程序集来创建的。 使用 F# Interactive 时,底层 .dll 文件不会被复制,而是直接加载到 F# 交互式进程中。

类型提供程序中的异常和诊断

所提供类型中所有成员的所有使用都可能会引发异常。 在所有情况下,如果类型提供程序引发异常,主机编译器会将错误属性设置为特定类型提供程序。

  • 类型提供程序异常绝不会导致内部编译器错误。

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

  • 当类型提供程序托管在 F# 编译器、F# 开发环境或 F# Interactive 中时,会捕获该提供程序的所有异常。 Message 属性始终是错误文本,不会显示堆栈跟踪。 如果要引发异常,可以引发以下示例: System.NotSupportedExceptionSystem.IO.IOExceptionSystem.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()中,后者表示生成代码片段的容器。

  • 提供程序必须具有一个程序集,该程序集具有实际支持 .NET .dll 文件,而该文件在磁盘上具有匹配 .dll 文件。

规则和限制

编写类型提供程序时,请记住以下规则和限制。

所提供的类型必须可访问

所有所提供类型都应可从非嵌套类型访问。 非嵌套类型是在调用TypeProviderForNamespaces构造函数或其他调用AddNamespace时指定的。 例如,如果提供程序提供类型 StaticClass.P : T,则必须确保 T 是非嵌套类型或在一个类型下嵌套。

例如,某些提供程序具有静态类,例如 DataTypes 包含这些 T1, T2, T3, ... 类型。 否则,错误表示已找到对程序集 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 日志记录。

另请参阅