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

前端 未结 3 1194
野趣味
野趣味 2021-01-20 15:03

If I create an array, then fill it, Kotlin believes that there may be nulls in the array, and forces me to account for this

val strings = arrayOfNulls

        
相关标签:
3条回答
  • 2021-01-20 15:39

    There is no way to tell this to the compiler. The type of the variable is determined when it is declared. In this case, the variable is declared as an array that can contain nulls.

    The fill() method does not declare a new variable, it only modifies the contents of an existing one, so it cannot cause the variable type to change.

    0 讨论(0)
  • 2021-01-20 15:39

    Why the filled array works fine

    The filled array infers the type of the array during the call from the lambda used as the second argument:

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

    produces Array<String>

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

    produces Array<String?>

    Therefore changing the declaration to the left of the = that doesn't match the lambda does not do anything to help. If there is a conflict, there is an error.

    How to make the arrayOfNulls work

    For the arrayOfNulls problem, they type you specify to the call arrayOfNulls<String> is used in the function signature as generic type T and the function arrayOfNulls returns Array<T?> which means nullable. Nothing in your code changes that type. The fill method only sets values into the existing array.

    To convert this nullable-element array to non-nullable-element list, use:

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

    Which is fine because your map call converts to a list anyway, so why not convert beforehand. Now depending on the size of the array this could be performant or not, the copy might be fast if in CPU cache. If it is large and no performant, you can make this lazy:

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

    Or you can stay with arrays by doing a copy, but really this makes no sense because you undo it with the map:

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

    Arrays really are not that common in Java or Kotlin code (JetBrains studied the statistics) unless the code is doing really low level optimization. It could be better to use lists.

    Given that you might end up with lists anyway, maybe start there too and give up the array.

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

    But, if you can't stop the quest to use arrays, and really must cast one without a copy...

    You can always write a function that does two things: First, check that all values are not null, and if so then return the array that is cast as not null. This is a bit hacky, but is safe only because the difference is nullability.

    First, create an extension function on 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>
     }
    

    Then use this function new function to do the conversion (element checked as not null cast):

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

    But I feel dirty even talking about this last option.

    0 讨论(0)
  • 2021-01-20 15:45

    A rule of thumb: if in doubts, specify the types explicitly (there is a special refactoring for that):

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

    So you see that strings1 contains nullable items, while strings2 does not. That and only that determines how to work with these arrays:

    // 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"}
    
    0 讨论(0)
提交回复
热议问题