7. 对象

本章继续领域建模的相关内容,在Scala中,“对象”(object)一词有着双重含义。Java中将其视为类的一个实例,但在Scala中,它同时是一个关键词。本章将阐述这个词(object)的两种含义。

本章前两节,将对象视为类的一个实例,解释如何将一个对象强制转换为另一个对象,并展示了在Scala中与Java的 .class 相似的实现方式。

其余的小节则介绍了object关键字在其他场景的作用。7.3小节展示了如何使用object创建单例(Singletons)这一最基本的使用。7.4小节展示了如何使用伴生对象(companion object)在类中添加静态成员。7.5小节展示了如何在伴生对象中使用apply方法作为构造类实例的另一种方式。

随后7.6小节展示了如何使用object创建一个静态工厂方法。7.7小节展示了如何将一个或多个特质(trait)组合到一个object中,技术上也被称之为具体化(reification)。最后,模式匹配是Scala中一个重要的话题,7.8小节展示了如何在伴生对象中使用unapply方法,使得类可以在match表达式[1]中使用。


[1]:在本书之前的版本中讨论了package objects相关的内容,但是这些将在Scala 3.0之后被弃用,所以在本书中将不做讨论。


7.1 对象的强制转换

问题

你需要将一个类的实例强制转换为另一个类型,如动态创建对象时。

解决方案

下面的例子将使用开源的Sphinx-4语音识别库,其中很多属性都定义在XML文件中,它的工作方式类似于在旧版本的Spring Framework框架中创建Bean。如在例子中,lookup方法返回的对象被强制转换为Recognizer类的实例:

    val recognizer = cm.lookup("recognizer").asInstanceOf[Recognizer]

上面的Scala代码等同于下面的Java代码:

    Recognizer recognizer = (Recognizer)cm.lookup("recognizer");

asInstanceOf方法定义在Scala的Any类中,因此它对所有的对象有效。

讨论

动态对象编程中对象的相互转换很常见。例如,在使用 SnakeYAML 库( https://oreil.ly/7mNDf )读取 YAML 配置文件时,就需要进行类型转换:

asInstanceOf方法并不只局限于此,也可以用来转换数字类型:

也可以在更复杂的代码中使用,如与Java代码交互时,传入Object实例数组:

如果使用java.net进行编程时,可以在创建一个HTTP URL连接时进行类型转换:

注意,这种编码方式会导致ClassCastException,比如下面REPL中的例子:

和往常一样,可以使用try/catch表达式来处理这种异常情况。

7.2 用classOf方法获取类的class实例

问题

当一个API要求你传入一个Class类型时,在Java中可以在对象上调用 .class,但是在Scala中不能这样做。

解决方案

使用Scala的classOf方法来替代Java的 .class,下面的例子展示了如何将TargetDataLine类型的类传给DataLine.Info方法:

作为对比,Java中的等价方式如下:

classOf方法定义在Scala的Predef对象中,因此不需要导入就可以在所有类中进行调用。

讨论

通过这种方式可以了解简单的反射技术。例如,下面的REPL例子展示了如何访问String类中的方法:

另见

  • The Scala Predef object( https://oreil.ly/A2vWS

7.3 用object创建单例

问题

你想创建一个单例对象(Singleton object),以保证只有一个类的实例存在。

解决方案

在Scala中使用object关键字创建单例对象。例如,创建一个单例对象用来代表键盘、鼠标或者一个披萨店的收银机:

随着CashRegister被定义为一个对象,它只能有一个实例,而且它调用方法的方式就像是Java中类调用静态方法一样:

讨论

一个单例对象只有一个类的实例。这种模式在创建工具类方法的时候很常见,比如StringUtils对象:

因为这些方法是定义在object中而不是类中,可以像Java中调用静态方法一样使用它们:

在使用Akka actor时,单例对象可以很好地重用消息。例如有若干的actors都可以接收开始和停止消息,可以创建如下(case)单例对象:

然后,这些对象可以被当做消费发送给actors:

另见

  • 参阅18章获取更多actors传递消息的例子。

  • 除了这种方式创建对象外,还可以通过“伴生对象”的方式让一个类同时拥有静态和非静态方法。请参阅下一小节的例子。

7.4 用伴生对象创建静态成员

问题

你想创建包含实例方法静态方法的类,但是Scala中没有static关键字。

解决方案

首先创建一个含有非静态成员(实例成员)的类,然后在同一个文件里中再定义一个与类名字相同的且含有“静态”成员的对象。这个对象被称为类的伴生对象(类也被称为该对象的伴生类)。

这种方式可以在类中创建静态成员(字段和方法),如下所示:

假设Pizza类和Pizza对象都定义在名为Pizza.scala的文件中,Pizza对象可以像Java类访问静态成员一样访问自己的成员:

也可以创建一个新的Pizza实例,像往常一样使用它:

TODO(松鼠图)

使用枚举常量

在实际应用中,不要使用字符串类型的常量值,应该使用枚举替代,具体可以参阅6.12小节。

讨论

这个定义方式很直白,虽然和Java有些不同:

  • 在同一个文件中定义类和对象,并赋予相同的名字。

  • 在对象内定义“静态”成员。

  • 在类中定义非静态成员(实例成员)。

在本节中,我用引号将静态一词括起来,是因为Scala的object中并没有静态成员的定义。但是在本文中,它们与Java中的静态成员具有相同的用途。

访问私有成员

类和其伴生对象能互相访问对方的私有成员。在下面的代码中,伴生对象的double方法可以访问类Foo的私有成员变量secret

类似的,在下面的代码中,printObj实例成员可以访问Foo对象的私有字段obj

另见

  • 7.6小节可以通过这种方式实现一个工厂模式。

7.5 使用对象的apply方法创建实例

问题

某些情况下,在伴生对象中创建apply方法作为类的构造函数可能更简洁、容易和方便,你期望了解这些方法。

解决方案

在5.2小节和5.4小节展示了如何创建一个或多个类的构造函数。还可以通过另一种方式,在类的伴生对象中使用apply方法创建构造函数,当然这并不是真正的构造函数,更像是函数调用或者工厂方法,但它们的用途类似。

创建一个含有apply方法的伴生对象只需要以下几个步骤,假设要为Person类创建构造函数:

  • 在同一个文件中定义一个Person类和Person对象。

  • Person类的构造函数变成私有。

  • Person对象中定义一个或多个apply方法作为类的构造器。

对于前两个步骤:

最后一步:

根据这个定义,就可以创建Person的实例,如下所示:

在Scala 2中,这种方式可以消除了在类名之前使用new关键字的需要。但是,由于Scala 3中的大多数情况下都不需要使用new,所以这种技术可以在工厂方法或者其他比较罕见的情况下使用。

讨论

Scala编译器对定义在伴生对象中的apply方法进行了特殊处理。本质上是因为在这里有一点Scala语法糖,所以当编译器看到这段代码时:

Scala编译器会在伴生对象中检测是否存在apply方法,然后将上面的代码转换成下面这段代码:

因此,apply方法实际上是一个工厂方法、普通函数或者构造器。从技术上来说,不是一个构造函数。

当需要使用这种方式创建多个构造函数时,可以在伴生对象中定义不同签名的apply方法:

然后可以用三种不同的方式创建Person实例:

由于apply只是一个函数,所以可以按照自己认为合适的方式实现它。例如,可以从一个元组,甚至是一个可变元组构造Person实例:

然后可以像如下使用这两个apply方法:

另见

  • 参阅7.6小节,如何使用apply方法创建一个静态工厂。

  • 参阅5.2小节,如何创建一个私有构造函数。参阅5.4小节,如何定义辅助构造函数。

  • apply方法使用起来像一个构造函数,unapply与之相反,被称为提取器(extractor),具体参阅7.8小节。

7.6 使用apply实现静态工厂方法

问题

为了将对象的创建逻辑放在统一的位置,你想在Scala中实现一个静态工厂方法。

解决方案

静态工厂是工厂模式的简化版本。要创建静态工厂,可以利用Scala语法糖的优势,在对象(通常是伴生对象)中使用apply方法来创建。

例如,假设要创建Animal工厂,让其返回CatDog类的实例。基于这个需求,可以在Animal类的伴生对象中定义apply方法,然后使用者可以像这样创建新的CatDog实例:

为了实现上述逻辑,首先创建一个名为Animal.scala的文件,然后第一步创建一个父的Animal特质,第二步让类去继承这个特质,第三步在伴生对象中定义一个合适的apply方法:

接着,创建一个Factory.scala文件,然后定义一个 @main 方法来测试一下:

运行main方法,输出如下:

这种方式的好处是只能通过工厂方法来创建DogCat的实例。直接创建将会编译失败:

讨论

实现静态工厂的方式有多种,因此,可以尝试不同的方式,尤其是以何种方式去访问Cat和Dog类。工厂方法的主旨在于确保具体的实例只能通过工厂方法创建;因此,类的构造函数应当对其他类隐藏。本节代码展示了其中一种解决问题的思路。

另见

本节使用一个简单的静态工厂,来展示Scala object的特性。有关如何在Scala中创建一个完整工厂方法的示例,可以参阅我的博客“A Scala Factory Pattern Example” ( https://oreil.ly/hZnnR )。

7.7 将特质具体化成对象

问题

你已经在特质中创建了一个或多个方法,现在想让它们变得具体化。或者,想知道下面最后一行的代码的具体含义:

解决方案

当看到一个object继承了一个或多个特质(trait),那么这个 object 就被用来具体化这些特质。具体化(reify)表示“把抽象的概念具体化”,在这种情况下,表示object从一个或多个特质中实例化一个单例对象。

例如,给定一个特质和两个类继承它:

在函数式编程中,还可以在特质中创建一系列方法:

一旦有了这个的特质,很多开发者接下来要做的就是将AnimalServices具体化为一个对象:

然后就可以使用AnimalServices中的方法:

TODO(乌鸦图)

关于“Service”命名

service的命名表示其提供了一系列公共服务方法可供外部使用者调用。我发现,当假设这些方法作为一系列web服务被调用时,这种命名很有意义。例如,当使用Twitter的REST API编写Twitter的客户端时,它提供的功能可以被认为是一系列的web服务。

讨论

这种方式通常用于函数式编程中,使用样例类进行数据建模,然后将相关函数放在特质中。通常使用步骤如下:

  1. 使用样例类对数据进行建模。

  2. 在特质中定义相关函数。

  3. 使用object具体化特质,可以按需结合多个特质。

一个略微真实的案例如下所示。首先,定义一个简单的数据模型:

接着,创建一系列服务,也就是与特质相对应的函数:

现在可以将所有这些特质具体化为一个完整的DogServices

最后可以像这样使用DogServices

更加具体!

有的时候想让代码变得更加具体,让特质变得参数化,比如像这样:

表示“此特质中的函数只能作用于此类型。” 在大型的应用程序中,这种技术可以帮助其他开发人员更容易地理解特质的用途。这也是静态类型语言的优势之一。

将这种技术应用于上述的样例类中,可以像这样修改特质:

然后像这样创建具体化的object

最后,下面的例子跟之前的使用方式一样:

另见

  • 当我第一次学习reify时,我不明白为什么会在这种情况下使用它,所以我做了一些研究,并在我的博客上总结了我的发现( https://oreil.ly/fweY0 )。

  • 参阅6.11小节获取更多关于创建模块的例子。

7.8 使用unapply实现模式匹配

问题

你想要在类中编写unapply方法,以便在match表达式中提取其中的字段。

解决方案

在类的伴生对象中定义合适返回签名的unapply方法。这里的解决方案分为两个步骤:

  1. 定义一个返回Stringunapply方法。

  2. 定义一个可以在match表达式中使用的unapply方法。

定义一个返回Stringunapply方法

为了开始展示unapply是如何工作的,这里有一个Person类,它有一个对应的伴生对象,该对象有一个unapply方法,该方法返回一个格式化的字符串:

使用该定义,可以像往常一样创建一个新的Person实例:

unapply方法的好处是,它提供了一种解构person实例的方法:

如上所示,将给定的Person实例解构为字符串的表示形式。在Scala中,当在一个伴生对象中放入一个unapply方法时,表示创建了一个extractor方法,这个方法可以从对象中提取字段。

定义一个可以在match表达式中使用的unapply方法

虽然上述示例展示了如何将Person解构为字符串,但如果要在match表达式中提取Person的字段,unapply方法需要返回特定类型:

  • 如果类中只有一个类型为A的参数时,返回一个 Option[A],也就是用Some封装一下这个参数。

  • 如果类中含有多个类型为A1、A2、An的参数时,返回一个 Option[(A1, A2 ... An)],也就是用一个包含这些参数的元组,然后使用Some封装一下。

如果由于某种原因,unapply方法无法将其参数解构为正确的值,请返回None

例如,如果用这个方法替换之前的unapply方法:

现在可以在match表达式中使用Person

REPL中展示了返回结果:

值的注意的是,样例类会自动生成unapply代码,但是如果不想使用样例类,又希望普通的类可以在match表达式中使用,那么就可以像这样在类中定义提取的unapply方法。

另见

  • 如果想知道unapply的命名由来,可能是因为在伴生对象中,这个“解构”过程基本上与编写apply方法相反。关于伴生对象中的apply方法被用做构建新实例的工厂方法,可以参阅5.15小节。

  • 查看Scala官方文档( https://oreil.ly/mqDBb )获取更多关于unapply方法的细节。

最后更新于

这有帮助吗?