【Scala之旅】类与对象
本节翻译自
综述:本节中你将会学习如何使用Scala实现类,以及Scala相比Java更加精简的表示法带来的便利。同时介绍了object的语法结构(Scala没有静态方法或静态字段,但object可以达到同样的效果)。
类
Scala 中的类是创建对象的模板。它们可以包含统称为成员的方法、值、变量、类型、对象、特征和类。之后将介绍类型、对象和特征。
定义类
最小的类定义就是关键字 class
和标识符。类名应该大写。
class User val user = new User
关键字 new
用于创建类的一个实例。User
有一个不带参数的默认构造函数,因为没有定义构造函数。但是,你通常需要构造函数和类体。下面是一个示例类的定义:
class Point(var x: Int, var y: Int) { def move(dx: Int, dy: Int): Unit = { x = x + dx y = y + dy } override def toString: String = s"($x, $y)" } val point1 = new Point(2, 3) point1.x // 2 println(point1) // prints (x, y)
这个 Point
类有四个成员:变量 x
和 y
,以及方法 move
和 toString
。与其他语言不同的是,主构造函数在类签名中 (var x:Int, var y:Int)
。move
方法接受两个整数参数,并返回 Unit
:不包含任何信息的值 ()
。这大致相当于java类语言中的 void
。另一方面,toString
不接受任何参数,但返回一个字符串值。由于 toString
覆盖了 AnyRef
中的 toString
,它被关键字 override
所标记。
构造器
通过提供默认值,构造函数可以具有可选参数,如下所示:
class Point(var x: Int = 0, var y: Int = 0) val origin = new Point // x and y are both set to 0 val point1 = new Point(1) println(point1.x) // prints 1
在这个版本的 Point
类中,x
和 y
的默认值为 0
,所以不需要参数。但是,因为构造函数会读取从左到右的参数,如果你只想传递一个 y
值,那么你需要加上该参数的名称。
class Point(var x: Int = 0, var y: Int = 0) val point2 = new Point(y=2) println(point2.y) // prints 2
这也是一种提高清晰度的好习惯。
私有成员和getter/setter语法
默认情况下,成员是公开的。使用 private
修饰符可使得它们对类外部来说是不可见的。
class Point { private var _x = 0 private var _y = 0 private val bound = 100 def x = _x def x_= (newValue: Int): Unit = { if (newValue < bound) _x = newValue else printWarning } def y = _y def y_= (newValue: Int): Unit = { if (newValue < bound) _y = newValue else printWarning } private def printWarning = println("WARNING: Out of bounds") } val point1 = new Point point1.x = 99 point1.y = 101 // prints the warning
在这个版本的 Point
类中,数据存储在私有变量 _x
和 _y
中。而定义的方法 def x
和 def y
则可以访问私有数据。def x_=
和 def y_=
用于验证和设置 _x
和 _y
的值。请注意 setter
的特殊语法:方法将 _=
附加到 getter
的标识符后面,并且跟着参数。
使用 val
和 var
的主构造函数参数是公开的。但是,因为 val
是不可变的,所以不能写下面的内容。
class Point(val x: Int, val y: Int) val point = new Point(1, 2) point.x = 3 // <-- does not compile
没有 val
或 var
的参数是私有值,只在类中可见。
class Point(x: Int, y: Int) val point = new Point(1, 2) point.x // <-- does not compile
混入类
“混入”是用来组成类的特性。
abstract class A { val message: String } class B extends A { val message = "I'm an instance of class B" } trait C extends A { def loudMessage = message.toUpperCase() } class D extends B with C val d = new D d.message // I'm an instance of class B d.loudMessage // I'M AN INSTANCE OF CLASS B
类 D
有一个超类 B
和一个混入类 C
。类只能有一个超类但可以有很多混入类(分别使用关键字 extend
和 with
)。混入类和超类可能具有相同的超类型。
现在让我们从一个抽象类开始,来看一个更有趣的例子:
abstract class AbsIterator { type T def hasNext: Boolean def next(): T }
这个类有一个抽象类型 T
和一个标准的迭代器方法。
接下来,我们将实现一个具体类(所有的抽象成员 T
、hasNext
和 next
都被实现):
class StringIterator(s: String) extends AbsIterator { type T = Char private var i = 0 def hasNext = i < s.length def next() = { val ch = s charAt i i += 1 ch } }
StringIterator
接受一个 String
并且可以对字符串进行遍历(例如:要查看字符串是否包含某个字符)。
现在,让我们创建一个也扩展了 AbsIterator
的特质。
trait RichIterator extends AbsIterator { def foreach(f: T => Unit): Unit = while (hasNext) f(next()) }
只要还有其他元素(while(hasNext)
),此特征通过不断调用提供的函数 f: T => Unit
在下一个元素(next()
)上来实现 foreach
。因为 RichIterator
是一个特质,它不需要去实现 AbsIterator
里的抽象成员。
我们希望将 StringIterator
和 RichIterator
的功能合并到一个类中。
object StringIteratorTest extends App { class RichStringIter extends StringIterator(args(0)) with RichIterator val richStringIter = new RichStringIter richStringIter foreach println }
新的 Iter
类有一个作为超类的 StringIterator
和一个作为混入类的 RichIterator
。
只有单一继承的话,我们就无法达到这样的灵活性。
嵌套类
在 Scala 中,可以让类作将其他类作为自己的成员。与java语言不同,嵌套类是封闭类的成员,在 Scala 中,嵌套类被绑定到外部对象。假设我们希望编译器在编译时阻止我们混合哪些 Node、属于哪些 Graph。路径依赖类型提供了一个解决方案。
为了说明这一差异,我们快速地概述了 Graph 数据类型的实现:
class Graph { class Node { var connectedNodes: List[Node] = Nil def connectTo(node: Node) { if (connectedNodes.find(node.equals).isEmpty) { connectedNodes = node :: connectedNodes } } } var nodes: List[Node] = Nil def newNode: Node = { val res = new Node nodes = res :: nodes res } }
这个程序表示一个 Graph 作为 Node 列表(List[Node]
)。每个 Node 都有一个它连接到的其他 Node 的列表(connectedNodes
)。class Node
是路径依赖类型,因为它嵌套在 class Graph
中。因此,connectedNodes
中的所有节点必须使用来自 newNode
同一实例的 Graph
创建。
val graph1: Graph = new Graph val node1: graph1.Node = graph1.newNode val node2: graph1.Node = graph1.newNode val node3: graph1.Node = graph1.newNode node1.connectTo(node2) node3.connectTo(node1)
为了清楚起见,我们明确声明了 node1
,node2
和 node3
的类型为 graph1.Node
,但是编译器可以推断出它。这是因为当我们调用 graph1.newNode
,它再调用 new Node
时,该方法使用特定于实例 graph1
的 Node
实例。
如果我们现在有两个 Graph,那么 Scala 的类型系统不允许将一个 Graph 中定义的 Node 与另一个 Graph 的 Node 混合,因为另一个 Graph 的 Node 具有不同的类型。 这是一个非法程序:
val graph1: Graph = new Graph val node1: graph1.Node = graph1.newNode val node2: graph1.Node = graph1.newNode node1.connectTo(node2) // legal val graph2: Graph = new Graph val node3: graph2.Node = graph2.newNode node1.connectTo(node3) // illegal!
graph1.Node
类型与 graph1.Node
类型不同。在 Java 中,前一个示例程序中的最后一行是正确的。对于这两个 Graph 的 Node,Java 将分配相同类型的 graph.node
。Node
的前缀是 Graph
类。在 Scala 中,这样的类型也可以表达,它被写成 Graph#Node
。如果我们想要连接不同 Graph 的 Node,我们必须按照以下方式改变我们初始 Graph
实现的定义:
class Graph { class Node { var connectedNodes: List[Graph#Node] = Nil def connectTo(node: Graph#Node) { if (connectedNodes.find(node.equals).isEmpty) { connectedNodes = node :: connectedNodes } } } var nodes: List[Node] = Nil def newNode: Node = { val res = new Node nodes = res :: nodes res } }注意,这个程序不允许我们将一个 Node 附加到两个不同的 Graph 上。如果我们想要删除这个限制,我们必须将变量 Node 的类型更改为
Graph#Node
对象
一个对象是一个只有一个实例的类。它被引用时被懒惰地创建,就像懒惰的val一样。
作为顶级的值,一个对象是一个单例。
作为封闭类或本地值的成员,它的行为完全像一个懒惰的val。
定义对象
一个对象是一个值。定义一个对象看起来和定义一个类一样,但使用的关键字是 object
:
object Box
下面是一个带有方法的对象的例子:
package logging object Logger { def info(message: String): Unit = println(s"INFO: $message") }
方法 info
可以从程序中的任何地方导入。像这样创建实用程序方法是单例对象的常见用例。
让我们看看如何在另一个包中使用 info
:
import logging.Logger.info class Project(name: String, daysToComplete: Int) class Test { val project1 = new Project("TPS Reports", 1) val project2 = new Project("Website redesign", 5) info("Created projects") // Prints "INFO: Created projects" }
由于 import 语句,import logging.Logger.info
,info
方法是可见的。
导入需要++导入符号++的“稳定路径”,并且对象是稳定的路径。
注意:如果一个 object
不是顶层的,而是嵌套在另一个类或对象中,那么该对象就像任何其他成员一样是“路径依赖的”。这意味着给定 class Milk
和 class OrangeJuice
两个饮料类型,一个类成员 class NutritionInfo
“取决于”封闭的实例,牛奶或橙汁。milk.NutritionInfo
与 oj.NutritionInfo
完全不同.
伴生对象
名称与某个类相同的对象称为伴生对象。相反,该类是对象的伴生类。但伴生类或对象可以访问其伴生的私人成员。在伴生类实例里使用伴生对象的方法和值是没有效果的。
import scala.math._ case class Circle(radius: Double) { import Circle._ def area: Double = calculateArea(radius) } object Circle { private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) } val circle1 = new Circle(5.0) circle1.area
class Circle
有一个特定于每个实例的成员 area
,而单例 object Circle
有一个可用于每个实例的方法 calculateArea
。
伴生对象也可以包含工厂方法:
class Email(val username: String, val domainName: String) object Email { def fromString(emailString: String): Option[Email] = { emailString.split('@') match { case Array(a, b) => Some(new Email(a, b)) case _ => None } } } val scalaCenterEmail = Email.fromString("[email protected]") scalaCenterEmail match { case Some(email) => println( s"""Registered an email |Username: ${email.username} |Domain name: ${email.domainName} """) case None => println("Error: could not parse email") }
object Email
包含从一个 String 可以创建一个 Email
的工厂 fromString
。在可能解析错误的情况下,我们将其作为 Option[Email]
返回。
注意:如果类或对象具有伴生,则两者必须在同一个文件中定义。 要在REPL中定义伴生,请将它们定义在同一行上或输入 :paste
模式。
Java 程序员的注意事项
Java 中的 static
成员被模仿为 Scala 中伴生对象的普通成员。
当使用Java代码中的伴生对象时,成员将在具有 static
修饰符的伴随类中定义。这称为静态转发(static forwarding)。 即使您没有自己定义伴生类,也会发生这种情况。