No Kotlin, qual é a maneira idiomática de lidar com valores anuláveis, referenciando-os ou convertendo-os

177

Se eu tiver um tipo anulável Xyz?, desejo referenciá-lo ou convertê-lo em um tipo não anulável Xyz. Qual é a maneira idiomática de fazer isso no Kotlin?

Por exemplo, este código está errado:

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

Mas se eu marcar nulo primeiro, é permitido, por quê?

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

Como altero ou trato um valor como não nullsem exigir a ifverificação, assumindo que tenho certeza de que nunca é realmente null? Por exemplo, aqui estou recuperando um valor de um mapa que posso garantir que existe e o resultado get()não é null. Mas eu tenho um erro:

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?"

O método get()considera possível que o item esteja ausente e retorne o tipo Int?. Portanto, qual é a melhor maneira de forçar o tipo do valor a não ser anulável?

Nota: esta pergunta é intencionalmente escrita e respondida pelo autor ( Perguntas respondidas automaticamente ), para que as respostas idiomáticas aos tópicos mais comuns do Kotlin estejam presentes no SO. Também para esclarecer algumas respostas realmente antigas escritas para alfas do Kotlin que não são precisas para o Kotlin atual.

Jayson Minard
fonte

Respostas:

296

Primeiro, você deve ler tudo sobre a Segurança Nula no Kotlin, que cobre os casos completamente.

No Kotlin, você não pode acessar um valor anulável sem ter certeza de que não está null( verificando condições nulas ) ou afirmando que certamente não está nullusando o !!operador sure , acessando-o com uma ?.chamada segura ou, finalmente, fornecendo algo que seja possivelmente nullum valor padrão usando o ?:Elvis Operator .

Para o seu primeiro caso na sua pergunta, você tem opções, dependendo da intenção do código, você usaria uma delas e todas são idiomáticas, mas têm 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 "Por que funciona quando marcado nulo", leia as informações de plano de fundo abaixo em transmissões inteligentes .

Para o seu segundo caso na sua pergunta na pergunta com Map, se você como desenvolvedor tiver certeza de que o resultado nunca será null, use o !!operador sure como uma afirmação:

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

ou em outro caso, quando o mapa PODE retornar um valor nulo, mas você pode fornecer um valor padrão, ele Mappróprio possui um 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

Informações básicas:

Nota: nos exemplos abaixo, estou usando tipos explícitos para tornar o comportamento claro. Com a inferência de tipo, normalmente os tipos podem ser omitidos para variáveis ​​locais e membros privados.

Mais sobre o !!operador certo

O !!operador afirma que o valor não é nullou gera um NPE. Isso deve ser usado nos casos em que o desenvolvedor está garantindo que o valor nunca seránull . Pense nisso como uma afirmação seguida por um elenco 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()

leia mais: !! Operador certo


Mais sobre nullverificação e conversão inteligente

Se você proteger o acesso a um tipo anulável com uma nullverificação, o compilador fará a conversão inteligente o valor no corpo da instrução como não anulável. Existem alguns fluxos complicados onde isso não pode acontecer, mas em casos comuns funciona bem.

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

Ou se você fizer um is verificar um tipo não anulável:

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

E o mesmo para expressões 'when' que também lançam com segurança:

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

// or

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

Algumas coisas não permitem que a nullverificação seja transmitida de forma inteligente para o uso posterior da variável. O exemplo acima usa uma variável local que de nenhuma maneira poderia ter mutado no fluxo da aplicação, se valou varesta variável não teve oportunidade de se transformar em umnull . Mas, em outros casos em que o compilador não pode garantir a análise de fluxo, isso seria um erro:

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
    }
}

O ciclo de vida da variável nullableIntnão é completamente visível e pode ser atribuído a partir de outros encadeamentos, a nullverificação não pode ser convertida de maneira inteligente em um valor não nulo. Consulte o tópico "Chamadas seguras" abaixo para obter uma solução alternativa.

Outro caso que não é confiável para uma conversão inteligente não ser mutada é uma valpropriedade em um objeto que possui um getter personalizado. Nesse caso, o compilador não tem visibilidade do que modifica o valor e, portanto, você receberá uma mensagem de erro:

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()
}

leia mais: Verificando condições nulas


Mais sobre o ?.operador Chamada segura

O operador de chamada segura retorna nulo se o valor à esquerda for nulo; caso contrário, continua a avaliar a expressão à direita.

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

Outro exemplo em que você deseja iterar uma lista, mas apenas se não estiver nullvazio, novamente o operador de chamada segura será útil:

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

Em um dos exemplos acima, tivemos um caso em que fizemos uma ifverificação, mas tivemos a chance de outro encadeamento alterar o valor e, portanto, nenhuma conversão inteligente . Podemos alterar este exemplo para usar o operador de chamada segura junto com a letfunção para resolver isso:

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
    }
}

leia mais: Chamadas seguras


Mais sobre o ?:operador Elvis

O operador Elvis permite fornecer um valor alternativo quando uma expressão à esquerda do operador é null:

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

Ele também tem alguns usos criativos, por exemplo, lança uma exceção quando algo é null:

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

ou retornar cedo de uma função:

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

leia mais: Elvis Operator


Operadores nulos com funções relacionadas

O Kotlin stdlib possui uma série de funções que funcionam muito bem com os operadores mencionados acima. Por exemplo:

// 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

Tópicos relacionados

No Kotlin, a maioria dos aplicativos tenta evitar nullvalores, mas nem sempre é possível. E às vezes nullfaz todo sentido. Algumas diretrizes para se pensar:

  • em alguns casos, garante diferentes tipos de retorno que incluem o status da chamada do método e o resultado, se for bem-sucedido. Bibliotecas como Result oferecem um tipo de resultado com êxito ou falha que também pode ramificar seu código. E a biblioteca Promessas para Kotlin, chamada Kovenant, faz o mesmo na forma de promessas.

  • para coleções como tipos de retorno sempre retornam uma coleção vazia em vez de a null, a menos que você precise de um terceiro estado de "não presente". O Kotlin possui funções auxiliares como emptyList()ouemptySet() para criar esses valores vazios.

  • ao usar métodos que retornam um valor nulo para o qual você tem um padrão ou alternativa, use o operador Elvis para fornecer um valor padrão. No caso de um Mapuso, o getOrElse()que permite a geração de um valor padrão em vez do Mapmétodo get()que retorna um valor nulo. O mesmo paragetOrPut()

  • ao substituir métodos de Java nos quais o Kotlin não tem certeza sobre a nulidade do código Java, você sempre pode eliminar a ?nulidade de sua substituição, se tiver certeza de qual deve ser a assinatura e a funcionalidade. Portanto, seu método substituído é maisnull seguro. O mesmo para implementar interfaces Java no Kotlin, altere a nulidade para ser o que você sabe que é válido.

  • observe as funções que já podem ajudar, como para String?.isNullOrEmpty()e String?.isNullOrBlank()que podem operar com segurança em um valor nulo e fazer o que você espera. De fato, você pode adicionar suas próprias extensões para preencher quaisquer lacunas na biblioteca padrão.

  • asserção funciona como checkNotNull()e requireNotNull()na biblioteca padrão.

  • funções auxiliares filterNotNull()que removem valores nulos de coleções ou listOfNotNull()para retornar uma lista de zero ou único item de um nullvalor possível .

  • existe também um operador de conversão Seguro (anulável) que permite que uma conversão para o tipo não anulável retorne nulo se não for possível. Mas não tenho um caso de uso válido para isso que não seja resolvido pelos outros métodos mencionados acima.

Jayson Minard
fonte
1

A resposta anterior é um ato difícil de seguir, mas aqui está uma maneira rápida e fácil:

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

Se realmente nunca for nulo, a exceção não ocorrerá, mas se for, você verá o que deu errado.

John Farrell
fonte
12
val algo: Xyz = createPossiblyNullXyz () !! lançará um NPE quando createPossiblyNullXyz () retornar nulo. É mais simples e segue as convenções para lidar com um valor que você sabe não é nulo
Steven Waterman