Dart 02 – Exceções, Classes e Mais

Este artigo é a parte [part not set] de 3 na série Dart

Neste segundo e último artigo da Linguagem Dart é apresentado os tópicos Exceções, Classes, Operador Cascata, Construtores, Construtores Nomeados, Getters e Setters, Static, Extends, Modificador Abstract, Interface e Polimorfismo, Sobrescrevendo Métodos e por fim Enum, uma forma sofisticada de criar constantes enumeradas no Dart.

Exceções

Exceções são erros indesejáveis que podem ocorrer no código, seja por uma entrada indevida de dados, erro de comunicação ou mesmo problema no código. No Dart é possível criar exceções e tratá-las adequadamente usando comandos como o throw, catch e try, apresentados a seguir.

Sentença try, catch e finally

A forma do Dart de capturar as exceções é com a sentença try e eventualmente um catch para coletar exceções inesperadas. Sua sintaxe é apresentada a seguir:

try {
  <comandos>;
} on <Exceção 1> {
  <comandos 1>;
} on <Exceção 2> {
  <comandos 2>;
 ...
} catch(e) {
  <comandos c>;
} finally {
  <comandos f>;
}

Caso o código em <comandos> gerem uma exceção esta é comparada às exceções filtradas, <Exceção 1>, <Exceção 2>, …, e o bloco de <comandos ??> correspondente é executado. Caso a exceção gerada não for nenhuma das filtradas anteriormente o catch recebe a exceção em e e o bloco <comandos c> será executado. Um print(e) imprime o toString() da exceção capturada pelo catch.

O último bloco de comandos, na sentença finally, será executo sempre, independentemente do sucesso ou não do código no primeiro bloco da sentença try.

O código a seguir mostra o bloco try em ação com a geração de uma exceção por uma divisão por zero na linha 3.

void main() {
  try {
    int resultado = 10 ~/ 0;
    print(resultado);
    
  } catch (e) {
    print('Erro: $e');
    print('Erro runtimeType: ${e.runtimeType}');
  }

  print('Final do main...');
}

Como o erro ocorre dentro do primeiro bloco do try, este é coletado pelo catch e as linhas 7 e 8 são executadas. Este código vai gerar a saída:

Erro: Unsupported operation: Result of truncating division is Infinity: 10 ~/ 0
Erro runtimeType: UnsupportedError
Final do main...

Um ponto importante a observar aqui é o runtimeType retornado desta exceção: UnsupportedError. Esse é o tipo da exceção gerada e pode ser empregada para filtrar com sentenças on <Exceção> {...

O código a seguir implementa duas exceções em sentenças on no try, nas linhas 9 e 12:

void main() {
  try {
    int resultado = 10 ~/ 4;
    print(resultado);
    
    int num = int.parse('123XX');
    print(num);
    
  } on UnsupportedError {
    print('Não é possível dividir por zero!');
    
  } on FormatException catch (e){
    print('String não pode ser convertido em num: "$e"');
    
  } catch (e) {
    print('Erro: $e');
    print('Erro runtimeType: ${e.runtimeType}');
    
  } finally {
    print('Final do try...');
  }

  print('Final do main...');
}

A linha 9 apenas filtra a exceção UnsupportedError e imprime uma mensagem caso ela ocorra. A linha 12 além de filtrar a exceção FormatException ela também coleta a exceção com um catch (e), na mesma sentença, para incrementar uma mensagem personalizada para o erro.

Na linha 19 foi adicionado a sentença finally para impressão de uma mensagem de final do bloco try. O bloco finally sempre será executado, tendo o código do bloco principal do try passado com ou sem exceções.

A saída deste código agora será:

2
String não pode ser convertido em num: "FormatException: 123XX"
Final do try...
Final do main...

Exceções Personalizadas e o Comando throw

Uma exceção no Dart pode ser criada declarando uma classe que implemente a classe Exception.

Como exemplo, imagine que tenhamos uma função de divisão o qual não pode dividir números negativos. Neste caso uma exceção como DivisaoPorNumeroNegativo deve ser gerada. O código a seguir implementa este problema:

void main() {
  try {
    print(divide(3, -2));
    
  } catch (e) {
    print(e);
  }
}

int divide(int a, int b) {
  if (b <= 0) {
    throw DivisaoPorNumeroNegativo();
  }

  int r = a ~/ b;
  return r;
}

class DivisaoPorNumeroNegativo implements Exception {
  @override
  String toString() {
    return 'Esta classe não suporta divisão por negativos';
  }
}

A Exceção é criada com a classe DivisaoPorNumeroNegativo nas linhas 19 à 24. O método toString() foi implementado apenas para criar uma saída personalizada a exceção gerada.

A função divide, linhas 10 à 17, apenas retorna a divisão entre dois inteiros. Mas na linha 11 ela verifica se o divisor é menor ou igual a zero e levanta a exceção com o comando throw, linha 12.

A função main emprega um try para filtrar e testar a exceção criada. A saída deste código será:

Esta classe não suporta divisão por negativos

Classes

O Dart é uma linguagem orientada a objetos com classes e herança baseada em mixin. Todo objeto é uma instância de uma classe e todas as classes descendem da classe Object, com exceção a objeto Null.

A herança baseada em mixin significa que, embora cada classe (exceto a classe principal, Object?) tenha exatamente uma superclasse, o corpo de uma classe pode ser reutilizado em várias hierarquias de classes.

Atributos

Os atributos de uma classe são declarados como a declaração de variável no escopo da classe:

class nomeClasse {
  tipo <atributo1>;
  tipo <atributo2> = valor;
  tipo? <atributo3>;
  ...
} 

O atributo pode ser declarado como:

  • não iniciado – neste caso terá de criar um construtor para iniciá-lo ou o código não compila;
  • iniciado – basta declarar um valor inicial para o atributo;
  • nullable – neste caso tem de adicionar uma interrogação após o tipo da variável.

Estes três de declaração de atributo estão representado na sintaxe acima.

O código a seguir implementa uma classe para representar um ponto na tela, classe Point:

void main() {
  Point a = Point();
  a.x = 10;
  a.y = 20;
  
  print('Point (${a.x}, ${a.y})');
}

class Point {
  int x = 0;
  int y = 0;
}

A classe Point é declarada nas linhas 10 à 18. Os atributos da classe x e y são iniciados com zeros nas linhas 11 à 13, pois fazer estes atributos nullable neste momento não me pareceu uma opção interessante.

Na linha 2 um objeto Point é instanciado na variável a. Os atributos deste objeto são iniciados nas linhas 3 e 4, acessados com o operador ponto (.), como em outras linguagens de programação. A linha 6 acessa os atributos do objeto Point e gera a saída abaixo no terminal:

Point (10, 20)

Métodos

Os métodos são funções da classe que geram propriedades e ações específicas da classe. Como exemplo a classe Point pode ter um método para retornar a distância do ponto a origem, e a outro ponto. Estes métodos são implementados no código a seguir:

import 'dart:math';
  
void main() {
  Point a = Point();
  a.x = 10;
  a.y = 20;
  
  Point b = Point();
  b.x = 40;
  b.y = 30;
  
  print('a: $a');
  print('b: $b');
  print('Distance a to b: ${a.distanteToPoint(b)}');
  print('Distance a to Origin: ${a.distanceToOrigin()}');
  print('Distance b to Origin: ${b.distanceToOrigin()}');
}

class Point {
  int x = 0;
  int y = 0;
  
  int distanceToOrigin() {
    return sqrt(x*x + y*y).round();
  }
  
  int distanteToPoint(Point other) {
    return sqrt(pow(x-other.x, 2) + pow(y-other.y,2)).round();
  }
  
  @override
  String toString() {
    return 'Point ($x $y)';
  }
}

O método distanceToOrigin, linhas 23 à 25, retorna a distância do ponto até a origem do sistema de coordenadas empregando o teorema de Pitágoras. O método round() é empregado para retornar um inteiro do double retornado pela função raiz quadrada, sqrt. Já o método distanteToPoint, linhas 27 à 29, retorna a distância do ponto até um segundo ponto passado como argumento other para a função.

Por fim, o método toString() é reescrito para formatar a saída do print usada nas linhas 12 e 13. O decorador “@override” é empregado apenas para sinalizar que o método toString() é uma rescrita do método de mesma assinatura da superclasse Objetc.

A saída deste código é apresentada abaixo:

a: Point (10 20)
b: Point (40 30)
Distance a to b: 32
Distance a to Origin: 22
Distance b to Origin: 50

Operador Cascata (..)

O operador cascata (..) permite que se faça várias operações a um objeto e retorne uma cópia do objeto alterado. Por exemplo, o Point do código anterior poderia ser iniciado com o operador cascata como segue:

import 'dart:math';
  
void main() {
  Point a = Point()..x = 10 ..y = 20;
  
  Point b = Point()
    ..x = 40
    ..y = 30;
  
  print('a: $a');
  print('b: $b');
}

class Point {
  int x = 0;
  int y = 0;
  
  int distanceToOrigin() {
    return sqrt(x*x + y*y).round();
  }
  
  int distanteToPoint(Point other) {
    return sqrt(pow(x-other.x, 2) + pow(y-other.y,2)).round();
  }
  
  @override
  String toString() {
    return 'Point ($x $y)';
  }
}

O operado é acionado em sequência, sem ponto de vírgula ou vírgula, justamente para ser aplicado a cada elemento. Na linha 4 o processo é apresentado em uma única linha, no entanto é de praxe quebrar em diversas linhas a aplicação do operado cascata para deixar a operação mais clara, linhas 6 à 8.

Na sequência a classe Point() é instanciada, em seguida o atributo x é iniciado e por fim o elemento y. Ao final o objeto é retornado para a variável b.

O código a seguir emprega o operador cascata em uma instância da classe List:

import 'dart:math';
  
void main() {
  List<int> lista = [2,6,4,0,4,2,4,7,9,6,4];
  
  List<int> lista2 = lista
    ..sort()
    ..remove(4)
    ..remove(4)
    ..remove(4)
    ..add(10);
  print(lista2); // imprime [0, 2, 2, 4, 6, 6, 7, 9, 10]
}

Ne ordem em que são aplicados os operadores em cascata é feito em lista um sort() dos elementos, na sequência remove três elementos 4, adiciona o elemento 10 e, por fim, retorna esta lista alretada para a lista2.

Observe que embora o método List().add não possui retorno a lista é retornada por referência para lista2.

Construtor

O construtor é uma forma de iniciar uma instância de uma classe de forma mais prática. Geralmente os construtores da classe são declarados no início da classe, mas nada impede qu seja feito após.

Os códigos a seguir mostram diferentes formas de criar um construtor padrão para a classe Point. No primeiro as variáveis xi e yi são coletadas no construtor e passadas para os seus atributos.

  Point(int xi, int yi) {
    x = xi;
    y = yi;
  } 

Uma segunda forma seria passando variáveis de mesmo nome dos atributos. Neste caso é necessário identificar com a keyword this os atributos da classe, para diferir das variáveis passadas.

  Point(int x, int y) {
    this.x = x;
    this.y = y;
  } 

Por fim uma forma mais simplificada e direta de implementar o mesmo código anterior seria escrever:

  Point(this.x, this.y);

Neste último caso os valores passados ao construtor são atribuídos diretamente aos atributos da classe. Anda é possível abrir colchetes e iniciar outras variáveis ou outros comandosque necessários:

  Point(this.x, this.y) {
    print('Object created...');
    ...
  };

O código a seguir o método distanceToOrigin é transformado em um atributo iniciado de dentro do construtor da classe.

import 'dart:math';
  
void main() {
  Point a = Point(10, 20);
  
  Point b = Point(40, 30);
  
  print('a: $a');
  print('b: $b');
  print('Distance a to b: ${a.distanteToPoint(b)}');
  print('Distance a to Origin: ${a.distanceToOrigin}');
  print('Distance b to Origin: ${b.distanceToOrigin}');
}

class Point {
  Point(this.x, this.y) {
    _distanceToOrigin();
  }
  
  int x = 0;
  int y = 0;
  late int distanceToOrigin;
  
  void _distanceToOrigin() {
    distanceToOrigin = sqrt(x*x + y*y).round();
  }
  
  int distanteToPoint(Point other) {
    return sqrt(pow(x-other.x, 2) + pow(y-other.y,2)).round();
  }
  
  @override
  String toString() {
    return 'Point ($x $y)';
  }
}

Para esta implementação a declaração “late int distanceToOrigin” foi adicionada na linha 22. O late é necessário para que o Dart Analyser compreenda que a variável distanceToOrigin será iniciada posteriormente, mas antes de ser acessada. Outra forma de resolver isto seria declarar o atributo distanceToOrigin como nullable, trocando a linha 22 por “int? distanceToOrigin“, ou simplemente iniciando-o com algum valor como zero.

O construtor do Point é declarado na linha 16 da forma simplificada, no entanto os colchetes são abertos para iniciar a variável distanceToOrigin chamando o método _distanceToOrigin().

No Dart atributos e métodos iniciados como um underline são considerados privados e não devem ser chamados externamente à classe. Bom, isto é uma convenção e não uma proibição, ou seja, é possível acessar o método point._distanceToOrigin() mas não é recomendável.

Construtores com Parâmetros Nomeados, Posicionais, …

Existe ainda uma forma de declarar parâmetros nomeados e não posicionais usando um colchetes entorno dos parâmetros. Para isto considere uma classe Pessoa que contenha os atributos nome e idade:

void main() {
  Pessoa p0 = Pessoa(nome: 'Vera', idade: 38);
  Pessoa p1 = Pessoa(idade: 25, nome: 'Sérgio');
  
  print(p0);
  print(p1);
}

class Pessoa {
  Pessoa({required this.nome, required this.idade});
  
  String nome;
  int idade;
  
  @override
  String toString() => ' Nome: $nome\nIdade: $idade\n';
}

Também é possível usar colchetes para parâmetros posicionais e opcionais. A bem da verdade, todas as discussões sobre parâmetros nomeáveis/não nomeáveis, posicionais/não posicionais e obrigatórios/opcionais feitos na seção Parâmetros de Funções, no artigo anterior, são válidos para a declaração dos construtores de uma classe, visto que estes também são funções.

Construtores Nomeados

Construtores nomeados são métodos construtores especiais definidos para iniciar a classe e, por isto, são acessíveis apenas pela classe e não por suas instâncias. Estes construtores são chamados com o operador ponto diretamente na classe, como NomeDaClasse.ConstrutorNomeado.

O código a seguir implementa um construtor nomeado para iniciar um Point diretamente de um elemento Json.

import 'dart:math';
import 'dart:convert';

void main() {
  Point a = Point(10, 20);
  
  String jsonPoint = '{"x": 40, "y": 30}';
  var jsonB = json.decode(jsonPoint);

  Point b = Point.fromJson(jsonB);

  print('a: $a');
  print('b: $b');
  print('Distance a to b: ${a.distanteToPoint(b)}');
  print('Distance a to Origin: ${a.distanceToOrigin}');
  print('Distance b to Origin: ${b.distanceToOrigin}');
}

class Point {
  Point(this.x, this.y) { 
    _distanceToOrigin();
  }

  Point.fromJson(var js) {
    x = js['x'];
    y = js['y'];
    _distanceToOrigin();
  }

  int x = 0;
  int y = 0;
  late int distanceToOrigin;

  void _distanceToOrigin() => distanceToOrigin = sqrt(x * x + y * y).round();
  
  int distanteToPoint(Point other) => sqrt(pow(x - other.x, 2) + pow(y - other.y, 2)).round();

  @override
  String toString() => 'Point ($x $y)';
}

O construtor fromJson é declarado nas linhas 24 à 28, iniciando os atributos da classe Point pelos elementos passados pela variável js. O ponto no formato Json é criado nas linhas 7 e 8, sendo a primeira uma string para representar o Json e a segunda a conversão da string em um Json na variável jsonB. A conversão é feita com o método json.decode do pacote dart:convert, importado na linha 2.

Para reduzir o código passei para declaração curta os métodos _distanceToOrigin(), distanteToPoint() e toString().

Getters e Setters

Getters e Setters são métodos especiais que provem acesso de escrita e leitura em atributos da classe que se deseja proteger.

O código a seguir implementa get e o set do distanceToOrigin:

import 'dart:math';
import 'dart:convert';

void main() {
  Point a = Point(30, 40);

  print('a: $a');
  print('Distance a to Origin: ${a.distanceToOrigin}');
  
  print('');

  a.distanceToOrigin = 100;
  print('a: $a');
  print('Distance b to Origin: ${a.distanceToOrigin}');
}

class Point {
  Point(this.x, this.y);

  Point.fromJson(var js) {
    x = js['x'];
    y = js['y'];
  }
  
  int x = 0;
  int y = 0;

  int get distanceToOrigin => sqrt(x * x + y * y).round();

  set distanceToOrigin(int distance) {
    double angle = atan(y / x);
    x = (distance * cos(angle)).round();
    y = (distance * sin(angle)).round();
  }

  int distanteToPoint(Point other) =>
      sqrt(pow(x - other.x, 2) + pow(y - other.y, 2)).round();

  @override
  String toString() => 'Point ($x $y)';
}

O get é implementado na linha 28, pela fórmula de Pitágoras, como antes. O set é implementado nas linhas 30 à 34, empregando um pouco de trigonometria para calcular os novos catetos.

Com o get e o set declarados, distanceToOrigin passa a se comportar como um atributo da classe podendo ser lido e escrito como um.

Operações de getter e setter devem ser feita com cautela. Lembre-se de que estas não são acessos a atributos de função e sim a um código que pode apresentar diferentes resultados a cada consulta. Este não é o caso deste código point5.dart, já que o retorno muda apenas se os atributos da classe forem alterados.

Modificador Static

O modificador static faz com que o método pertença apenas a classe e não aos objetos instanciados da classe. Desta forma o atributo/método declarado como static é acessível apenas pela classe.

No código a seguir foram removidos alguns trechos do código em point5.dart apenas para salientar a adição do modificador static:

void main() {
  Point a = Point(30, 40);
  // print(a.version);
  print(Point.version);
}

class Point {
  Point(this.x, this.y);
  
  int x = 0;
  int y = 0;
  static String version = '1.0.12';
  
  @override
  String toString() => 'Point ($x $y)';
}

Na linha 12 foi adicionado o atributo version para a classe Point. Este atributo foi declarado como static e por isto ele não é alcançável por uma instância de Point (linha 3) e sim pela classe (linha 4).

Também é possível declarar um método static e através deste método acessar atributos e outros métodos statics, mas não atributos do objeto.

void main() {
  Point a = Point(30, 40);
  // print(a.version);
  print(Point.version);
}

class Point {
  Point(this.x, this.y);

  int x = 0;
  int y = 0;
  static String version = '1.0.12';

  static changeVersion(String newVersion) {
    version = newVersion;
    // x++;
  }
  
  @override
  String toString() => 'Point ($x $y)';
}

Nas linhas 14 à 17 foi adicionado o método static changeVersion(). A linha 16 gera erro se descomentada pois ela tenta acessar/alterar um atributo do objeto.

O contrário é possível, o objeto pode acessar o valor de um atributo static. Para isto vou transformar o atributo version em um atributo da classe, e uma atributo baseVersion para um atributo static:

void main() {
  Point a = Point(30, 40);
  
  Point.baseVersion = '1.2.16';
  Point b = Point(30, 40);
  
  print('a: $a - v${a.version}');
  print('b: $b - v${b.version}');
}

class Point {
  Point(this.x, this.y);

  int x = 0;
  int y = 0;
  static String? staticBaseVersion;
  final String _version = staticBaseVersion ?? '1.0.0';
  
  static set baseVersion(String version) {
    staticBaseVersion = version;
  }
  
  static String get baseVersion => staticBaseVersion!;
  
  String get version => _version;
  
  @override
  String toString() => 'Point ($x $y)';
}

Neste código usei o staticBaseVersion como o atributo static e implementei os métodos static set (linhas 19 à 21) e get (linha 23) para alterá-lo. Um método get também foi declarado para acessar o valor do atributo privado _version, linha 25. Outro ponto interessante neste código e que o Dart Analyzer identifica que o atributo privado _version é alterado apenas na sua criação e por isto deve ser declarado como uma constante do tipo final.

Obviamente não havia necessidade de implementar os Getters e o Setter, apenas aproveitei a oportunidade para por em prática estes conceitos. O ponto importante do código é que uma vez ajustado o staticBaseVersion, linha 4, todos os objetos instanciados da classe Point serão instanciados com a versão alterada. Executando este código ele irá imprimir:

a: Point (30 40) - v1.0.0
b: Point (30 40) - v1.2.16

Herança – extends

No Dart a herança é promovida estendendo a classe principal com a keyword extends. Por exemplo a classe Pessoa, no código pessoa0.dart, pode ser estendida para criar as classes PessoaFisica e PessoaJuridica como segue:

void main() {
  Pessoa p0 = Pessoa(nome: 'Vera', idade: 6);
  Pessoa p1 = PessoaFisica(idade: 25, nome: 'Sérgio', cpf: '123.456.789-12');
  PessoaJuridica p2 = PessoaJuridica(nome: 'Samira', idade: 35, cpf: '234.567.890-23', cnpj: '24.764.785/0001-54');
  
  print(p0);
  
  print(p1);
  p1.incrementaIdade();
  
  print(p2);
  p2.incrementaIdade();
  p2.abrirImpresa();
}

class Pessoa {
  Pessoa({required this.nome, required this.idade});
  
  String nome;
  int idade;
  
  void incrementaIdade() { 
    idade++;
    print('$idade anos. Feliz abiversário $nome!\n');
  }
  
  @override
  String toString() => ' Nome: $nome\nIdade: $idade\n';
}

class PessoaFisica extends Pessoa {
  PessoaFisica({required super.nome, required super.idade, required this.cpf});
  late final String cpf;
  
  @override
  String toString() => '${super.toString()}  CPF: $cpf\n';
  
  void vaTrabalha() => print('$nome indo para o trabalho...\n');
}

class PessoaJuridica extends PessoaFisica {
  PessoaJuridica({required super.nome, required super.idade, required super.cpf, required this.cnpj});
  late final String cnpj;
  
  @override
  String toString() => '${super.toString()} CNPJ: $cnpj\n';
  
  void abrirImpresa() => print('$nome abrindo a empresa.\n');
}

Keyword Super

A keyword super é usada para se referir a classe herdada, classe mãe ou superclasse como é chamado no Dart. Nas linhas 22 e 26, da classe PessoaFisica, o super se refere a classe Pessoa, já nas linhas 30 e 34, da classe PessoaJuridica, o super se refere a classe PessoaFisica.

Um fato interessante sobre o Dart é que como PessoaFisica e PessoaJuridica são subclasses da classe Pessoa é possível declarar um objeto Pessoa e iniciar com o construtor de PessoaFisica ou PessoaJuridica. No entanto, isto cria algumas limitações sobre o alcance do objeto instanciado. No código acima, na linha 3, p1 é uma instância de Pessoa, mas empregou o construtor de PessoaFisica para instanciá-lo. Isto faz com que p1 tenha acesso apenas aos métodos e atributos compartilhados pela superclasse Pessoa, ou seja, o método vaTrabalha() não é alcançável por p1, mas p1.nome, idade, e incrementaIdade() são, pois fazem parte da superclasse Pessoa. Entretanto, os métodos sobrescritos da classe PessoaFisica são empregados no lugar dos métodos da classe Pessoa.

Em resumo, na linha 3 p1 é declarado como Pessoa mas instanciado como PessoaFisica, portanto os atributos de Pessoa são alcançáveis por p1, bem como os métodos de PessoaFísica se forem sobrescritos aos de Pessoa.

A saída do código é apresentada abaixo.

 Nome: Vera
Idade: 6

 Nome: Sérgio
Idade: 25
  CPF: 123.456.789-12

26 anos. Feliz abiversário Sérgio!

 Nome: Samira
Idade: 35
  CPF: 234.567.890-23
 CNPJ: 24.764.785/0001-54

36 anos. Feliz abiversário Samira!

Samira abrindo a empresa.

Este comportamento ocorre como uma proteção do Dart Analyzer, uma vez que o sistema não sabe se o seu objeto é uma Pessoa, PessoaFisica ou PessoaJuridica, já que ambas herdam de Pessoa. Uma forma de contornar isto é fazendo uma consulta para identificar a classe do objeto.

O código a seguir mostra este ponto.

void main() {
  Pessoa p0 = Pessoa(nome: 'Vera', idade: 6);
  Pessoa p1 = PessoaFisica(idade: 25, nome: 'Sérgio', cpf: '123.456.789-12');
  PessoaJuridica p2 = PessoaJuridica(nome: 'Samira', idade: 35, cpf: '234.567.890-23', cnpj: '24.764.785/0001-54');
  
  print(p0);
  
  print(p1);
  p1.incrementaIdade();
  if (p1 is PessoaFisica) {
    print('O CPF de ${p1.nome} é ${p1.cpf}\n');
    p1.vaTrabalha();
  }
  
  print(p2);
  p2.incrementaIdade();
  p2.abrirImpresa();
}

...

Neste código omiti as declarações das classes Pessoa, PessoaFisica e PessoaJuridica, já que estas não são alteradas. A consulta à classe é feita na linha 10 com o operador is. Este operador verifica se p1 é uma instância de PessoaFisica. Sendo esta pode acessar os métodos e tributos de PessoaFisica, linha 11 e 12. Isto irá imprimir:

O CPF de Sérgio é 123.456.789-12

Sérgio indo para o trabalho...

Obviamente este código apenas explora esta característica, no entanto o próximo traz uma aplicação mais adequada.

O código a seguir explora este polimorfismo da superclasse através de uma lista de objetos do tipo Pessoa:

 void main() {
  Pessoa p0 = Pessoa(nome: 'Vera', idade: 6);
  PessoaFisica p1 = PessoaFisica(idade: 25, nome: 'Sérgio', cpf: '123.456.789-12');
  PessoaJuridica p2 = PessoaJuridica(nome: 'Samira', idade: 35, cpf: '234.567.890-23', cnpj: '24.764.785/0001-54');
  
  List<Pessoa> listaPessoas = [];
  
  listaPessoas
    ..add(p0)
    ..add(p1)
    ..add(p2);
  
  Pessoa px = listaPessoas.last;
  if (px is PessoaJuridica) {
    px.abrirImpresa();
  }
}

...

Neste caso p0, p1 e p2 são instâncias de Pessoa, PessoaFisica e PessoaJuridica, respectivamente. A linha 6 é criada uma lista de Pessoa, listaPessoas, onde os elementos p0, p1 e p2 são adicionados em cascata, linhas 8 à 11.

Para acessar na totalidade as propriedades de um dado elemento da lista, por exemplo o último elemento da lista, é necessário identificar o seu tipo base, o que é feito no if da linha 14. Uma vez identificado o Dart Analyser permite que as demais propriedades do tipo identificado sejam acessadas, linha 15.

Para terminar simplifiquei os construtores para atributos posicionais e obrigatórios e usei o super para invocar os construtores das classes herdadas, ou invés de atributos herdados, linhas 40 e 50.

void main() {
  Pessoa p0 = Pessoa('Vera', 6);
  PessoaFisica p1 = PessoaFisica('Sérgio', 25, '123.456.789-12');
  PessoaJuridica p2 = PessoaJuridica('Samira', 35, '234.567.890-23', '24.764.785/0001-54');

  List<Pessoa> listaPessoas = [];

  listaPessoas
    ..add(p0)
    ..add(p1)
    ..add(p2);

  for (Pessoa pessoa in listaPessoas) {
    if (pessoa is PessoaJuridica) {
      pessoa.abrirImpresa();
    } else if (pessoa is PessoaFisica) {
      pessoa.vaTrabalha();
    } else {
      print('${pessoa.nome} é uma criança.\n');
    }
  }
}

class Pessoa {
  Pessoa(this.nome, this.idade);

  String nome;
  int idade;

  void incrementaIdade() {
    idade++;
    print('$idade anos. Feliz abiversário $nome!\n');
  }

  @override
  String toString() => ' Nome: $nome\nIdade: $idade\n';
}

class PessoaFisica extends Pessoa {
  PessoaFisica(nome, idade, this.cpf) : super(nome, idade);
  late final String cpf;

  @override
  String toString() => '${super.toString()}  CPF: $cpf\n';

  void vaTrabalha() => print('$nome indo para o trabalho...\n');
}

class PessoaJuridica extends PessoaFisica {
  PessoaJuridica(nome, idade, cpf, this.cnpj) : super(nome, idade, cpf);
  late final String cnpj;

  @override
  String toString() => '${super.toString()} CNPJ: $cnpj\n';

  void abrirImpresa() => print('$nome abrindo a empresa.\n');
}

Modificador Abstract

O modificador abstract cria classes que não podem ser instanciadas, mas podem ser empregados para estender classes derivadas.

No código a seguir é definido uma classe abstrata Mamiferos, e duas subclasses: Gatos e Caes:

void main() {
  Caes c0 = Caes('Odie', 3);
  Gatos g0 = Gatos('Garfield', 6);
  
  print(c0);
  print(g0);
  c0.latir();
  g0.miar();
  c0.dormir();
  g0.dormir();
  c0.comer();
  g0.comer();
}

abstract class Mamiferos {
  String nome;
  int idade;
  late int _mamas;
  
  Mamiferos(this.nome, this.idade);
  
  void comer() => print('$nome: Comendo...');
  
  void dormir() => print('$nome: Dormindo...');
  
  set mamas(int mamas) => _mamas = mamas;
  
  int get mamas => _mamas;
  
  @override
  String toString() => 'Mamifero $nome de $idade anos.';
}

class Gatos extends Mamiferos {
  int vidas;
  
  Gatos(nome, idade, [this.vidas = 7]) : super(nome, idade) {
     mamas = 6;
    if (vidas > 7 || vidas < 0) {
      vidas = 7;
    }
  }
  
  @override
  String toString() => 'Gato $nome de $idade anos (ainda possui $vidas vidas)';
  
  void miar() => print('$nome: Miau....');
  
  @override
  void dormir() => print('$nome: Dormindo e ronronando...');
}

class Caes extends Mamiferos {
  int tolice;
  
  Caes(nome, idade, [this.tolice = 100]) : super(nome, idade) {
    mamas = 8;
  }
  
  @override
  String toString() => 'Cachorro $nome de $idade anos (nível de tolice ${tolice}%)';
  
  void latir() => print('$nome: AU AU AU AU ...');
  
  @override
  void comer() => print('$nome: Comendo e bagunçando.');
}

Para deixar o código mais curto optei por declarações curtas nas funções. No entanto o código é bem simples: A classe Mamiferos, linhas 15 à 32, é uma ótima candidata para uma classe abstrata, uma vez que não temos pets mamíferos e sim cães, gatos, pássaros, … As classes Gatos, linhas 34 à 51, e a classe Caes, linhas 53 à 67, são extensões da classe Mamiferos, ou seja herdam os atributos e métodos de Mamiferos. Alguns métodos de Mamiferos são sobreposto, override, para gerar uma saída customizada para a classe em questão, como toString(), dormir() e comer().

A saída deste código é apresentada a seguir:

Cachorro Odie de 3 anos (nível de tolice 100%)
Gato Garfield de 6 anos (ainda possui 7 vidas)
Odie: AU AU AU AU ...
Garfield: Miau....
Odie: Dormindo...
Garfield: Dormindo e ronronando...
Odie: Comendo e bagunçando.
Garfield: Comendo...

Uma classe abstract também pode ser usada como um assinatura de métodos e atributos para outras classes estendidas implementarem. Desta forma uma classe abstract pode funcionar como uma interface.

void main() {
  Caes c0 = Caes('Odie', 3);

  print(c0);
  c0.dormir();
  c0.comer();
}

abstract class Mamiferos {
  String nome;
  int idade;

  Mamiferos(this.nome, this.idade);

  void comer();

  void dormir();
}

class Caes extends Mamiferos {
  int tolice;

  Caes(nome, idade, [this.tolice = 100]) : super(nome, idade);

  @override
  void comer() => print('Cachorro $nome comendo...');

  @override
  void dormir() => print('Cachorro $nome dormindo...');
}

Observe que a classe Mamiferos agora possui apenas as assinaturas dos métodos comer e dormir. Desta forma o Dart Analyser não permite a compilação até que estes métodos sejam implementados nas subclasses que estendem Mamiferos, neste caso a classe Caes. Isto é feito nas linhas 26 e 29.

Interface e Polimorfismo

Embora isto funcione como uma interface o Dart possui uma forma explicita de declarar interfaces com a cláusula implements. De uma forma geral é de praxe criar uma classe abstrata, como a classe interface, com as assinaturas dos métodos e atributos a serem implementados pelas classes que implementem a interface. As subclasses que implementam a interface devem satisfazer aos requisitos da interface, implementando todos os métodos e atributos solicitados. Em geral não se adiciona atributos a classe interface, mas é possível fazê-lo no Dart.

O código a seguir implementa uma interface Server, linhas 9 à 13, o qual deve possuir três métodos: get address, reader e writer. As classes que implementarem esta interface, FTPServer e EmailServer, implementam os métodos da classe interface, linhas 15 à 53. Este contrato é assinado pela keyword implements, colocada após do nome da classe a ser implementada, linhas 15 e 35.

void main() {
  FTPServer FTP = FTPServer('ftp.usp.br');
  EmailServer Email = EmailServer('gmail.com');

  comunicate('Hello', Email);
  comunicate('ld-linux.so', FTP);
}

abstract class Server {
  String get address;
  String reader();
  bool write(String value);
}

class FTPServer implements Server {
  FTPServer(this._address);
  final String _address;

  @override
  String get address => _address;

  @override
  String reader() {
    print('reader: $_address');
    return 'reading... $_address';
  }

  @override
  bool write(String value) {
   print(' write: $_address: "$value"');
   return true;
  }
}

class EmailServer implements Server {
  EmailServer(this._address);
  final String _address;

  @override
  String get address => _address;

  @override
  String reader() {
    print('reader: $_address');
    return 'reading $_address...';
  }

  @override
  bool write(String value) {
    print(' write: $_address "$value"');
    return true;
  }
}

bool comunicate(String send, Server server) {
  print('='*40);
  print('Server: ${server.address}');
  print('  Send: $send');
  print('${'-'*12} Server Response ${'-'*11}');
  server.reader();
  if (server.write('Server Test...')) {
    print('Server ${server.address}: ok...');
  }
  print('${'='*40}\n');
  return true;
}

No final do código é acrescentado uma função polimórfica, que responde a objetos instanciados pelas classes FTPServer e EmailServer. A saída deste código é apresentada a seguir.

========================================
Server: gmail.com
  Send: Hello
------------ Server Response -----------
reader: gmail.com
 write: gmail.com "Server Test..."
Server gmail.com: ok...
========================================

========================================
Server: ftp.usp.br
  Send: ld-linux.so
------------ Server Response -----------
reader: ftp.usp.br
 write: ftp.usp.br: "Server Test..."
Server ftp.usp.br: ok...
========================================

Também é possível a uma classe implementar várias interfaces. Isto se faz apenas adicionando as diversas classes interfaces, após a keyword implements, separadas por vírgula.

O próximo código implementa as classes abstratas Paint e Shape para serem usadas como interfaces nas subclasses Cube e Sphere. Na sequência duas funções polimorfas são implementadas: allVolume e allSurface. Estas funções retornam o volume total e a área total, respectivamente, de uma lista de objetos Shape passados para as funções.

import 'dart:math';

void main() {
  Sphere s0 = Sphere(15);
  Cube c0 = Cube(0.10, 'm', 'red', true);

  List<Shape> shapes = [s0, c0];

  print('Surface: ${allSurface(shapes).round()} cm²');
  print(' Volume: ${allVolume(shapes).round()} cm³');
  print('Cube c0 is ${c0.color} (alpha ${c0.alpha})');
}

abstract class Paint {
  set color(String color);
  String get color;
  set alpha(bool alpha);
  bool get alpha;
}

abstract class Shape {
  late String unit;
  double surface();
  double volume();
}

class Cube implements Paint, Shape {
  Cube(this.edge, [this.unit = 'cm', this._color = 'black', this._alpha = false]);

  bool _alpha;
  String _color;
  double edge;
  @override
  String unit;

  @override
  set alpha(bool alpha) => _alpha = alpha;

  @override
  bool get alpha => _alpha;

  @override
  set color(String color) => _color = color;

  @override
  String get color => _color;

  @override
  double surface() => 6 * pow(edge, 2).toDouble();

  @override
  double volume() => pow(edge, 3).toDouble();
}

class Sphere implements Shape {
  Sphere(this.radius, [this.unit = 'cm']);

  double radius;
  @override
  String unit;

  @override
  double surface() => 4 * pi * pow(radius, 2);

  @override
  double volume() => (4 / 3) * pi * pow(radius, 3);
}

double allSurface(List<Shape> shapes) {
  double surface = 0;

  for (Shape shape in shapes) {
    if (shape.unit == 'cm') {
      surface += shape.surface();
    } else {
      surface += shape.surface() * 1e4;
    }
  }

  return surface;
}

double allVolume(List<Shape> shapes) {
  double volume = 0;

  for (Shape shape in shapes) {
    if (shape.unit == 'cm') {
      volume += shape.volume();
    } else {
      volume += shape.volume() * 1e6;
    }
  }

  return volume;
}

Observe que a classe Cube implementa as duas interfaces Paint e Shape, linha 27. A saída deste código é apresentada a seguir:

Surface: 3427 cm²
 Volume: 15137 cm³
Cube c0 is red (alpha true)

Sobrescrevendo Métodos noSuchMethod(), toString()

Subclasses podem sobrescrever instâncias de métodos, getters e setters. Do Dart um método sobrescrito deve sinalizado com o decorador @overide antes da declaração do método. Métodos sobrescritos devem possuir a mesma assinatura que o método da superclasse e, por consequência, o mesmo retorno.

Nos exemplos de código apresentados neste texto um dos métodos mais sobrescrito foi o método toString(). Este método pertence a classe Object e ele gera uma representação string do objeto.

Um outro método interessante de se implementar uma sobrescrita é o método noSuchMethod(). Este método é invocado sempre que se tenta acessar um atributo ou método inexistente na classe. Em geral o Dart Analyzer irá detectar tais erros, no entanto, em variáveis declaracas como dynamic isto pode ser tornar um problema, já que a verificação estática é desabilitada.

O código a seguir imprementa a classe People com o método noSuchMethod, linhas 23 à 25. Observe que na instanciação do objeto p0 é declarado do tipo dynamic. Por conta disto p0 aceita a consulta a qualquer método/atributo, ainda que não declarado. Isto é feito na linha 33, onde é solicitado o uso do método add do objeto p0.

import 'package:intl/intl.dart';

class People {
  String name;
  DateTime birthDate;
  double? weight;
  double? height;

  final DateFormat formatter = DateFormat('dd/MM/yyyy');

  People(this.name, this.birthDate);

  @override
  String toString() {
    return 
      '        Nome: $name\n'
      '  Nascimento: ${formatter.format(birthDate)}\n'
      '      Altura: ${height}m\n'
      '        Peso: ${weight}kg\n';
  }

  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ${invocation.memberName}');
  }
}

void main() {
  dynamic p0 = People('Rogério Santos', DateTime.utc(1978, 8, 14));
  p0.weight = 78.0;
  p0.height = 1.68;

  p0.add(12);
  print(p0);
}

Executando este código será gerado a saída abaixo:

ERROR: You tried to use a non-existent member: Symbol("add")
        Nome: Rogério Santos
  Nascimento: 14/08/1978
      Altura: 1.68m
        Peso: 78kg

O próximo código implementa a classe People, com os atributos de uma pessoa genérica, com nome, data de nascimento, peso e altura:

Enum

Enum é uma classe especial que representa um conjunto de números fixados de valores constantes.

enum Tristate {active, neutral, inactive}

void main() {
  Tristate status = Tristate.inactive;
  
  switch (status) {
    case Tristate.active:
      print('Está ativo: ${status.index} (${Tristate.values[status.index]})');
      break;
    case Tristate.neutral:
      print('Está neutro: ${status.index} (${Tristate.values[status.index]})');
      break;
    case Tristate.inactive:
      print('Está inativado: ${status.index} (${Tristate.values[status.index]})');
      break;
  }
}

No código acima cada estado de Tristate (active, neutral, inactive) corresponderá a um índice 0, 1 ou 2, gerado automaticamente na declaração.

O Tristate.values[status.index] retorna o estado do Tristate correspondente ao status.index. Este código gera a saída:

Está inativado: 2 (Tristate.inactive)

A partir da versão 2.17 o Dart passou a suportar uma versão mais avançada de enum, mais semelhante a classes com suporte a diversos atributos, métodos e construtores.

O código a seguir implementa um Tristate mais complexo, com dois atributos e os métodos para os operadores maior e menor:

enum Tristate {
  active(name: 'inactive', value: 10), 
  neutral(name: 'neutral', value: 20), 
  inactive(name: 'active', value: 30);
  
  final String name;
  final int value;
  
  const Tristate({
    required this.name, 
    required this.value,
  });
  
  bool operator >(Tristate other) => value > other.value;
  bool operator <(Tristate other) => value < other.value;
}

void main() {
  Tristate status0 = Tristate.inactive;
  Tristate status1 = Tristate.active;
  
  print('status0 ${status0.name} = (index: ${status0.index}  value: ${status0.value})');
  print('status1 ${status1.name} = (index: ${status1.index}  value: ${status1.value})');
  print('');
  print('status1 > status0: ${status1 > status0}');
  print('status1 < status0: ${status1 < status0}');
}

Agora cada estado possui dois atributos: name com o nome do estado, e value com um valor numérico para o estado, iniciados nas linhas 2 à 4. Estes atributos devem ser declarados como constantes por um final, linhas 6 e 7. E para terminar um construtor para o enum deve ser declarado como constante, const, e seus argumentos com required, linhas 9 à 12.

Isto já é o suficiente para o enum Tristate funcionar, mas o grande destaque vem com o suporte a métodos deste enhanced enum. Nas linhas 14 e 15 são implementados os operadores maior e menor, utilizando o value, para fazer a comparação dos estados. Com isto os estados podem ser comparados como ilustra as linhas 25 e 26.

Embora o Tristate agora possua um valor (atributo value) ele ainda possui o seu index gerado internamente e de forma automática. Estes são impressos nas linhas 22 e 23. A saída deste código é apresentada a seguir:

status0 active = (index: 2  value: 30)
status1 inactive = (index: 0  value: 10)

status1 > status0: false
status1 < status0: true

Considerações Finais

Embora estes dois artigos sejam bem longos, eles não cobrem a totalidade da linguagem Dart, mas apenas o básico para se ter um bom domínio da sintaxe e estrutura da linguagem. Aconselho recorrer a leitura da documentação online Dart.dev, principalmente do Core Libraries, que dão uma visão mais técnica do Dart, com muitos exemplos.

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.