我如何告诉 Kotlin 数组或集合不能包含空值?

How can I tell Kotlin that an array or collection cannot contain nulls?

如果我创建一个数组,然后填充它,Kotlin 认为数组中可能有空值,并强制我考虑这一点

val strings = arrayOfNulls<String>(10000)
strings.fill("hello")
val upper = strings.map { it!!.toUpperCase() } // requires it!!
val lower = upper.map { it.toLowerCase() } // doesn't require !!

创建一个填充数组没有这个问题

val strings = Array(10000, {"string"})
val upper = strings.map { it.toUpperCase() } // doesn't require !!

如何告诉编译器 strings.fill("hello") 的结果是一个 NonNull 数组?

无法将此告知编译器。变量的类型在声明时就确定了。在这种情况下,变量被声明为可以包含空值的数组。

fill()方法不声明新变量,只是修改已有变量的内容,所以不会引起变量类型的改变。

一条经验法则:如果有疑问,请明确指定类型(对此有一个特殊的重构):

val strings1: Array<String?> = arrayOfNulls<String>(10000)
val strings2: Array<String>  = Array(10000, {"string"})

因此您看到 strings1 包含可为空的项,而 strings2 不包含。只有那个决定了如何使用这些数组:

// You can simply use nullability in you code:
strings2[0] = strings1[0]?.toUpperCase ?: "KOTLIN"

//Or you can ALWAYS cast the type, if you are confident:
val casted = strings1 as Array<String>

//But to be sure I'd transform the items of the array:
val asserted = strings1.map{it!!}
val defaults = strings1.map{it ?: "DEFAULT"}

为什么填充数组工作正常

填充数组在调用期间从用作第二个参数的 lambda 推断数组的类型:

val strings = Array(10000, {"string"})

产生Array<String>

val strings  = Array(10000, { it -> if (it % 2 == 0) "string" else null })

产生Array<String?>

因此,更改 = 左侧与 lambda 不匹配的声明没有任何帮助。有冲突就报错

如何使 arrayOfNulls 工作

对于 arrayOfNulls 问题,他们将您指定给调用的类型 arrayOfNulls<String> 在函数签名中用作泛型类型 T 和函数 arrayOfNulls returns Array<T?> 这意味着可以为空。您的代码中没有任何内容会更改该类型。 fill 方法仅将值设置到现有数组中。

要将此可空元素数组转换为非可空元素列表,请使用:

val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings = nullableStrings.filterNotNull()
val upper = strings.map { it.toUpperCase() } // no !! needed

这很好,因为您的 map 调用无论如何都会转换为列表,所以为什么不事先转换。现在取决于数组的大小,这可能是高效的,如果在 CPU 缓存中,副本可能会很快。如果它很大而且没有性能,你可以让它变得懒惰:

val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings = nullableStrings.asSequence().filterNotNull()
val upper = strings.map { it.toUpperCase() } // no !! needed

或者您可以通过复制来保留数组,但这实际上没有任何意义,因为您可以使用 map:

撤消它
val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings: Array<String> = Array(nullableStrings.size, { idx -> nullableStrings[idx]!! })

数组在 Java 或 Kotlin 代码中并不常见(JetBrains 研究了统计数据),除非代码进行了非常低级的优化。使用列表可能会更好。

考虑到您最终可能会得到列表,也许也可以从这里开始并放弃数组。

val nullableStrings = listOf("a","b",null,"c",null,"d")
val strings =  nullableStrings.filterNotNull()

但是,如果你不能停止使用数组的任务,并且真的必须在没有副本的情况下施放一个...

您始终可以编写一个函数来做两件事:首先,检查所有值是否不为空,如果是,则 return 转换为非空的数组。这有点 hacky,但之所以安全,只是因为区别在于可空性。

首先,在 Array<T?> 上创建一个扩展函数:

 fun <T: Any> Array<T?>.asNotNull(): Array<T> {
    if (this.any { it == null }) {
        throw IllegalStateException("Cannot cast an array that contains null")
    }
    @Suppress("CAST_NEVER_SUCCEEDS")
    return this as Array<T>
 }

然后使用这个函数 new 函数进行转换(元素检查为非空转换):

val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings = nullableStrings.asNotNull() // magic!
val upperStrings = strings.map { it.toUpperCase() } // no error

但即使谈论最后一个选项我也觉得很脏。