Valores null podem (e com certeza vão), em algum momento, resultar em uma NullPointerException. Negligenciar aspectos que podem resultar essas exceptions podem trazer problemas difíceis de serem encontrados e resolvidos. Pense em uma cadeia de objetos que formam uma hierarquia de objetos dependentes. Se qualquer desses objetos permitir um valor null e em algum momento futuro um outro objeto precisar desse valor, uma NullPointerException será lançada. Entretanto, pode ser difícil encontrar o responsável por aquele valor ter se tornado null. Por outro lado, código defensivo para precaver e para lidar com essas exceptions podem tornar o código mais verboso e diminuir a produtividade.
Por isso muitas linguagens modernas possuem uma propriedade chamada Null-Safety (Void-Safety), essa propriedade garante que um objeto nunca será nulo, ou se for nulo exige-se o devido tratamento para não lançar erro. Os melhores exemplos de linguagens com essa propriedade são Kotlin e Swift.
Tomando Kotlin como exemplo, o restante deste texto apresenta as características que um sistema de tipos em uma linguagem deve ter para ser Null-Safety. Essas características podem apresentar leves variações de sintaxe de uma linguagem para a outra, mas o conceito é mesmo.
Verificação em tempo de compilação
Em linguagens Null-Safety, a verificação de nulidade é feita em tempo de compilação, enquanto que em outras linguagens é feita em tempo de execução apenas quando o determinado tipo é requerido, originando assim as exceptions em runtime.
Em linguagens modernas que são Null-Safety, essas exceptions em runtime são convertidas em erros de compilação. Com um sistema de tipos Null-Safety o compilador pode detectar muitos erros possíveis e diminuir drasticamente erros em runtime.
null em Kotlin
Em Kotlin nenhum valor pode ser null a não ser que seja explicitado. Os valores que podem ser null devem ser marcados pelo desenvolvedor e devidamente tratados para que não aconteçam erros.
Veja como declarar duas variáveis em Kotlin, uma que não pode ser null e outra que pode ter a inicialização postergada.
var name: String = null var nameNull: String? = null
A primeira variável é inválida. O compilador avisa que não se pode atribuir null a um valor not-null. Por padrão todas as variáveis em Kotlin são not-null. Enquanto isso, a segunda variável, nameNull, pode receber um valor null pois foi declarada com o operador ‘?’ (interrogação) após o nome do tipo, esse operador indica que essa variável pode receber null.
Toda variável marcada com o operador interrogação, que permite valores null, deve receber tratamento especial quando for utilizada, caso contrário o compilador lança erros de compilação. Por isso a linguagem é Null-Safety, ela te obriga a dizer se uma variável pode ser null e, se pode, toda vez que ela for utilizada será preciso utilizar mecanismos da linguagem para lidar com esse tipo de variável a fim de evitar possíveis erros em tempo de execução.
Operadores para lidar com tipos que podem ser null
Safe call operator ‘?.’. Ele fornece uma verificação antes de chamar um método em um objeto que pode ser null. Por exemplo, veja a diferença para transformar todas as letras da variável nameNull em maiúsculas usando a verificação tradicional e o safe call operator:
// método tradicional de verificação if (nameNull != null) { nameNull = nameNull.toUpperCase() } // safe call operator, se nameNull for null, o resultado será null nameNull = nameNull?.toUpperCase()
Null-coalescing operator ‘?:’. Esse operador faz a verificação de nulidade como o safe call operator, mas ao invés de retornar null, retorna um valor padrão. Por exemplo, veja a diferença para transformar todas as letras da variável nameNull em maiúsculas ou retornar um valor padrão caso nameNull seja null:
// método tradicional de verificação e retorno de valor padrão if (nameNull != null) { nameNull = nameNull.toUpperCase() } else { nameNull = “EMPTY" } // safe call operator, se nameNull for null, o resultado será null nameNull = nameNull?.toUpperCase() ?: “EMPTY”
Safe cast operator ‘as?’. Esse operador faz a verificação de tipos e retorna null caso as variáveis não sejam do mesmo tipo. Veja a implementação de dois casts, o primeiro utilizando a abordagem tradicional e o segundo utilizando o cast operator.
// método tradicional de cast em Kotlin if (nameNull != null && nameNull is String) { newName = nameNull } // cast operator newName = nameNull as? String
Not null assertion ‘!!’. Ao testar o código, como todos esses mecanismos de proteção, um valor null pode passar despercebido e, ao invés de gerar um erro que pare a execução do programa, gere um erro de negócio difícil de identificar. Muitas vezes queremos saber se um valor está null em determinado momento. Pra isso existe o not null assertion, ele lança uma NullPointerException caso o valor seja null. O trecho de código abaixo lança uma exception, pois nameNull não foi iniciada.
// null assertion lança NullPointerException se o valor for null nameNull!!
let function. A let function permite verificar uma expressão, checar o resultado para saber se é null e armazenar o resultado para ser usado. Considere o exemplo abaixo, o primeiro utiliza métodos tradicionais de verificação para enviar um e-mail caso nameNull não seja null, o segundo exemplo utiliza let function para fazer a mesma coisa.
// método tradicional para verificar se um valor é null e realizar uma operação sobre o valor if (nameNull != null) { sendEmail(nameNull) } // let function, se o valor for null, nenhuma operação é realizada nameNull?.let { sendEmail(it) }
Em Kotlin, essas são as principais características para lidar com variáveis que podem ser null e evitar erros em tempo de execução. Essas características são muito semelhantes em outras linguagens que também são Safety-Null pois o conceito é o mesmo, fazer as verificações de nulidade em tempo de compilação.