您好,登錄后才能下訂單哦!
這篇文章主要介紹“Scala類型系統和功能有哪些”,在日常操作中,相信很多人在Scala類型系統和功能有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Scala類型系統和功能有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
概要
到上次為止由羽生田先生介紹了Scala語法的特點,這一講我作為嘉賓來介紹一下Scala的類型系統和相關功能。本次介紹的重點是Java與Scala之間類層次的差異、范型的協變與逆變、實存類型(Existential Type)、結構類型(Structural Type)和復合類型(Compound Type)。
與Java相似之處
Scala類型系統的基礎部分是與Java非常相像的。Scala與Java一樣有單一的根類,Java通過接口來實現多重繼承,而Scala則通過特征(trait)來實現(Scala的特征可以包含實現代碼,這當然是與Java接口不同的。不過由于特征自己具有類型的功能,所以對于沒有包含實現代碼的特征,可以認為與Java的接口是等價的)。
不過在幾點上面Scala具有與Java不同的部分,或者相比Java增加的功能部分。下文將以與Java相比的不同點或增加點為重點來說明一下Scala的類型系統。
Scala的類層次(1) - Any、AnyVal、AnyRef
本連載的第四回也提到過,Scala的類型層次與Java的相應部分是非常相似的。首先,Scala中存在單一的根類Any,所有類型都直接或間接地由Any類繼承而來。這與Java中的所有引用類型的根類是java.lang.Object是一樣的。
另一方面也有與Java不同的部分。首先Scala不存在類似于Java中的基礎(Primitive)類型。那是
怎么一回事呢?Scala中所有與基礎類型相當的類型都成為了類,并且這些類都是繼承了Any的AnyVal類的子類。
另外,所有的引用類型都成AnyRef類的間接或直接子類。前面說了Any類類似于Java中的java.lang.Object類,但是從實際意義上來看,因該說對應于java.lang.Object的因該是AnyRef(図 3-1的a)。
不過離題一下,對于Scala中相當于基礎類型的類,可以把整數的(Byte、Short、Int、Long)和浮點數的(Float、Double)看作是相鄰的兄弟類關系,那Java中可以默認轉換的比如byte->int,在Scala中是否需要顯示的類型轉換呢,如果需要的話那大家會覺得好麻煩呀。實際上Scala中具有使用戶能夠定義自己的默認類型轉換功能的隱式轉換(implicit conversion)功能。對于類似于Java中的byte->int、float->double等默認轉換功能,在Scala標準庫中定義了與此相當的隱式轉換功能,所以用戶是不需要顯示轉換的。這個隱式轉換是非常有趣的功能,在以后的連載里可能會有詳細說明。
Scala的類層次(2) - Nothing、Null
與Java不同的是,Scala中存在所謂的底(bottom)類型,那就是Nothing類。Nothing是所有類型的子類,也就是說可以將Nothing類型賦值給任意類型的變量,但是Nothing類型的值并不存在。
大家可能認為“沒有值的類型有什么用呢?”。但是Nothing類型絕對在,表示沒有返回值的函數的返回類型,或者在后述的范型中表示空的集等方面發揮著重要的作用。
另外還存在是所有的引用類型(AnyRef)的子類,可以賦值給所有引用類型變量的類型Null。Null類型的值只有null(實際上Java中也有Null類型,擔負著與Scala中Null類型相似的任務。與Scala不同的是,Java中沒有顯示定義Null類型的機會,所以基本上沒有人會意識得到的)。(図 3-1的b)表示了上述類型間的關系。
范型基礎
一句話來說,范型就是定義以類型為參數的類或接口(Scala中為特征)的功能。Java里從JDK5開始就有了范型,想必知道的人應該比較多了,下面就簡單舉例說明一下。
例如,假設有如下的代碼片段。這里java.util.List是范型接口,String就是賦給它的類型參數。
java.util.List< String> strs = new java.util.ArrayList< String>();
這樣,就可以用如下方法將String類型(或子類型)的對象加入List中了。
strs.add("hoge");
如下所示,如將String以外的對象加入List則會發生編譯錯誤。
strs.add(new java.util.Date());
這樣一來,就可以開發類型安全的通用集(collection)庫了。在Java5之前的集庫是用Object來實現的。但是向集中加入元素時并沒有進行正確的類型檢查,而且從集中取出元素時還要做強制的類型轉換,導致舊的集庫在類型安全方面有一些問題。進一步來說,光從類型定義看不出該集包含的是何種元素,所以在可讀性方面也有不足。
Scala的范型與Java是非常相似的,基本上可以同樣地使用,只是在標記方法上有些區別。以下是同剛才Java代碼基本相同的Scala代碼。
var strs: java.util.List[String] = new java.util.ArrayList
Scala中用[..]來代替了Java中的< ..>來表現類型參數表。附帶提一下,與Java有一點小的不同,Scala在new ArrayList時不需要指定String類型參數,這是編譯器的類型推斷起了效用(顯示指定也是可以的)。
Scala中定義范型類的方法也基本與Java相同。下面是通過范型用Java定義的不可變單方向列表類。這里在類名Link后聲明了用< >括著的類型參數T。這個類型參數T在Link類的定義中可以像一般類型那樣使用。
class Link< T> { final T head; final Link< T> tail; Link(T head, Link< T> tail) { this.head = head; this.tail = tail; } }
同樣可以用Scala來定義與上述完全相同的范型列表。
class Link[T](val head: T, val tail: Link[T])
從此可知,除了一些細微的標識差別,Scala中也可以方便地使用范型。
范型的協變與逆變
光從到此為止的說明來看,可能有人會以為Scala是僅僅把Java中的范型改變了一下標識符號。但是Scala中的范型有幾個與Java不同的明顯差異,其中之一就是這里提到的協變與逆變。
協變
范型中所謂的協變大致來說是這樣的東西。首先假設有類G(或者接口和特征)和類型T1、T2。在T1是T2的子類的情況下如果G< T1>也是G< T2>的子類,那么類G就是協變的。
僅如此說明的話比較難以理解,那就舉例說明一下。如下所示,假設有類型為java.util.List< Object>的變量s1和類型為java.util.List< String>的變量s2。
java.util.List< Object> s1 = ...; java.util.List< String> s2 = ...;
String是Object的子類,Java中并不允許將s2賦值給s1,將會產生編譯錯誤。因此,雖然String是Object的子類,但是java.util.List< String>并不是java.util.List< Object>的子類,所以用Java的范型所定義的類或接口并不是協變的。這并不是由于Java范型的靈活性不好,而是因為協變的范型在保證類型的安全性上有一些問題。
假定允許s1=s2;。s1是容納Object類型的元素的,所以如下所示可以加入java.util.Date類型的對象。
s1.add(new java.util.Date());
但是由于語句s1=s2;,s1被指向了s2,這樣容納String元素的List變量s2就可以加入java.util.Date對象了。這樣好不容易通過范型來保證的類型安全性(java.util.List< String>里只有String)就被破壞了。正因為有如此問題所以Java的范型不是協變的。
附帶提一下,對于Java5之前就存在的數組來說,數組的元素類型A如果是數組元素類型B的子類,那么A的數組類型也是B的數組類型的子類,也就是說Java中的數組是協變的。這樣一來,如下所示即使是違背了類型安全性的數組之間的賦值(沒有強制類型轉換)代碼也能通過編譯器檢查。
String[] s2 = new String[1]; Object[] s1 = s2; s1[0] = new java.util.Date(); //執行時拋出ArrayStoreException異常
如上所述,Java中的范型不是協變的是有理由的,但是有些情況下這種限制表現得過于強了。比如,以使用前述的不可變Link類為例。這種情況下,一旦創建不可變Link的實例之后,與Java的List不同,對于該實例是不能進行寫操作(如add)的,這樣的話將Link< String>賦值給Link< Object>也就可以認為沒有問題了,但是在Java中這是不允許的。
Scala的范型,在沒有特定指定的情況下也是和Java一樣,是非協變的。例如使用前述的Link類編寫如下代碼后將會出現編譯錯誤。
val link: Link[Any] = new Link[String]("FOO", null) ...
錯誤提示如下。敘述的錯誤原因是在應該出現Ling[Any]的地方但是出現了Link[String],而這正是Link不是協變的結果。
fragment of Link.scala):2: error: type mismatch; found : this.Link[String] required: this.Link[Any] val link: Link[Any] = new Link[String]("FOO", null)
但是,Scala的類或特征的范型定義中,如果在類型參數前面加入+符號,就可以使類或特征變為協變了。下面是在Scala中定義協變類的實驗。題材是前述的Link類,在類型參數T前加了一個+符號。
class Link[+T](val head: T, val tail: Link[T])
把Link類如此定義之后,前面出現編譯錯誤的代碼就可以順利通過編譯了。另外,如果試圖定義不能保證類型安全的協變范型將會出現編譯錯誤。例如在定義非可變的數據結構時,這種限制就會帶來一些問題。例如對于前面的Link類,追加一個將作為參數傳入的元素放在列表頭并返回新列表的方法prepend。
class Link[+T](val head: T, val tail: Link[T]) { def prepend(newHead: T): Link[T] = new Link(newHead, this) }
prepend方法并沒有改變原來Link類實例的狀態,因該是沒有問題的。但是,編譯之后會產生如下編譯錯誤。
ink.scala:2: error: covariant type T occurs in contravariant position in type T of value newHead def prepend(newHead: T): Link[T] = new Link(newHead, this)
實際上,范型變為協變之后就不能把類型參數不加修改的放在成員方法的參數上(這里是newHead)了。但是,通過將成員方法定義為范型,并按照如下所示描述后就可以避免該問題了(具體原因這里略而不談)。
class Link[+T](val head: T, val tail: Link[T]) { def prepend[U >: T](newHead: U): Link[U] = new Link(newHead, this) }
在Java里也可以定義范型方法,正如范型類型定義,通過用類型參數來參數化方法,從而定義了類型安全的范型方法。例如連載第五回出場的List類的map方法就是范型方法。
verride final def map[B](f : (A) => B) : List[B]
map方法將以參數形式傳入的函數f應用于List的所有元素,并將函數的應用結果組成列表后返回。但是參數函數f的返回結果是什么在定義map方法是不知道的,所以用類型參數B來使map成為范型方法,從而使它可以通用于各種類型了。
范型方法是通過在方法名后直接用[..]來括住類型參數方式來定義的。用[]括住的類型參數在方法中可以作為一般類型來使用。而且在類型參數之后加上>:或< :符號后,可以將類型參數所表示的類型限制為某一類型子類或父類。例如,[U< :T]的情況下,U必須是T的子類;[U>:T]的情況下,U必須是T的父類。
逆變
另一方面,范型中的逆變是這樣的東西。首先假設有類G(或者接口和特征)和類型T1、T2。在T1是T2的子類的情況下如果G< T2>也是G< T1>的子類(注意左右與協變是相反的),那么類G就是逆變的。
與協變一樣,下面舉例說明一下。首先假設有類型為java.util.List< Object>的變量s1,類型為java.util.List< String>的變量s2。
java.util.List< Object> s1 = ...; java.util.List< String> s2 = ...;
String是Object的子類,由于Java的范型規則不允許表達式s2=s1,所以將會出現編譯錯誤。這里雖然String是Object的子類,但是java.util.List< Object>并不是java.util.List< String>的子類,所以Java的范型并不是逆變的。如果Java的范型是逆變的話,那同協變時情況一樣,將會產生類型安全上的問題。
假設允許表達式s2=s1。由于s2的元素類型是String,所以從列表中取出元素后返回的類型因該是String。因此,如下代碼因該是成立的。
String str = s2.get(0);
但是,s2所指的列表s1的元素類型是Object,所以s1列表中的取出的元素并不僅限于String,這在類型安全性上就有問題了。
對于Scala的范型,如果沒有特別指示,與Java一樣也不是逆變的。假設有如下含有apply方法的LessTan類(apply方法的邏輯是當a小于b時返回true,否則返回false)。
abstract class LessThan[T] { def apply(a: T, b: T): Boolean }
如下使用了LessThan類的方法將會出現編譯錯誤。
val hashCodeLt: LessThan[Any] = new LessThan[Any] { def apply(a: Any, b: Any): Boolean = a.hashCode < b.hashCode } val strLT: LessThan[String] = hashCodeLt ...
編譯錯誤的文本如下。顯示的錯誤原因是在因該出現LessThan[String]的地方出現了LessThan[Any],由此看見LessThan類不是逆變的。
(fragment of Comparator.scala):5: error: type mismatch; found : this.LessThan[Any] required: this.LessThan[String] val strLT: LessThan[String] = hashCodeLt
但是,在類或特征的定義中,在類型參數之前加上一個-符號,就可定義逆變范型類和特征了。下面嘗試一下定義Scala的逆變類。題材是前面的LessThan類,如下所示在LessThan定義的類型參數前加上-符號。
abstract class LessThan[-T] { def apply(a: T, b: T): Boolean }
將LessThan類如此定義之后,前面錯誤代碼的編譯就可以通過了。另外,如果將類型定義為逆變后會發生類型安全性問題,則編譯器將報編譯錯誤。
實存(Existantial)類型
前面說過了Java范型沒有協變和逆變特性,但是通過使用Java的通配符功能后可以獲得與協變與逆變相近的效果。通配符不是標記在類型定義的地方,而是在類型使用的地方,可以在使用類型處加上G< ? extends T1>或G< ? super T1>。
前者與協變相對應,當T2是T1的子類時,G< T2>是G< ? exnteds T1>的子類。后者與逆變相對應,T1是T2的子類時,G< T2>是G< ? super T1>的子類。因此,以下的代碼將能正常編譯。
java.util.List< String> s1 = ...; java.util.List< ? extends Object> s2 = s1; //對應協變 java.util.List< Object> s3 = ...; java.util.List< ? super String> s4 = s3; //對應逆變 ...
由于通配符是標記在使用類型的地方,所以每次定義協變或逆變的變量時都要使用它,缺點是比較麻煩。另一方面,即使是沒有定義為協變或逆變的范型類型,也可以將其以協變或逆變的方式處理是它的優點。
Scala中也可以通過使用實存類型方法類實現與Java中通配符相同的功能。例如,下述Scala代碼可以實現與上述Java代碼相同的功能。
//java.util.List[_ < : Any] (省略形式) var s1: java.util.List[String] = new java.util.ArrayList var s2: java.util.List[T] forSome { type T < : Any } = s1 //java.util.List[_ >: String] (省略形式) var s3: java.util.List[Any] = new java.util.ArrayList var s4: java.util.List[T] forSome { type T >: String} = s3 ...
結構(Structural)類型
在類似于Java的語言中只有在類定義好之后才能確定他們的繼承關系。假設有如下A、B、C三個Java類定義。
class A { void call() {} } class B extends A { void call() {} } class C { void call() {} }
這時假如有方法void foo(A a),那么類型A和B的實例可以作為參數傳遞給它,但是類型C卻不能傳遞(雖然C中同樣定義了方法call)。
這是由于,相比于類B通過exnteds A語句明確標識了與A的繼承關系,而C則沒有明確標示出與A的繼承關系,這樣C就不是A的子類了。這對于Java和C#(C++的情況特殊除外)這類靜態語言來說是理所當然的事,所以意識到的人因該不多吧。另一方面,在動態語言社區中,把這稱為duck typing,較普遍的看法是“無論是否有繼承關系,只要在對象中存在需要的方法就可以了。”。以剛才的代碼為例,A、B和C中都定義了call方法,如果foo方法中只調用call方法的話,那么類C的實例也可以作為參數傳給foo。
時常聽到這種說法“正是由于動態語言中沒有靜態類型檢查所以才可以使用duck typing功能。”。但是稍微仔細想一下,雖然是靜態語言,但是在編譯時還是知道類型持有了哪些方法的。理論上,即使不犧牲靜態類型檢查,也應該可以描述只要含有某一方法集合就OK的類型的。
Scala中通過結構類型來描述這種類型。結構類型的使用方法比較簡單,只要在類型聲明的地方,用{}將所需方法的聲明括起來就可以了。
def foo(callable: { def call: Unit }) = ...
另外在列舉方法的個數比較多的情況下,可以如下所示來定義別名,這樣就不用每次都列出所有方法了,只需使用結構類型的別名即可。
type Callable = { def call: Unit }
復合(Compound)類型
在使用Java時,是否有過想用“即繼承了類型A又實現了接口B”的類型的想法呢?Java中除了類型參數的限制之外,對于這種類型也沒有定義方法。然而,Scala中則可以非常簡單地描述這種類型。描述方法就是在一般的類名稱之后用with來連接附加的類型。
例如對于如下的變量f來說,只要是實現了java.io.Closeable和Readable接口的對象,誰都可以賦值給變量f。
var f: java.io.Closeable with Readable = ..
到此,關于“Scala類型系統和功能有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。