Kotlin 中的 typealias 別名
今天一起來研究一下 Kotlin 中一個特殊的語法 typealias (別名),關于 typealias 類型別名,可能有的 Kotlin 開發人員接觸到過,有的還沒有碰到過。接觸過的,可能也用得不多,不知道如何更好地使用它。本篇文章會闡述了什么是類型別名、類型別名的使用場景、類型別名的實質原理、類型別名和 import as 對比以及類型別名中需要注意的坑。看完這篇文章,仿佛打開 kotlin 中的又一個新世界,你將會很神奇發現一個小小 typealias 卻如此強大,深入實質原理你又會發現原來也挺簡單的,但是無不被 kotlin 這門語言設計思想所折服,使用它可以大大簡化代碼以及提升代碼的可讀性。那么對于 Kotlin 的初學者以及正在使用 kotlin 開發的你來說,它可能會對你很有幫助。
1. 為什么需要 typealias
我們在寫 Kotlin 可能會寫很多 lambda 表達式 (閉包),并把它作為函數參數傳遞,可能閉包類型基本都一樣。比如下面這段代碼:
interface RestaurantPatron {
fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}
可以看到 RestaurantPatron
接口中三個函數參數都是同一個 lambda 表達式類型: Organization<(Currency, Coupon?) -> Sustenance>
,很多類型的代碼被擠在一起的時候,就很容易迷失在代碼的細節中,所以這樣聲明看起來不簡潔也不利于維護,代碼可讀性下降。那么此時就需要 typealias 改變這一切,使用 typealias 就可以很好地優化上面的場景,代碼如下:
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance> //typealias關鍵字聲明一個Restaurant別名
interface RestaurantPatron {
fun makeReservation(restaurant: Restaurant)//在后面函數參數類型定義中就可以使用這個別名
fun visit(restaurant: Restaurant)
fun complainAbout(restaurant: Restaurant)
}
優化后的代碼看上去容易理解多,而且看到它時,在代碼中的疑惑也會少很多。此外還能很好避免了在整個 RestaurantPatron 接口中大量重復的類型,那么就不用每次去寫 Organization<(Currency, Coupon?) -> Sustenance>
,我們僅僅只有一種類型 Restaurant
即可。這樣也就意味著如果我們需要修改這種復雜類型也是很方便的。例如,如果我們需要將原來的 Organization<(Currency, Coupon?) -> Sustenance>
修改為 Organization<(Currency, Coupon?) -> Meal>
,我們僅僅只需要改變一處即可,而不是像原來那樣定義需要修改三個地方。
2. 什么是 typealias
我們已經了解過如何簡單地去聲明一個類型 typealias 以及為什么需要 typealias,那么接下來會一起研究 typealias 原理是什么。
當處理類型別名的時候,我們有兩個類型需要去思考:
- 別名 (alias)
- 底層類型 (underlying type)
可以把上述例子理解為本身是一個別名 (如 UserId), 或者包含別名 (如 List) 的縮寫類型,當 Kotlin 編譯器編譯代碼時,所有使用到的相應縮寫類型將會擴展成原來的全類型。一起看個完整的例子
class UniqueIdentifier(val value: Int)
typealias UserId = UniqueIdentifier
val firstUserId: UserId = UserId(0)
當編譯器處理上述代碼時,所有對 UserId 的引用都會擴展成 UniqueIdentifier,換句話說,在編譯期間,編譯器大部分是做了類似于在代碼中搜索別名 (UserId) 所有用到的地方,然后將代碼中用到的地方逐字地將其別名替換成全稱類型名 (UniqueIdentifier) 的工作。
可能已經注意到我說的是大部分場景。 這是因為除了逐字替換原理,有一些特殊情況下 Kotlin 不完全是通過逐字替換原理來實現。但是大部分場景下,我們只需記住 typealias 是基于逐字替換原理即可。順便說一下,IntelliJ IDEA 和 AndroidStudio 已經很智能了,它門會對類型別名有一些很好的支持。例如,可以在代碼中看到別名和底層類型:
并且能夠給出很好文檔和代碼提示
總結下,實際上 typealias 別名并不會去真的創建一個新的類型,而僅僅是取了一個別名,然后在編譯器編譯期間,把別名又類似逐字替換原理換回實際的類型,這樣就能保證編譯正常也不會產生新的類型。
3. 如何使用 typealias
使用 typealias 別名非常簡單,只需要使用 typealias 關鍵字在代碼頂層聲明即可,然后只要在后面需要用到對應別名聲明對應的類型即可。這里用 Kotlin 集合中的 ArrayList 源碼舉例,它實際上就是一個 java.lang.ArrayList 的一個別名。
@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 用于多數通用場景
// Classes and Interfaces (類和接口)
typealias RegularExpression = String
typealias IntentData = Parcelable
// Nullable types (可空類型)
typealias MaybeString = String?
// Generics with Type Parameters (類型參數泛型)
typealias MultivaluedMap<K, V> = HashMap<K, List<V>>
typealias Lookup<T> = HashMap<T, T>
// Generics with Concrete Type Arguments (混合類型參數泛型)
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 (函數類型)
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 用于構造器函數特殊場景
如果底層類型有一個構造器,那么它的類型別名也可以使用。甚至可以在一個可空類型的別名上調用構造函數!
class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?
//使用別名來構造對象
val member = MaybeTeamMember("Miguel")
// 以上代碼不會是逐字擴展成如下無法編譯的代碼
val member = TeamMember?("Miguel")
// 而是轉換成如下代碼
val member = TeamMember("Miguel")
所以可以看到編譯時的擴展并不總是逐字擴展的,在這個例子中就是很有效的說明。
如果底層類型本身就沒有構造器 (例如接口或者類型投影),自然地你也不可能通過別名來調用構造器。
4.3 typealias 用于伴生對象 compaion object
可以通過含有伴生對象類的別名來調用該類的伴生對象中的屬性和方法。即使底層類型具有指定的具體類型參數,也是如此。一起看下如下代碼:
class Container<T>(var item: T) {
companion object {
const val classVersion = 5
}
}
// 注意此處的String是具體的參數類型
typealias BoxedString = Container<String>
// 通過別名獲取伴侶對象的屬性
val version = BoxedString.classVersion
// 這行代碼不會是擴展成如下無法編譯的代碼
val version = Container<String>.classVersion
// 它是會在即將進入編譯期會擴展成如下代碼
val version = Container.classVersio
5. typealias 與 import as 的區別
其實在 Kotlin 中還有一個非常類似于類型別名 (type lias) 的概念,叫做 Import As. 它允許你給一個類型、函數或者屬性一個新的命名,然后你可以把它導入到一個文件中。例如:
import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder
在這種情況下,我們從 NotificationCompat 導入了 Builder 類,但是在當前文件中,它將以名稱 NotificationBuilder 的形式出現。
你是否遇到過需要導入兩個同名的類的情況?
如果有,那么你可以想象一下 Import As 將會帶來巨大的幫助,因為它意味著你不需要去限定這些類中某個類。
例如,查看以下 Java 代碼,我們可以將數據庫模型中的 User 轉換為 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());
}
}
由于此代碼處理兩個不同的類,但是這兩個類都叫 User,因此我們無法將它們兩者都同時導入。相反,我們只能將其中某個以類名 + 包名全稱使用 User。
利用 Kotlin 中的 Import As, 就不需要以全稱類名的形式使用,僅僅只需要給它另一個命名,然后去導入它即可。
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}")
}
此時的你,或許想知道,類型別名 (type alias) 和 Import As 之間的區別?畢竟,您還可以用 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}")
}
沒錯,事實上,除了元數據 (metadata) 之外,這兩個版本的 UserService 都可以編譯成相同的字節碼!
所以,問題來了,你怎么去選擇你需要那一個?它們之間有什么不同?這里列舉了一系列有關 typealias 和 import as 各自支持特性情況如下:
目標對象 | Typealias 別名 | import as |
---|---|---|
Interfaces and Classes (接口和類) | YES | NO |
Nullable Types (可空類型) | YES | NO |
Generics with Type Params (泛型類型參數) | YES | NO |
Function Types (函數類型) | YES | NO |
Enum (枚舉類型) | YES | YES |
Enum Member (枚舉成員) | NO | YES |
object (對象表達式) | YES | YES |
object Function (對象表達式函數) | NO | YES |
object Properties (對象表達式屬性) | NO | YES |
此外還需要注意的是:
- 類型別名可以具有可見性修飾符,如
internal
和private
,而它訪問的范圍是整個文件; - 如果您從已經自動導入的包中導入類,例如
kotlin.*
或kotlin.collections*
,那么您必須通過該名稱引用它。 例如,如果您要將import kotlin.String
寫為RegularExpression
,則String
的用法將引用java.lang.String
.
6. 總結
到這里,有關 Kotlin 中的 typealias 的別名就闡述完畢了。相信你對 typealias 的認識更深了,并且知道它和 import as 之間區別以及分別使用場景。下面有幾點結論總結需要理解和記憶:
- 類型別名 (typealias) 不會創建新的類型,只是給現有類型取了另一個名稱而已;
- typealias 實質原理,大部分情況下是在編譯時期采用了逐字替換的擴展方式,還原成真正的底層類型;但是不是完全是這樣的,正如本文例子提到的那樣;
- typealias 只能定義在頂層位置,不能被內嵌在類、接口、函數等內部。