Programming In Scala筆記-第十九章、類型參數,協變逆變,上界下界

  本章主要講Scala中的類型參數化。本章主要分成三個部分,第一部分實現一個函數式隊列的數據結構,第二部分實現該結構的內部細節,最後一個部分解釋其中的關鍵知識點。接下來的實例中將該函數式隊列命名爲Queue

一、函數式隊列

  函數式隊列是一種具有以下三種操作方法的數據結構,並且這些操作都必須在常量時間內完成:

  • head,返回該隊列中的第一個元素
  • tail,返回除第一個元素之外的所有元素組成的新隊列
  • enqueue,將新元素加入原有隊列從而得到一個新隊列

      函數式隊列不同於可變隊列(mutable queue)。從上面三種操作可以看出,函數式隊列對象的內容不可變,新增一個元素時得到的是一個新的隊列對象。
    我們期望Queue具有的功能是,執行下面兩句代碼後,

val q = Queue(1, 2, 3)
val q1 = q enqueue 4

  q的元素仍然是1, 2, 3,而q1的元素則爲1, 2, 3, 4

1、基於List類型的Queue實現

  由於Queue類型是不可變的,那麼在實現上面三個方法時也不能基於可變的數據結構來設計。由於List類型也是非可變的,並且也支持直接操作頭尾元素。下面基於List來實現第一版Queue類型。

class SlowAppendQueue[T](elems: List[T]) {
  def head = elems.head
  def tail = new SlowAppendQueue(elems.tail)
  def enqueue(x: T) = new SlowAppendQueue(elems ::: List(x))
}

  對Queue的操作都可以轉換到對List的操作上。但是,對於enqueuq操作,時間複雜度會線性相關於當前Queue對象中的元素個數,不是我們要求的常量時間複雜度。

  如果將enqueue操作改造成常量時間複雜度的話,將傳入的List對象進行reverse後調用以下函數,對於headtail的操作時間複雜度又不是常量的了,如下所示,

class SlowHeadQueue[T](smele: List[T]) {
  def head = smele.last
  def tail = new SlowHeadQueue(smele.init)
  def enqueue(x: T) = new SlowHeadQueue(x :: smele)
}

  考慮一下,如果將以上兩個函數進行合併,就可以實現三個操作都是常量時間複雜度的Queue了。在最終的Queue類型中維持兩個List對象,一個是leading,該對象中保存了Queue對象前半段的元素,另一個對象trailing包含了Queue對象後半段元素的反序列。那麼Queue對象最終的元素爲leading ::: trailing.reverse

class Queue[T](private val leading: List[T], private val trailing: List[T]) {
  private def mirror =
    if (leading.isEmpty)
      new Queue(trailing.reverse, Nil)
    else
      this

  def head = mirror.leading.head
  def tail = {
    val q = mirror
    new Queue(q.leading.tail, q.trailing)
  }

  def enqueue(x: T) =
    new Queue(leading, x :: trailing)
}

二、信息隱藏

  第一節中最後已經實現了最開始我們需要的Queue,但是從實現上看並不完美,因爲它將實現細節暴露了出來。即可以在Queue類型的主構造函數中看到該類中有兩個List對象。並且,這個Queue對象並不是我們從直觀上所理解的那樣,因爲構造它時居然需要兩個List對象,而不是我們直觀上想的那樣只需要傳入一個List對象。
  接下來從多方面對Queue作進一步的改造,隱藏一些不必要以及對用戶不友好的信息。

1、私有構造器和工廠方法

  我們知道,在Java中可以將一個構造器定義爲私有的,從而不對外暴露該類的構造方法。在Scala中一個類的主構造函數由類定義時由類參數以及類的實現間接的定義了。然而,還是可以將Scala的主構造函數使用private關鍵字進行隱藏,private關鍵字寫在類名和類參數之間,如下所示

class Queue[T] private (
  private val leading: List[T],
  private val trailing: List[T]
)

  Scala的私有構造方法只能被該類本身,以及該類的伴生對象訪問。此時直接使用Queue類的主構造函數生成一個Queue對象,會報以下錯誤,
  這裏寫圖片描述

  不能調用主構造函數生成新的Queue對象,就需要提供新的方法了。最先想到的辦法就是,新建一個輔助構造函數,通過輔助構造函數來生成Queue對象。比如

def this() = this(Nil, Nil)     // 生成空的Queue
def this(elems: T*) = this(eles.toList, Nil)     // 使用T*,傳入多個參數

  除了輔助構造函數外,我們還可以定義一個外部可以訪問的工廠方法來生成新的Queue對象。下面在Queue類文件中新建一個伴生對象Queue,並實現一個apply方法,

object Queue {
  def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)
}

  由於在伴生對象中定義的工廠方法名爲apply,所以在調用該方法時的使用Queue(1, 2, 3)很像直接調用Queue類的構造函數。但是注意,這裏實質上是直接調用了伴生對象Queueapply方法。

2、可選方案:私有對象

  私有構造器和私有變量是隱藏類信息的一個方法,另一個更加嚴苛的隱藏方法是直接將該類定義爲私有的,再提供給外界一個只包含公有方法的trait。如下所示

trait Queue[T] {
  def head: T
  def tail: Queue[T]
  def enqueue(x: T): Queue[T]
}

object Queue {
  def apply[T](xs: T*): Queue[T] = new QueueImpl[T](xs.toList, Nil)

  private class QueueImpl[T](
    private val leading: List[T],
    private val trailing: List[T]
  ) extends Queue[T] {
    def mirror =
      if (leading.isEmpty)
        new QueueImpl(trailing.reverse, Nil)
      else
        this

    def head: T = mirror.leading.head
    def tail: QueueImpl[T] = {
      val q = mirror
      new QueueImpl(q.leading.tail, q.trailing)
    }
    def enqueue(x: T) = new QueueImpl(leading, x :: trailing)
  }
}

三、協變和逆變

  這裏主要講到協變和逆變的概念。

  上面代碼中的Queue是trait,而不是一個類型,並且Queue需要接收一個類型參數T。在不指定T的情況下,無法定義Queue類型的對象。比如下面這行代碼就會報錯,

def doesNotCompile(q: Queue) {}

  報錯如下,
  這裏寫圖片描述

  因爲代碼在編譯時是不知道T的具體類型是什麼的。但是如果爲Queue指定一個特殊的類型參數,例如Queue[String], Queue[Int], Queue[AnyRef],程序就能正常編譯,如下所示,

def doesCompile(q: Queue[AnyRef]) {}

  運行結果如下,
  這裏寫圖片描述

  Queue在是這裏一個泛型trait,而Queue[String]是一個類型。帶泛型參數的trait是泛型trait,帶泛型參數的類是泛型類。Queue[Int], Queue[String]都是泛型trait Queue[T] 的特定實現形式。

1、協變

  那麼,假設類型S是類型T的子類,Queue[S]是否是Queue[T]的子類呢?如果是的話,那麼就可以稱Queue對於其類型參數T是協變的。對於這種只有一個類型參數的泛型,可以直接成Queue是協變的。泛型類Queue是協變的,意味着在上面的doesCompile方法中,傳入一個Queue[String]類型的參數時程序也能夠正常執行,因爲這裏可以接收的參數類型是Queue[AnyRef],並且StringAnyRef的子類。
  
  雖然從直觀理解上,Queue[String]類型是Queue[AnyRef]類型的子類,但是一般情況下,在Scala中泛型類都是非協變的。即在前面的Queue泛型trait代碼中,Queue[String]並不是Queue[AnyRef]的子類。

  如果需要指定泛型類對某個類型參數具有協變性,需要在泛型類定義時最前面那個類型參數前加+符號,指定該泛型類對該指定參數具有協變性。對泛型trait Queue進行協變改造,將其第一行改成如下形式。

trait Queue[+T] {...}

2、逆變

  有沒有想過,在上面的改造代碼中,將+換成-,即下面這種情況,是什麼情況?

trait Queue[-T] {...}

  如果你往這方面進行思考了,那麼恭喜你,你已經開始嘗試逆變的寫法了。
  在這種寫法下,如果類型T是類型S的子類,那麼Queue[S]Queue[T]的子類。正好與協變是相反的!
  

3、可變數據無協變

  在函數式編程世界裏,許多類型天然就具有協變特性。然而,當引入可變數據時(mutable data),情況就不是這樣的了。即經常使用可變數據時,大多數類都不具備協變特性。這是爲什麼呢?看一下下面這段代碼,

class Cell[T](init: T) {
  private[this] var current = init
  def get = current
  def set(x: T) { current = x }
}

  這段代碼中的Cell類既不是協變,也不是逆變的,而是不變的。這段代碼可以正常執行,
  這裏寫圖片描述

  假如我們將Cell定義成Cell[+T]類型,再看一下運行結果,運行報出一個Error信息,
  這裏寫圖片描述

  爲什麼會報錯?我們暫時性忽略上面的這個報錯,假設其可以正常執行。那麼繼續執行下面這幾行代碼,

val c1 = new Cell[String]("abc")
val c2: Cell[Any] = c1
c2.set(1)
val s: String = c1.get

  首先定義一個Cell[String]類型的變量c1,由於具有協變性,接下來將c1賦值給Cell[Any]類型變量c2。由於是引用型變量,實際上c1c2執行的是同一個具體對象。此時通過c2set方法,將current變量的值變更成Int型的1,這是不會報錯的。接下來再通過c1get方法,獲取current的值。對於c1來說,currentString類型的,但是已經通過c2將其改成了Int類型。這時候c1獲取current變量時就會類型不匹配而報錯了。

四、下界和上界

1、下界

  下界使用符合>:表示,比如下面這段代碼中表示類型U是類型T的父類,即此處的類型U最少爲T,不能比T的類型更低。

def enqueue[U >: T](x: U) = new Queue[U](leading, x :: trailing)

2、上界

  上界使用符合<:表示,比如T <: U表示需要類型T是類型U的子類,類型T不能比U的類型更高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章