Pular para o conteúdo principal

Recursos da Linguagem Kotlin - Null Safety

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

Como sabemos, o uso do operador é sempre necessário quando vamos chamar um método de uma variável que pode ser nula, no primeiro exemplo, a variável name apesar de ser uma String? contém o valor Kotlin, portanto, neste caso o método toUppercase() será chamado normalmente.

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.

Elvis testando uma propriedade que pode ser null.

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.

Código Fonte

Referências

Comentários

Postagens mais visitadas do Blog