【Scala之旅】類與對象

本節翻譯自html

綜述:本節中你將會學習如何使用Scala實現類,以及Scala相比Java更加精簡的表示法帶來的便利。同時介紹了object的語法結構(Scala沒有靜態方法或靜態字段,但object能夠達到一樣的效果)。java

Scala 中的類是建立對象的模板。它們能夠包含統稱爲成員的方法、值、變量、類型、對象、特徵和類。以後將介紹類型、對象和特徵。node

定義類

最小的類定義就是關鍵字 class 和標識符。類名應該大寫。程序員

class User
val user = new User

關鍵字 new 用於建立類的一個實例。User 有一個不帶參數的默認構造函數,由於沒有定義構造函數。可是,你一般須要構造函數和類體。下面是一個示例類的定義:dom

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 所標記。ide

構造器

經過提供默認值,構造函數能夠具備可選參數,以下所示:函數

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

這也是一種提升清晰度的好習慣。ui

私有成員和getter/setter語法

默認狀況下,成員是公開的。使用 private 修飾符可以使得它們對類外部來講是不可見的。scala

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("scala.center@epfl.ch")
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)。 即便您沒有本身定義伴生類,也會發生這種狀況。

相關文章
相關標籤/搜索