前言:生活不是等暴风雨过去,而是学会在风雨中跳舞。
无论你是编写执行在云端的数据流程还是低功耗手机的应用程序,大多数的开发者都希望他们的代码能快速运行。现在,Kotlin 最新实验性的特性内联类允许创建我们想要的数据类型,并且还不会损失我们需要的性能。
比如在管理系统中有这样一个需求:
向新用户发送电子邮件 - 在注册后四天
因为已经编写好邮件系统,你可以启动邮件调度程序的界面,如下:
interface MailScheduler {
fun sendEmail(email: Email, delay: Int)
}
这个函数都知道怎么调用它,但是为了将邮件延迟四天,你会传递什么参数?
这个 delay 参数类型是 Int ,但是我们仅仅知道它只是一个 Integer,但是我们并不知道它的单位是什么。如果你传入的是天,但是单位是小时,那么你传入的是 24*4 小时,又或者它的单位是分钟,秒,毫秒呢?
我们该如何去优化这个代码,变的更好?
如果编译器能强制指定正确的单位,例如,接收参数类型不是 Int,是一个强类型: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 就是为了解决这样的问题而设计的。
有时候,业务逻辑需要围绕某种类型创建包装器。但是,由于额外的堆分配,它引入了运行时开销。此外,如果被包装的类型是原语类型,性能损失会很严重,因为原语类型通常由运行时进行了大量优化,而它们的包装器没有得到任何特殊处理。
为了解决这些问题,Kotlin 引入了一种特殊的类,叫做内联类,它是通过在类名前放置一个 Inline 修饰符来声明的:
inline class Hours(val value: Int) {
fun toMinutes() = Minutes(value * 60)
}
那么这个类将作为你定义值的强类型,并且在许多情况下,它和常规非内联类相比性能成本几乎相同。
你可以像其他任何类一样实例化和使用内联类,你可能需要在代码中的某个位置引用里面包装的普通值,这个位置通常在与另一个库或系统的边界处。在一点上,你可以想通常使用其他任何类一样访问这个值。
要知道的关键术语
内联类包装基础类型的值,并且这个值也是有类型的,我们把它称之为基础类型:
为什么内联类可以高性能执行?
那么内联类为什么可以和普通类更好好的执行呢?
你可以这样去实例化一个内联类:
val hous = Hours(24)
实际上该类并没有在编译器中实例化,事实上,就 JVM 而言,上面的代码等同于下面的代码:
val hous = 24
正是你所看到的,在此编译版本的代码中没 Hours 这个概念,它只是将基础值分配给int类型的变量,同理,你使用内联类作为函数参数的类型时也是这样的:
fun wait(hours: Hours) {
}
它其实是有效编译成下面这样的:
void wait(int hours) {
}
所以,我们的代码中内联了基础类型和基础值。也就是说,编译后的代码只使用了 int 证数类型,因此我们避免在避免在堆内存上创建和访问对象的开销成本。
但是,你还记得 Hours 的 toMinutes()
函数吗?因为编译后的代码使用的是 int 而不是 Hours 对象实例,想象以下调用 toMinutes()
函数到底会发生什么呢?
inline class Hours(private 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 除了一些基础值之外还有其他一些数据,怎么办?注意:内联类除了基础值之外不能有任何其他成员属性。
(1)内联类必须在主构造函数中初始化一个属性。而且只有一个构造函数参数,这个参数必须是只读 val 的。
inline class Password(val value: String)//公有构造函数,属性私有val
inline class Password2(var value: String) //compile error,参数只能是只读val的
inline class Password3(val value: Int, val name: String) //compile error,构造函数只运行一个参数
fun test() {
//实际上类 Password 没有实例化,运行时securePassword 只包含String
val securePassword = Password("Don't try this in production")
}
这就是内联类的主要特性,它激发了 inline 这个名称:类的数据被 inline 到它的使用中(类是于内联函数的内容被内联到调用站点中)。
注意:内联类只有在 Kotlin1.3 之后才可用,它目前还是处于实践阶段的语法,正在被积极开发和完善。
(2)内联类必须有且有一个构造函数参数,这个参数可以是私有 private 的,但是构造函数必须是公有的 public:
inline class Password4 private constructor(val value: String) //compile error,构造方法不能是私有的
inline class Password5 constructor(private val value: String) //compile success
(3)内联类的内部是允许成员属性的,只要它们仅基于构造器中那个基础值计算,或者从可以静态解释的某个值或对象计算,来自单例,顶级对象,常量等。
内联类支持常规类的一些功能。特殊来说,它们被允许声明属性和函数:
const val MINUTES_PER_HOUR = 60
inline class People(val name: String) {
val length: Int
//属性getter作为静态方法调用
get() = name.length
val minutes get() = name.length * MINUTES_PER_HOUR
fun greet() {
//作为静态方法调用
println("People == $name")
}
}
//调用
fun test() {
val name = People("Kotlin")
name.greet()
println("name长度 == ${
name.length}")
}
打印数据如下:
People == Kotlin
name长度 == 6
(4)内联类不允许类继承,即不允许继承其他类,也不允许被其他类继承。
open class Animal//基类
inline class Chicken(val name: String) : Animal() {
} // compile error,内联类不允许类继承
open inline class Tiger(val name: String) {
} // compile error,内联类只能是final的
禁止内联类参加与类层次结构,这意味着内联类不能拓展其他类,必须是 final 的。
(5)内联类可以实现接口,而不是继承基类:
interface Printable {
fun prettyPrint(): String
}
inline class Student(val name: String) : Printable {
override fun prettyPrint(): String = "内联类继承接口:$name"
}
//调用
fun test() {
val student = Student("Android")
println(student.prettyPrint())//让然作为静态方法调用
}
内联类 Student 继承了接口 Printable ,打印数据如下:
内联类继承接口:Android
(6)内联类必须在顶层声明,嵌套、内部类不能内联的。
inline class InlineClass(val name: String)
class Outer {
inline class Inner() {
} // compile error,内联类只允许在顶级声明
}
(7)目前,也不支持内联类枚举。
inline enum class Orientation {
NORTH,
SOUTH,
EAST,
WEST
}
(8)注意,内联类的成员有一些限制:
在生成的代码中,Kotlin 编译器为每一个内联类保留一个包装器。内联类实例可以在运行时表示为包装器或底层类型。这类似于 Int 可以表示为原语 int 或 Integer。
Kotlin 编译器更喜欢使用底层类型而不是包装器来产生性能最好的优化代码。然而,有时有必要保留包装器。根据经验,只要内联类被用作另一种类型,它们就会被装箱。
interface IA
inline class Our(val i: Int) : IA
fun asInline(our: Our) {
}
fun <T> asGeneric(t: T) {
}
fun asInterface(i: IA) {
}
fun asNullable(our: Our?) {
}
fun <T> id(x: T): T = x
//调用
fun main() {
val our = Our(42)
asInline(our)//未装箱:作为Our本身使用
asGeneric(our)//已装箱:用作通用类型T
asInterface(our)//已装箱:用作类型IA
asNullable(our)//已装箱:用作"Our?",这是与Our不同的
//下面,"our"首先被装箱(传递给"id"时),然后是未被装箱(从"id"返回时)
//最后,"C"包含无框表示(只有"42"),比如"our"
val o = id(our)
}
因为内联类既可以表示为基础值,也可以表示为包装器,所以引用相等对它们没有意义,因此被禁止。
由于内联类被编译成它们的底层类型,它可能会导致各种模糊的错误,例如平台签名冲突:
inline class UInt(val i: Int)
//在JVM上表示为"public final void compute(int x)"
fun compute(x: Int) {
}
//在JVM上也表示为"public final void compute(int x)"
fun compute(x: UInt) {
}
为了减轻这类问题,通过在函数名中添加一些稳定的 hashcode 来破坏使用内联类的函数。因此 fun compute(x: UInt)
将表示为 public final void compute-
,这样就解决了冲突问题。
注意:在 Java 中是一个无效的符号,这意味着不可能调用从 Java 中接受内联类的函数。
它们都包含基础类型,所以内联类很容易与类型别名混淆,但是一些关键的差异使得它们在不同场景下得以应用。
类型别名为基础类型提供备用名称。例如你可以为 String 这样常见的类型添加别名,并为其在指定上下文有意义的描述性名称,比如:UserName ,UserName 类型的变量实际上是源代码和编译代码中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.length} is too short.")
}
}
注意到我们必须这样 name.value.length
而不是 name.length
,我们必须解开包装器取出里面的值。name 表示 UserName,name.value 表示获取 UserName 中的参数值 value。
但是最大的区别在于分配兼容有关,内联类为你提供的是类型安全,而类型别名没有,类型别名与其基础类型相同。例如:
typealias UserName = String
typealias PassWord = String
fun authenticate(userName: UserName, password: PassWord) {
}
fun test() {
val userName: UserName = "Jack"
val password: PassWord = "12346"
authenticate(userName, password)
}
在这种情况下,UserName 和 PassWord 仅仅是 String 类型的另一个不同名称而已,即使将 UserName 和 PassWord 的位置弄反,但是编译器依然认为是合法的。
另一方面,如果你对上面同一案例使用内联类,那么编译器会报错:
inline class UserName(val value: String)
inline class PassWord(val value: String)
fun authenticate(userName: UserName, password: PassWord) {
}
fun test() {
val userName: UserName = UserName("Jack")
val password: PassWord = PassWord("12346")
authenticate(password, userName)// compile error,参数类型不匹配
}
然而,关键的区别是类型别名与它们的基础类型分配兼容(以及与具有相同基础类型的其他类型别名分配兼容),而内联类则不是。
换句话说,内联类引入了一个真正的新类型,而类型别名只是为现有类型引入了一个替代名称(别名)。
内联类的设计师在 Alpha 中进行的,这意味着不会为将来的版本提供兼容性保证。当在 Kotlin1.3+ 中使用内联类时,会报一个警告,表明该特性还没有作为稳定性发布。
为了消除警告,必须通过传递编译器参数 -Xinline-classes
来选择使用此特性。
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += "-Xinline-classes"
}
<configuration>
<args>
<arg>-Xinline-classes</arg>
</args>
</configuration>
至此,本文结束!
源码地址:https://github.com/FollowExcellence/KotlinDemo-master
请尊重原创者版权,转载请标明出处:https://blog.csdn.net/m0_37796683/article/details/109224751 谢谢!