【Scala類型系統(tǒng)】類型參數(shù)化和變化型注解

引言

類型參數(shù)化(Parameterized Types)可以用來編寫泛型類和特質(zhì)谷市,比如定義Set[T]案站,這使得我們可以創(chuàng)建諸如Set[String]的類型注益。而變化型注解(Variance Annotation)定義了參數(shù)化類型的繼承關(guān)系带斑,比如Set[String]是Set[AnyRef]的子類型模捂。
這些語法可以讓我們實(shí)現(xiàn)信息隱藏技術(shù),同時它們也是編寫庫程序的基礎(chǔ)贤壁。

類型參數(shù)化

這里以水果盒的代碼作為例子:

abstract class Fruit {
  def name: String
}
class Orange extends Fruit {
  def name = "orange"
}
class Apple extends Fruit {
  def name = "apple"
}

abstract class Box {
  def fruit: Fruit
  def contains(aFruit: Fruit) =
    fruit.name.equals(aFruit.name)
}
class OrangeBox(orange: Orange) extends Box {
  def fruit: Orange = orange
}
class AppleBox(apple: Apple) extends Box {
  def fruit: Apple = apple
}

上面的例子悼枢,定義了Box的兩個子類,OrangeBox和AppleBox脾拆,這考慮的類型安全馒索,因?yàn)閒ruit方法的返回值受到了Orange和Apple類型的特別限制。這也同樣帶來了維護(hù)代碼量和類型安全的矛盾名船。
基于這個問題绰上,Scala允許使用參數(shù)化類型,即你可以使用類型參數(shù)來代替實(shí)際的類型渠驼。類型參數(shù)可以認(rèn)為是一個限制類型的別名蜈块。

根據(jù)類型繼承關(guān)系,Scala編譯器允許AppleBox或者OrangeBox的實(shí)例賦值給Box類型變量,在Box子類中fruit方法的實(shí)現(xiàn)返回了Apple或者Orange類型百揭,這同樣可以賦值給Fruit類型的變量爽哎。

變化型注解

  • 假設(shè)聲明了class Orange extends Fruit,而后class Box[A]中的A可以有前綴+或-器一。
  • A课锌,沒有任何注解,是不變型祈秕。該狀態(tài)下渺贤,Box[Orange]Box[Fruit]沒有任何繼承關(guān)系
  • +A,是協(xié)變類型请毛。此時癣亚,Box[Orange]Box[Fruit]的子類型,并且變量聲明val f: Box[Fruit] = new Box[Orange]()是允許的获印。
  • -A述雾,是逆變類型。此時兼丰,Box[Fruit]Box[Orange]的子類型玻孟,并且變量聲明val f: Box[Orange] = new Box[Fruit]()是允許的。

從總體來看鳍征,變化類型參數(shù)可以被認(rèn)為是擴(kuò)展泛型編程的類型檢查范圍的工具黍翎。它提供了額外的類型安全,即保證類型安全的前提下艳丛,為開發(fā)者提供了利用類型層級的可能性匣掸。

子類型多態(tài)

協(xié)變和逆變注解使得通過類型參數(shù)的繼承關(guān)系來推斷泛化類型的繼承關(guān)系,利用該注解可以限制類型參數(shù)在泛型類中一些可能的使用氮双,Scala編譯器針對不恰當(dāng)?shù)氖褂眠M(jìn)行檢查和報錯碰酝。

abstract class Box[+F <: Fruit] {
  def fruit: F
  def contains(aFruit: Fruit) = fruit.name.equals(aFruit.name)
}
class OrangeBox(orange: Orange) extends Box[Orange] {
  def fruit = orange
}
class AppleBox(apple: Apple) extends Box[Apple] {
  def fruit = apple
}

var fruitBox: Box[Fruit] = new AppleBox(new Apple)
var fruit: Fruit = fruitBox.fruit

上面的例子中,Box[+F]類型參數(shù)是協(xié)變的戴差,那么將Box[Apple]賦值給Box[Fruit]類型變量fruitBox是合法的送爸。類型變量F用在函數(shù)返回類型中是合理的(比如Box.fruit),在fruitBox.fruit方法的調(diào)用中暖释,保證返回給Fruit類的實(shí)例是一個更加具體的類型袭厂,由于類型參數(shù)是協(xié)變的,需要返回一個比Fruit類型更加具象的類型球匕。

協(xié)變和逆變點(diǎn)

函數(shù)在參數(shù)上是逆變的纹磺,在返回值上則是協(xié)變的。通常而言亮曹,對于某個對象消費(fèi)的值適用逆變橄杨,而對于它產(chǎn)出的值適用協(xié)變秘症。
如果一個對象同時消費(fèi)和產(chǎn)出某值,則類型應(yīng)該保持不變讥珍。這通常適用于可變數(shù)據(jù)結(jié)構(gòu),比如標(biāo)準(zhǔn)庫中的Array窄瘟、ArrayBuffer衷佃、ListBuffer等。

Scala編譯器檢查變化型注解的機(jī)制

為了核實(shí)變化型注解的正確性蹄葱,Scala編譯器會把類或特質(zhì)中可能用到類型參數(shù)的地方分類為正氏义,負(fù)或中立。注解了+號的類型參數(shù)只能被用在正的位置上图云,而注解了-號的類型參數(shù)只能用在負(fù)的位置上惯悠。沒有變化型注解的類型參數(shù)可以用于任何位置。
Scala編譯器在檢查泛型類時竣况,會跟蹤所有使用類型參數(shù)的位置克婶,然后根據(jù)代碼中變化型的位置來決定正確與否。如果類型參數(shù)出現(xiàn)在了禁止的位置丹泉,編譯器將會報錯情萤。詳細(xì)的檢查規(guī)則可以參見這里

為了對用到類型參數(shù)的地方進(jìn)行分類摹恨,編譯器首先從類型參數(shù)的聲明開始筋岛,然后進(jìn)入更深的內(nèi)嵌層。
處于聲明類的最頂層被劃為正晒哄,默認(rèn)情況下睁宰,更深的內(nèi)嵌層的地方的分類會與它外層一致,但仍有幾種特殊情況可以使得類型參數(shù)的類型發(fā)生翻轉(zhuǎn):

  1. 方法的值參數(shù)位置的參數(shù)類型
  1. 方法的類型參數(shù)子句位置的參數(shù)類型
  2. 類型參數(shù)的下界的下界參數(shù)類型
  3. 參數(shù)化類型的類型參數(shù)寝凌,當(dāng)類型參數(shù)是逆變時
    類型的類型參數(shù)位置柒傻,比如C[Arg]的Arg,也有可能被翻轉(zhuǎn)较木,如果C的類型參數(shù)標(biāo)注了+诅愚,那么類別不變;如果C的類型參數(shù)標(biāo)注了-劫映,那么當(dāng)前類別被翻轉(zhuǎn)违孝。

舉例解釋:

  • def method(parameter: T)中,參數(shù)T在逆變的位置泳赋,遵循第一條規(guī)則
  • def method[U <: T]中雌桑,參數(shù)T在逆變的位置,遵循第二條規(guī)則
  • def method[U >: T]中祖今,參數(shù)T在協(xié)變的位置校坑,循序第二條和第三條規(guī)則
  • class Box[-A]中拣技,Box被聲明為逆變,此情況下耍目,def method(parameter: Box[T])中的T應(yīng)該是協(xié)變的位置膏斤,遵循第一條和第四條規(guī)則

下界

在一個方法的類型參數(shù)子句中的下界參數(shù),為何從逆變位置翻轉(zhuǎn)成協(xié)變邪驮?
Box[+T]中的協(xié)變類型參數(shù)T莫辨,根據(jù)子類型多態(tài)性,只能變得更加具象毅访,也就是說沮榜,受到T的限制,其實(shí)類型將沿著類型層級往下走喻粹。如果將T作為下界蟆融,使用U >: T中的U來作為類型約束,則是可行的守呜。

class Box[+T](fruit: T) {
  def method[U >: T](p: U) = { new Box[U](p) }
}

var apple: Apple = new Apple
var box: Box[Fruit] = new Box[Orange](new Orange)
box = box.method(apple)

這個例子中型酥,即使box在實(shí)例化的時候復(fù)制為一個子類型Box[Orange],Box.method的調(diào)用仍然是合法的查乒。
通過語法U >: T冕末,定義了T為U的下界。結(jié)果侣颂,U必須是T的超類型档桃,method的參數(shù)也變?yōu)轭愋蚒而不是類型T,而方法返回值也變成了Box[U]憔晒,取代了Box[T]藻肄。
通過把Apple對象加入到Box[Orange]中,返回結(jié)果為Box[Fruit]類型拒担。

合法的變化型位置

下面的幾種變化型參數(shù)均為合法的語法格式:

  • abstract class Box[+A]{ def foo(): A }
  • abstract class Box[-A]{ def foo(a: A) }
  • abstract class Box[+A]{ def foo[B >: A](b: B) }
  • abstract class Box[-A]{ def foo[B <: A](): B}

參考資料

The Scala Type System: Parameterized Types and Variances

轉(zhuǎn)載請注明作者Jason Ding及其出處
Github博客主頁(http://jasonding1354.github.io/)
GitCafe博客主頁(http://jasonding1354.gitcafe.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.reibang.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354進(jìn)入我的博客主頁

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嘹屯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子从撼,更是在濱河造成了極大的恐慌州弟,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件低零,死亡現(xiàn)場離奇詭異婆翔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掏婶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門啃奴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雄妥,你說我怎么就攤上這事最蕾∫浪荩” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵瘟则,是天一觀的道長黎炉。 經(jīng)常有香客問我,道長醋拧,這世上最難降的妖魔是什么慷嗜? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮趁仙,結(jié)果婚禮上洪添,老公的妹妹穿的比我還像新娘垦页。我一直安慰自己雀费,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布痊焊。 她就那樣靜靜地躺著盏袄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪薄啥。 梳的紋絲不亂的頭發(fā)上辕羽,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音垄惧,去河邊找鬼刁愿。 笑死,一個胖子當(dāng)著我的面吹牛到逊,可吹牛的內(nèi)容都是我干的铣口。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼觉壶,長吁一口氣:“原來是場噩夢啊……” “哼脑题!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铜靶,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叔遂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后争剿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體已艰,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年蚕苇,在試婚紗的時候發(fā)現(xiàn)自己被綠了旗芬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡捆蜀,死狀恐怖疮丛,靈堂內(nèi)的尸體忽然破棺而出幔嫂,到底是詐尸還是另有隱情,我是刑警寧澤誊薄,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布履恩,位于F島的核電站,受9級特大地震影響呢蔫,放射性物質(zhì)發(fā)生泄漏切心。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一片吊、第九天 我趴在偏房一處隱蔽的房頂上張望绽昏。 院中可真熱鬧,春花似錦俏脊、人聲如沸全谤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽认然。三九已至,卻和暖如春漫萄,著一層夾襖步出監(jiān)牢的瞬間卷员,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工腾务, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毕骡,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓岩瘦,卻偏偏與公主長得像未巫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子担钮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分箫津,“神秘”的通配符狭姨,讓我看了幾遍《Java...
    珞澤珈群閱讀 7,860評論 12 51
  • 前言 人生苦多,快來 Kotlin 苏遥,快速學(xué)習(xí)Kotlin饼拍! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,218評論 9 118
  • 讀《快學(xué)Scala 》一書的摘要 Scala 運(yùn)行于JVM之上田炭,擁有海量類庫和工具师抄,兼顧函數(shù)式編程和面向?qū)ο蟆?在...
    abel_cao閱讀 1,282評論 0 8
  • 變量初始化可以用用 _ 作占位符,賦值為默認(rèn)值教硫,字符串 null叨吮,F(xiàn)loat辆布、Int、Double 等為 0var...
    FaDeo_O閱讀 923評論 0 0
  • 本文大量參考Thinking in java(解析茶鉴,填充)锋玲。 定義:多態(tài)算是一種泛化機(jī)制,解決了一部分可以應(yīng)用于多...
    谷歌清潔工閱讀 465評論 0 2