一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 3 天,点击查看活动详情
📢📢📢 最近我们团队在翻译《Effective Kotlin: Best practices》,关注我们,我们会定时更新,欢迎指正,欢迎收藏 😁
翻译: fengerzhang 校对: jiajianchen
条目 3:尽早消除平台类型
TL;DR:
- 平台类型指的是从另一种语言获取并且未知可空性的类型;
- 我们可以通过改变编码方式来尽早消除平台类型;
- 对于公开的类、方法和参数应该尽量使用注解来指定可空性。
Kotlin 引入的空安全机制「null-safety」是很棒的。Java 因为 NPE「Null-Pointer Exceptions」的问题而饱受诟病,而 Kotlin 的安全机制成功减少甚至完全消除了 NPE。然而,在没有空安全机制的语言(如 Java 或者 C)和 Kotlin 之间的互相调用的场景下,这个问题并不能得到完全的解决。
想象一下,在 Java 中一个方法返回的类型是 String ,那么在 Kotlin 里面应该是个什么类型?
如果它使用了 @Nullable 注解的话,那么在 Kotlin 中我们应该将其视作 String?
;如果使用了 @NonNull 注解,我们则直接将其视为非空的 String
,但是如果它并没有使用任何一个注解呢?
// Java
public class JavaTest {
public String giveName() {
// ...
}
}
我们可能会统一把这种代码的返回值都视作可空的字符串,这样做确实很安全,因为在 Java 中所有东西都是可空的。然而当我们经常将对象视作非空的话,我们的代码最终将会难以避免地大量使用非空断言符号“ !! ”。
当我们需要从 Java 中获取泛型类型,这会带来更麻烦的问题。
假设有一个 Java API 返回的类型是List<User>
,并且不带任何注解,如果 Kotlin 默认将类型视为可空,那么当我们需要非空的 User 数据,我们不仅要对整个 List 增加非空断言,还要过滤一遍其中 User 的空值:
// Java
public class UserRepo {
public List<User> getUsers() {
// ...
}
}
Kotlin 调用如下
val users: List<User> = UserRepo().users!!.filterNotNull()
甚至,如果一个方法返回类型是 List<List<User>>
的时候又该怎么做呢?
val users: List<List<User>> = UserRepo().groupedUsers!!.map { it!!.filterNotNull() }
List
至少还有 map 和 filterNotNull 这些能力,但是对于其他的泛型类,可空性会带来更麻烦的问题。
因此,我们将在 Java 中定义且未声明其是否可空的类型,在 Kotlin 中定义成一种特殊类型——平台类型「Platform Type」,而不是简单地将类型视为可空。
概括来说就是,平台类型指的是从其他语言传递过来的,但不知道其可空性的类型。
平台类型的表示方式是在其类型名称后加一个感叹号表示,例如 String!
。当然,这种表示方式并不能在实际代码中使用,也就是说不能直接在编程语言中写下来。当 Kotlin 的变量或属性被一个平台类型赋值或者分配的时候,我们可以去推断它但不能直接显式设置它。除此之外,我们可以自己选择想要使用的类型:可空或者非空类型。
val repo = UserRepo() // Java 类
val user1 = repo.user // user1 的类型是平台类型,即 User!
val user2: User = repo.user // 可以定义 user2 为非空类型 User
val user3: User? = repo.user // 可以定义 user3 为可空类型 User?
基于上述事实,从 Java 中获取泛型也不成问题:
val users: List<User> = UserRepo().users
val users: List<List<User>> = UserRepo().groupedUsers
这种做法存在风险,比如我们认为某个值是非空的,但实际上这个值是可空的。出于安全考虑,我建议从 Java 获取一个平台类型的时候,需要慎重考虑接收的类型。即使一个函数现在的返回值不是 null
,也不意味着它以后也永远不会返回 null
。如果代码设计者没有在注解或者注释中确切的指定其是否可空,他们可以在不改变代码契约的情况下引入上述的这种行为。
如果你需要执行 Java 和 Kotlin 互调的操作,请引入 @Nullable
和 @NotNull
注解。
// Java
import org.jetbrains.annotations.NotNull;
public class UserRepo {
public @NotNull User getUser() {
// ...
}
}
当我们需要更好地支持 Kotlin 开发时,这一步非常重要(对 Java 开发也同样重要)。在 Kotlin 成为主流的编程语言(成为 Android 开发的推荐语言)之后,Android 官方对 API 其中一个非常重要的改动是,对许多的公开类型都增加了下面的注解,这也让 Android API 对 Kotlin 使用变得更加的友好。
目前已经支持许多种不同类型的注解,包括以下这些:
- JetBrains(在
org.jetbrains.annotations
库中定义的@Nullable
和@NotNull
) - Android(在
androidx.annotation
/com.android.annotations
/android.support.annotations
库中定义的@Nullable
和@NonNull
) - JSR-305 (在
javax.annotation
库中定义的@Nullable
、@CheckForNull
和@Nonnull
) - JavaX(在
javax.annotation
库中定义的@Nullable
、@CheckForNull
和@Nonnull
) - FindBugs(在
edu.umd.cs.findbugs.annotations
库中定义的@Nullable
、@CheckForNull
、@PossiblyNull
和@NonNull
) - ReactiveX(在
io.reactivex.annotations
库中定义的@Nullable
和@NonNull
) - Eclipse(在
org.eclipse.jdt.annotation
库中定义的@Nullable
和@NonNull
) - Lombok(在
lombok
库中定义的@NonNull
)
或者,你也可以使用 JSR 305 中定义的 @ParametersAreNonnullByDefault
注解,,这个注解可以将 Java 中的所有类型默认标记为非空。
我们也可以在 Kotlin 的代码中做一些修改。**我建议是完全消除这些平台类型来确保代码安全。**原因可以参考下面的示例中 statedType()
和 platformType()
两个函数结果之间的差异:
// Java
public class JavaClass {
public String getValue() {
return null;
}
}
// Kotlin 调用
fun statedType() {
val value: String = JavaClass().value // 建议的做法:尽早消除平台类型
// ...
println(value.length)
}
fun platformType() {
val value = JavaClass().value // 不建议的做法:平台类型会一直传递下去
// ...
println(value.length)
}
在这两种情况下,如果开发人员都认为 getValue()
不会返回 null
,但是实际上却返回了 null
值的话,这在两种情况下都会导致 NPE,但是错误发生的位置有所不同。
- 在
stateType()
中,NPE 会在我们从 Java 中获取值的同一行中抛出。很明显,我们错误地将返回值假定为非空类型并最终接收了空值。我们只需要更改这个类型为可空并调整其余代码以适应此更改。 - 在
platformType()
中,当我们将此值用作不可为空时,可能会在一些更复杂的表达式的中间抛出 NPE。当变量的类型是平台类型时可以被视为可空或者不可空,这样的变量可能可以安全地使用几次,然后最后不安全地抛出 NPE。当我们使用这些属性时,编译器并不能识别出来它的可空性,这跟在 Java 中的情况很像。但在 Koltin 中,我们并不期望在使用一个对象时产生 NPE。迟早有人会不安全地使用它,并且最后得到运行时异常,这里出现异常的原因还不太容易能找到。
fun statedType() {
val value: String = JavaClass().value // NPE
// ...
println(value.length)
}
fun platformType() {
val value = JavaClass().value
// ...
println(value.length) // NPE
}
更危险的是,平台类型可能会被进一步传播。例如,我们可能会将一个平台类型作为我们接口的一部分:
interface UserRepo {
fun getUserName() = JavaClass().value
}
在这种情况下,方法推断类型是平台类型。这意味着任何人都可以决定它是否可以为空。甚至可能在定义时将其视为可为空的,而在使用时其视为不可为空的:
class RepoImpl: UserRepo {
override fun getUserName(): String? {
return null
}
}
fun main() {
val repo: UserRepo = RepoImpl()
val text: String = repo.getUserName() // NPE in runtime
print("User name length is ${text.length}")
}
平台类型的传递是导致问题的根本原因。基于安全考虑,我们应该尽早消除它们。在这种情况下,IDEA IntelliJ 会发出警告:
总结
来自另外一种语言获取并且未知可空性的类型称为平台类型。这种类型具有一定的风险,应该被尽早消除,并且不让他们传递下去。其中一个好的做法是在公开的 Java 构造函数、方法和字段上使用注解来指定可空性,因为这些元素对于 Java 和 Kotlin 开发人员来说都是非常宝贵的信息。