类型扩展

类型扩展(也称为扩充)是一系列功能,用于向之前定义的对象类型添加新成员。 这三个功能分别为:

  • 内部类型扩展
  • 可选类型扩展
  • 扩展方法

每个功能可用于不同的方案,并且有不同的优缺点。

语法

// Intrinsic and optional extensions
type typename with
    member self-identifier.member-name =
        body
    ...

// Extension methods
open System.Runtime.CompilerServices

[<Extension>]
type Extensions() =
    [<Extension>]
    static member extension-name (ty: typename, [args]) =
        body
    ...

内部类型扩展

内部类型扩展是一种用于扩展用户定义类型的类型扩展。

内部类型扩展必须在所扩展的类型所在的相同文件和相同命名空间或模块中定义。 任何其他定义都将导致它们变成可选类型扩展

内部类型扩展有时是将功能与类型声明分离的一种更简洁的方式。 以下示例演示如何定义内部类型扩展:

namespace Example

type Variant =
    | Num of int
    | Str of string
  
module Variant =
    let print v =
        match v with
        | Num n -> printf "Num %d" n
        | Str s -> printf "Str %s" s

// Add a member to Variant as an extension
type Variant with
    member x.Print() = Variant.print x

使用类型扩展可以分离以下各项:

  • Variant 类型的声明
  • 根据“形状”打印 Variant 类的功能
  • 一种使用对象样式 . 表示法访问打印功能的方法

这是将所有内容定义为 Variant 上的成员的替代方法。 尽管它本身并不是一种更好的方法,但在某些情况下,它可以更清晰地表示功能。

内部类型扩展将编译为所扩充的类型的成员,并在通过反射检查类型时出现在类型中。

可选类型扩展

可选类型扩展是在所扩展类型的原始模块、命名空间或程序集外部显示的扩展。

可选类型扩展对于扩展你自己尚未定义的类型很有用。 例如:

module Extensions

type IEnumerable<'T> with
    /// Repeat each element of the sequence n times
    member xs.RepeatElements(n: int) =
        seq {
            for x in xs do
                for _ in 1 .. n -> x
        }

现在,只要 Extensions 模块在你所使用的范围内打开,你就可以访问 RepeatElements,就像它是 IEnumerable<T> 的成员一样。

通过反射检查扩展类型时,可选扩展不会出现在扩展类型中。 可选扩展必须在模块中,并且仅当包含扩展的模块打开或在范围内时,它们才在范围内。

可选扩展成员将编译为静态成员,其对象实例作为第一个参数隐式传递。 但是,根据它们的声明方式,它们的行为就像它们是实例成员或静态成员一样。

C# 或 Visual Basic 使用者也看不到可选扩展成员。 它们只能用于其他 F# 代码。

内部和可选类型扩展的一般限制

可以在类型变量受约束的泛型类型上声明类型扩展。 要求是扩展声明的约束与声明类型的约束相匹配。

但是,即使声明类型与类型扩展之间的约束相匹配,也有可能通过扩展成员的主体来推断约束,该扩展成员对类型参数的要求与对声明类型的要求不同。 例如:

open System.Collections.Generic

// NOT POSSIBLE AND FAILS TO COMPILE!
//
// The member 'Sum' has a different requirement on 'T than the type IEnumerable<'T>
type IEnumerable<'T> with
    member this.Sum() = Seq.sum this

无法将此代码与可选类型扩展配合使用:

  • 实际上,Sum 成员对 'Tstatic member get_Zerostatic member (+))的约束与类型扩展定义的约束不同。
  • 如果将类型扩展修改为具有与 Sum 相同的约束,则不再匹配对 IEnumerable<'T> 定义的约束。
  • 如果将 member this.Sum 更改为 member inline this.Sum,将引发类型约束不匹配的错误。

我们需要的是“浮在空中”并且可以像在扩展类型一样呈现的静态方法。 这样一来,就需要用到扩展方法。

扩展方法

最后,扩展方法(有时称为“C# 样式扩展成员”)可以在 F# 中声明为类上的静态成员方法。

如果要在类型变量受约束的泛型类型上定义扩展,扩展方法很有用。 例如:

namespace Extensions

open System.Collections.Generic
open System.Runtime.CompilerServices

[<Extension>]
type IEnumerableExtensions =
    [<Extension>]
    static member inline Sum(xs: IEnumerable<'T>) = Seq.sum xs

使用时,只要 Extensions 已打开或在范围内,此代码将使其看起来就像在 IEnumerable<T> 上定义了 Sum 一样。

为了使扩展可用于 VB.NET 代码,需要在程序集级别使用一个额外的 ExtensionAttribute

module AssemblyInfo
open System.Runtime.CompilerServices
[<assembly:Extension>]
do ()

其他注解

类型扩展还具有以下属性:

  • 任何可以访问的类型均可扩展。
  • 内部和可选类型扩展可以定义任何成员类型,而不仅仅是方法。 例如,还可以定义扩展属性。
  • 语法中的 self-identifier 标记表示正在调用的类型的实例,就像普通成员一样。
  • 扩展成员可以是静态成员或实例成员。
  • 类型扩展上的类型变量必须与声明类型的约束匹配。

类型扩展也存在以下限制:

  • 类型扩展不支持虚拟或抽象方法。
  • 类型扩展不支持替代方法作为扩充。
  • 类型扩展不支持静态解析的类型参数
  • 可选类型扩展不支持构造函数作为扩充。
  • 不能在类型缩写上定义类型扩展。
  • 类型扩展对 byref<'T> 无效(尽管可以声明它们)。
  • 类型扩展对属性无效(尽管可以声明它们)。
  • 可以定义重载其他同名方法的扩展,但如果存在不明确的调用,F# 编译器会优先使用非扩展方法。

最后,如果一种类型存在多个内部类型扩展,则所有成员都必须是唯一的。 对于可选类型扩展,同一类型的不同类型扩展中的成员可以具有相同的名称。 仅当客户端代码打开定义相同成员名称的两个不同范围时,才会发生多义性错误。

另请参阅