在 GitHub 上编辑此页面

程序化结构类型

动机

一些用例,例如建模数据库访问,在静态类型语言中比在动态类型语言中更笨拙:使用动态类型语言,将行建模为记录或对象,并使用简单的点符号选择条目(例如 row.columnName)非常自然。

在静态类型语言中实现相同的体验需要为数据库操作产生的每种可能的行(包括来自联接和投影的行)定义一个类,并建立一个方案来映射行与其表示它的类之间的关系。

这需要大量的样板代码,导致开发人员为了更简单的方案而放弃静态类型的优势,在这些方案中,列名用字符串表示并传递给其他运算符(例如 row.select("columnName"))。这种方法放弃了静态类型的优势,而且仍然不如动态类型版本自然。

结构类型在我们需要在动态上下文中支持简单的点表示法而不失去静态类型优势的情况下很有帮助。它们允许开发人员使用点表示法并配置字段和方法的解析方式。

示例

这是一个结构类型 Person 的示例

type Person = Record { val name: String; val age: Int }

类型 Person 为其父类型 Record 添加了一个细化,该细化定义了两个字段 nameage。我们说细化是结构化的,因为 nameage 没有在父类型中定义。但它们仍然作为类型 Person 的成员存在。

这使我们能够在编译时检查访问是否有效

val person: Person = ???
println(s"${person.name} is ${person.age} years old.") // works
println(person.email) // error: value email is not a member of Person

Record 是如何定义的,person.name 是如何解析的?

Record 是一个扩展标记特质 scala.Selectable 并定义一个方法 selectDynamic 的类,该方法将字段名映射到其值。选择结构类型的成员是对此方法调用的语法糖。选择 person.nameperson.age 由 Scala 编译器转换为

person.selectDynamic("name").asInstanceOf[String]
person.selectDynamic("age").asInstanceOf[Int]

例如,Record 可以定义如下

class Record(elems: (String, Any)*) extends Selectable:
  private val fields = elems.toMap
  def selectDynamic(name: String): Any = fields(name)

这使我们能够像这样创建 Person 的实例

val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]

在本例中,父类型Record是一个泛型类,它可以在其elems参数中表示任意记录。此参数是类型为String的标签和类型为Any的值的配对序列。当我们创建一个Person作为Record时,我们必须使用类型转换断言该记录定义了正确类型字段。Record本身类型太弱,因此编译器在没有用户帮助的情况下无法知道这一点。在实践中,结构类型与其底层泛型表示之间的连接很可能由数据库层完成,因此不会成为最终用户的关注点。

除了selectDynamic之外,Selectable类有时还会定义一个方法applyDynamic。然后可以使用它来转换结构成员的函数调用。因此,如果aSelectable的实例,则像a.f(b, c)这样的结构调用将转换为

a.applyDynamic("f")(b, c)

使用 Java 反射

使用SelectableJava 反射,我们可以从不相关的类中选择成员。

在使用 Java 反射进行结构调用之前,应该考虑其他方法。例如,有时可以使用类型类获得更模块化更高效的架构。

例如,我们希望通过调用其close方法为FileInputStreamChannel类提供行为,但是,这些类不相关,即没有具有close方法的共同超类型。因此,在下面我们定义了一个结构类型Closeable,它定义了一个close方法。

type Closeable = { def close(): Unit }

class FileInputStream:
  def close(): Unit

class Channel:
  def close(): Unit

理想情况下,我们会在这两个类中添加一个公共接口来定义close方法,但是它们是在我们无法控制的库中定义的。作为折衷方案,我们可以使用结构类型来定义autoClose方法的单个实现

import scala.reflect.Selectable.reflectiveSelectable

def autoClose(f: Closeable)(op: Closeable => Unit): Unit =
  try op(f) finally f.close()

调用f.close()需要Closeable扩展Selectable以识别和调用接收器f中的close方法。通过上面显示的reflectiveSelectable导入,基于Java 反射,启用了对Selectable的通用隐式转换。然后,“幕后”发生的事情如下

  • 隐式转换将f包装在scala.reflect.Selectable(它是Selectable的子类型)的实例中。

  • 然后编译器将包装的f上的close调用转换为applyDynamic调用。最终结果是

    reflectiveSelectable(f).applyDynamic("close")()
    
  • reflectiveSelectable结果中applyDynamic的实现使用Java 反射在运行时查找并调用f引用的值中具有零个参数的close方法。

这样的结构调用往往比正常的函数调用慢得多。reflectiveSelectable的强制导入充当一个信号,表明正在发生一些效率低下的事情。

注意: 在 Scala 2 中,Java 反射是结构类型唯一可用的机制,它会自动启用,无需使用 `reflectiveSelectable` 转换。但是,为了警告低效的调度,Scala 2 需要语言导入 `import scala.language.reflectiveCalls`。

可扩展性

可以定义新的 `Selectable` 实例来支持除 Java 反射之外的其他访问方式,这将启用诸如本文开头给出的数据库访问示例之类的用法。

本地 Selectable 实例

扩展 `Selectable` 的本地类和匿名类比其他类具有更精细的类型。以下是一个示例

trait Vehicle extends reflect.Selectable:
  val wheels: Int

val i3 = new Vehicle: // i3: Vehicle { val range: Int }
  val wheels = 4
  val range = 240

i3.range

此示例中 `i3` 的类型为 `Vehicle { val range: Int }`。因此,`i3.range` 是有效的。由于基类 `Vehicle` 没有定义 `range` 字段或方法,我们需要结构化调度来访问初始化 `id3` 的匿名类的 `range` 字段。结构化调度由 `Vehicle` 的基特征 reflect.Selectable 实现,它定义了必要的 `selectDynamic` 成员。

`Vehicle` 也可以扩展 scala.Selectable 的其他子类,该子类以不同的方式实现 `selectDynamic` 和 `applyDynamic`。但如果它根本不扩展 `Selectable`,则代码将不再类型检查

trait Vehicle:
  val wheels: Int

val i3 = new Vehicle: // i3: Vehicle
  val wheels = 4
  val range = 240

i3.range // error: range is not a member of `Vehicle`

不同之处在于,不扩展 `Selectable` 的匿名类的类型只是从类的父类型形成的,没有添加任何细化。因此,`i3` 现在只有类型 `Vehicle`,选择 `i3.range` 会产生“成员未找到”错误。

请注意,在 Scala 2 中,所有本地类和匿名类都可以生成具有细化类型的值。但是,由这些细化定义的成员只能使用语言导入 reflectiveCalls 选择。

scala.Dynamic 的关系

这里显然与 scala.Dynamic 有些联系,因为两者都以编程方式选择成员。但也有一些区别。

  • 完全动态选择不是类型安全的,但结构化选择是,只要结构化类型与底层值的对应关系如前所述。

  • 两种访问操作 `selectDynamic` 和 `applyDynamic` 在两种方法之间共享。在 `Selectable` 中,`applyDynamic` 也可能接受 java.lang.Class 参数,指示方法的形式参数类型。

  • updateDynamicDynamic 独有的,但如前所述,这一事实可能会发生变化,不应将其用作假设。

更多细节