程序化结构类型
动机
一些用例,例如建模数据库访问,在静态类型语言中比在动态类型语言中更笨拙:使用动态类型语言,将行建模为记录或对象,并使用简单的点符号选择条目(例如 row.columnName
)非常自然。
在静态类型语言中实现相同的体验需要为数据库操作产生的每种可能的行(包括来自联接和投影的行)定义一个类,并建立一个方案来映射行与其表示它的类之间的关系。
这需要大量的样板代码,导致开发人员为了更简单的方案而放弃静态类型的优势,在这些方案中,列名用字符串表示并传递给其他运算符(例如 row.select("columnName")
)。这种方法放弃了静态类型的优势,而且仍然不如动态类型版本自然。
结构类型在我们需要在动态上下文中支持简单的点表示法而不失去静态类型优势的情况下很有帮助。它们允许开发人员使用点表示法并配置字段和方法的解析方式。
示例
这是一个结构类型 Person
的示例
type Person = Record { val name: String; val age: Int }
类型 Person
为其父类型 Record
添加了一个细化,该细化定义了两个字段 name
和 age
。我们说细化是结构化的,因为 name
和 age
没有在父类型中定义。但它们仍然作为类型 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.name
和 person.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
。然后可以使用它来转换结构成员的函数调用。因此,如果a
是Selectable
的实例,则像a.f(b, c)
这样的结构调用将转换为
a.applyDynamic("f")(b, c)
使用 Java 反射
使用Selectable
和Java 反射,我们可以从不相关的类中选择成员。
在使用 Java 反射进行结构调用之前,应该考虑其他方法。例如,有时可以使用类型类获得更模块化和更高效的架构。
例如,我们希望通过调用其close
方法为FileInputStream
和Channel
类提供行为,但是,这些类不相关,即没有具有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
参数,指示方法的形式参数类型。 -
updateDynamic
是Dynamic
独有的,但如前所述,这一事实可能会发生变化,不应将其用作假设。