【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 类有四个成员:变量 xy,以及方法 movetoString。与其他语言不同的是,主构造函数在类签名中 (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 类中,xy 的默认值为 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 xdef y 则可以访问私有数据。def x_=def y_= 用于验证和设置 _x_y 的值。请注意 setter 的特殊语法:方法将 _= 附加到 getter 的标识符后面,并且跟着参数。

使用 valvar 的主构造函数参数是公开的。但是,因为 val 是不可变的,所以不能写下面的内容。

class Point(val x: Int, val y: Int)
val point = new Point(1, 2)
point.x = 3  // <-- does not compile

没有 valvar 的参数是私有值,只在类中可见。

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。类只能有一个超类但可以有很多混入类(分别使用关键字 extendwith)。混入类和超类可能具有相同的超类型。

现在让我们从一个抽象类开始,来看一个更有趣的例子:

abstract class AbsIterator {
  type T
  def hasNext: Boolean
  def next(): T
}

这个类有一个抽象类型 T 和一个标准的迭代器方法。

接下来,我们将实现一个具体类(所有的抽象成员 ThasNextnext 都被实现):

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 里的抽象成员。

我们希望将 StringIteratorRichIterator 的功能合并到一个类中。

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)

为了清楚起见,我们明确声明了 node1node2node3 的类型为 graph1.Node,但是编译器可以推断出它。这是因为当我们调用 graph1.newNode,它再调用 new Node 时,该方法使用特定于实例 graph1Node 实例。

如果我们现在有两个 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.nodeNode 的前缀是 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.infoinfo 方法是可见的。

导入需要++导入符号++的“稳定路径”,并且对象是稳定的路径。

注意:如果一个 object 不是顶层的,而是嵌套在另一个类或对象中,那么该对象就像任何其他成员一样是“路径依赖的”。这意味着给定 class Milkclass OrangeJuice 两个饮料类型,一个类成员 class NutritionInfo “取决于”封闭的实例,牛奶或橙汁。milk.NutritionInfooj.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)。 即使您没有自己定义伴生类,也会发生这种情况。

相关推荐