[译]Kotlin中内联类(inline class)彻底解析(一)

翻译说明:java

原标题: An Introduction to Inline Classes in Kotlin安全

原文地址: typealias.com/guides/intr…app

原文做者: Dave Leedside

不管你是编写执行在云端的大规模数据流程程序仍是低功耗手机运行的应用程序,大多数的开发者都但愿他们的代码可以快速运行。如今,Kotlin的最新实验性的特性内联类容许建立咱们想要的数据类型,而且还不会损失咱们须要的性能!函数

在这一系列新文章中,咱们将从上到下完全研究一番内联类!post

在本篇文章中,咱们将会研究inline class是什么, 它的工做原理是什么以及在使用它的时候咱们如何去权衡选择。而后,在接下来的文章中,咱们将深刻了解内联类的内容,以确切了解它是如何实现的,并研究它如何与Java进行互操做。性能

请记住-这是一个实验阶段的语法特性,而且它正在被积极开发和完善。当前这篇文章是基于Kotlin-1.3M1版本的内联类实现。测试

若是你想本身去尝试使用它,我还写了一篇配套文章how to enable them in your IDE,以便您能够当即开始使用内联类和其余Kotlin 1.3功能!优化

强类型和普通值: 内联类的案例

星期一早上8点,在给本身倒了一杯新鲜的热气腾腾的咖啡以后,而后在项目管理系统中领到一份任务。上面写道:ui

向新用户发送欢迎电子邮件 - 在注册后四天

由于已经编写好了邮件系统,您能够启动邮件调度程序的界面,正如你下面所看到的:

interface MailScheduler {
    fun sendEmail(email: Email, delay: Int)
}
复制代码

看看这个函数,你知道你须要调用它...可是为了将电子邮件延迟4天,你会传递什么参数呢?

这个delay参数类型是Int. 因此咱们仅仅知道这是一个Integer,可是咱们并不知道它的单位是什么-你是应该传入4天呢? 或者它表明几个小时,若是是这样你传入的应该是96(24 * 4)。又或者它的单位是分钟、秒、毫秒...

咱们应该如何去优化这个代码呢?

怎样才能让这个代码变得更好呢?

若是编译器可以强制指定正确的时间单位。例如,假设接收参数类型不是Int,让咱们更新下interface中函数,让它接收一个强类型Minutes

interface MailScheduler {
    fun sendEmail(email: Email, delay: Minutes)
}
复制代码

如今咱们有了强类型系统为咱们工做! 咱们不可能向这个函数发送一个Seconds类型参数,由于它只接受Minutes类型的参数!考虑如下代码与先前版本相好比何可以在很大程度上减小错误:

val defaultDelay = Days(2)

fun send(email: Email) {
    mailScheduler.sendEmail(email, defaultDelay.toMinutes())
}
复制代码

当咱们能够充分利用类型系统时,咱们提升了代码的健壮性。

可是开发者一般不会选择去为了单一普通值作个包装器类,而更可能是经过传递Int、Float、Boolean这种基础类型。

为何会这样呢?

一般,因为性能缘由,咱们反对建立这样的强类型。您可能还记得,JVM上的内存看起来像这样:

当咱们建立一个基本类型的局部变量(即函数内定义的函数参数和变量)时 - 如Int、Float、Boolean - 这些值被存储在部分JVM 内存堆栈中。将这些基础类型的值存储在堆栈上所涉及的性能开销并不大。

在另外一方面,每当咱们实例化一个对象时,该对象实例就存储在JVM堆上了。咱们在存储和使用对象实例时会有性能损失 - 堆分配和内存提取的性能代价很高。虽然看起来每一个对象性能开销微不足道,可是累积起来,它对代码运行速度产生严重的影响。

若是咱们可以在不受性能影响的状况下得到强类型系统的全部好处,那不是很好吗?

实际上,Kotlin新特性inline class就是为了解决这样的问题而设计的。

让咱们一块儿来看看

内联类的介绍

内联类很容易去建立-仅仅须要在你定义的类前面加上inline关键字便可。

inline class Hours(val value: Int) {
    fun toMinutes() = Minutes(value * 60)
}
复制代码

就是这样!这个类如今将做为您定义值的强类型,而且在许多状况下,它和常规非内联类相比性能成本几乎相同。

您能够像任何其余类同样实例化和使用内联类。您最终可能须要在代码中的某个位置引用里面包装的普通值 - 这个位置一般是在与另外一个库或系统的边界处。 固然,在这一点上,您能够像一般使用任何其余类同样访问这个值。

您应该知道的关键术语

内联类包装基础类型的值。而且这个值也是有个类型的,咱们把它称之为基础类型

为何内联类能够高性能执行

那么,内联类为何能够和普通类更好地执行呢?

你能够像这样去实例化一个内联类

val period = Hours(24)
复制代码

...实际上该类并未在编译代码中实例化!事实上,就JVM而言,实际上至关于下面这样的代码......

int period = 24;
复制代码

正如您所看到的,在此编译版本的代码中没有Hours概念 - 它只是将基础值分配给int类型的变量! 一样,当您使用内联类做为函数参数的类型时也是这样的:

fun wait(period: Hours) { /* ... */ }
复制代码

...它能够有效地编译成以下这样......

void wait(int period) { /* ... */ }
复制代码

所以,咱们的代码中内联了基础类型和基础值。换句话说,编译后的代码只使用了int整数类型,所以咱们避免了在堆内存上建立和访问对象的开销成本。

可是请等一下!

还记得Hours类有一个名为toMinutes()的函数吗?由于编译后的代码使用的是int而不是Hours对象实例,所以想像一下调用toMinutes()函数时会发生什么呢?

inline class Hours(val value: Int) {
    fun toMinutes() = Minutes(value * 60)
}
复制代码

Hours.toMinutes()的编译代码以下所示:

public static final int toMinutes(int $this) {
	return $this * 60;
}
复制代码

若是咱们在Kotlin中调用Hours(24).toMinutes(),它能够有效地编译为toMinutes(24).

没问题,确实能够像这样处理函数,可是类成员属性呢?若是咱们但愿Hours除了主要的基础值以外还包括其余一些数据,该怎么办?

一切事情都是有它的权衡的,那么这是其中之一 - 内联类除了基础值以外不能有任何其余成员属性。让咱们探讨其余的。

权衡和使用限制

如今咱们知道内联类能够经过编译代码中的基础值来表示,咱们已经准备好了解使用它们时应注意哪些使用限制。

首先,内联类必须包含一个基础值,这就意味它须要一个主构造器来接收 这个基础值,此外它必须是只读的(val)。你能够定义你想要的基础值变量名。

inline class Seconds()              // nope - needs to accept a value!
inline class Minutes(value: Int)    // nope - value needs to be a property
inline class Hours(var value: Int)  // nope - property needs to be read-only
inline class Days(val value: Int)   // yes!
inline class Months(val count: Int) // yes! - name it what you want
复制代码

若是有须要,能够将该属性设为私有的,但构造函数必须是公有的。

inline class Years private constructor(val value: Int) // nope - constructor must be public
inline class Decades(private val value: Int)           // yes!
复制代码

内联类中不能包含init block初始化块。我会在下一篇发表的文章中探讨内联类如何与Java进行互操做,这点将会完全说明白。

inline class Centuries(val value: Int) {
	// nope - "Inline class cannot have an initializer block"
    init { 
        require(value >= 0)
    }
}
复制代码

正如咱们在上面发现的那样,除了一个基础值以外,咱们的内联类主构造器不能包含其余任何成员属性。

// nope - "Inline class must have exactly one primary constructor parameter"
inline class Years(val count: Int, val startYear: Int)
复制代码

可是呢,它的内部是能够拥有成员属性的,只要它们仅基于构造器中那个基础值计算,或者从能够静态解析的某个值或对象计算 - 来自单例,顶级对象,常量等。

object Conversions {
    const val MINUTES_PER_HOUR = 60    
}

inline class Hours(val value: Int) {
    val valueAsMinutes get() = value * Conversions.MINUTES_PER_HOUR
}
复制代码

不容许类继承 - 内联类不能继承另外一个类,而且它们不能被另外一个类继承。 (Kotlin 1.3-M1在技术上确实容许内联类继承另外一个类,但在即将发布的版本中会对此进行更正)

open class TimeUnit
inline class Seconds(val value: Int) : TimeUnit() // nope - cannot extend classes

open inline class Minutes(val value: Int) // nope - "Inline classes can only be final"
复制代码

若是您须要将内联类做为子类型,那很好 - 您能够实现接口而不是继承基类。

interface TimeUnit {
	val value: Int
}

inline class Hours(override val value: Int) : TimeUnit  // yes!
复制代码

内联类必须在顶级声明。嵌套/内部类不能内联的。

class Outer {
	 // nope - "Inline classes are only allowed on top level"
    inline class Inner(val value: Int)
}

inline class TopLevelInline(val value: Int) // yes!
复制代码

目前,也不支持内联枚举类。

// nope - "Modifier 'inline' is not applicable to 'enum class'"
inline enum class TimeUnits(val value: Int) {
    SECONDS_PER_MINUTE(60),
    MINUTES_PER_HOUR(60),
    HOURS_PER_DAY(24)
}
复制代码

Type Aliases(类型别名) 与 Inline Classes(内联类)对比

由于它们都包含基础类型,因此内联类很容易与类型别名混淆。可是有一些关键的差别使它们在不一样的场景下得以应用。

类型别名为基础类型提供备用名称。例如,您能够为String这样的常见类型添加别名,并为其指定在特定上下文中有意义的描述性名称,好比UsernameUsername类型的变量其实是源代码和编译代码中String类型的变量同一个东西,只是不一样名称而已。例如,您能够这样作:

typealias Username = String

fun validate(name: Username) {
    if(name.length < 5) {
        println("Username $name is too short.")
    }
}
复制代码

注意到咱们是能够在name上直接调用.length的,这是由于name实际上就是个String,尽管咱们在声明参数类型的时候使用的是别名Username.

在另外一面,内联类其实是基础类型的包装器,所以当你须要使用基础值的时候,须要作拆箱操做。例如咱们使用内联类来重写上面别名的例子:

inline class Username(val value: String)

fun validate(name: Username) {
    if (name.value.length < 5) {
        println("Username ${name.value} is too short.")
    }
}
复制代码

注意到咱们是必须这样name.value.length而不是name.length,咱们必须解开这个包装器取出里面的值。

可是最大的区别在于与分配兼容性有关。内联类为你提供类型的安全性,类型别名则没有。 类型别名与其基础类型相同。例如,看以下代码:

typealias Username = String
typealias Password = String

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = "joe.user"
    val password: Password = "super-secret"
    authenticate(password, username)
}
复制代码

在这种状况下,UsernamePassword仅仅是String另外一个不一样名称而已,甚至你能够将UsernamePassword任意调换位置。实际上,这正是咱们在上面的代码中所作的 - 当咱们调用authenticate()函数时,即便咱们将UsernamePassword位置弄反了,但编译器依然认为是合法的。

另外一方面,若是你对上面同一个案例使用内联类,那么编译器将会很幸运告诉你这是不合法的:

inline class Username(val value: String)
inline class Password(val value: String)

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = Username("joe.user")
    val password: Password = Password("super-secret")
    authenticate(password, username) // <--- Compiler error here! =)
}
复制代码

这点很是强大!这强大到写代码时候就告诉咱们,咱们写了一个bug。咱们无需等待自动测试、QA工程师或用户告诉咱们。很是棒!

包装

你本身准备开始尝试内联类了吗? 若是是,请当即阅读 how to enable inline classes

虽然咱们已经介绍了相关的基础知识,但在使用它们时要记住一些使人困惑的注意和限制。实际上,若是你不了解内部的原理,使用内联类可能会写出比正常普通类运行速度更慢的代码。

在下一篇文章中,咱们将深刻研究内联类底层工做原理,以便于你在运用的时候更加高效。

译者有话说

还记得上篇Kotlin新特性的文章吗,实际上关于inline class内容在上篇基本讲的很清楚了。可是上篇文章篇幅有限,特定找了一篇比较全面的inline class相关英文文章再次梳理和巩固内联类的知识。而且原文做者更是写了一系列有关inline class的文章,从它的使用到基本介绍最后到剖析内部原理,讲得很是清楚。固然我还会继续翻译他的最后一篇深刻inline class内部原理文章。欢迎你们持续关注~~~

Kotlin系列文章,欢迎查看:

原创系列:

翻译系列:

实战系列:

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

相关文章
相关标签/搜索