Kotlin語(yǔ)言中的泛型設(shè)計(jì)哲學(xué)

文 | 歐陽(yáng)鋒

Kotlin語(yǔ)言的泛型設(shè)計(jì)很有意思屿讽,但并不容易看懂勇哗。關(guān)于這個(gè)部分的官方文檔铺敌,我反復(fù)看了好幾次烫罩,終于弄明白Kotlin語(yǔ)言泛型設(shè)計(jì)的背后哲學(xué)刽射。這篇文章將講述Kotlin泛型設(shè)計(jì)的整個(gè)思考過(guò)程及其背后的哲學(xué)思想屈芜,希望可以解答你心中的疑問(wèn)拍棕。不過(guò)揪荣,可以預(yù)見(jiàn)地宇葱,即使看完瘦真,你也未必完全明白這篇文章在說(shuō)什么,但至少希望你通過(guò)這篇文章可以快速掌握Kotlin泛型的用法黍瞧。

Kotlin泛型的設(shè)計(jì)初衷

我們認(rèn)為诸尽,Kotlin是一門(mén)比Java更優(yōu)秀的JVM編程語(yǔ)言,Kotlin泛型設(shè)計(jì)的初衷就是為了解決Java泛型設(shè)計(jì)中一些不合理的問(wèn)題印颤。這樣說(shuō)可能不夠直觀您机,看下面這個(gè)例子:

 List<String> strs = new ArrayList<>();
// 這里將導(dǎo)致編譯錯(cuò)誤,Java語(yǔ)言不允許這樣做
 List<Object> objs = strs;

很明顯年局,String和Object之間存在著安全的隱式轉(zhuǎn)換關(guān)系际看。存放字符串的集合應(yīng)該可以自由轉(zhuǎn)換為對(duì)象集合。這很合理矢否,不是嗎仲闽?

如果你這樣認(rèn)為的話,就錯(cuò)了僵朗!繼續(xù)往下看赖欣,我們擴(kuò)展這個(gè)程序:

List<String> strs = new ArrayList<>();
List<Object> objs = strs;
objs.add(1);

String s = strs.get(0);

很明顯屑彻,這不合理!我們?cè)诘谝粋€(gè)位置存入了整型數(shù)值1顶吮,卻在取的時(shí)候?qū)⑺?dāng)成了字符串社牲。strs本身是一個(gè)字符串集合,用字符串接收讀取的數(shù)據(jù)的邏輯是合理的悴了。卻因?yàn)殄e(cuò)誤的類型轉(zhuǎn)換導(dǎo)致了不安全寫(xiě)入出現(xiàn)了運(yùn)行時(shí)類型轉(zhuǎn)換問(wèn)題膳沽,因此,Java語(yǔ)言不允許我們這樣做让禀。

大多數(shù)情況下,這種限制沒(méi)有問(wèn)題陨界⊙沧幔可是,在某些情況下菌瘪,這并不合理腮敌。看下面的例子:

interface List<T> {
    void addAll(List<T> t);
}

public void copy(List<String> from, List<Object> to) {
   to.addAll(from);
}

這是一個(gè)類型絕對(duì)安全的操作俏扩,但在Java語(yǔ)言中這依然是不允許的糜工。原因是,泛型是一個(gè)編譯期特性录淡,一旦指定捌木,運(yùn)行期類型就已經(jīng)固定了。換而言之嫉戚,泛型操作的類型是不可變的刨裆。這就意味著,List<String>并不是List<Object>的子類型彬檀。

為了允許正確執(zhí)行上述操作帆啃,Java語(yǔ)言增加了神奇的通配符操作魔法。

interface List<T> {
  void addAll(List<? extends T> t);
}

? extends T意味著集合中允許添加的類型不僅僅是T還包括T的子類窍帝,但這個(gè)集合中可以添加的類型在集合參數(shù)傳入addAll時(shí)就已經(jīng)確定了努潘。因此,這并不影響參數(shù)集合中可以存放的數(shù)據(jù)類型坤学,它帶來(lái)的一個(gè)直接影響就是addAll方法參數(shù)中終于可以傳入泛型參數(shù)是T或者T的子類的集合了疯坤,即上面的copy方法將不再報(bào)錯(cuò)。

這很有意思拥峦,在使用通配符之前我們并不能傳入類型參數(shù)為子類型的集合贴膘。使用通配符之后,居然可以了略号!這個(gè)特性在C#被稱之為協(xié)變(covariant)刑峡。

協(xié)變這個(gè)詞來(lái)源于類型之間的綁定洋闽。以集合為例,假設(shè)有兩個(gè)集合L1突梦、L2分別綁定數(shù)據(jù)類型F诫舅、C,并且F宫患、C之間存在著父子關(guān)系刊懈,即F、C之間存在著一種安全的從C->F的隱式轉(zhuǎn)換關(guān)系娃闲。那么虚汛,集合L1和L2之間是否也存在著L2->L1的轉(zhuǎn)換關(guān)系呢?這就牽扯到了原始類型轉(zhuǎn)換到綁定類型的集合之間的轉(zhuǎn)換映射關(guān)系皇帮,我們稱之為“可變性”卷哩。如果原始類型轉(zhuǎn)換和綁定類型之間轉(zhuǎn)換的方向相同,就稱之為“協(xié)變”属拾。

用一句話總結(jié)協(xié)變:如果綁定對(duì)象和原始對(duì)象之間存在著相同方向的轉(zhuǎn)換關(guān)系将谊,即稱之為協(xié)變

PS:以上關(guān)于協(xié)變的概念來(lái)自筆者的總結(jié)渐白,更嚴(yán)謹(jǐn)?shù)母拍钫?qǐng)參考C#官方文檔尊浓。

文章開(kāi)頭我們將不可變泛型通過(guò)通配符使其成為了可變泛型參數(shù),現(xiàn)在我們知道這種行為叫做協(xié)變纯衍。很明顯栋齿,協(xié)變轉(zhuǎn)換中寫(xiě)入是不安全的。因此襟诸,協(xié)變行為僅僅用于讀取褒颈。如果需要寫(xiě)入怎么辦呢?這就牽扯到了另外一個(gè)概念逆變(contravariance)励堡。

逆變協(xié)變恰恰相反谷丸,即如果F、C之間存在著父子轉(zhuǎn)換關(guān)系应结,L1刨疼、L2之間存在著從L1->L2的轉(zhuǎn)換關(guān)系。其綁定對(duì)象的轉(zhuǎn)換關(guān)系與原始對(duì)象的轉(zhuǎn)換關(guān)系恰好相反鹅龄。Java語(yǔ)言使用關(guān)鍵字super(揩慕?super List)實(shí)現(xiàn)逆變

舉個(gè)例子:假設(shè)有一個(gè)集合List<? super String>扮休,你將可以安全地使用add(String)或set(Int迎卤,String)方法。但你不能通過(guò)get(Int)返回String對(duì)象玷坠,因?yàn)槟銦o(wú)法確定返回的對(duì)象是否是String類型蜗搔,你最終只能得到Object劲藐。

因此,我們認(rèn)為樟凄,逆變可以安全地寫(xiě)入數(shù)據(jù)聘芜,但并不能安全地讀取,即最終不能獲取具體的對(duì)象數(shù)據(jù)類型缝龄。

為了簡(jiǎn)化理解汰现,我們引入官方文檔中 Joshua Bloch

Joshua Bloch calls those objects you only read from Producers, and those you only write to Consumers. He recommends: "For maximum flexibility, use wildcard types on input parameters that represent producers or consumers"說(shuō)的一句話。

Joshua Bloch是Java集合框架的創(chuàng)始人叔壤,他把那些只能讀取的對(duì)象叫做生產(chǎn)者瞎饲;只能寫(xiě)入的對(duì)象叫做消費(fèi)者。為了保證最大靈活性炼绘,他推薦在那些代表了生產(chǎn)者和消費(fèi)者的輸入?yún)?shù)上使用通配符指定泛型企软。

相對(duì)于Java的通配符,Kotlin語(yǔ)言針對(duì)協(xié)變逆變引入兩個(gè)新的關(guān)鍵詞outin饭望。

out用于協(xié)變,是只讀的形庭,屬于生產(chǎn)者铅辞,即用在方法的返回值位置。而in用于逆變萨醒,是只寫(xiě)的斟珊,屬于消費(fèi)者,即用在方法的參數(shù)位置富纸。

用英文簡(jiǎn)記為:POCI = Producer Out , Consumer In囤踩。

如果一個(gè)類中只有生產(chǎn)者,我們就可以在類頭使用out聲明該類是對(duì)泛型參數(shù)T協(xié)變的:

interface Link<out T> {
    fun node(): T
}

同樣地晓褪,如果一個(gè)類中只有消費(fèi)者堵漱,我們就可以在類頭使用in聲明該類是對(duì)泛型參數(shù)T逆變的:

interface Repo<in T> {
    fun add(t: T)
}

out等價(jià)于Java端的? extends List通配符,而in等價(jià)于Java端的? super List通配符涣仿。因此勤庐,類似下面的轉(zhuǎn)換是合理的:

interface Link<out T> {
    fun node(): T
}

fun f1(linkStr: Link<String>) {
    // 這是一個(gè)合理的協(xié)變轉(zhuǎn)換
    val linkAny: Link<Any> = linkStr
}

interface Repo<in T> {
    fun add(t: T)
}

fun f2(repoAny: Repo<Any>) {
    // 這是一個(gè)合理的逆變轉(zhuǎn)換
    val repoStr: Repo<String> = repoAny
}

小結(jié):協(xié)變和逆變

協(xié)變逆變對(duì)于Java程序員來(lái)說(shuō)是一個(gè)全新的概念,為了便于理解好港,我用一個(gè)表格做一個(gè)簡(jiǎn)單的總結(jié):

- 協(xié)變 逆變
關(guān)鍵字 out in
讀寫(xiě) 只讀 可寫(xiě)
位置 返回值 參數(shù)
角色 生產(chǎn)者 消費(fèi)者

類型投影

在上面的例子中愉镰,我們直接在類體聲明了泛型參數(shù)的協(xié)變或逆變類型。在這種情況下钧汹,就嚴(yán)格限制了該類中只允許出現(xiàn)該泛型參數(shù)的消費(fèi)者或者生產(chǎn)者丈探。很顯然,這種場(chǎng)景并不多見(jiàn)拔莱,大多數(shù)情況下碗降,一個(gè)類中既存在著消費(fèi)者又存在著生產(chǎn)者隘竭。為了適應(yīng)這種場(chǎng)景,我們可以將協(xié)變或逆變聲明寫(xiě)在方法參數(shù)中遗锣。Kotlin官方將這種方式叫做 類型投影(Type Projection)货裹。

這里我們直接使用官方文檔的例子:

class Array<T>(val size: Int) {
    fun get(index: Int): T { /* ... */ }
    fun set(index: Int, value: T) { /* ... */ }
}

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 

// 由于泛型參數(shù)的不變性,這里將出現(xiàn)問(wèn)題
copy(ints, any) 

很明顯精偿,我們希望from參數(shù)可以接收元素為Any或其子類的任意元素弧圆,但我們并不希望修改from,以防止出現(xiàn)類似文章開(kāi)頭的問(wèn)題笔咽。因此搔预,我們可以在from參數(shù)中添加out修飾,使其協(xié)變:

fun copy(from: Array<out Any>, to: Array<Any>) {
}

一旦添加out修飾符叶组,你就會(huì)發(fā)現(xiàn)拯田,當(dāng)你嘗試調(diào)用set方法的時(shí)候,編譯器將會(huì)提示你在out修飾的情況下禁止調(diào)用該方法甩十。

注:Java語(yǔ)言在使用”協(xié)變“的情況下船庇,from參數(shù)依然可以調(diào)用set方法。從這里可以看出侣监,Kotlin語(yǔ)言在泛型安全控制上比Java更加精細(xì)鸭轮。

星號(hào)投影

除了上述明確的類型投影方式之外,還有一種非常特殊的投影方式橄霉,稱之為星號(hào)投影(star projection)窃爷。

在某些情況下,我們并不知道具體的類型參數(shù)信息姓蜂。為了適應(yīng)這種情況按厘,Java語(yǔ)言中我們會(huì)直接忽略掉類型參數(shù):

class Box<T> {
     public void unPack(T t) {
          ...
     }
}

// 在不確定類型參數(shù)的情況下,我們會(huì)這樣做
Box box = new Box();

在Kotlin語(yǔ)言中钱慢,我們使用星號(hào)對(duì)這種情況進(jìn)行處理逮京。因?yàn)椋琄otlin針對(duì)泛型有嚴(yán)格的讀寫(xiě)區(qū)分束莫。同樣地造虏,使用*號(hào)將限制泛型接口的讀寫(xiě)操作:

  • Foo<out T: TUpper>,這種情況下麦箍,T是協(xié)變類型參數(shù)漓藕,上邊界是TUpper。Foo<*>等價(jià)于Foo<out TUpper>挟裂,這意味著你可以安全地從Foo<*>讀取TUpper類型享钞。
  • Foo<in T>,在這種情況下,T是逆變類型參數(shù)栗竖,下邊界是T暑脆。Foo<*>等價(jià)于Foo<in Nothing>,這意味著在T未知的情況下狐肢,你將無(wú)法安全寫(xiě)入Foo<*>添吗。
  • Foo<T: TUpper>,在這種情況下份名,T是不可變的碟联。Foo<*>等價(jià)于你可以使用Foo<out TUpper>安全讀取值,寫(xiě)入等價(jià)于Foo<in Nothing>僵腺,即無(wú)法安全寫(xiě)入鲤孵。

泛型約束

在泛型約束的控制上,Kotlin語(yǔ)言相對(duì)于Java也技高一籌辰如。在大多數(shù)情況下普监,泛型約束需要指定一個(gè)上邊界。這同Java一樣琉兜,Kotlin使用冒號(hào)代替extends:

fun <T: Animal> catch(t: T) {}

在使用Java的時(shí)候凯正,經(jīng)常碰到這樣一個(gè)需求。我希望泛型參數(shù)可以約束必須同時(shí)實(shí)現(xiàn)兩個(gè)接口豌蟋,但遺憾的是Java語(yǔ)言并沒(méi)有給予支持廊散。令人驚喜的是,Kotlin語(yǔ)言對(duì)這種場(chǎng)景給出了自己的實(shí)現(xiàn):

fun <T> swap(first: List<T>, second: List<T>) where T: CharSequence, 
                                                    T: Comparable<T> {
    
} 

可以看到夺饲,Kotlin語(yǔ)言使用where關(guān)鍵字控制泛型約束存在多個(gè)上邊界的情況,此處應(yīng)該給Kotlin鼓掌施符。

總結(jié)

Kotlin語(yǔ)言使用協(xié)變逆變來(lái)規(guī)范可變泛型操作往声,out關(guān)鍵字用于協(xié)變,代表生產(chǎn)者戳吝。in關(guān)鍵字用于逆變浩销,代表消費(fèi)者。out和in同樣可以用于方法參數(shù)的泛型聲明中听哭,這稱之為類型投影慢洋。在針對(duì)泛型類型約束的處理上,Kotlin增加了多個(gè)上邊界的支持陆盘。

Kotlin語(yǔ)言最初是希望成為一門(mén)編譯速度比Scala更快的JVM編程語(yǔ)言普筹!為了更好地設(shè)計(jì)泛型,我們看到它從C#中引入了協(xié)變逆變的概念隘马。這一次太防,我想,它至少同時(shí)站在了Scala和C#的肩膀上酸员。


我是歐陽(yáng)鋒蜒车,關(guān)于逆變協(xié)變的故事讳嘱,我還沒(méi)有說(shuō)完...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市酿愧,隨后出現(xiàn)的幾起案子沥潭,更是在濱河造成了極大的恐慌,老刑警劉巖嬉挡,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钝鸽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡棘伴,警方通過(guò)查閱死者的電腦和手機(jī)寞埠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)焊夸,“玉大人仁连,你說(shuō)我怎么就攤上這事≮逅耄” “怎么了饭冬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)揪阶。 經(jīng)常有香客問(wèn)我昌抠,道長(zhǎng),這世上最難降的妖魔是什么鲁僚? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任炊苫,我火速辦了婚禮,結(jié)果婚禮上冰沙,老公的妹妹穿的比我還像新娘侨艾。我一直安慰自己,他們只是感情好拓挥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布唠梨。 她就那樣靜靜地躺著,像睡著了一般侥啤。 火紅的嫁衣襯著肌膚如雪当叭。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天盖灸,我揣著相機(jī)與錄音蚁鳖,去河邊找鬼。 笑死赁炎,一個(gè)胖子當(dāng)著我的面吹牛才睹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼琅攘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼垮庐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起坞琴,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤哨查,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后剧辐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體寒亥,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年荧关,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了溉奕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忍啤,死狀恐怖加勤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情同波,我是刑警寧澤鳄梅,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站未檩,受9級(jí)特大地震影響戴尸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冤狡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一孙蒙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悲雳,春花似錦挎峦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)翅阵。三九已至歪玲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掷匠,已是汗流浹背滥崩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留讹语,地道東北人钙皮。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親短条。 傳聞我的和親對(duì)象是個(gè)殘疾皇子导匣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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

  • 前言 人生苦多,快來(lái) Kotlin 茸时,快速學(xué)習(xí)Kotlin贡定! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,146評(píng)論 9 118
  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分,“神秘”的通配符渠牲,讓我看了幾遍《Java...
    珞澤珈群閱讀 7,770評(píng)論 12 51
  • 寫(xiě)在開(kāi)頭:本人打算開(kāi)始寫(xiě)一個(gè)Kotlin系列的教程旋炒,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,421評(píng)論 1 3
  • 轉(zhuǎn)載文章签杈,出處: https://blog.kotliner.cn/2017/06/26/kotlin-gener...
    _10_01_閱讀 683評(píng)論 0 0
  • 【泡泡/薇薇】20170525學(xué)習(xí)力踐行D10 今天看《教出樂(lè)觀的孩子》第5章瘫镇,怎么批評(píng)孩子或當(dāng)孩子面批評(píng)自己,我...
    蘭_8d1a閱讀 138評(píng)論 0 0