在学习scala过程中一个常见的问题是:当你需要建立抽象类型时,抽象类型成员和泛型类型参数如何抉择?对于那些不熟悉差异的人来说,Scala中的类型参数类似于Java中的类型参数,除了Java的尖括号:
interface Collection<T> {
// ...
}
scala使用方括号:
// Type parameter version
trait Collection[T] {
// ...
}
Scala中的抽象类型成员在Java中没有等效成员。在这两种语言中,classes,interfaces(Java中)和traits(Scala中)可以将methods和fields作为成员。在Scala中,class或trait也可以具有type成员,正如Java中的methods可以是抽象的,methods, fields, type都可以在Scala中抽象。以下是抽象type在Scala中的示例:
// Type member version
trait Collection {
type T
// ...
}
在这两种情况下,抽象类型都可以在子类型中具体化。下面是类型参数版本的子类型Collection,指定具体类型String为T:
// Type parameter version
trait StringCollection extends Collection[String] {
// ...
}
下面是类型成员的版本亚型Collection中也指定了具体类型String为T:
// Type parameter version
trait StringCollection extends Collection {
type T = String
// ...
}
这看起来,实际上也确实是,实现相同目标的两种不同方式。那么什么情况下需要在它们之间做出选择呢?我在接受采访时问了马丁奥德斯基这个问题。他说同时具有抽象类型成员和泛型类型参数的一个原因是正交性:
有两种抽象概念:参数化和抽象成员。在Java中也是,但它取决于你抽象的东西。在Java中,您有抽象方法,但不能将方法作为参数传递。您没有抽象字段,但可以将值作为参数传递。同样,您没有抽象类型成员,但您可以将类型指定为参数。所以在Java中也有这三者,但这里对不同事物所能进行的抽象是有很明显的不同的。你可以说这种区别是毫无根据的。
我们在Scala中尽量做到完整、正交。我们为这三类成员:method、field、type制定了相同的构建原则。因此可以有abstract field,method也可以作为参数传递,type也可以进行抽象。从概念上来看,我们可以用另一个来模拟一个。至少在理论上,我们可以将各种参数化表达为面向对象的抽象形式。所以从某种意义上说,你可以说Scala是一种更正交和完整的语言。
他还讲了实践中抽象类型成员和泛型类型参数之间的区别:
实际上,当你对许多不同的东西使用类型参数时,它会导致参数爆炸,通常,在参数范围内。在1998年ECOOP上,Kim Bruce,Phil Wadler和我有一篇论文,我们展示了当你增加你不知道的东西的数量时,典型的程序会以二次方式增长。所以有很好的理由不做参数,而是有这些抽象的成员,因为他们没有给你这种二次爆炸。
当他给出这个答案时,我不确定我到底知道他在说什么,但我想我现在可以更深入地了解这种差异。我默认使用泛型类型参数,可能是因为我有C ++和Java背景,并且比类型成员更熟悉参数化类型。但是,我遇到了一个设计问题,我最终用抽象类型成员解决,而不是泛型类型参数。
问题是我想在ScalaTest中提供特征,允许用户编写可以传递fixture object的测试。这将使人们可以函数式编程方式写测试,而不是传统的JUnit setUp 和tearDown方法的命令式样式。为了提供这个能力,我需要允许用户使用具体的类型参数或成员来指定fixture object的类型。换句话说,我要在ScalaTest API中提供:
trait FixtureSuite [F] {
// ...
}
或者:
trait FixtureSuite {
type F
// ...
}
在任何一种情况下,F都是要传递给测试的fixture参数的类型,而这是需要在子类中实现的。下面是一个需要StringBuilder类型参数来进行测试的一个示例:
class MySuite extends FixtureSuite[StringBuilder] {
// ...
}
下面是一个具体测试套件的示例,需要StringBuilder作为抽象类型成员来进行测试的示例:
class MySuite extends FixtureSuite {
type F = StringBuilder
// ...
}
到目前为止没有太大区别。然而,另一个功能是我想允许用户可以创建traits,指定feature的类型,并且可以混入到suite classe中。这将允许用户将常用fixtures写入traits,以便于可以混合到需要它们的任何suite class中。这时就会产生区别。下面是使用泛型类型参数的方式:
trait StringBuilderFixture { this:FixtureSuite [StringBuilder] =>
// ...
}
“ this: FixtureSuite[StringBuilder]”是一种self type,表示trait StringBuilderFixture 必须混入一个FixtureSuite[StringBuilder]。而相对的,采用类型成员方法时,这个特性会是什么样子:
trait StringBuilderFixture {this:FixtureSuite =>
type F = StringBuilder
// ...
}
到目前为止,仍然只有外观差异,但类型成员方法的痛点即将出现。一旦用户尝试将StringBuilderFixture混入到suite class中时,您就会看到可用性差异。在类型参数方法中,用户必须重复类型参数,即使它在trait中已经定义了:
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
但在抽象类型成员方法中,不需要这样的重复:
class MySuite extends FixtureSuite with StringBuilderFixture {
// ...
}
由于存在这种差异,我选择将fixture的类型设定为ScalaTest中的抽象类型成员,而不是泛型类型参数。我命名了类型成员FixtureParam,以使代码的读者更清楚该类型的用途。(此功能在ScalaTest 1.0中。有关详细信息,请查看trait的scaladoc文档FixtureSuite。)在ScalaTest的未来版本中,我计划添加允许将多个fixture对象传递给测试的新trait。例如,如果要将三个不同的fixture对象传递给测试,您将能够这样做,但是您需要指定三种类型,每个参数一个。因此,如果我采用类型参数方法,您的套件类可能最终看起来像这样:
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
而使用类型成员方法,它将如下所示:
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
抽象类型成员和泛型类型参数之间的另一个细微差别是,当指定泛型类型参数时,用户看不到类型参数的名称。因此有人看到这行代码:
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
用户不知道为StringBuilder指定的别名是什么。而类型参数的名称就在抽象类型成员方法的代码中:
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
在后一种情况下,代码的读者可以看到这StringBuilder是“FixtureParam”。他们仍然需要弄清楚“FixtureParam”的含义,但他们至少可以在不查看文档的情况下获取该类型的名称。我认为会有一些设计情况,其中显式类型成员名称会更好,而情况会更糟。我认为在ScalaTestFixtureSuite中将FixtureParam的名字显示地暴露出来使代码更具有可读性。但对于collections我不会这样做。我喜欢new ListBuffer[String],而非new ListBuffer { type E = String }。
到目前为止,我对抽象类型成员的经验是,当你想让人们通过trait混入这些类型的定义时,它们主要是比泛型类型参数更好的选择。当您认为在定义类型成员名称时明确给出类型成员名称将提高代码可读性时,你也可以考虑使用它们。