加入收藏 | 设为首页 | 会员中心 | 我要投稿 宿州站长网 (https://www.0557zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 教程 > 正文

如何防止Kotlin里的陷阱?

发布时间:2021-12-07 16:02:33 所属栏目:教程 来源:互联网
导读:最近 Kotlin 特别流行,并且我也赞同 Kotlin 是一个经过深思熟虑后被设计出的语言,除了下面提到的缺点之外。我会在本文向你分析一些我在开发过程中遇到的陷阱,并且教你如何避免他们。 谜一样的 null 在 Kotlin 当中,你可以不用考虑在你的代码中如何处理 n

最近 Kotlin 特别流行,并且我也赞同 Kotlin 是一个经过深思熟虑后被设计出的语言,除了下面提到的缺点之外。我会在本文向你分析一些我在开发过程中遇到的陷阱,并且教你如何避免他们。
 
谜一样的 null
在 Kotlin 当中,你可以不用考虑在你的代码中如何处理 null 的问题,这会让你忘记 null 是无处不在的这个说法,只不过被隐藏了起来。看看下面这个表面看起来没有问题的类:
 
class Foo {
    private val c: String
    init {
        bar()
        c = ""
    }
    private fun bar() {
        println(c.length)
    }
}
如果你尝试初始化这个类,那么代码就会抛出一个 NullPointerException。因为 bar 方法尝试在 c 变量初始化之前就访问它。
 
尽管这个代码本身就是有问题的,才导致异常抛出。但是更糟糕的是你的编译器不会发现这一点。
 
Kotlin 可以帮你在绝大部分情况下避免 null,但是你不能因此而忘记 null 的存在。否则迟早有一天你会碰上类似的问题。
 
来自 JDK 的 null
Kotlin 的标准库能够很好地处理 null。但是如果你使用了 JDK 中的类,你需要自己处理关于 JDK 方法调用可能产生的空指针。
 
大部分情况下 Kotlin 的标准库就足够了,但是有时你需要使用到 ConcurrentHashMap:
 
val map = ConcurrentHashMap<String, String>()
map["foo"] = "bar"
val bar: String = map["foo"]!!
这时,你需要使用 !! 操作符。但某些情况下你还可以使用像 (?) 这样的对 null 安全的操作符来替换它。尽管如此,当你使用 !! 或者 ? ,或者编写了一个适配器来使用 Java 类库的时候,你会发现代码因为这些修改而变的混乱。这是你无法避免的问题。
 
你还可能会碰上更多更可怕的问题。当你使用 JDK 类中的方法的时候,返回值可能是null,而且没有什么像 Map 访问一样的语法糖。
 
考虑如下例子:
 
val queue: Queue<String> = LinkedList()
queue.peek().toInt()
这种情况下,你使用了可能返回 null 值的 peek 方法。但是 Kotlin 编译器不会提示你这个问题,所以当你的 Queue 是空队列的的时候,可能会触发 NullPointerException 异常。
 
问题在于我们使用的 Queue 是 JDK 的一个接口,并且当你查看 peek 方法的文档时:
 
/**
  * Retrieves, but does not remove, the head of this queue,
  * or returns {@code null} if this queue is empty.
  *
  * @return the head of this queue, or {@code null} if this queue is empty
  */
  E peek();
文档中说 peek 方法会返回一个 E 类型的对象,但是 Kotlin 认为 E 是不可空的。在接下来的 Kotlin 版本中可能会解决这个问题,但是现在当你在你的工程中使用类似接口的时候,一定要注意:
 
val queue: Queue<String?> = LinkedList()
queue.peek()?.toInt()
内部 it
当一个 lambda 表达式只有一个参数的时候,你可以在你的代码中将其省略,并用 it 代替。
 
it:单参数的内部名称。当你表达式只有一个参数的时候,这是一个很有用的特性,声明的过程可以省略(就像 ->),并且参数名称为 it。
 
问题是,当你的代码中存在向下面例子一样的嵌套函数的时候:
 
val list = listOf("foo.bar", "baz.qux")
list.forEach {
    it.split(".").forEach {
        println(it)
    }
}
it 参数会混淆。解决方法就是像下面这样显示的声明:
 
list.forEach { item ->
    item.split(".").forEach { part ->
        println(part)
    }
}
看起来是不是好多了!
 
隐藏的复制
注意观察下面的类:
 
data class Foo(val bars: MutableList<String>)
data 类提供了一系列的方法,并且你可以通过拷贝得到其镜像。猜猜下面的代码会输出什么?
 
val bars = mutableListOf("foobar", "wombar")
val foo0 = Foo(bars)
val foo1 = foo0.copy()
bars.add("oops")
println(foo1.bars.joinToString())
控制台会输出 foobar, wombar, oops。问题出在 copy 方法并没有真正地复制一个完整的对象, 而是复制了对象的引用。当你忘记编写单元测试类,并且将你的 data 类按照不可变类来传递的时候,就可能出现这种问题。
 
解决方法就是当你使用 data 类的时候一定要多加小心,并且当你必须将其作为值对象的时候,像下面这样:
 
data class Foo(val bars: List<String>)
data 类还有一个问题:其 equals / hashCode 方法所用到的属性不可变。你只能通过手工重写这些方法的方式来修改返回值。谨记上面这一点。
 
内部方法暴露
仔细思考下面的例子:
 
class MyApi {
    fun operation0() {
    }
    internal fun hiddenOperation() {            
    }
}
当你在 Kotlin 的项目中引用这个类的时候,internal 关键字是生效的。但是当你从一个 Java 项目中使用的时候,hiddenOperation 就变成了一个公共方法!为了避免这种情况,我建议使用接口的方式来隐藏实现的细节:
 
interface MyApi {
    fun operation0()
}
class MyApiImpl: MyApi {
    override fun operation0() {
    }
    internal fun hiddenOperation() {
    }
}
特殊的全局扩展
毫无疑问,扩展函数的功能非常重要。但通常,能力越大责任越大。例如,你可以编写全局的 JDK 类扩展函数。但是当这个函数只在本地上下文中有意义,却是全局可见的时候,就会带来很多麻烦。
 
fun String.extractCustomerName() : String {
    // ...
}
每个跳转到你的方法的人都会不知所措。所以我认为在你编写这样的方法之前务必三思。下面就是一个建议:
 
/**
 * Returns an element of this [List] wrapped in an Optional
 * which is empty if `idx` is out of bounds.
 */
fun <T> List<T>.getIfPresent(idx: Int) =
        if (idx >= size) {
            Optional.empty()
        } else {
            Optional.of(get(idx))
        }
/**
 * Negates `isPresent`.
 */
fun <T> Optional<T>.isNotPresent() = isPresent.not()
lambdas Unit 返回值 vs Java SAM 转换
 
如果你的函数参数是 lambdas 表达式,并且返回值类型是 Unit 的时候,你可以省略return 关键字:
 
fun consumeText(text: String, fn: (String) -> Unit) {
}
// usage
consumeText("foo") {
    println(it)
}
这是一个很有趣的特性,但是当你在 Java 代码中调用该方法的时候会比较尴尬:
 
consumeText("foo", (text) -> {
    System.out.println(text);
    return Unit.INSTANCE;
});
这对于 Java 端来说是不友好的,如果你想在 Java 中成功调用该方法,你需要定义如下接口:
 
nterface StringConsumer {
    fun consume(text: String)
}
fun consumeText(text: String, fn: StringConsumer) {
}
然后你就能使用 Java 的 SAM 转换。
 
consumeText("foo", System.out::println);
但是在 Kotlin 这边看起来就很糟糕了:
 
consumeText("foo", object: StringConsumer {
    override fun consume(text: String) {
        println(text)

(编辑:宿州站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

推荐文章