En Kotlin, ¿cuál es la forma idiomática de tratar los valores que aceptan valores NULL, haciendo referencia a ellos o convirtiéndolos?

Resuelto Jayson Minard asked hace 8 años • 4 respuestas

Si tengo un tipo que acepta valores NULL Xyz?, quiero hacer referencia a él o convertirlo a un tipo que no admite NULL Xyz. ¿Cuál es la forma idiomática de hacerlo en Kotlin?

Por ejemplo, este código tiene un error:

val something: Xyz? = createPossiblyNullXyz()
something.foo() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?"

Pero si marco nulo primero, está permitido, ¿por qué?

val something: Xyz? = createPossiblyNullXyz()
if (something != null) {
    something.foo() 
}

¿Cómo cambio o trato un valor como si no nullsin requerir la ifverificación, suponiendo que esté seguro de que realmente nunca es así null? Por ejemplo, aquí estoy recuperando un valor de un mapa que puedo garantizar que existe y el resultado get()no lo es null. Pero tengo un error:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")
something.toLong() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?"

El método get()cree que es posible que falte el elemento y devuelve el tipo Int?. Por lo tanto, ¿cuál es la mejor manera de forzar que el tipo de valor no sea anulable?

Nota: esta pregunta está escrita y respondida intencionalmente por el autor ( Preguntas con respuesta propia ), de modo que las respuestas idiomáticas a los temas más frecuentes de Kotlin estén presentes en SO. También para aclarar algunas respuestas muy antiguas escritas para alfa de Kotlin que no son precisas para el Kotlin actual.

Jayson Minard avatar Dec 29 '15 01:12 Jayson Minard
Aceptado

Primero, debe leer todo sobre Null Safety en Kotlin, que cubre los casos a fondo.

En Kotlin, no se puede acceder a un valor que acepta valores NULL sin estar seguro de que no lo es null( comprobar si hay condiciones nulas ), o afirmar que seguramente no está nullutilizando el !!operador seguro , acceder a él con una ?.llamada segura o, por último, dar algo que posiblemente sea nullun valor predeterminado utilizando el ?:operador de Elvis .

Para el primer caso de su pregunta, tiene opciones según la intención del código. Usaría una de estas, y todas son idiomáticas pero tienen resultados diferentes:

val something: Xyz? = createPossiblyNullXyz()

// access it as non-null asserting that with a sure call
val result1 = something!!.foo()

// access it only if it is not null using safe operator, 
// returning null otherwise
val result2 = something?.foo()

// access it only if it is not null using safe operator, 
// otherwise a default value using the elvis operator
val result3 = something?.foo() ?: differentValue

// null check it with `if` expression and then use the value, 
// similar to result3 but for more complex cases harder to do in one expression
val result4 = if (something != null) {
                   something.foo() 
              } else { 
                   ...
                   differentValue 
              }

// null check it with `if` statement doing a different action
if (something != null) { 
    something.foo() 
} else { 
    someOtherAction() 
}

Para saber "¿Por qué funciona cuando se marca nulo?", lea la información general a continuación sobre transmisiones inteligentes .

Para su segundo caso en su pregunta en la pregunta con Map, si usted, como desarrollador, está seguro de que el resultado nunca será null, use !!el operador sure como afirmación:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")!!
something.toLong() // now valid

o en otro caso, cuando el mapa PODRÍA devolver un valor nulo pero usted puede proporcionar un valor predeterminado, entonces Maptiene un getOrElsemétodo :

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.getOrElse("z") { 0 } // provide default value in lambda
something.toLong() // now valid

Información de contexto:

Nota: en los ejemplos siguientes estoy usando tipos explícitos para aclarar el comportamiento. Con la inferencia de tipos, normalmente los tipos se pueden omitir para variables locales y miembros privados.

Más sobre el !!operador seguro

El !!operador afirma que el valor no es nullo arroja un NPE. Esto debe usarse en los casos en los que el desarrollador garantiza que el valor nunca será null. Piense en ello como una afirmación seguida de un reparto inteligente .

val possibleXyz: Xyz? = ...
// assert it is not null, but if it is throw an exception:
val surelyXyz: Xyz = possibleXyz!! 
// same thing but access members after the assertion is made:
possibleXyz!!.foo()

leer más: !! Operador seguro


Más sobre nullcomprobación y lanzamientos inteligentes

Si protege el acceso a un tipo que acepta valores NULL con una nullmarca, el compilador convertirá de manera inteligente el valor dentro del cuerpo de la declaración para que no admita NULL. Hay algunos flujos complicados en los que esto no puede suceder, pero en casos comunes funciona bien.

val possibleXyz: Xyz? = ...
if (possibleXyz != null) {
   // allowed to reference members:
   possiblyXyz.foo()
   // or also assign as non-nullable type:
   val surelyXyz: Xyz = possibleXyz
}

O si realiza una isverificación de un tipo que no admite valores NULL:

if (possibleXyz is Xyz) {
   // allowed to reference members:
   possiblyXyz.foo()
}

Y lo mismo para las expresiones 'cuándo' que también se transmiten de forma segura:

when (possibleXyz) {
    null -> doSomething()
    else -> possibleXyz.foo()
}

// or

when (possibleXyz) {
    is Xyz -> possibleXyz.foo()
    is Alpha -> possibleXyz.dominate()
    is Fish -> possibleXyz.swim() 
}

Algunas cosas no permiten que la nullverificación realice una conversión inteligente para el uso posterior de la variable. El ejemplo anterior utiliza una variable local que de ninguna manera podría haber mutado en el flujo de la aplicación, ya sea que valesta varvariable no haya tenido oportunidad de mutar a un archivo null. Pero, en otros casos donde el compilador no puede garantizar el análisis de flujo, esto sería un error:

var nullableInt: Int? = ...

public fun foo() {
    if (nullableInt != null) {
        // Error: "Smart cast to 'kotlin.Int' is impossible, because 'nullableInt' is a mutable property that could have been changed by this time"
        val nonNullableInt: Int = nullableInt
    }
}

El ciclo de vida de la variable nullableIntno es completamente visible y puede asignarse desde otros subprocesos; la nullverificación no se puede convertir de manera inteligente en un valor que no admite valores NULL. Consulte el tema "Llamadas seguras" a continuación para obtener una solución alternativa.

Otro caso en el que una conversión inteligente no puede confiar para que no mute es una valpropiedad en un objeto que tiene un captador personalizado. En este caso, el compilador no tiene visibilidad de lo que muta el valor y, por lo tanto, recibirá un mensaje de error:

class MyThing {
    val possibleXyz: Xyz? 
        get() { ... }
}

// now when referencing this class...

val thing = MyThing()
if (thing.possibleXyz != null) {
   // error: "Kotlin: Smart cast to 'kotlin.Int' is impossible, because 'p.x' is a property that has open or custom getter"
   thing.possiblyXyz.foo()
}

leer más: Comprobando condiciones nulas


Más sobre el ?.operador Safe Call

El operador de llamada segura devuelve nulo si el valor de la izquierda es nulo; de lo contrario, continúa evaluando la expresión de la derecha.

val possibleXyz: Xyz? = makeMeSomethingButMaybeNullable()
// "answer" will be null if any step of the chain is null
val answer = possibleXyz?.foo()?.goo()?.boo()

Otro ejemplo en el que desea iterar una lista, pero solo si no está nullvacía, nuevamente el operador de llamada segura resulta útil:

val things: List? = makeMeAListOrDont()
things?.forEach {
    // this loops only if not null (due to safe call) nor empty (0 items loop 0 times):
}

En uno de los ejemplos anteriores, tuvimos un caso en el que hicimos una ifverificación pero tuvimos la posibilidad de que otro hilo mutara el valor y, por lo tanto, no se realizó una conversión inteligente . Podemos cambiar este ejemplo para usar el operador de llamada segura junto con la letfunción para resolver esto:

var possibleXyz: Xyz? = 1

public fun foo() {
    possibleXyz?.let { value ->
        // only called if not null, and the value is captured by the lambda
        val surelyXyz: Xyz = value
    }
}

leer más: Llamadas seguras


Más sobre el ?:operador de Elvis

El operador de Elvis le permite proporcionar un valor alternativo cuando una expresión a la izquierda del operador es null:

val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()

También tiene algunos usos creativos, por ejemplo, generar una excepción cuando algo es null:

val currentUser = session.user ?: throw Http401Error("Unauthorized")

o para regresar temprano de una función:

fun foo(key: String): Int {
   val startingCode: String = codes.findKey(key) ?: return 0
   // ...
   return endingValue
}

leer más: Operador de Elvis


Operadores nulos con funciones relacionadas

Kotlin stdlib tiene una serie de funciones que funcionan muy bien con los operadores mencionados anteriormente. Por ejemplo:

// use ?.let() to change a not null value, and ?: to provide a default
val something = possibleNull?.let { it.transform() } ?: defaultSomething

// use ?.apply() to operate further on a value that is not null
possibleNull?.apply {
    func1()
    func2()
}

// use .takeIf or .takeUnless to turn a value null if it meets a predicate
val something = name.takeIf { it.isNotBlank() } ?: defaultName

val something = name.takeUnless { it.isBlank() } ?: defaultName

Temas relacionados

En Kotlin, la mayoría de las aplicaciones intentan evitar nullvalores, pero no siempre es posible. Y a veces nulltiene mucho sentido. Algunas pautas para pensar:

  • en algunos casos, garantiza diferentes tipos de devolución que incluyen el estado de la llamada al método y el resultado si se realiza correctamente. Bibliotecas como Result le brindan un tipo de resultado de éxito o fracaso que también puede bifurcar su código. Y la biblioteca de Promesas para Kotlin llamada Kovenant hace lo mismo en forma de promesas.

  • para colecciones como tipos de devolución siempre devuelven una colección vacía en lugar de null, a menos que necesite un tercer estado de "no presente". Kotlin tiene funciones auxiliares como emptyList()oemptySet() para crear estos valores vacíos.

  • cuando utilice métodos que devuelvan un valor anulable para el cual tiene un valor predeterminado o alternativo, utilice el operador de Elvis para proporcionar un valor predeterminado. En el caso de un Mapuso getOrElse()que permite generar un valor predeterminado en lugar de Mapun método get()que devuelve un valor que acepta valores NULL. Igual porgetOrPut()

  • Al anular métodos de Java en los que Kotlin no está seguro de la nulidad del código Java, siempre puede eliminar la ?nulidad de su anulación si está seguro de cuál debe ser la firma y la funcionalidad. Por lo tanto, su método anulado es más nullseguro. Lo mismo ocurre con la implementación de interfaces Java en Kotlin, cambie la capacidad de nulidad para que sea lo que sabe que es válido.

  • Mire las funciones que ya pueden ayudar, como for String?.isNullOrEmpty()y String?.isNullOrBlank()que pueden operar con un valor anulable de forma segura y hacer lo que espera. De hecho, puedes agregar tus propias extensiones para llenar cualquier vacío en la biblioteca estándar.

  • La afirmación funciona como checkNotNull()y requireNotNull()en la biblioteca estándar.

  • funciones auxiliares como filterNotNull()las que eliminan valores nulos de las colecciones o listOfNotNull()para devolver una lista de elementos únicos o cero de un nullvalor posible.

  • También existe un operador de conversión segura (que acepta valores NULL) que permite que una conversión a un tipo que no admite NULL devuelva un valor NULL si no es posible. Pero no tengo un caso de uso válido para esto que no se resuelva con los otros métodos mencionados anteriormente.

Jayson Minard avatar Dec 28 '2015 18:12 Jayson Minard

La respuesta anterior es difícil de seguir, pero aquí hay una manera rápida y fácil:

val something: Xyz = createPossiblyNullXyz() ?: throw RuntimeError("no it shouldn't be null")
something.foo() 

Si realmente nunca es nulo, la excepción no ocurrirá, pero si alguna vez lo es, verás qué salió mal.

John Farrell avatar May 10 '2017 05:05 John Farrell