美文网首页
3 亿美元的 bug,Kotlin 帮你避免 | 内联类 val

3 亿美元的 bug,Kotlin 帮你避免 | 内联类 val

作者: super可乐 | 来源:发表于2022-08-05 14:05 被阅读0次

    3亿美元的 bug

    假设有这样一个方法:

    interface Timer{
        fun delay(long: Long, block: () -> Long)
    }
    

    从方法声明可以猜出功能是“延迟long后执行block,并且要求 block 返回一个 Long。”

    至于延迟的是秒还是毫秒?block 的返回值表示什么意思?不得而知。

    不得不查看接口实现类:

    class Timer1 : Timer{
        override fun delay(long: Long, block: () -> Long) {
            handler.postDelayed({
                val seconds = block()
                print(seconds)
            }, long)
        }
    }
    

    从 Timer1 的实现可以得知,时间间隔是毫秒,而 block 返回值是秒。

    但项目中可能同时存在下面这样的实现:

    class Timer2 : Timer {
        override fun delay(long: Long, block: () -> Long) {
            GlobalScope.launch {
                delay(long.toDuration(DurationUnit.SECONDS))
                val milliseconds = block()
                print(milliseconds)
            }
        }
    }
    

    此时,时间间隔是秒,而 block 返回值是毫秒。

    这就很头痛了,因为使用 Timer 接口时,不知道该如何传参。

    理论上接口是一种抽象,在使用它时不需要关心内部实现细节。

    显然,Timer 的定义破坏了接口的抽象性。为了保证不出错,在使用时不得不这样做:

    val timer = ...
    val delaySeconds = 1
    if( timer is Timer2 ) {
        timer.delay(seconds) {...}
    } else if( timer is Timer1 ) {
        timer.delay(seconds*1000) {...}
    }
    

    这样的话,Timer 接口还有什么存在的必要?

    多态是编程语言支持的一种特性,这种特性使得静态的代码运行时可能产生动态的行为,这样一来编程时不需要为类型所烦恼,可以编写统一的处理逻辑而不是依赖特定的类型。”

    这段话摘自如何“好好利用多态”写出又臭又长又难以维护的代码?| Feeds 流重构方案。上面 Timer 接口的现状恰恰是这段话的反面。

    但若不这样做,程序就会发生错误。这类错误中最著名的就是“the Mars Climate Orbiter”,即 NASA 的火星气候探测器。该项目耗资 3 亿美元,却因程序 bug 导致失败。项目中有一个方法返回的值是以lbf·s为单位,而与之配套的另一个方法的入参是以N·s为单位。在物理世界里它们相差十万八千里,但在计算机的世界里它们都表达成double

    修复1:语义弱约束

    Timer.delay() 方法的参数 long 缺乏语义,在具体业务场景中 long 可以表达非常多的语义,比如:时间戳、毫秒、秒、纳秒等等。

    可以通过有意义的变量命名来约束参数的语义:

    interface Timer{
        fun delay(seconds: Long, block: () -> Long)
    }
    

    这的确可以为参数增加语义,但对返回值就无能为力了,比如 block 的返回值还是语义不明。

    除了在参数名上做文章,也可以在类型名上做文章:

    typealias Second = Long
    
    interface Timer{ 
        fun delay(seconds: Second, block: () -> Long) 
    }
    

    看上去引入了一个新的类型Second,但对于编译器来说SecondLong是一个东西的两个名字。编译器并不会因为你传入了毫秒而报错。

    这其实是错误使用typealias的一个示范,typealias 应该用于“化简名字”,比如:

    // 把一个长 lambda 化简,取一个表达语义的别名,如此一来方法签名就可以被简化
    typealias OnWindowClick = (x: Int, y: Int, view: View) -> Boolean
    fun setOnWindowClickListener(block: (x: Int, y: Int, view: View) -> Boolean) {}
    fun setOnWindowClickListener(block: OnWindowClick) {}
    
    // 将一个嵌套泛型化简
    typealias ViewCache = HashMap<String, List<View>>
    

    typealias 隐藏了细节,降低了复杂度,增加了代码可读性。

    还有一种约束语义的方式是添加注释

    interface Timer {
        /**
         * @param seconds,the seconds to delay
         * @param block,the block to be invoked after [seconds], 
         *       the return value of it is the consumed time in seconds
         */
        fun delay(seconds: Long, block: () -> Long)
    }
    

    修复2:语义强约束

    上述这两种约束语义的方式都不是强制性的。假设接口的实现者都阅读了注释并按照规定实现接口,但也无法保证调用者不把毫秒传给 seconds 参数。

    可以通过新增一个类型,让编译器帮我们做类型检查:

    data class Second(val value: Long)
    
    interface Timer{
        fun delay(second: Second, block: () -> Second)
    }
    

    现在如下的调用在编译前就会报错:

    timer.delay(1000L){...} // 传入 1000 ms,来表示延迟一秒
    

    现在想延迟一秒,必须这样做:

    timer.delay(Second(1)){...}
    

    在方法调用处,通过类型强行提示,可以避免明明想延迟一秒,但却写出这样的代码timer.delay(Second(1000))

    不过这样做是有性能代价的,因为原本是基础类型的赋值,现在变成需要构建新的包装对象(在堆中分配内存,并在栈中指向这块内存)。

    修复3:内联类

    为了解决这种问题,Kotlin 在 1.3.0 推出了inline class,在 1.5.0 用value class取而代之。它的用法如下:

    @JvmInline
    value class Second(val value: Long)
    
    interface Timer{
        fun delay(second: Second, block: () -> Second) 
    }
    

    通过关键词value+@JvmInline声明了一个内联类。然后就可以像这样延迟一秒执行:

    timer.delay(Second(1)){...}
    

    因为发生了内联,这行调用和上一节的不同之处在于,当 kotlin 编译成 java 后,内联类型不会被创建,而是将其成员内联到调用处。不过在编译之前会进行类型检查,即下面这样的调用会报错:

    timer.delay(1L){...}
    

    内联类在保证类型安全的同时做到了零性能损耗。

    对内联类做个总结,它通常用于约束语义,并以零性能损耗的方式通过编译器保证类型安全

    内联类注意事项

    参数限制

    内联类只能在构造方法中声明一个成员参数,在多参数场景下,只能退而求其次使用性能略差的data class

    成员变量/方法 & 实现接口

    普通类具备的功能,内联类几乎都具备:

    @JvmInline
    value class Name(val s: String) {
        // init 代码块
        init {
            require(s.length > 0) { }
        }
        // 计算型成员变量(没有backing field)
        val length: Int
            get() = s.length
        // 成员方法
        fun greet() {
            println("Hello, $s")
        }
    }
    
    fun main() {
        val name = Name("Kotlin")
        name.greet() // greet 被编译成静态方法
        println(name.length) // length属性的get方法也被编译成静态方法
    }
    

    内联类也可以实现接口:

    interface Printable {
        fun prettyPrint(): String
    }
    
    @JvmInline
    value class Name(val s: String) : Printable {
        override fun prettyPrint(): String = "Let's $s!"
    }
    
    fun main() {
        val name = Name("Kotlin")
        println(name.prettyPrint()) // prettyPrint 被编译成静态方法
    }
    

    但是内联类不能被继承。

    内联条件

    内联类的成员被内联到调用处是有条件的,条件是 “内联类没有被当成其他类型使用” 。若不满足这个条件,内联会失败,此时会发生装箱,即内联类被当成一个包装类被构建,就没有性能优势了:

    interface I
    
    // 一个实现了接口的内联类
    @JvmInline
    value class Foo(val i: Int) : I
    
    fun asInline(f: Foo) {}
    fun <T> asGeneric(x: T) {}
    fun asInterface(i: I) {}
    fun asNullable(i: Foo?) {}
    
    fun <T> id(x: T): T = x
    
    fun main() {
        val f = Foo(42)
        asInline(f)    // 内联成功:因为内联类被当成其原本的类型Foo使用
        asGeneric(f)   // 内联失败: 因为内联类被当成 T 使用
        asInterface(f) // 内联失败: 因为内联类被当成 I 使用
        asNullable(f)  // 内联失败: 以为内联类被当成 Foo? 使用
    }
    

    在 Java 中使用

    @JvmInline
    value class UInt(val x: Int)
    fun compute(x: Int) { }
    fun compute(x: UInt) { }
    

    上述两个方法被编译成 java 代码后,拥有完全相同的签名。为了解决这个问题,系统会自动为第二个方法名追加一个哈希码以示区别,它最终会被表达成public final void compute-<hashcode>(int x)

    为了能在在 Java 中调用带内联的方法,可以为其添加@JvmName注解:

    @JvmInline
    value class UInt(val x: Int)
    
    fun compute(x: Int) { }
    
    @JvmName("computeUInt")
    fun compute(x: UInt) { }
    

    通过注解为带内联的方法取一个别名。

    作者:唐子玄
    链接:https://juejin.cn/post/7113704624422387720
    更多资讯请关注【公众号】

    相关文章

      网友评论

          本文标题:3 亿美元的 bug,Kotlin 帮你避免 | 内联类 val

          本文链接:https://www.haomeiwen.com/subject/fcspwrtx.html