快學Scala第18章----高級類型

本章要點

  • 單例類型可用于方法串接和帶對象參數的方法。
  • 類型投影對所有外部類型的對象都包含了其內部類的實例。
  • 類型別名給類型指定一個短小的名稱。
  • 結構類型等效于“鴨子類型”。
  • 存在類型為泛型類型的通配參數提供了統一形式。
  • 使用自身類型來表明某特質對混入它的類或對象的類型要求。
  • “蛋糕模式”用自身類型來實例依賴注入。
  • 抽象類型必須在子類中被具體化。
  • 高等類型帶有本身為參數化類型的類型參數。

單例類型

給定任何引用v,你可以得到類型v.type,它有兩個可能的值: v 和 null 。例如我們想讓方法返回this,從而實現方法的串接調用:

class Document {
  def setTitle(title: String) = {...; this}
  def setAuthor(author: String) = {...; this}
  ...
}

// 級聯使用
val article = new Document
article.setTitle("Whatever Floats Your Boat").setAuthor("Cay Horstmann")

不過,要是還有子類,問題就來了:

class Book extends Document {
  def addChapter(chapter: String) = {...; this}
  ...
}

val book = new Book()
book.setTitle("Scala for the Impatient").addChapter("chapter1")  // error

由于setTitle返回的是this, Scala將返回類型推斷為Document。但Document并沒有addChapter方法。
解決方法是聲明setTitle的返回類型為this.type :

def setTitle(title: String): this.type = {...; this}

這樣book.setTitle("...")返回類型就是book.type。
如果你想定義一個接受object實例作為參數的方法,你也可以使用單例類型。例如

object Title

class Document {
  private var useNextArgAs: Any = null
  def set(obj: Title.type): this.type = {useNextArgAs = obj; this}
  def to(arg: String) = if (useNextArgAs == Title) title = arg; else ...
  ...
}

class Book extends Document {}
book.set(Title).to("Scala for the Impatient")

注意Title.type參數,你不能用:

def set(obj: Title) ...  // error, Title代指的是單例對象,而不是類型

類型投影

在第5章的嵌套類中已經有過例子。http://blog.csdn.net/dwb1015/article/details/51706746

路徑

形如: com.horstmann.impatient.chatter.Member 這樣的表達式稱之為路徑。在最后的類型前,路徑的所有組成部分都必須是“穩定的”,也就是說它必須指定到單個、有窮的范圍。組成的部分必須是以下當中的一種:

  • 對象
  • val
  • this、super、super[S]、C.this、C.super或C.super[S]
    路徑的組成部分不能是類。也不能是var。 例如:
val aly = new Network.Member  // error Network是類

var chatter = new Network
val fred = new chatter.Member   // error  chatter不穩定

類型別名

對于復雜類型,你可以用type關鍵字創建一個簡單的別名,這和C/C++的typedef相同。

class Book {
  import scala.collection.mutable._
  type Index = HashMap[String, (Int, Int)]
}

類型別名必須被嵌套在類或對象中。它不能出現在Scala文件的頂層。


結構類型

所謂的“結構類型”指的是一組關于抽象方法、字段和類型的規格說明,這些抽象方法、字段和類型是滿足該規格的類型必須具備的。

def appendLines(target: { def append(str: String): Any }, lines: Iterable[String]) {
  for (l <- lines) { target.append(l); target.append("\n") }
}

你可以對任何具備append方法的類的實例調用appendLines方法。這比定義一個有appendLines方法的特質更為靈活,因為你不能總是能夠將該特質添加到使用的類上。
但是類型結構背后使用的是反射,而反射調用的開銷比較大。因此,你應該只在需要抓住那些無法共享一個特質的類的共通行為的時候才使用結構類型。


復合類型

復合類型的定義形式如下:

T1 with T2 with T3 ...

在這里,要想成為該復合類型的實例,某個值必須滿足每一個類型的要求才行。例如:

val image = new ArrayBuffer[java.awt.Shape with java.io.Serializable]

val rect = new Ractangle(5, 10, 20, 30)
image += rect   // OK , Rectangle是Serializable的
image += new Area(rect)  // error, Area是Shape但不是Serializable的

你可以把結構類型的聲明添加到簡單類型或復合類型。例如:

Shape with Serializable { def contains(p: Point): Boolean}

該類型必須既是一個Shape的子類型也是Serializable的子類型,并且還必須有一個帶Point參數的contains方法。

從技術上講,

// 結構類型
{ def append(str: String): Any }
// 是如下代碼的簡寫
AnyRef { def append(str: String): Any }

// 復合類型
Shape with Serializable 
// 是如下代碼的簡寫
Shape with Serializable  {}

中置類型

中置類型是一個帶有兩個類型參數的類型,以“中置”語法表示,類型名稱寫在兩個類型參數之間。例如:

String Map Int
// 而不是
Map[String, Int]

存在類型

存在類型被加入Scala是為了與Java的類型通配符兼容。存在類型的定義是在類型表達式之后跟上forSome{...},花括號中包含了type和val聲明。例如:

Array[T] forSome {type T <: JComponent}
// 這與類型通配符效果相同
Array[_ <: JComponent]

Scala的類型通配符只不過是存在類型的語法糖。例如:

Array[_]
// 等同于
Array[T] forSome { type T }

Map[_, _]
// 等同于
Map[T, U] forSome { type T; type U }

你也可以在forSome代碼塊中使用val聲明,因為val可以有自己的嵌套類型。例如:

n.Member forSome { val n: Network}

Scala類型系統

type.png

自身類型

在第十章 http://blog.csdn.net/dwb1015/article/details/51761510 已經有過介紹。
**注意: **自身類型不會自動繼承, 你需要重復自身類型的聲明:

trait ManagedException extends LoggedException {
  this: Exception => 
     ...
}

依賴注入

在Scala中,你可以通過特質和自身類型達到一個簡單的依賴注入的效果。

trait Logger {
  def log(msg: String) 
}

class ConsoleLogger(str: String) extends Logger {
  def log(msg: String) = {
    println("Console: " + msg)
  }
}

class FileLogger(str: String) extends Logger {
  def log(msg: String) = {
    println("File: " + msg)
  }
}

// 用戶認證特質有一個對日志功能的依賴
trait Auth {
  this: Logger =>
    def login(id: String, password: String): Boolean
}

// 應用邏輯有賴于上面兩個特質
trait App {
  this: Logger with Auth =>
    ...
}

// 組裝應用
object MyApp extends App with FileLogger("test.log") with MockAuth("users.txt")

像這樣使用特質的組合有些別扭。畢竟,一個應用程序并非是認證器和文件日志器的合體。更自然的表示方式可能是通過實例變量來表示組件。
蛋糕模式給出了更好的設計。在這個模式當中,你對每個服務都提供一個組件特質,該特質包含:

  • 任何所依賴的組件,以自身類型表述。
  • 描述服務接口的特質。
  • 一個抽象的val,該val將被初始化成服務的一個實例。
  • 可以有選擇性的包含服務接口的實現。
trait LoggerComponent {
  trait Logger { ... }
  val logger: Logger
  class FileLogger(file: String) extends Logger { ... }
  ...
}

trait AuthComponent {
  this: LoggerComponent =>    // 讓我們可以訪問日志器
  
  trait Auth { ... }
  val auth: Auth
  class MockAuth(file: String) extends Auth { ... }
  ...
}

// 使用組件
object AppComponents extends  LoggerComponent with AuthComponent {
  val logger = new FileLogger("test.log")
  val auth = new MockAuth("users.txt")
}

抽象類型

類或特質可以定義一個在子類中被具體化的抽象類型。例如:

trait Reader {
  type Contents
  def read(fileName: String): Contents
}

class StringReader extends Reader {
  type Contents = String
  def read(fileName: String) = Source.fromFile(filename, "UTF-8").mkString
}

class ImageReader extends Reader {
  type Contents = BufferedImage
  def read(fileName: String) = ImageIO.read(new File(fileName))
}

同樣的效果可以通過類型參數來實現:

trait Reader[C] {
  def read(fileName: String): C
}

class StringReader extends Reader[String] {
  def read(fileName: String) = Source.fromFile(filename, "UTF-8").mkString
}

class ImageReader extends Reader[BufferedImage] {
  def read(fileName: String) = ImageIO.read(new File(fileName))
}

那種方式更好呢?Scala經驗法則:

  • 如果類型是在類被實例化時給出,則使用類型參數。
  • 如果類型是在子類中給出,則使用抽象類型。

抽象類型可以有類型界定,就和參數類型一樣。例如:

trait Listener {
  type Event <: java.util.EventObject
  ...
}

// 子類必須提供一個兼容的類型
trait ActionListener extends Listener {
  type Event = java.awt.event.ActionEvent
}

家族多態

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容