val b = a?: 0,a 是 Double 類型,那 b 是什麼類型?
前面有朋友看了我的文章之後,表示都不敢用 Kotlin 了。這事兒要辯證的看待,Java 坑那麼多,你還不是照樣用么?而 Kotlin 本身坑很少,大多數都是為了照顧 Java 而出現的一些有意思的問題,對於這些問題的深挖可以讓我們看得更多,想得更深,了解得更多,然則與坑斗,其樂無窮也。
本文基於 Kotlin 1.1.4。
垃圾知乎編輯器,我還是貼公眾號地址吧:val b = a?: 0,a 是 Double 類型,那 b 是什麼類型?
文章貼上來,圖就沒了。
---------------------
1. 數值類型的推導
我們的標題其實已經說得很清楚了,我把完整的代碼貼出來:
var a: Double? = nulln val b = a?: 0n
問題就是,請問b的類型。
這個問題看上去似乎並沒有什麼難度,在 Kotlin 當中,所有數值類型都是Number的子類,也就是說Double和Int都是它的子類,這種情況下,b的類型應該毫無疑問的是Number。
真的是這樣嗎?
很遺憾,IntelliJ 告訴我們,b的類型是Any。
注意,這裡是變數b的類型推導,b指向的內存的類型取決於真實的內存數據。
為什麼會這樣?難道我發現了一個編譯器的 Bug?
2. 普通類繼承的推導
有了這個發現,我倒要試試看,是不是所有類的推導都會直接推導為Any。
先聲明下面的類型:
interface Parentnclass ChildA: Parentnclass ChildB: Parentn
看下我們的測試代碼:
var childA: ChildA? = nulln val childOrParent = childA?: ChildB()n
有了前面的經驗,我就有點兒擔心 Kotlin 會把childOrParent這個變數推導成Any了,不過結果卻並不是這樣:
推導的類型是Parent,是合乎情理的。
3. 位元組碼分析
面對這個類型的結果差異,我瞬間想到了看看位元組碼,
val b = a?: 0n
對應的位元組碼:
LINENUMBER 8 L2n L3n ICONST_0n INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;n L4n ASTORE 2n
注意,此處為了阻止編譯器優化位元組碼,我們需要對變數b有操作,例如在後面添加println(b),否則位元組碼可能與文中有出入
而:
val childOrParent = childA?: childBn
對應的位元組碼:
LINENUMBER 21 L10n L11n ALOAD 4n CHECKCAST com/bennyhuo/Parentn L12n ASTORE 5n
為啥前面就沒有CHECKCAST呢?位元組碼是生成的結果,不是類型推導的原因,通過這個結果我們只能推測到類型推導的結果在第一個那裡就被推導為Any了。
當然,如果你願意,你也可以明確指定b的類型:
val b: Number = a?: 0n
這時候位元組碼也會變成:
LINENUMBER 8 L2n L3n ICONST_0n INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;n CHECKCAST java/lang/Number //注意,這裡有強轉啦n L4n ASTORE 2n
儘管這樣會達到我們的目的,但並不能解釋前面我們遇到的問題。
4. 幾個猜想
最近在看《柔軟的宇宙》,科學家們在發現問題的時候總是先來個猜想,然後想辦法通過實踐來證明。前面被數值的基本類型的映射坑了太多把了,所以我想一定是因為後面的那個0被識別成了 Java 基本類型的int。
那麼我們想辦法把這個這個0變成裝箱類型會怎麼樣呢?
var a: Double? = nulln val b = a?: "0".toInt()n
結果,b仍然是Any。換句話說,b的類型推導實際上與 Java 的基本類型沒有任何關係。
難道只是Number的問題? 這時候我突然想到前面剛剛被坑過的AtomicInteger,試了一下:
var a: Double? = nulln val b = a?: AtomicInteger(0)n
結果再一次打臉,這次b的類型居然就是Number了。
想來想去,這可能就是 Kotlin 編譯器在求兩個類型的公共父類的時候有些奇怪的東西我沒有 GET 到,那這個奇怪的東西究竟是什麼呢?
5. Google 不到的東西,只有源碼會告訴我
吃螃蟹,就得做好為別人栽樹的思想準備。像 Kotlin 這樣的新語言,很多時候 Google 也不會告訴我們答案,這也是很多人望而生卻的原因。
為了搞清楚編譯器是怎麼做的,我們需要把Kotlin的源碼拖下來,編譯運行,打斷點調試,找到一個叫做TypeBoundsImpl的類,這個類實際上就是負責計算公共父類的,有興趣的朋友也可以自行研讀一下它的computeValues方法,我們在這裡只簡單介紹一下公共父類的計算方法:
Int和Double除了有個公共父類Number之外,還都實現了Comparable介面,所以在計算公共父類的時候,先把他們都羅列出來,然後最終變成了求Number和Comparable的公共父類,那麼自然就是Any了。
而我們再來看看另外的情形:
AtomicInteger和Double只有一個公共父類Number,不像前面還有個公共父介面Comparable,這樣問題就簡單了,直接把b的類型推導成Number而不是Any。
那麼對於我們自定義的那一組例子,結果也類似:
不過我們稍加修改,結果就又是一番情景了:
interface Parentnclass ChildA: Parent, Serializablenclass ChildB: Parent, Serializablen
這下你能想明白是為啥了吧?
同樣的,在 YouTrack 上面還有這樣的一個 Issue,Common super type for different enum items is Any instead of common declared super type,原因也是類似的。
6. 再問個為什麼
這裡有人肯定還是覺得奇怪,因為Int和Double的父類和介面都一樣呀,為啥推導的結果不是Number呢?
顯然這裡 Kotlin 的開發者也是很糾結的,既然可以推導成Number,那麼推導成Comparable可以不可以呢?換句話說,對於兩個類型有兩個以上沒有繼承關係的公共父類(介面)的情形,推導的結果會有歧義,可能也是為了消除這種歧義,Kotlin 編譯器採用了一種比較穩妥的方式來處理,不偏袒任何一方,直接將推導的結果定為Any也是合情合理的。
這時候如果你明確知道自己想要什麼,例如前面的例子,我們想要b的類型是Number而不是Comparable,那麼只需要顯式的為b聲明類型就可以了。
7. 看看其他語言怎麼做
對於類似的情形,C# 直接報錯:
即便C和D有公共父類, C# 仍然需要你明確他們的類型,大家可以參考 StackOverflow 上面的討論:No implicit conversion when using conditional operator
當然,如果能像 Scala 那樣推導,也是不錯滴:
但不是所有的類都有 Scala 的交集類型(intersection type )。
歡迎關注微信公眾號 Kotlin
推薦閱讀:
※Kotlin雜談(四) - Coroutines(二): Channel
TAG:Kotlin |