Var anulável e elenco inteligente
Considere o seguinte bloco de Kotlin.
var nullableInt: Int? = null
if (nullableInt != null) {
val checkedInt: Int = nullableInt
print("not-null-branch")
} else {
print("null-branch")
}
O Android Studio me diz que o elenco inteligente de Int?para Intnão é possível, pois nullableInté mutável. Eu entendo que isso pode ser um problema no código multithread.
Uma maneira de lidar com o problema é fazer uma conversão explícita com val checkedInt: Int = nullableInt!!, mas se eu usar o codificado em um ambiente multithread, isso não é aconselhável.
Fechar duplicatas
Existem algumas perguntas bem próximas sobre o SO com relação a esse tópico. No entanto, não encontro uma resposta satisfatória em nenhuma das que encontrei:
Em Kotlin, qual é a maneira idiomática de lidar com valores anuláveis, referenciando-os ou convertendo-os, discute por que o problema surge, mas não fornece nenhuma sugestão sobre como lidar com ele
O Kotlin evita o smart cast para verificação de nulo tem um branch if-not-null que retorna um valor não nulo, portanto, a ?.let{} :? {}construção funciona lá. Visto que meu ramo não nulo retorna nulo, ambos os ramos seriam executados.
Kotlin "Smart cast é impossível, porque a propriedade poderia ter sido alterada a esta altura" refere-se apenas a uma ramificação não nula e nenhuma ramificação nula, portanto, a ?.let{}construção parece correta. Nesse tópico, eles fornecem a sugestão de fazer uma cópia local antes da ifdeclaração, o que poderia ser viável no meu caso também. Infelizmente, não é muito elegante e espero que haja outra alternativa.
Existe alguma maneira de lidar com essa ramificação condicional nula de maneira nula segura sem fazer uma cópia?
Eu entendo que a resposta pode ser potencialmente "depende". Se for esse o caso, diga-o e explique o porquê.
Respostas
Use em .letvez de?.let
Como a .letfunção de extensão é definida para todos os tipos, incluindo os anuláveis, você pode realmente chamá-la sem o ?.operador de chamada segura . Quando você faz isso, o lambda sempre será chamado, mesmo para nullvalores. O parâmetro dentro do letbloco será anulável se o receptor for anulável.
No entanto, o parâmetro lambda é elegível para conversão inteligente , porque não é mutável.
Aqui está a diferença:
x.let { it -> /* This always runs. 'it' can be null if 'x' is null */ }
x?.let { it -> /* This only runs if 'x' is not null. 'it' is never null. */ }
Aplicando isso ao seu exemplo, você poderia escrever isto:
var nullableInt: Int? = null
nullableInt.let {
if (it != null) {
doSomethingWith(it)
} else {
doSomethingElse()
}
}
Você não pode iniciar a conversão de uma propriedade porque outro thread pode estar modificando-a. Não há uma maneira lógica de contornar isso. A linguagem já fornece uma maneira insegura de fazer isso com o !!que você mencionou.
Fazer uma cópia local da referência (manualmente ou usando uma função de escopo como withou let) é trivial, portanto, não é algo com que você precise se preocupar.
Minha opinião pessoal é que uma variável local é a maneira mais limpa e legível de fazer isso. Você evita o aninhamento de blocos que teria com funções de escopo.
val myVar = myProp
if (myVar != null) {
} else {
}
Acho que a maneira mais limpa com as funções de escopo é usar with. A leitura é melhor do que letquando você lida com os dois ramos.
with(myProp) {
if (this != null) {
} else {
}
}
Para uma maneira concisa de lidar com os dois ramos, o seguinte funciona. Eu considero alsoser um pouco mais robusto do que letpara esta situação, porque você não pode executar acidentalmente os dois ramos retornando um nulo do primeiro lambda. Mas a legibilidade é prejudicada por ser tão conciso.
myProp?.also {
// not null it
} ?: run {
// null
}