Kotlin 中的 typealias 別名
今天一起來研究一下 Kotlin 中一個(gè)特殊的語法 typealias (別名),關(guān)于 typealias 類型別名,可能有的 Kotlin 開發(fā)人員接觸到過,有的還沒有碰到過。接觸過的,可能也用得不多,不知道如何更好地使用它。本篇文章會(huì)闡述了什么是類型別名、類型別名的使用場景、類型別名的實(shí)質(zhì)原理、類型別名和 import as 對比以及類型別名中需要注意的坑??赐赀@篇文章,仿佛打開 kotlin 中的又一個(gè)新世界,你將會(huì)很神奇發(fā)現(xiàn)一個(gè)小小 typealias 卻如此強(qiáng)大,深入實(shí)質(zhì)原理你又會(huì)發(fā)現(xiàn)原來也挺簡單的,但是無不被 kotlin 這門語言設(shè)計(jì)思想所折服,使用它可以大大簡化代碼以及提升代碼的可讀性。那么對于 Kotlin 的初學(xué)者以及正在使用 kotlin 開發(fā)的你來說,它可能會(huì)對你很有幫助。
1. 為什么需要 typealias
我們在寫 Kotlin 可能會(huì)寫很多 lambda 表達(dá)式 (閉包),并把它作為函數(shù)參數(shù)傳遞,可能閉包類型基本都一樣。比如下面這段代碼:
interface RestaurantPatron {
fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}
可以看到 RestaurantPatron
接口中三個(gè)函數(shù)參數(shù)都是同一個(gè) lambda 表達(dá)式類型: Organization<(Currency, Coupon?) -> Sustenance>
,很多類型的代碼被擠在一起的時(shí)候,就很容易迷失在代碼的細(xì)節(jié)中,所以這樣聲明看起來不簡潔也不利于維護(hù),代碼可讀性下降。那么此時(shí)就需要 typealias 改變這一切,使用 typealias 就可以很好地優(yōu)化上面的場景,代碼如下:
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance> //typealias關(guān)鍵字聲明一個(gè)Restaurant別名
interface RestaurantPatron {
fun makeReservation(restaurant: Restaurant)//在后面函數(shù)參數(shù)類型定義中就可以使用這個(gè)別名
fun visit(restaurant: Restaurant)
fun complainAbout(restaurant: Restaurant)
}
優(yōu)化后的代碼看上去容易理解多,而且看到它時(shí),在代碼中的疑惑也會(huì)少很多。此外還能很好避免了在整個(gè) RestaurantPatron 接口中大量重復(fù)的類型,那么就不用每次去寫 Organization<(Currency, Coupon?) -> Sustenance>
,我們僅僅只有一種類型 Restaurant
即可。這樣也就意味著如果我們需要修改這種復(fù)雜類型也是很方便的。例如,如果我們需要將原來的 Organization<(Currency, Coupon?) -> Sustenance>
修改為 Organization<(Currency, Coupon?) -> Meal>
,我們僅僅只需要改變一處即可,而不是像原來那樣定義需要修改三個(gè)地方。
2. 什么是 typealias
我們已經(jīng)了解過如何簡單地去聲明一個(gè)類型 typealias 以及為什么需要 typealias,那么接下來會(huì)一起研究 typealias 原理是什么。
當(dāng)處理類型別名的時(shí)候,我們有兩個(gè)類型需要去思考:
- 別名 (alias)
- 底層類型 (underlying type)
可以把上述例子理解為本身是一個(gè)別名 (如 UserId), 或者包含別名 (如 List) 的縮寫類型,當(dāng) Kotlin 編譯器編譯代碼時(shí),所有使用到的相應(yīng)縮寫類型將會(huì)擴(kuò)展成原來的全類型。一起看個(gè)完整的例子
class UniqueIdentifier(val value: Int)
typealias UserId = UniqueIdentifier
val firstUserId: UserId = UserId(0)
當(dāng)編譯器處理上述代碼時(shí),所有對 UserId 的引用都會(huì)擴(kuò)展成 UniqueIdentifier,換句話說,在編譯期間,編譯器大部分是做了類似于在代碼中搜索別名 (UserId) 所有用到的地方,然后將代碼中用到的地方逐字地將其別名替換成全稱類型名 (UniqueIdentifier) 的工作。
可能已經(jīng)注意到我說的是大部分場景。 這是因?yàn)槌酥鹱痔鎿Q原理,有一些特殊情況下 Kotlin 不完全是通過逐字替換原理來實(shí)現(xiàn)。但是大部分場景下,我們只需記住 typealias 是基于逐字替換原理即可。順便說一下,IntelliJ IDEA 和 AndroidStudio 已經(jīng)很智能了,它門會(huì)對類型別名有一些很好的支持。例如,可以在代碼中看到別名和底層類型:
并且能夠給出很好文檔和代碼提示
總結(jié)下,實(shí)際上 typealias 別名并不會(huì)去真的創(chuàng)建一個(gè)新的類型,而僅僅是取了一個(gè)別名,然后在編譯器編譯期間,把別名又類似逐字替換原理換回實(shí)際的類型,這樣就能保證編譯正常也不會(huì)產(chǎn)生新的類型。
3. 如何使用 typealias
使用 typealias 別名非常簡單,只需要使用 typealias 關(guān)鍵字在代碼頂層聲明即可,然后只要在后面需要用到對應(yīng)別名聲明對應(yīng)的類型即可。這里用 Kotlin 集合中的 ArrayList 源碼舉例,它實(shí)際上就是一個(gè) java.lang.ArrayList 的一個(gè)別名。
@SinceKotlin("1.1") public actual typealias ArrayList<E> = java.util.ArrayList<E>
@SinceKotlin("1.1") public actual typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
@SinceKotlin("1.1") public actual typealias HashMap<K, V> = java.util.HashMap<K, V>
@SinceKotlin("1.1") public actual typealias LinkedHashSet<E> = java.util.LinkedHashSet<E>
@SinceKotlin("1.1") public actual typealias HashSet<E> = java.util.HashSet<E>
4. typealias 的使用場景
4.1 typealias 用于多數(shù)通用場景
// Classes and Interfaces (類和接口)
typealias RegularExpression = String
typealias IntentData = Parcelable
// Nullable types (可空類型)
typealias MaybeString = String?
// Generics with Type Parameters (類型參數(shù)泛型)
typealias MultivaluedMap<K, V> = HashMap<K, List<V>>
typealias Lookup<T> = HashMap<T, T>
// Generics with Concrete Type Arguments (混合類型參數(shù)泛型)
typealias Users = ArrayList<User>
// Type Projections (類型投影)
typealias Strings = Array<out String>
typealias OutArray<T> = Array<out T>
typealias AnyIterable = Iterable<*>
// Objects (including Companion Objects) (對象,包括伴生對象)
typealias RegexUtil = Regex.Companion
// Function Types (函數(shù)類型)
typealias ClickHandler = (View) -> Unit
// Lambda with Receiver (帶接收者的Lambda)
typealias IntentInitializer = Intent.() -> Unit
// Nested Classes and Interfaces (嵌套類和接口)
typealias NotificationBuilder = NotificationCompat.Builder
typealias OnPermissionResult = ActivityCompat.OnRequestPermissionsResultCallback
// Enums (枚舉類)
typealias Direction = kotlin.io.FileWalkDirection
// (but you cannot alias a single enum *entry*)
// Annotation (注解)
typealias Multifile = JvmMultifileClass
4.2 typealias 用于構(gòu)造器函數(shù)特殊場景
如果底層類型有一個(gè)構(gòu)造器,那么它的類型別名也可以使用。甚至可以在一個(gè)可空類型的別名上調(diào)用構(gòu)造函數(shù)!
class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?
//使用別名來構(gòu)造對象
val member = MaybeTeamMember("Miguel")
// 以上代碼不會(huì)是逐字?jǐn)U展成如下無法編譯的代碼
val member = TeamMember?("Miguel")
// 而是轉(zhuǎn)換成如下代碼
val member = TeamMember("Miguel")
所以可以看到編譯時(shí)的擴(kuò)展并不總是逐字?jǐn)U展的,在這個(gè)例子中就是很有效的說明。
如果底層類型本身就沒有構(gòu)造器 (例如接口或者類型投影),自然地你也不可能通過別名來調(diào)用構(gòu)造器。
4.3 typealias 用于伴生對象 compaion object
可以通過含有伴生對象類的別名來調(diào)用該類的伴生對象中的屬性和方法。即使底層類型具有指定的具體類型參數(shù),也是如此。一起看下如下代碼:
class Container<T>(var item: T) {
companion object {
const val classVersion = 5
}
}
// 注意此處的String是具體的參數(shù)類型
typealias BoxedString = Container<String>
// 通過別名獲取伴侶對象的屬性
val version = BoxedString.classVersion
// 這行代碼不會(huì)是擴(kuò)展成如下無法編譯的代碼
val version = Container<String>.classVersion
// 它是會(huì)在即將進(jìn)入編譯期會(huì)擴(kuò)展成如下代碼
val version = Container.classVersio
5. typealias 與 import as 的區(qū)別
其實(shí)在 Kotlin 中還有一個(gè)非常類似于類型別名 (type lias) 的概念,叫做 Import As. 它允許你給一個(gè)類型、函數(shù)或者屬性一個(gè)新的命名,然后你可以把它導(dǎo)入到一個(gè)文件中。例如:
import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder
在這種情況下,我們從 NotificationCompat 導(dǎo)入了 Builder 類,但是在當(dāng)前文件中,它將以名稱 NotificationBuilder 的形式出現(xiàn)。
你是否遇到過需要導(dǎo)入兩個(gè)同名的類的情況?
如果有,那么你可以想象一下 Import As 將會(huì)帶來巨大的幫助,因?yàn)樗馕吨悴恍枰ハ薅ㄟ@些類中某個(gè)類。
例如,查看以下 Java 代碼,我們可以將數(shù)據(jù)庫模型中的 User 轉(zhuǎn)換為 service 模型的 User。
package com.example.app.service;
import com.example.app.model.User;
public class UserService {
public User translateUser(com.example.app.database.User user) {
return new User(user.getFirst() + " " + user.getLast());
}
}
由于此代碼處理兩個(gè)不同的類,但是這兩個(gè)類都叫 User,因此我們無法將它們兩者都同時(shí)導(dǎo)入。相反,我們只能將其中某個(gè)以類名 + 包名全稱使用 User。
利用 Kotlin 中的 Import As, 就不需要以全稱類名的形式使用,僅僅只需要給它另一個(gè)命名,然后去導(dǎo)入它即可。
package com.example.app.service
import com.example.app.model.User
import com.example.app.database.User as DatabaseUser
class UserService {
fun translateUser(user: DatabaseUser): User =
User("${user.first} ${user.last}")
}
此時(shí)的你,或許想知道,類型別名 (type alias) 和 Import As 之間的區(qū)別?畢竟,您還可以用 typealias 消除 User 引用的沖突,如下所示:
package com.example.app.service
import com.example.app.model.User
typealias DatabaseUser = com.example.app.database.User
class UserService {
fun translateUser(user: DatabaseUser): User =
User("${user.first} ${user.last}")
}
沒錯(cuò),事實(shí)上,除了元數(shù)據(jù) (metadata) 之外,這兩個(gè)版本的 UserService 都可以編譯成相同的字節(jié)碼!
所以,問題來了,你怎么去選擇你需要那一個(gè)?它們之間有什么不同?這里列舉了一系列有關(guān) typealias 和 import as 各自支持特性情況如下:
目標(biāo)對象 | Typealias 別名 | import as |
---|---|---|
Interfaces and Classes (接口和類) | YES | NO |
Nullable Types (可空類型) | YES | NO |
Generics with Type Params (泛型類型參數(shù)) | YES | NO |
Function Types (函數(shù)類型) | YES | NO |
Enum (枚舉類型) | YES | YES |
Enum Member (枚舉成員) | NO | YES |
object (對象表達(dá)式) | YES | YES |
object Function (對象表達(dá)式函數(shù)) | NO | YES |
object Properties (對象表達(dá)式屬性) | NO | YES |
此外還需要注意的是:
- 類型別名可以具有可見性修飾符,如
internal
和private
,而它訪問的范圍是整個(gè)文件; - 如果您從已經(jīng)自動(dòng)導(dǎo)入的包中導(dǎo)入類,例如
kotlin.*
或kotlin.collections*
,那么您必須通過該名稱引用它。 例如,如果您要將import kotlin.String
寫為RegularExpression
,則String
的用法將引用java.lang.String
.
6. 總結(jié)
到這里,有關(guān) Kotlin 中的 typealias 的別名就闡述完畢了。相信你對 typealias 的認(rèn)識(shí)更深了,并且知道它和 import as 之間區(qū)別以及分別使用場景。下面有幾點(diǎn)結(jié)論總結(jié)需要理解和記憶:
- 類型別名 (typealias) 不會(huì)創(chuàng)建新的類型,只是給現(xiàn)有類型取了另一個(gè)名稱而已;
- typealias 實(shí)質(zhì)原理,大部分情況下是在編譯時(shí)期采用了逐字替換的擴(kuò)展方式,還原成真正的底層類型;但是不是完全是這樣的,正如本文例子提到的那樣;
- typealias 只能定義在頂層位置,不能被內(nèi)嵌在類、接口、函數(shù)等內(nèi)部。