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

翻译说明:html

原标题: All About Type Aliases in Kotlinjava

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

原文做者: Dave Leeds程序员

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

但愿你在现实生活中没有像这样的对话,可是这样情景可能会出如今你的代码中。安全

例如,看下这个代码:app

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

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

幸运的是,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的类添加类型别名。

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

咱们来比较两种不一样的方式对类型命名:

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

两种状况下的底层类型都是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工具支持可以让您深刻了解底层类型。在正确的地方使用,它们可使您的代码更易于阅读和理解。

译者有话说

  • 一、为何我要翻译这篇博客?

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

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

关于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,欢迎加入咱们~~~

相关文章
相关标签/搜索