Olá pessoal continuando o tema Kotlin, hoje iremos ver como funciona o sistema de Null Safety do Kotlin, e entender como funciona este poderoso recurso.
Trabalhando com Instâncias
Antes de nos aprofundarmos no null safety, precisamos entender o porque as principais linguagens de programação sofrem com o famoso null.
Quando instanciamos uma classe e criamos um novo objeto na memória, temos acesso a este objeto a partir de uma referência, ou seja, esta referência aponta para um endereço da memória onde se encontra o objeto.
Então basicamente, concluímos que o acesso a um objeto é feito a partir de uma referência, e caso esta esteja nula, o famoso NullPointerException acontece.
Agora que já temos uma base do problema, vamos entender como o Kotlin trouxe uma forma segura de lidar com isto.
Hieraquia de Tipos em Kotlin
Para entendermos como o Kotlin lida com NullSafety, antes vamos precisamos entender sobre declaração de variáveis, e sobre a classe Any, que é a super class de todos os objetos em Kotlin.
Criando Variáveis
Em Kotlin temos várias maneiras de criar variáveis, onde podemos especificar o tipo da variável, ou usar a inferência de tipos, que é quando o compilador especifica o tipo da variável de acordo com o valor que esta sendo atribuído.
fun main(){ //Tipo String atribuido através de inferência de tipos val name = "Kotlin" //Tipo String atribuido especificando o tipo da variável val address:String = "Avenida" //Tipo Language atribuido através de inferência de tipos val language = Language(1, "Kotlin") //Tipo Language atribuido especificando o tipo da variável val language2:Language = Language(2, "Java") println("$name - $address - $language - $language2") }
Um fator importante, é saber a diferença entre var e val, em Kotlin, a declaração val significa que a variável só pode ser declarada uma unica vez, ou seja, é uma variável imutável, já a declaração usando var permite a redeclaração, portanto é uma variável mutável, onde o seu valor pode ser alterado.
O Tipo Any
Todo objeto em Kotlin possui como super class a classe Any, ou seja, similar ao Java onde todas as classes herdam Object, em Kotlin, a classe Any é a nossa super class.
Classe Any, da biblioteca padrão do Kotlin (https://kotlinlang.org/) |
Para entender como isso funciona, vamos criar uma classe chamada Language e criar algumas instâncias:
data class Language(val id:Long, val name:String) fun main(){ val client = Language(1, "Kotlin") val client2:Any = Language(2, "Java") println(client) println(client2) }
No exemplo acima, criamos uma classe chamada Language e criamos 2 instâncias, uma chamada client onde a linguagem faz a inferência de tipos para Language, e uma chamada client2, onde atribuímos a instância a um tipo Any.
Na client2, perceba que forçamos o cast up para Any, isso é possível porque "toda classe em Kotlin é um tipo Any".
A instância da variável client2 é uma Language, mas como estamos vendo a como um tipo Any não temos acesso as propriedades id e name, esta é uma situação comum em linguagem OO.
Um ponto importante de lembrar, é que caso o objeto seja do tipo null, sua super class será Any?, iremos entender a diferença entre os tipos no decorrer do tutorial.
Entendendo os tipos null e non-null
Agora vamos realizar uma modificação no código anterior, para entendermos as diferenças entre tipos null e non-null.
fun main(){ var client = Language(1, "Kotlin") var client2:Any = Language(2, "Java") client = null client2 = null println(client) println(client2) }
Ao executarmos esse código teremos o seguinte erro:
Null can not be a value of a non-null type Language
Null can not be a value of a non-null type Any
O compilador nos diz: "Não pode setar null para um tipo non-null", a primeira vista isso parece estranho, pois estamos apenas setando null para um tipo, mas isso acontece porque estamos tentando atribuir null a uma variavel non-null.
Os Tipos Non-null
Como o próprio nome diz, um tipo non-null é aquele que NÃO permite valores null, ou seja, o seu valor sempre será uma referência válida, isso é importante porque conseguimos ter uma segurança contra as NullPointerExceptions.
Para termos um tipo non-null não é necessário realizar nenhuma ação, pois os tipos non-null são os tipos padrões da linguagem, portanto quando temos Any, String, Int, Double, são todos non-null.
Um outro ponto interessante, é que caso atribuirmos null para um tipo non-null, o compilador irá acusar o erro de compilação, e isso é muito bom pois evita erros em Runtime.
fun main(){ val name = "Kotlin" val language:String = "Java" val year = 2021 var month:Int = 2 println("$name - $language - $year - $month") }
Acima vemos algumas declarações de variáveis non-null, onde podemos informar o tipo explicitamente, ou utilizar da inferências de tipos.
Os Tipos Null
Os tipos null são aqueles que podem conter valores nulos, em Kotlin, um tipo null deve ser definido usando ? no final do tipo, por exemplo: String?, Int?, Any?, Double?, etc.
fun main(){ /* * Tipo val definido como String? * Declarado logo na inicialização * Vale lembrar que val não ter sua referencia alterada após o valor ser declarado */ val name:String? = "Kotlin" println(name) /* * Tipo val definido como Double? * Na declaração setamos o tipo, mas não atribuimos o valor * Atribuimos o valor posteriormente * Vale lembrar que val não ter sua referencia alterada após o valor ser declarado */ val money:Double? money = 100.50 println(money) //Tipo var pode ter seu valor ao alterado var age:Int? age = 30 println(age) //Tipo var pode ter seu valor ao alterado var year:Int? = null year = 2021 println(year) }
Acima temos algumas fomas de se criar tipos null, o uso de tipos null fica a critério do desenvolvedor, portanto, você deve analisar em sua modelagem se uma determinada variável deve ou não conter null como valor.
Agora vamos entender como podemos lidar com valores null e non-null dentro do fluxo de um sistema, vamos conhecer algumas funções e operadores que a linguagem fornece para manipular ambos os tipos.
Lidando com null e non-null
É comum analisarmos códigos onde são utilizados estruturas condicionais para determinar se um objeto esta nulo ou não, isto é ruim porque deixa o código menos legível, mas em Kotlin temos várias formas de lidar com este problema.
Operador de Chamada Segura
O Operador de chamada de segura é sem dúvida oque você vai mais utilizar no dia a dia, ele é apenas o sinal de ? que deve usado em chamadas de métodos em variáveis de tipos null.
fun main(){ val name:String? = "Kotlin" println(name?.toUpperCase()) val age:Int? = null println(age?.toBigDecimal()) }
No segundo exemplo, perceba que age:Int? esta nulo, aqui em outras linguagens receberíamos uma NullPointerException, mas em Kotlin isso não acontece, porque o método toBigDecimal() não será chamado, pois ao utilizar o operador de chamada segura o método só será invocado caso o valor NÃO seja nulo.
Operador Elvis
O operador Elvis garante que caso a expressão a esquerda resulte em null, o código após o operador ?: seja executado, sua sintaxe lembra o operador ternário, mas é exclusivo para utilização em casos onde uma expressão pode resultar em null.
Sintaxe do operador Elvis. |
data class Car(val name:String, val value:Double?) fun main(){ //Instancia e value com valores val car1 = Car("BMW", 20000.00) val result1 = car1.value ?: "Não tem valor definido." println("Car1 tem o valor: $result1") //Instancia com value null val car2 = Car("Audi", null) val result2 = car2.value ?: "Não tem valor definido." println("Car2 tem o valor: $result2") //Instancia null val car3:Car? = null val result3 = car3?.value ?: "Não tem valor definido." println("Car3 tem o valor: $result3") }
No exemplo acima, o Elvis foi utilizado em 3 diferentes situações, onde nas 2 primeiras, o valor que pode resultar em null é o campo value da classe Car.
No primeiro exemplo, caso a chamada car1.value seja null, o resultado atribuído a variável result1 será a mensagem "Não tem valor definido", mas aqui a propriedade value tem o valor de 20000.00, portanto, este será o valor atribuído a variável result1.
Já na chamada car2.value, a propriedade value esta null, portanto, a mensagem "Não tem valor definido" será atribuída a variável result2.
Aqui neste ultimo exemplo, a variável car3 é do tipo null, portanto, já sabemos que iremos utilizar o operador de chamada segura para chamar os métodos a partir de car3, como o operador Elvis testa se expressão resulta em null, e a variável car3 está nula, o valor atribuído a result3 será "Não tem valor definido".
Passagem de Argumentos
Agora que já sabemos a diferença entre os tipos null e non-null, temos que analisar algumas regras utilizadas na passagem de argumentos para funções e métodos.
Basicamente temos as seguintes regras:
- Uma referência non-null pode ser atribuída para uma null;
- Uma referência null não pode ser atribuída a uma non-null.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | fun showBook(book:Book) = println(" O livro é ${book.name}") fun showBookOrMessage(book:Book?){ val result = book?.let{ " O livro é ${it.name}" } ?: "Livro não existe" println(result) } fun main(){ val book = Book(1, "Kotlin em Ação") showBook(book) showBookOrMessage(book) val book2:Book? = null //Erro de Compilação //showBook(book2) showBookOrMessage(book2) } |
Em nosso exemplo temos duas funções, uma chamada showBook que recebe um Book como parâmetro, e outra showBookOrMessage que recebe um Book?.
Respeitando a regra de passagem de argumentos, sabemos que a função showBookOrMessage pode receber um Book ou Book?, e a função showBook somente um tipo Book, esta é a forma com que o Kotlin garante a segurança para trabalhar com tipos null e non-null em chamadas de funções e métodos.
Operador Not-Null
Um outro operador que devemos conhecer é o Not-Null, ele permite que possamos converter um tipo null para non-null, sua declaração acontece usando o !! na chamada que desejamos mudar o valor.
Um ponto importante, é que caso o operador !! seja utilizado em um local tenha o valor null, irá provocar uma NullPointerException, porque ao usarmos o !!, informamos ao compilador que "este local não contém um valor null" e qualquer problema irá acontecer em Runtime.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | fun showMessageSize(str:String){ println("A mensagem possui ${str.length} caracteres") } fun main(){ val message1 = "Kotlin para Backend" showMessageSize(message1) val message2:String? = "Programando em Kotlin" showMessageSize(message2!!) val message3:String? = null showMessageSize(message3!!) } |
Aqui temos uma função chamada showMessageSize, esta recebe uma String como parâmetro, portanto, uma String? não seria válida como argumento, nesta situação podemos utilizar o !! e forçar que nossa String? possa ser vista como uma String.
Na linha 14, usamos o operador !! para dizer que nossa variável message2, que é do tipo String? seja convertida para String, dessa forma podemos encaminhá-la como argumento a função showMessageSize, aqui tudo acontece como o esperado, pois a varável message2 possui um valor válido, mesmo tendo sido declarada como uma String?.
Na linha 18 temos um exemplo similar ao anterior, mas aqui a variável message3 esta nula, e como usamos o operador !! para forçar a passagem do argumento como uma String, na função showMessageSize irá acontecer uma NullPointerException, pois estamos chamando o método length em uma referência nula.
Conclusão
Com certeza um dos principais recursos do Kotlin é o Null Safety, isso torna o código muito mais simples e garante uma integridade com relação a referência de objetos.
Comentários
Postar um comentário