Dart Null Safety: O guia definitivo para Tipos Non-Nullable
A introdução do Null Safety marca um marco importante para a linguagem Dart. O Null Safety ajuda você a evitar uma classe inteira de problemas, capturando erros nulos durante o desenvolvimento e não no tempo de execução.
Este artigo descreve o que mudou e mostra como usar os novos recursos Null Safety por exemplo.
O Null Safety está disponível como uma versão estável com o Flutter 2.0 e é ativado por padrão para todos os projetos criados a partir da versão Flutter 2.2. Você pode tentar com o Dartpad.
Algum contexto
As referências nulas foram introduzidas pela primeira vez em 1965 na linguagem de programação ALGOL e, desde então, foram adotadas pela maioria das linguagens de programação convencionais.
No entanto, os erros nulos são tão comuns que as referências nulas foram chamadas de O erro de um bilhão de dólares.
Então, vamos ver o que mudou no Dart para resolver isso.
Sistema Dart Type
Antes de abordar a Null Safety, vamos falar sobre o sistema do tipo Dart.
Dart é dito ter um sistema de tipo de som. Quando escrevemos código Dart, o verificador de tipos garante que não podemos escrever algo assim:
int age = "hello world"; // Um valor do tipo `String` não pode ser atribuído a uma variável do tipo `int`
Este código produz um erro nos dizendo que “um valor String
não pode ser atribuído a uma variável do tipo int
“.
Da mesma forma, quando escrevemos uma função no Dart, podemos especificar um tipo de retorno:
int square(int value) { return value * value; }
Por causa da segurança de tipo, o Dart pode garantir com 100% de confiança que essa função sempre retorna um int
.
A segurança de tipo nos ajuda a escrever programas mais seguros e a raciocinar mais facilmente sobre o código.
Mas a segurança de tipo por si só não pode garantir que uma variável (ou valor de retorno) não seja null
.
Como resultado, esse código compila, mas gera uma exceção em tempo de execução:
square(null); // Exceção não tratada: NoSuchMethodError: O método '*' foi chamado em null.
Neste exemplo, é fácil identificar o problema. Mas em grandes bases de código é difícil acompanhar o que pode e o que não pode ser null
.
As verificações null
de tempo de execução podem atenuar o problema, mas adicionam mais ruído:
int square(int value) { assert(value != null); // for debugging if (value == null) throw Exception(); return value * value; }
O que realmente queremos aqui é dizer ao Dart que o argumento de valor nunca deve ser null
.
É necessária uma solução melhor – e agora nós a temos. 😎
Segurança do Dart Null: Benefícios
O Dart 2.12 habilita o Sound Null Safety por padrão e traz três benefícios principais:
- Podemos escrever código null-safe com fortes garantias de tempo de compilação. Isso nos torna produtivos porque Dart pode nos dizer quando estamos fazendo algo errado.
- Podemos declarar mais facilmente nossa intenção. Isso leva a APIs que são autodocumentadas e mais fáceis de usar.
- O compilador Dart pode otimizar nosso código, resultando em programas menores e mais rápidos.
Então vamos ver como a Null Safety funciona na prática.
Declarando Variáveis Não Nulas
A principal mudança de idioma é que todos os tipos agora não são anuláveis por padrão.
Isso significa que este código não compila:
void main() { int age; // non-nullable age = null; // Um valor do tipo `Null` não pode ser atribuído a uma variável do tipo 'int' }
Ao usar variáveis não anuláveis, devemos seguir uma regra importante:
Variáveis não nulls devem sempre ser inicializadas com valores non-null.
Se você raciocinar nesse sentido, será mais fácil entender todas as novas mudanças de sintaxe.
Vamos revisitar este exemplo:
int square(int value) { return value * value; }
Aqui, tanto o argumento de valor quanto o valor de retorno agora são garantidos como não null
.
Como resultado, verificações null
em tempo de execução não são mais necessárias e esse código agora produz um erro em tempo de compilação:
square(null); // O tipo de argumento 'Null' não pode ser atribuído ao tipo de parâmetro 'int'
Mas se todos os tipos agora são não anuláveis por padrão, como podemos declarar variáveis anuláveis?
Declarando Variáveis Nuláveis
O ? símbolo é o que precisamos:
String? name; // inicializado como null por padrão int? age = 36; // inicializado para não nulo age = null; // pode ser reatribuído a null
Observação: você não precisa inicializar uma variável anulável antes de usá-la. Ele é inicializado como null por padrão.
Aqui estão algumas outras maneiras de declarar variáveis anuláveis:
// nullable function argument void openSocket(int? port) { // port can be null } // nullable return type String? lastName(String fullName) { final components = fullName.split(' '); return components.length > 1 ? components.last : null; } // using generics T? firstNonNull<T>(List<T?> items) { // returns first non null element in list if any return items.firstWhere((item) => item != null); }
Dica: você pode declarar variáveis anuláveis em qualquer lugar do seu código com o ? sintaxe.
Variáveis anuláveis são uma boa maneira de expressar a ausência de um valor, e isso é útil em muitas APIs.
Ao projetar uma API, pergunte a si mesmo se uma variável deve ser anulável ou não e declare-a de acordo.
Mas há casos em que sabemos que algo não pode ser nulo, mas não podemos provar para o compilador. Nesses casos, o operador de asserção pode ajudar.
O operador de afirmação
Podemos usar o operador de asserção ! para atribuir uma expressão anulável a uma variável não anulável:
int? maybeValue = 42; int value = maybeValue!; // valid, value is non-nullable
Ao fazer isso, estamos dizendo ao Dart que talvezValue não é nulo e é seguro atribuí-lo a uma variável não anulável.
Observe que aplicar o operador de asserção a um valor nulo lançará uma exceção de tempo de execução:
String? name; print(name!); // NoSuchMethodError: '<Unexpected Null Value>' print(null!); // NoSuchMethodError: '<Unexpected Null Value>'
Quando suas suposições estão erradas, o ! operador leva a exceções de tempo de execução.
Às vezes, precisamos trabalhar com APIs que retornam valores anuláveis. Vamos revisitar a função lastName:
String? lastName(String fullName) { final components = fullName.split(' '); return components.length > 1 ? components.last : null; }
Aqui o sistema de tipos não pode ajudar. Se soubermos que a função retornará um valor não nulo para um determinado argumento, devemos atribuí-lo a uma variável não anulável o mais rápido possível.
Isso é feito com o ! operador:
// prefer this: String last = lastName('Marcos Luiz')!; // to this: String? last = lastName('Marcos Luiz');
Resumindo:
- Tente criar variáveis não anuláveis quando possível, pois elas terão a garantia de não serem
nulas
em tempo de compilação. - Se você sabe que uma expressão anulável não será nula, você pode atribuí-la a uma variável não anulável com o
!
operador.
Análise de fluxo: promoção
O Dart pode facilitar sua vida levando em consideração verificações nulas
em variáveis anuláveis:
int absoluteValue(int? value) { if (value == null) { return 0; } // if we reach this point, value is non-null return value.abs(); }
Aqui usamos uma instrução if
para retornar antecipadamente se o argumento de valor for nulo.
Além desse ponto, o valor
não pode ser nulo
e é tratado (ou promovido) para um valor não anulável. Portanto, podemos usar com segurança value.abs(
) em vez de value?.abs()
(com o operador null-aware).
Da mesma forma, podemos lançar uma exceção se o valor for nulo
:
int absoluteValue(int? value) { if (value == null) { throw Exception(); } // if we reach this point, value is non-null return value.abs(); }
Mais uma vez, o valor
é promovido para um valor não anulável e o operador com reconhecimento de nulo ?.
Não é necessário.
Resumindo:
- Use verificações nulas iniciais para retornar antecipadamente ou lançar exceções
- Após verificações nulas, as variáveis anuláveis são promovidas para não anuláveis
E depois que uma variável anulável foi marcada como nula, o Dart permite que você a use como uma variável não anulável, o que é muito bom.
int sign(int x) { int result; // non-nullable print(result.abs()); // invalid: 'result' must be assigned before it can be used if (x >= 0) { result = 1; } else { result = -1; } print(result.abs()); // ok now return result; }
Contanto que uma variável não anulável receba um valor antes de ser usada, o Dart está feliz.
Usando variáveis não anuláveis com classes
Variáveis de instância em classes devem ser inicializadas se não forem anuláveis:
class BaseUrl { String hostName; // O campo de instância não anulável 'hostName' deve ser inicializado int port = 80; // ok }
Se uma variável de instância não anulável não puder ser inicializada com um valor padrão, defina-a com um construtor:
class BaseUrl { BaseUrl(this.hostName); String hostName; // now valid int port = 80; // ok }
Argumentos nomeados e posicionais não anuláveis
Com Null Safety, argumentos nomeados não anuláveis devem sempre ser obrigatórios ou ter um valor padrão.
Isso se aplica a métodos regulares, bem como a construtores de classe:
void printAbs({int value}) { // 'value' não pode ter um valor nulo devido ao seu tipo e nenhum valor padrão não nulo é fornecido print(value.abs()); } class Host { Host({this.hostName}); // 'hostName' não pode ter um valor nulo devido ao seu tipo e nenhum valor padrão não nulo é fornecido final String hostName; }
Podemos corrigir o código acima com o novo modificador obrigatório, que substitui a antiga anotação @required:
void printAbs({required int value}) { print(value.abs()); } class Host { Host({required this.hostName}); final String hostName; }
E quando usamos as APIs acima, o Dart pode nos dizer se estamos fazendo algo errado:
printAbs(); //O parâmetro nomeado 'value' é obrigatório, mas não há argumento correspondente printAbs(value: null); // O tipo de argumento 'Null' não pode ser atribuído ao tipo de parâmetro 'int' printAbs(value: -5); // ok final host1 = Host(); // O parâmetro nomeado 'hostName' é obrigatório, mas não há argumento correspondente final host2 = Host(hostName: null); // O tipo de argumento 'Null' não pode ser atribuído ao tipo de parâmetro 'String' final host3 = Host(hostName: "example.com"); // ok
Os parâmetros posicionais estão sujeitos às mesmas regras:
Conteudo
class Host { Host(this.hostName); // ok final String hostName; } class Host { Host([this.hostName]); // O parâmetro 'hostName' não pode ter um valor 'null' devido ao seu tipo, e nenhum valor padrão não nulo é fornecido final String hostName; } class Host { Host([this.hostName = "www.capsistema.com.br"]); // ok final String hostName; } class Host { Host([this.hostName]); // ok final String? hostName; }
Entre variáveis anuláveis e não anuláveis, argumentos nomeados e posicionais, valores obrigatórios e padrão, há muito o que entender. Se você estiver confuso, lembre-se da regra de ouro:
Variáveis não nulas devem sempre ser inicializadas com valores não nulos.
Para entender completamente todos os recursos de Null Safety, pratique usá-los com o Dartpad. O Dart dirá se você está fazendo algo errado – então leia as mensagens de erro com atenção. 🔍
Operador em cascata com null-aware
Para lidar com a Null Safety, o operador cascata agora ganha uma nova variante null-aware: ?…
Exemplo:
Path? path; // não fará nada se o caminho for nulo path ?..moveTo(0, 0) ..lineTo(0, 2) ..lineTo(2, 2) ..lineTo(2, 0) ..lineTo(0, 0);
As operações em cascata acima só serão executadas se o path
não for null
.
O operador de cascata com reconhecimento de nulo pode causar curto-circuito, portanto, apenas um operador ?..
é necessário no início da sequência.
Operador de subscrito com null-awere
Até agora, verificar se uma coleção era nula antes de usar o operador subscrito era verbose:
int? first(List<int>? items) { return items != null ? items[0] : null; // verificação nula para evitar erros nulos em tempo de execução }
O Dart 2.9 apresenta o null
operador com reconhecimento de ?[]
, o que torna isso muito mais fácil:
int? first(List<int>? items) { return items?[0]; }
A palavra-chave ‘late’
Use a palavra-chave late
para inicializar uma variável quando ela for lida pela primeira vez, em vez de quando for criada.
Um bom exemplo é ao inicializar variáveis em initState()
:
class ExampleState extends State { late final TextEditingController textEditingController; @override void initState() { super.initState(); textEditingController = TextEditingController(); } }
Melhor ainda, initState()
pode ser removido completamente:
class ExampleState extends State { // late - será inicializado quando usado pela primeira vez (no método build) late final textEditingController = TextEditingController(); }
É comum usar late
em combinação com final
, para adiar a criação de variáveis somente leitura para quando elas forem lidas pela primeira vez.
Isso é ideal ao criar variáveis cujo inicializador faz algum trabalho pesado:
late final taskResult = doHeavyComputation();
Quando usado dentro de um corpo de função, late
e final
podem ser usados assim:
void foo() { late final int x; x = 5; // ok x = 6; // A variável local final tardia já está definitivamente inicializada }
Embora eu não recomende usar variáveis tardias dessa maneira. Porque esse estilo pode resultar em erros de tempo de execução não óbvios. Exemplo:
class X { late final int x; void set1() => x = 1; void set2() => x = 2; } void main() { X x = X(); x.set1(); print(x.x); x.set2(); // LateInitializationError: O campo 'x' já foi inicializado print(x.x); }
Conteudo
Ao declarar uma variável non-nullable late
, prometemos que ela não será nula em tempo de execução, e o Dart nos ajuda com algumas garantias em tempo de compilação.
Mas eu recomendo usar o late com moderação e sempre inicializar as variáveis late quando forem declaradas.
Variáveis estáticas e globais
Todas as variáveis globais agora devem ser inicializadas quando são declaradas, a menos que estejam late
:
int global1 = 42; // ok int global2; // A variável não anulável 'global2' deve ser inicializada late int global3; // ok
O mesmo se aplica a variáveis de classe estáticas:
class Constants { static int x = 10; // ok static int y; // A variável não anulável 'y' deve ser inicializada static late int z; // ok }
Mas como eu disse antes, eu não recomendo usar late
dessa forma, pois pode levar a erros de tempo de execução.
Conclusão
Null Safety é uma grande mudança para a linguagem Dart e ajuda você a escrever um código melhor e mais seguro, desde que você o use corretamente.
Para novos projetos, lembre-se de todas as alterações de sintaxe que abordamos. Isso pode parecer muito para absorver, mas tudo se resume a isso:
Toda vez que você declarar uma variável no Dart, pense se ela deve ser anulável ou não. Este é um trabalho extra, mas levará a um código melhor e o Dart o ajudará ao longo do caminho.
Para projetos existentes criados antes do Null Safety, as coisas são um pouco mais complicadas e você precisa migrar seu código primeiro. Isso envolve várias etapas que devem ser seguidas na ordem correta. Aqui está um estudo de caso prático mostrando como fazer isso: