Scala 泛型


參考,

Programing in Scala

scala中的協變和逆變

----------------------------------------------------------------------------------------------------------------------------------------------------

首先Scala和Clojure雖然都是基於JVM的FP語言, 但其實差異挺大的, Scala更像Java, 而Clojure更象Lisp, base在不同的兩極, 向中間靠近
所以對於Clojure而言, 你不需要太精通Java, 但Scala不行, 一個Scala工程師一定是一個資深的Java工程師

所以這里如果對Java的泛型不了解, 就很難理解Scala的泛型

 

Scala的泛型本質上和Java的沒有區別, 因為本身就是基於Java泛型的
所以Scala默認也是不支持泛型的協變的

為什么不直接支持泛型的協變?
因為不是所有的情況下都存在協變的, 默認支持協變會帶來問題

case1, 有可賦值字段的情況下
比如下面的例子, 如果默認支持協變, 那么下面對Cell的使用看上去就沒有問題, 但實際使用會報錯, 因為你set一個int, 卻讀出string

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

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

 

case2, 在沒有可賦值字段的情況下

class StrangeIntQueue extends Queue[Int] {
  override def append(x: Int) = {
    println(Math.sqrt(x)) //開方操作
    super.append(x)
  }
}

val x: Queue[Any] = new StrangeIntQueue
x.append("abc")  //會發生對string做開方的情況, 所以StrangeIntQueue是無法支持協變的

所以結論就是, 協變並不是適用於所有情況的, 必須開發者自己判斷, 在需要和支持協變的地方加上協變標記
比如在Scala中對於List就是支持協變的(class List [+A]), 但對於可變list就是不可協變的(class MutableList [A])


數組的協變

在Java中數組是個特例容器, 它是默認支持協變的, 當然它也無法避免協變帶來的問題

String[] a1 = { "abc" };
Object[] a2 = a1;
a2[0] = new Integer(17);
String s = a1[0]; // ArrayStore Exception

之所以在Java中這么設計, 因為在Java數組設計的時候, 還沒有泛型, 設計者希望數組也有和類型一樣的泛化效果, 所以加上了協變
但是當后來加上泛型后, 因為考慮到兼容性, 數組的協變就一直被保留下來了

而在Scala中, 沒有必要繼續保持這種特例
所以在Scala中, 數組也是默認不支持協變的

scala> val a1 = Array("abc")
a1: Array[java.lang.String] = Array(abc)
scala> val a2: Array[Any] = a1
<console>:5: error: type mismatch;
found : Array[java.lang.String]
required: Array[Any]
val a2: Array[Any] = a1

 

Variance

協變

[+T], covariant (or “flexible”) in its type parameter T

類似Java中的(? extends T), 即可以用T和T的子類來替換T

 

逆變

[-T], contravariant, 類似(? supers T)
if T is a subtype of type S, this would imply that Queue[S] is a subtype of Queue[T]

相對於協變,t是s的子類,那么Queue[T]也是Queue[S]的子類

逆變是反過來,當t是s的子類,而Queue[S]反而是Queue[T]的子類

看看如何理解,

Liskov Substitution Principle (里氏替換原則)

It is safe to assume that a type T is a subtype of a type U if you can substitute a value of type T wherever a value of type U is required. 
The principle holds if T supports the same operations as U and all of T’s operations require less and provide more than the corresponding operations in U.

根據里氏替換原則, 如果你可以用T替換U, 那么可以認為T是U的子類
這說明T支持U的所有接口, 並且T的操作需要的less和提供的more

所以對於逆變, 當T是S的子類的時候, 你可以用Queue[S]去替換Queue[T], 根據里氏替換原則, 意味着Queue[S]是Queue[T]的子類, 跟T和S的關系相反, 所以稱為”逆”

這個比較難於理解, 在什么地方會用到?

比如下面的OutputChannel,
String是AnyRef的子類, 但是OutputChannel[AnyRef], 卻是OutputChannel[String]的子類, 怎么理解?
對於OutputChannel[String], 支持的操作就是輸出一個string, 同樣OutputChannel[AnyRef]也一定可以支持輸出一個string, 因為它支持輸出任意一個AnyRef(它要求的比OutputChannel[String]少)
但反過來就不行, OutputChannel[String]只能輸出String, 顯然不能替換OutputChannel[AnyRef]

trait OutputChannel[-T]
{
    def write(x: T)
}

 理解這個要用廣義的子類概念,即里氏替換原則,用感性你很難理解的
子類,require less,provide more

 

上界, 下界

Scala的上界和下界比較難理解, 因為和Java里面的界不是一個意思...

Java中, (? extends T), T稱為上界, 比較容易理解, 代表T和T的子類, (? supers T), T稱為下界

 

Scala中, 界卻用於泛型類中的方法的參數類型上, 如下面的例子,
對於Queue中的append的類型參數直接寫T, 會報錯 (error: covariant type T occurs in contravariant position in type T of value x)
這個地方比較復雜, 簡單的說就是Scala內部實現是, 把類中的每個可以放類型的地方都做了分類(+, –, 中立), 具體分類規則不說了
對於這里最外層類[+T]是協變, 但是到了方法的類型參數時, 該位置發生了翻轉, 成為-逆變的位置, 所以你把T給他, 就會報錯說你把一個協變類型放到了一個逆變的位置上

所以這里的處理的方法就是, 他要逆變, 就給他個逆變, 使用[U >: T], 其中T為下界, 表示T或T的超類, 這樣Scala編譯器就不報錯了

class Queue[+T] (private val leading: List[T],
                 private val trailing: List[T] ) {
  def append[U >: T](x: U) = new Queue[U](leading, x :: trailing) //使用T的超類U來替換T
}

同樣對於上界也是,

trait OutputChannel[-T]
{
    def write [U<:T] (x: U) //使用T的子類U來替換T
}

 

泛型數組, ClassManifest

Scala 2.8中新的數組

http://www.scala-blogs.org/2008/10/manifests-reified-types.html

http://www.scala-lang.org/api/2.9.2/scala/reflect/ClassManifest.html

http://www.scala-lang.org/api/current/#scala.reflect.Manifest

What is a Manifest in Scala and when do you need it?

http://scala-programming-language.1934581.n4.nabble.com/What-s-the-difference-between-ClassManifest-and-Manifest-td2125122.html

http://blogs.atlassian.com/2012/12/scala-and-erasure/

 

又是一個晦澀的問題
Mention generics to anyone who knows much about them and they’ll usually have an opinion on type reification (具體化) and erasure (擦除).
Different platforms have different strategies for their generics implementations. C++使用的reification
而Java的泛型系統使用的是erasure,

當然泛型擦除的最大問題是, it doesn’t tell you at runtime what its actual type is (在運行期無法知道類型信息), 除了手工的將java.lang.Class<T> objects作為參數傳入

public class ClassTypeCapture<T> {
    Class<T> kind; 
    public ClassTypeCapture(Class<T> kind) { //T本身無法保留到運行期, 故傳入class對象
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg); //使用class.isInstance
    }
}

當然這個不是優雅的方案
Scala has the ability to automagically produce values via implicit resolution. It also (in 2.8/2.9) has a feature known as Class Manifests.

Manifest. An object of type Manifest[T] provides complete information about the type T

Starting with version 2.7.2, Scala has added manifests, an undocumented (and still experimental) feature for reifying types.
They take advantage of a pre-existing Scala feature: implicit parameters.

def arr[T] = new Array[T](0)    // does not compile, 運行時無法知道什么是T
def arr[T](implicit m: Manifest[T]) = new Array[T](0)  // compiles, 將Manifest[T]當作隱含參數傳入
def arr[T: Manifest] = new Array[T](0)    // shorthand for the preceding, 縮寫

Scala的方案比Java的優雅一些, 但本質是一樣的, 因為利用了implicit parameters特性, 這個manifest對象(類似class對象)會由編譯器自動作為隱式參數加入 

 

<P extends Page> P visit(Class<P> pageClass) {…} //Java
def goto[P <: Page: Manifest]: P = app.visit(manifest[P].erasure.asInstanceOf[Class[P]]) //Scala

a. using manifest[P], a shorthand for implicitly[Manifest[P]]
b. def erasure: Class[_] //2.10中被runtimeClass: Class[_]替換, 返回Class對象,其中泛型信息已經被擦除

this says, given some type parameter P that is a subtype of Page and has-a Manifest[P], call app.visit and pass the class instance that we implicitly summon. Lastly (and unfortunately) we don’t in the type know the erased class’s type, but by construction we know it is correct so we cast using asInstanceOf[Class[P]].

 

ClassManifest, weaker form which can be constructed from knowing just the top-level class of a type, without necessarily knowing all its argument types.

A ClassManifest[T] is an opaque descriptor for type T.
It is used by the compiler to preserve information necessary for instantiating Arrays in those cases where the element type is unknown at compile time.
編譯器用於保留必要的數組的類型信息到運行期, 以便於在運行期實例化泛型數組
對於泛型的運行期實例化, ClassManifest就足夠, 不需要Manifest

Manifest和ClassManifest的區別

type T = Foo[Bar[Baz]]

then Manifest[T] represents the entire Foo[Bar[Baz]] type, whereas ClassManifest[T] only represents Foo[_].

For instantiating Arrays, you only need to know whether it's a primitive array or a reference array, so only the ClassManifest is needed. (But a Manifest is also a ClassManifest...)

雖然不完全准確, 但這樣便於理解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM