[译]有关Kotlin类型别名(typealias)你需要知道的一切

6,719

翻译说明:

原标题: All About Type Aliases in Kotlin

原文地址: typealias.com/guides/all-…

原文作者: Dave Leeds

你是否经历过像下面的对话?

希望你在现实生活中没有像这样的对话,但是这样情景可能会出现在你的代码中。

例如,看下这个代码:

interface RestaurantPatron {
    fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
    fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
    fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}

当你看到很多类型的代码被挤在一起的时候,你很容易迷失在代码的细节中。事实上,仅仅看这些函数的声明就感觉挺吓人的。

幸运的是,Kotlin为我们提供了一种简单的方法来将复杂类型简化成更具可读性的别名。

在这篇文章中:

  • 我们将学习关于类型别名的一切内容以及他们的工作原理。
  • 然后,我们将看看你可能会使用到关于它们的一些方法。
  • 然后,我们将会看下有关它们需要注意的点。
  • 最后,我们来看看一个类似的概念, Import As, 并看看它们之间的比较。

介绍Type Aliases(类型别名)

一旦我们为某个概念创造了一个术语,其实我们就没必要每次谈论到它的时候都要去描述一下这个概念,我们只需要使用这个术语就可以了! 所以让我们代码也去做类似事情吧。让我们来看看这个复杂的类型并给它一个命名。

针对上面的代码,我们将通过创建一个类型的别名来优化它:

typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance>

现在,在每个描述restaurant概念的地方,而不是每次都去写出 Organization<(Currency, Coupon?) -> Sustenance> 声明,而是可以像下面这样表达出 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>,我们仅仅只需要改变一处即可,而不是像原来那样定义需要修改三个地方。

typealias Restaurant = Organization<(Currency, Coupon?) -> Meal>

简单!

你或许会思考...

可读性

你可能会对自己说,“我不明白这是如何有助于代码的可读性的...,由于上述的示例中参数的名称已经明确表明了restaurant的概念,为什么我还需要一个Restaurant类型呢?难道我们不能使用具体的参数名称和抽象类型吗?”

是的,参数的名称确实它应该可以更具体地表示类型,但是我们上面的RestaurantPatron接口的别名版本仍然更具有可读性,并且也不容易受到侵入

然而,有些情况下是没有命名的,或者说他们没有一个确切类型名称,例如Lambda表达式的类型:

interface RestaurantService {
    var locator: (String, ZipCode) -> List<Organization<(Currency, Coupon?) -> Sustenance>>
}

在上面那段代码中,仍然在表示locator这个lambda表示式正在返回一个restaurant的列表,但是获取这些表示含义的信息唯一线索就是接口的名称。然而仅仅从locator函数类型中没有那么明确得到,因为冗长的类型定义已经失去了含义本质。

而下面的这个版本,只需要看一眼就能很容易理解:

interface RestaurantService {
    var locator: (String, ZipCode) -> List<Restaurant>
}

间接性

你或许会想,“等等,我需要更多地考虑类型别名吗?之前没有类型别名的时候,把确切真实的类型直接暴露在外部声明处,现在却要将他们隐藏在别名的后面”

当然,我们已经引入了一层间接寻址-有些被别名掩盖了具体类型细节。但是作为程序员,我们一直在做着隐藏命名背后的细节事情。例如:

  • 我们不会把具体常量数值9.8写到我们代码中,而是我们创建一个静态常量ACCELERATION_DUE_TO_GRAVITY,在代码中直接使用静态常量。
  • 我们不会把一个表达式 6.28 * radius 实现写在代码任何地方,而是把这个表达式放入到一个 circumference() 函数中去,然后在代码中去调用circumference() 函数

记住-如果我们需要去查看别名背后隐藏细节是什么,仅仅只需要在IDE中使用Command+Click即可。

继承性

或者你也许在想,"我为什么需要一个类型别名呢?我可以使用继承方式,来继承这个复杂类型" 如下所示:

class Restaurant : Organization<(Currency, Coupon?) -> Sustenance>()

没错,在这种情况下,你确实可以通过其详细的类型参数对 Organization 类进行子类化。事实上,你可能在Java中看到了这一点。

但是类型别名适用性很广,它也适用于你不能或通常不会去继承的类型。例如:

  • open 一些的类 例如:String,或者Java中的Optional<T>
  • Kotlin中的单例对象实例( object )。
  • 函数类型,例如: (Currency, Coupon?) -> Sustenance
  • 甚至函数接收者类型,例如: Currency.(Coupon?) -> Sustenance

在文章后面的部分,我们将更多地比较类型别名方法和继承方法。

理解类型别名(Type Aliases)

我们已经了解过如何简单地去声明一个类型别名。现在让我们放大一些,这样我们就可以了解创建时发生的原理!

当处理类型别名的时候,我们有两个类型需要去思考:

  • 别名(alias)
  • 底层类型(underlying type)

据说它本身是一个别名(如UserId),或者包含别名(如List<UserId>)的缩写类型

当Kotlin编译器编译您的代码时,所有使用到的相应缩写类型将会扩展成原来的全类型。让我们看一个更为完整例子。

class UniqueIdentifier(val value: Int)

typealias UserId = UniqueIdentifier

val firstUserId: UserId = UserId(0)

当编译器处理上述代码时,所有对 UserId 的引用都会扩展到 UniqueIdentifier

换句话说,在扩展期间,编译器大概做了类似于在代码中搜索别名(UserId)所有用到的地方,然后将代码中用到的地方逐字地将其别名替换成全称类型名(UniqueIdentifier)的工作。

你可能已经注意到我使用了“大部分”和“大概”等字样。 这是因为,虽然这是我们理解类型别名的一个好的起点,但有一些情况下Kotlin不完全是通过逐字替换原理来实现。 我们将马上阐述这些内容! 现在,我们只需记住这个逐字替换原理通常是有效的。

顺便说一下,如果你使用IntelliJ IDEA,你会很高兴发现IDE对类型别名有一些很好的支持。例如,您可以在代码中看到别名和底层类型:

并且可以快速查看声明文档:

类型别名和类型安全

现在我们已经了解了类型别名的基础知识,下面我们来探讨另一个例子。这一个使用多个别名例子:

typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier

interface Store {
    fun purchase(user: UserId, product: ProductId): Receipt
}

一旦我们拿到了我们 Store 的一个实例,我们可以进行购买:

val receipt = store.purchase(productId, userId)

此时,你是否注意到什么了?

我们意外地把我们的调用参数顺序弄反了! userId应该是第一个参数,而productId应该是第二个参数!

为什么编译器没有提示我们这个问题呢?

如果我们按照上面的逐字替换原理,我们可以模拟编译器扩展出的代码:

哇!两个参数类型都扩展为相同的底层类型!这意味着可以将它们混在一起使用,并且编译器无法分辨出对应参数。

一个重大的发现: 类型别名不会创建新的类型。他们只是给现有类型取了另一个名称而已

当然,这也就是为什么我们可以给一个没有子类继承的非 open的类添加类型别名。

虽然你可能认为这总是一件坏事,但实际上有些情况下它是有帮助的!

我们来比较两种不同的方式对类型命名:

  • 1、使用 类型别名
  • 2、使用 继承 去创建一个子类型(如上面的继承部分所述)。

两种情况下的底层类型都是String提供者,它只是一个不带参数并返回String的函数。

typealias AliasedSupplier = () -> String
interface InheritedSupplier : () -> String

现在,我们去创建一对函数去接收这些提供者:

fun writeAliased(supplier: AliasedSupplier) = 
        println(supplier.invoke())

fun writeInherited(supplier: InheritedSupplier) = 
        println(supplier.invoke())

最后,我们准备去调用这些函数:

writeAliased { "Hello" }
writeInherited { "Hello" } // Zounds! A compiler error!(编译器错误)

使用lambda表达式的类型别名方式可以正常运行,而继承方式甚至不能编译!相反,它给了我们这个错误信息:

Required: InheritedSupplier / Found: () -> String

事实上,我发现实际调用writeInherited()的唯一方法,像下面这样拼凑一个冗长的内容。

writeInherited(object : InheritedSupplier {
    override fun invoke(): String = "Hello"
})

所以在这种情况下,类型别名方式相比基于继承的方式上更具有优势。

当然,在某些情况下,类型安全将对您更为重要,在这种情况下,类型别名可能不适合您的需求。

类型别名的例子

现在我们已经很好地掌握了类型别名,让我们来看看一些例子!这里将为你提供一些关于类型别名的建议:

// 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

你可以基于类型别名可以做很酷的操作

正如我们所看到的一样,一旦创建了别名就可以在各种场景中使用它来代替底层类型,比如:

  • 在声明变量类型、参数类型和返回值类型的时候
  • 在作为类型参数约束和类型参数的时候
  • 在使用比较类型is或者强转类型的as的时候
  • 在获得函数引用的时候

除了以上那些以外,它还有一些其他的用法细节。让我们一起来看看:

构造器(Constructors)

如果底层类型有一个构造器,那么它的类型别名也是如此。你甚至可以在一个可空类型的别名上调用构造函数!

class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?

// Constructing with the alias: 使用别名来构造对象
val member =  MaybeTeamMember("Miguel")

// The above code does *not* expand verbatim to this (which wouldn't compile):(以上代码不会是逐字扩展成如下无法编译的代码)
val member = TeamMember?("Miguel")

// Instead, it expands to this:(而是扩展如下代码)
val member = TeamMember("Miguel")

所以你可以看到编译时的扩展并不总是逐字扩展的,在这个例子中就是很有效的说明。

如果底层类型本身就没有构造器(例如接口或者类型投影),自然地你也不可能通过别名来调用构造器。

伴生对象

你可以通过含有伴生对象类的别名来调用该类的伴生对象中的属性和方法。即使底层类型具有指定的具体类型参数,也是如此。一起来看下:

class Container<T>(var item: T) {
    companion object {
        const val classVersion = 5
    }
}

// Note the concrete type argument of String(注意此处的String是具体的参数类型)
typealias BoxedString = Container<String>

// Getting a property of a companion object via an alias:(通过别名获取伴侣对象的属性:)
val version = BoxedString.classVersion

// The line above does *not* expand to this (which wouldn't compile):(这行代码不会是扩展成如下无法编译的代码)
val version = Container<String>.classVersion

// Instead, it expands to this:(它是会在即将进入编译期会扩展成如下代码)
val version = Container.classVersio

我们再次看到Kotlin并不总是逐字替换扩展的,特别是在其他情况下是有帮助的。

需要注意的点

在你使用类型别名的时候,这有一些注意的点你需要记住。

只能定义在顶层位置

类型别名只能定义在代码顶层位置,换句话说,他们不能被内嵌到一个类、对象、接口或者其他的代码块中。如果你执意要这样做,你将会得到一个来自编译器的错误:

Nested and local type aliases are not supported.(不支持嵌套和本地类型别名)

然而,你可以限制类型别名的访问权限,比如像常见的访问权限修饰符internalprivate。所以如果你想要让一个类型别名只能在一个类中被访问,你只需要将类型别名和这个类放在同一个文件即可,并且这个别名标记为private来修饰,比如像这样:

private typealias Message = String

object Messages {
    val greeting: Message = "Hello"
}

有趣的是,这个private类型别名可以出现在公共区域,例如以上的代码 greeting: Message

与Java的互操作性

你能在Java代码中使用Kotlin的类型别名吗?

你不能,它们在Java中是不可见的。

但是,如果在Kotlin代码你有引用类型别名,类似这样的:

typealias Greeting = String

fun welcomeUser(greeting: Greeting) {
    println("$greeting, user!")
}

虽然你的Java代码不能使用别名,但是可以通过使用底层类型继续与它交互,类似这样:

// Using type String here instead of the alias Greeting(使用String类型,而不是使用别名Greeting)
String hello = "Hello";
welcomeUser(hello);

递归别名

总的来说可以为别名取别名:

typealias Greeting = String
typealias Salutation = Greeting 

然而,你明确不能有一个递归类型别名定义:

typealias Greeting = Comparable<Greeting>

编译器会抛出如下异常信息:

Recursive type alias in expansion: Greeting

类型投影

如果你创建了一个类型投影,请注意你期望的样子。例如,我们有这样的代码:

class Box<T>(var item: T)
typealias Boxes<T> = ArrayList<Box<T>>

fun read(boxes: Boxes<out String>) = boxes.forEach(::println)

然后我们就期望它这样定义:

val boxes: Boxes<String> = arrayListOf(Box("Hello"), Box("World"))
read(boxes) // Oops! Compiler error here.(这里有编译错误)

这个报错误的原因是 Boxes<out String> 会扩展成 ArrayList<Box<out T>> 而不是 ArrayList<out Box<out T>>

Import As: 类型别名(Type Alias)的亲兄弟

这里有个非常类似于类型别名(type alias)的概念,叫做 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 各自支持特性情况如下:

目标对象(Target) 类型别名(Type Alias) Import As
Interfaces and Classes yes yes
Nullable Types yes no
Generics with Type Params yes no
Generics with Type Arguments yes no
Function Types yes no
Enum yes yes
Enum Members no yes
object yes yes
object Functions no yes
object Properties no yes

正如你所看到的,一些目标对象仅仅被支持一种或多种。

这儿有一些内容需要被牢记:

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

顺便说一下,如果您是Android开发人员,并且在您的项目中使用到了 Kotlin Android Extensions,那么使用import as将是一个美妙的方式去重命名来自于Activity中对应布局的id,将原来布局中下划线分割的id,可以重命名成驼峰形式,使你的代码更具有可读性。例如:

import kotlinx.android.synthetic.main.activity.upgrade_button as upgradeButton

这可以使您从findViewById()(或Butter Knife)转换到Kotlin Android Extensions变得非常简单!

总结

使用类型别名是一种很好的方式,它可以为复杂,冗长和抽象的类型提供简单,简洁和特定于域的名称。它们易于使用,并且IDE工具支持可让您深入了解底层类型。在正确的地方使用,它们可以使您的代码更易于阅读和理解。

译者有话说

  • 1、为什么我要翻译这篇博客?

typealias类型别名,可能有的Kotlin开发人员接触到过,有的还没有碰到过。接触过的,可能也用得不多,不知道如何更好地使用它。这篇博客非常好,可以说得上是Kotlin中的typealias的深入浅出。它阐述了什么是类型别名、类型别名的使用场景、类型别名的实质原理、类型别名和import as对比以及类型别名中需要注意的坑。看完这篇博客,仿佛打开kotlin中的又一个新世界,你将会很神奇发现一个小小typealias却如此强大,深入实质原理你又会发现原来也挺简单的,但是无不被kotlin这门语言设计思想所折服,使用它可以大大简化代码以及提升代码的可读性。所以对于Kotlin的初学者以及正在使用kotlin开发的你来说,它可能会对你有帮助。

  • 2、这篇博客中几个关键点和注意点。

关于typealias我之前有篇博客浅谈Kotlin语法篇之Lambda表达式完全解析(六)也大概介绍了下,但是这篇博客已经介绍的非常详细,这里再次强调其中比较重要几点:

  • 类型别名(typealias)不会创建新的类型。他们只是给现有类型取了另一个名称而已.
  • typealias实质原理,大部分情况下是在编译时期采用了逐字替换的扩展方式,还原成真正的底层类型;但是不是完全是这样的,正如本文例子提到的那样。
  • typealias只能定义在顶层位置,不能被内嵌在类、接口、函数等内部
  • 使用import as对于已经使用Kotlin Android Extension 或者anko库的Android开发人员来说非常棒。看下以下代码例子:

没有使用import as

//使用anko库直接引用布局中下划线id命名,看起来挺别扭,不符合驼峰规范。
import kotlinx.android.synthetic.main.review_detail.view.*

class WidgetReviewDetail(context: Context, parent: ViewGroup){

     override fun onViewCreated() {
		mViewRoot.run {
			review_detail_tv_checkin_days.isBold()
			review_detail_tv_course_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			review_detail_tv_elevator_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			review_detail_tv_word_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
		}
	}

	override fun renderWidget(viewModel: VModelReviewDetail) = with(viewModel) {
			mViewRoot.review_detail_iv_avatar.loadUrl(url = avatarUrl)
			mViewRoot.review_detail_tv_checkin_days.text = labelCheckInDays
			mViewRoot.review_detail_tv_word_num.text = labelWordNum
			mViewRoot.review_detail_tv_elevator_num.text = labelElevatorNum
			mViewRoot.review_detail_tv_course_num.text = labelCourseNum
	}
}	

使用import as 整体代码更加简单和更具有可读性,此外还有一个好处就是布局文件ID变了,只需要import as声明处修改即可,无需像之前那样每个用到的地方都需要修改

注意的一点是如果给每个View组件都用import as感觉又回到重新回到findViewById的,又会产生冗长声明,这里建议你慎重使用。但是此处出发点不一样,目的在于简化冗长的id命名的使用。

import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_checkin_days as tvCheckInDays
import kotlinx.android.synthetic.main.review_detail.view.review_detail_iv_avatar as ivAvatar
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_word_num as tvWordNum
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_elevator_num as tvElevatorNum
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_course_num as tvCourseNum

class WidgetReviewDetail(context: Context, parent: ViewGroup){

        override fun onViewCreated() {
		mViewRoot.run {
			tvCheckInDays.isBold()
			tvCourseNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			tvElevatorNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
			tvWordNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
		}
	}

	override fun renderWidget(viewModel: VModelReviewDetail) {
		with(viewModel) {
			mViewRoot.ivAvatar.loadUrl(url = avatarUrl)
			mViewRoot.tvCheckInDays.text = labelCheckInDays
			mViewRoot.tvWordNum.text = labelWordNum
			mViewRoot.tvElevatorNum.text = labelElevatorNum
			mViewRoot.tvCourseNum.text = labelCourseNum
		}
	}
}

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~