在 GitHub 上编辑此页面

程序化结构类型 - 更多细节

语法

SimpleType    ::= ... | Refinement
Refinement    ::= ‘{’ RefineStatSeq ‘}’
RefineStatSeq ::=  RefineStat {semi RefineStat}
RefineStat    ::= ‘val’ VarDcl | ‘def’ DefDcl | ‘type’ {nl} TypeDcl

结构类型的实现

标准库定义了一个通用的标记特征 scala.Selectable

trait Selectable extends Any

标准库中提供了一个依赖于 Java 反射Selectable 实现:scala.reflect.Selectable。对于 Java 反射不可用的平台,可以设想其他实现。

Selectable 的实现必须提供 selectDynamicapplyDynamic 方法中的一个或两个。这些方法可以是 Selectable 实现的成员,也可以是扩展方法。

selectDynamic 方法接受一个字段名称,并返回与 Selectable 中该名称关联的值。它应该具有以下形式的签名

def selectDynamic(name: String): T

通常,返回类型 TAny

scala.Dynamic 不同,updateDynamic 方法没有特殊含义。但是,我们保留将来赋予其含义的权利。因此,建议不要在 Selectable 中定义任何名为 updateDynamic 的成员。

applyDynamic 方法用于应用于参数的选择。它接受一个方法名称,以及可能代表其参数类型的 Class,以及要传递给函数的参数。它的签名应该具有以下两种形式之一

def applyDynamic(name: String)(args: Any*): T
def applyDynamic(name: String, ctags: Class[?]*)(args: Any*): T

这两个版本都将实际参数传递给 args 参数。第二个版本另外接受一个 java.lang.Class 的可变参数,用于标识方法的参数类。如果使用 Java 反射实现 applyDynamic,则需要这样的参数,但在其他情况下也可能有用。selectDynamicapplyDynamic 也可以在使用子句中接受其他上下文参数。这些参数在调用点以正常方式解析。

给定一个类型为 C { Rs } 的值 v,其中 C 是一个类引用,Rs 是结构化细化声明,并且给定类型为 Uv.a,我们考虑三种不同的情况

  • 如果 U 是一个值类型,我们将 v.a 映射到

    v.selectDynamic("a").asInstanceOf[U]
    
  • 如果 U 是一个方法类型 (T11, ..., T1n)...(TN1, ..., TNn): R 并且它不是一个依赖方法类型,我们将 v.a(a11, ..., a1n)...(aN1, ..., aNn) 映射到

    v.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn)
      .asInstanceOf[R]
    

    如果此调用解析为采用 `Class[?]*` 参数的第二种形式的 `applyDynamic` 方法,我们将进一步将此调用重写为

    v.applyDynamic("a", c11, ..., c1n, ..., cN1, ... cNn)(
      a11, ..., a1n, ..., aN1, ..., aNn)
      .asInstanceOf[R]
    

    其中每个 `c_ij` 都是形式参数 `Tij` 类型(即 `classOf[Tij]`)的文字 `java.lang.Class[?]`。

  • 如果 `U` 既不是值类型也不是方法类型,或是一个依赖方法类型,则会发出错误。

请注意,`v` 的静态类型并不一定需要符合 `Selectable`,也不需要具有 `selectDynamic` 和 `applyDynamic` 作为成员。只要存在可以将 `v` 转换为 `Selectable` 的隐式转换,并且选择方法也可以作为 扩展方法 提供,就足够了。

结构化类型的局限性

  • 无法通过结构化调用调用依赖方法。

  • 细化不能引入重载:如果细化指定了方法 `m` 的签名,并且 `m` 也在细化的父类型中定义,则新签名必须正确覆盖现有签名。

  • 结构化细化的子类型化必须保留擦除后的参数类型:假设我们要证明 `S <: T { def m(x: A): B }`。然后,像往常一样,`S` 必须有一个可以接受类型为 `A` 的参数的成员方法 `m`。此外,如果 `m` 不是 `T` 的成员(即细化是结构化的),则会应用一个附加条件。在这种情况下,`S` 的成员 *定义* `m` 将有一个类型为 `A'` 的参数。附加条件是 `A'` 和 `A` 的擦除是相同的。以下是一个示例

    class Sink[A] { def put(x: A): Unit = {} }
    val a = Sink[String]()
    val b: { def put(x: String): Unit } = a  // error
    b.put("abc") // looks for a method with a `String` parameter
    

    倒数第二行类型不正确,因为类 `Sink` 中 `put` 的参数类型的擦除是 `Object`,但 `b` 类型中 `put` 的参数的擦除是 `String`。这个附加条件是必要的,因为我们将不得不求助于某种(目前未知的)反射形式来调用 `b` 类型中像 `put` 这样的结构化成员。该条件确保细化的静态已知参数类型在运行时与所选调用目标的参数类型在擦除后一致。

    大多数反射调度算法需要知道确切的擦除参数类型。例如,如果上面的示例能够类型检查,那么最后一行上的调用 b.put("abc") 将在 b 的运行时类型中查找一个带有 String 参数的 put 方法。但是 put 方法来自 Sink 类,它接受一个 Object 参数。因此,该调用将在运行时失败,并出现 NoSuchMethodException 错误。

    人们可能会希望有一种“更智能”的反射调度算法,它不需要精确的参数类型匹配。不幸的是,只要重载是可能的,这总是会导致歧义。例如,继续上面的示例,我们可以引入一个新的 Sink 的子类 Sink1,并将 a 的定义更改如下

    class Sink1[A] extends Sink[A] { def put(x: "123") = ??? }
    val a: Sink[String] = Sink1[String]()
    

    现在,b 的运行时类型中存在两个 put 方法,它们分别具有擦除参数类型 ObjectString。然而,动态调度仍然需要调用第一个 put 方法,即使第二个看起来更匹配。

    对于我们实际上可以在不知道精确参数类型的情况下实现反射的情况(例如,如果静态重载被动态分派的多分派方法取代),存在一个逃生舱口。对于扩展 scala.Selectable.WithoutPreciseParameterTypes 的类型,签名检查将被省略。示例

    trait MultiMethodSelectable extends Selectable.WithoutPreciseParameterTypes:
      // Assume this version of `applyDynamic` can be implemented without knowing
      // precise parameter types `paramTypes`:
      def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
    
    class Sink[A] extends MultiMethodSelectable:
      def put(x: A): Unit = {}
    
    val a = new Sink[String]
    val b: MultiMethodSelectable { def put(x: String): Unit } = a  // OK
    

与 Scala 2 结构类型之间的差异

  • Scala 2 通过 Java 反射支持结构类型。与 Scala 3 不同,结构调用不依赖于 Selectable 等机制,并且无法避免反射。
  • 在 Scala 2 中,细化可以引入重载。
  • 在 Scala 2 中,可变的 var 允许在细化中使用。在 Scala 3 中,它们不再被允许。
  • Scala 2 不会对结构类型的子类型施加“相同擦除”限制。它允许某些调用在运行时失败。

上下文

有关更多信息,请参阅 重新思考结构类型