Var anulável e elenco inteligente

Aug 30 2020

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

3 Sam Aug 30 2020 at 11:35

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()
    }
}
2 Tenfour04 Aug 30 2020 at 12:57

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
}