服务端相关 / Kotlin 中的 typealias(别名)

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 都可以编译成相同的字节码!
所以,问题来了,你怎么去选择你需要那一个?它们之间有什么不同?这里列举了一系列有关 typealiasimport 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

此外还需要注意的是:

  • 类型别名可以具有可见性修饰符,如 internalprivate ,而它访问的范围是整个文件;
  • 如果您从已经自动导入的包中导入类,例如 kotlin.*kotlin.collections* ,那么您必须通过该名称引用它。 例如,如果您要将 import kotlin.String 写为 RegularExpression ,则 String 的用法将引用 java.lang.String .

6. 总结

到这里,有关 Kotlin 中的 typealias 的别名就阐述完毕了。相信你对 typealias 的认识更深了,并且知道它和 import as 之间区别以及分别使用场景。下面有几点结论总结需要理解和记忆:

  • 类型别名 (typealias) 不会创建新的类型,只是给现有类型取了另一个名称而已;
  • typealias 实质原理,大部分情况下是在编译时期采用了逐字替换的扩展方式,还原成真正的底层类型;但是不是完全是这样的,正如本文例子提到的那样;
  • typealias 只能定义在顶层位置,不能被内嵌在类、接口、函数等内部。