A linguagem Dart: quando Java e C# não são suficientemente nítidos

Publicados: 2022-03-11

Em 2013, o lançamento oficial 1.0 do Dart recebeu alguma imprensa – como a maioria das ofertas do Google – mas nem todos estavam tão ansiosos quanto as equipes internas do Google para criar aplicativos críticos para os negócios com a linguagem Dart. Com a reconstrução bem pensada do Dart 2 cinco anos depois, o Google parecia ter provado seu compromisso com a linguagem. De fato, hoje ele continua ganhando força entre os desenvolvedores, especialmente os veteranos de Java e C#.

A linguagem de programação Dart é importante por alguns motivos:

  • Ele tem o melhor dos dois mundos: é uma linguagem compilada e de tipo seguro (como C# e Java) e uma linguagem de script (como Python e JavaScript) ao mesmo tempo.
  • Transpila para JavaScript para uso como front-end da web.
  • Ele é executado em tudo e compila para aplicativos móveis nativos, para que você possa usá-lo para quase tudo.
  • Dart é semelhante a C# e Java em sintaxe, então é rápido de aprender.

Aqueles de nós do mundo C# ou Java de sistemas corporativos maiores já sabem por que segurança de tipo, erros de tempo de compilação e linters são importantes. Muitos de nós hesitamos em adotar uma linguagem “script” por medo de perder toda a estrutura, velocidade, precisão e capacidade de depuração a que estamos acostumados.

Mas com o desenvolvimento do Dart, não precisamos abrir mão de nada disso. Podemos escrever um aplicativo móvel, cliente da Web e back-end na mesma linguagem — e obter todas as coisas que ainda amamos em Java e C#!

Para isso, vamos analisar alguns exemplos importantes da linguagem Dart que seriam novos para um desenvolvedor C# ou Java, que resumiremos em um PDF da linguagem Dart no final.

Observação: este artigo abrange apenas o Dart 2.x. A versão 1.x não estava “totalmente preparada” - em particular, o sistema de tipos era consultivo (como TypeScript) em vez de obrigatório (como C# ou Java).

1. Organização do Código

Primeiro, entraremos em uma das diferenças mais significativas: como os arquivos de código são organizados e referenciados.

Arquivos de origem, escopo, namespaces e importações

Em C#, uma coleção de classes é compilada em um assembly. Cada classe tem um namespace e, muitas vezes, os namespaces refletem a organização do código-fonte no sistema de arquivos — mas, no final, o assembly não retém nenhuma informação sobre o local do arquivo de código-fonte.

Em Java, os arquivos de origem fazem parte de um pacote e os namespaces geralmente estão de acordo com a localização do sistema de arquivos, mas no final, um pacote é apenas uma coleção de classes.

Portanto, ambas as linguagens têm uma maneira de manter o código-fonte um pouco independente do sistema de arquivos.

Por outro lado, na linguagem Dart, cada arquivo de origem deve importar tudo a que se refere, incluindo seus outros arquivos de origem e pacotes de terceiros. Não há namespaces da mesma maneira, e você geralmente se refere a arquivos por meio do local do sistema de arquivos. Variáveis ​​e funções podem ser de nível superior, não apenas classes. Dessa forma, o Dart é mais parecido com um script.

Portanto, você precisará mudar seu pensamento de “uma coleção de classes” para algo mais como “uma sequência de arquivos de código incluídos”.

O Dart suporta organização de pacotes e organização ad-hoc sem pacotes. Vamos começar com um exemplo sem pacotes para ilustrar a sequência de arquivos incluídos:

 // file1.dart int alice = 1; // top level variable int barry() => 2; // top level function var student = Charlie(); // top level variable; Charlie is declared below but that's OK class Charlie { ... } // top level class // alice = 2; // top level statement not allowed // file2.dart import 'file1.dart'; // causes all of file1 to be in scope main() { print(alice); // 1 }

Tudo o que você faz referência em um arquivo de origem deve ser declarado ou importado dentro desse arquivo, pois não há nível de “projeto” e nenhuma outra forma de incluir outros elementos de origem no escopo.

O único uso de namespaces no Dart é dar um nome às importações, e isso afeta como você se refere ao código importado desse arquivo.

 // file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }

Pacotes

Os exemplos acima organizam o código sem pacotes. Para usar pacotes, o código é organizado de uma maneira mais específica. Aqui está um exemplo de layout de pacote para um pacote chamado apples :

  • apples/
    • pubspec.yaml — define o nome do pacote, dependências e outras coisas
    • lib/
      • apples.dart —importações e exportações; este é o arquivo importado por qualquer consumidor do pacote
      • src/
        • seeds.dart —todos os outros códigos aqui
    • bin/
      • runapples.dart a função principal, que é o ponto de entrada (se for um pacote executável ou incluir ferramentas executáveis)

Então você pode importar pacotes inteiros em vez de arquivos individuais:

 import 'package:apples';

Aplicativos não triviais devem sempre ser organizados como pacotes. Isso alivia muito a necessidade de repetir os caminhos do sistema de arquivos em cada arquivo de referência; além disso, eles correm mais rápido. Também facilita compartilhar seu pacote no pub.dev, onde outros desenvolvedores podem facilmente pegá-lo para uso próprio. Os pacotes usados ​​pelo seu aplicativo farão com que o código-fonte seja copiado para o seu sistema de arquivos, para que você possa depurar o quanto quiser nesses pacotes.

2. Tipos de dados

Há grandes diferenças no sistema de tipos do Dart a serem observadas, em relação a nulos, tipos numéricos, coleções e tipos dinâmicos.

Nulos em todos os lugares

Vindo de C# ou Java, estamos acostumados a tipos primitivos ou de valor distintos dos tipos de referência ou objeto . Os tipos de valor são, na prática, alocados na pilha ou em registradores, e cópias do valor são enviadas como parâmetros de função. Os tipos de referência são alocados no heap e apenas ponteiros para o objeto são enviados como parâmetros de função. Como os tipos de valor sempre ocupam memória, uma variável com tipo de valor não pode ser nula e todos os membros do tipo de valor devem ter valores inicializados.

Dart elimina essa distinção porque tudo é um objeto; todos os tipos, em última análise, derivam do tipo Object . Então, isso é legal:

 int i = null;

Na verdade, todas as primitivas são inicializadas implicitamente como null . Isso significa que você não pode assumir que os valores padrão de inteiros são zero, como está acostumado em C# ou Java, e pode ser necessário adicionar verificações nulas.

Curiosamente, mesmo Null é um tipo, e a palavra null se refere a uma instância de Null :

 print(null.runtimeType); // prints Null

Não há tantos tipos numéricos

Ao contrário da variedade familiar de tipos inteiros de 8 a 64 bits com sabores assinados e não assinados, o tipo inteiro principal do Dart é apenas int , um valor de 64 bits. (Há também BigInt para números muito grandes.)

Como não há matriz de bytes como parte da sintaxe da linguagem, o conteúdo do arquivo binário pode ser processado como listas de inteiros, ou seja, List<Int> .

Se você está pensando que isso deve ser terrivelmente ineficiente, os designers já pensaram nisso. Na prática, existem diferentes representações internas dependendo do valor inteiro real usado em tempo de execução. O tempo de execução não aloca memória de heap para o objeto int se puder otimizar isso e usar um registro de CPU no modo unboxed. Além disso, a biblioteca byte_data oferece UInt8List e algumas outras representações otimizadas.

Coleções

Coleções e genéricos são muito parecidos com o que estamos acostumados. A principal coisa a notar é que não há arrays de tamanho fixo: basta usar o tipo de dados List onde quer que você use um array.

Além disso, há suporte sintático para inicializar três dos tipos de coleção:

 final a = [1, 2, 3]; // inferred type is List<int>, an array-like ordered collection final b = {1, 2, 3}; // inferred type is Set<int>, an unordered collection final c = {'a': 1, 'b': 2}; // inferred type is Map<string, int>, an unordered collection of name-value pairs

Portanto, use a List de Dart onde você usaria uma matriz Java, ArrayList ou Vector ; ou uma matriz C# ou List . Use Set onde você usaria um HashSet Java/C#. Use Map onde você usaria um Java HashMap ou C# Dictionary .

3. Digitação Dinâmica e Estática

Em linguagens dinâmicas como JavaScript, Ruby e Python, você pode fazer referência a membros mesmo que eles não existam. Aqui está um exemplo de JavaScript:

 var person = {}; // create an empty object person.name = 'alice'; // add a member to the object if (person.age < 21) { // refer to a property that is not in the object // ... }

Se você executar isso, person.age será undefined , mas será executado de qualquer maneira.

Da mesma forma, você pode alterar o tipo de uma variável em JavaScript:

 var a = 1; // a is a number a = 'one'; // a is now a string

Por outro lado, em Java, você não pode escrever código como o acima porque o compilador precisa conhecer o tipo e verifica se todas as operações são válidas - mesmo se você usar a palavra-chave var:

 var b = 1; // a is an int // b = "one"; // not allowed in Java

Java só permite codificar com tipos estáticos. (Você pode usar a introspecção para fazer algum comportamento dinâmico, mas não faz parte diretamente da sintaxe.) JavaScript e algumas outras linguagens puramente dinâmicas só permitem que você codifique com tipos dinâmicos.

A linguagem Dart permite:

 // dart dynamic a = 1; // a is an int - dynamic typing a = 'one'; // a is now a string a.foo(); // we can call a function on a dynamic object, to be resolved at run time var b = 1; // b is an int - static typing // b = 'one'; // not allowed in Dart

Dart tem o pseudo-tipo dynamic que faz com que toda a lógica de tipo seja tratada em tempo de execução. A tentativa de chamar a.foo() não incomodará o analisador estático e o código será executado, mas falhará em tempo de execução porque não existe tal método.

O C# era originalmente como o Java e, posteriormente, adicionou suporte dinâmico, portanto, Dart e C# são praticamente os mesmos a esse respeito.

4. Funções

Sintaxe da declaração de função

A sintaxe da função no Dart é um pouco mais leve e divertida do que em C# ou Java. A sintaxe é qualquer uma destas:

 // functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression

Por exemplo:

 void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';

Passagem de Parâmetro

Como tudo é um objeto, incluindo primitivos como int e String , a passagem de parâmetros pode ser confusa. Embora não haja passagem de parâmetro ref como em C#, tudo é passado por referência e a função não pode alterar a referência do chamador. Como os objetos não são clonados quando passados ​​para funções, uma função pode alterar as propriedades do objeto. No entanto, essa distinção para primitivos como int e String é efetivamente discutível, pois esses tipos são imutáveis.

 var id = 1; var name = 'alice'; var client = Client(); void foo(int id, String name, Client client) { id = 2; // local var points to different int instance name = 'bob'; // local var points to different String instance client.State = 'AK'; // property of caller's object is changed } foo(id, name, client); // id == 1, name == 'alice', client.State == 'AK'

Parâmetros opcionais

Se você está nos mundos C# ou Java, provavelmente já amaldiçoou situações com métodos confusos e sobrecarregados como estes:

 // java void foo(string arg1) {...} void foo(int arg1, string arg2) {...} void foo(string arg1, Client arg2) {...} // call site: foo(clientId, input3); // confusing! too easy to misread which overload it is calling

Ou com parâmetros opcionais do C#, há outro tipo de confusão:

 // c# void Foo(string arg1, int arg2 = 0) {...} void Foo(string arg1, int arg3 = 0, int arg2 = 0) {...} // call site: Foo("alice", 7); // legal but confusing! too easy to misread which overload it is calling and which parameter binds to argument 7 Foo("alice", arg2: 9); // better

C# não requer a nomeação de argumentos opcionais em sites de chamada, portanto, refatorar métodos com parâmetros opcionais pode ser perigoso. Se alguns sites de chamadas forem legais após a refatoração, o compilador não os capturará.

Dart tem uma forma mais segura e muito flexível. Em primeiro lugar, métodos sobrecarregados não são suportados. Em vez disso, existem duas maneiras de lidar com parâmetros opcionais:

 // positional optional parameters void foo(string arg1, [int arg2 = 0, int arg3 = 0]) {...} // call site for positional optional parameters foo('alice'); // legal foo('alice', 12); // legal foo('alice', 12, 13); // legal // named optional parameters void bar(string arg1, {int arg2 = 0, int arg3 = 0}) {...} bar('alice'); // legal bar('alice', arg3: 12); // legal bar('alice', arg3: 12, arg2: 13); // legal; sequence can vary and names are required

Você não pode usar os dois estilos na mesma declaração de função.

Posição da palavra-chave async

C# tem uma posição confusa para sua palavra-chave async :

 Task<int> Foo() {...} async Task<int> Foo() {...}

Isso implica que a assinatura da função é assíncrona, mas na verdade apenas a implementação da função é assíncrona. Qualquer uma das assinaturas acima seria uma implementação válida desta interface:

 interface ICanFoo { Task<int> Foo(); }

Na linguagem Dart, async está em um lugar mais lógico, denotando que a implementação é assíncrona:

 Future<int> foo() async {...}

Escopo e Fechamentos

Assim como C# e Java, o Dart tem escopo lexical. Isso significa que uma variável declarada em um bloco sai do escopo no final do bloco. Portanto, o Dart lida com os fechamentos da mesma maneira.

Sintaxe da propriedade

Java popularizou o padrão get/set de propriedade, mas a linguagem não possui nenhuma sintaxe especial para isso:

 // java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }

C# tem sintaxe para isso:

 // c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }

O Dart tem propriedades de suporte de sintaxe ligeiramente diferentes:

 // dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }

5. Construtores

Os construtores de dardos têm um pouco mais de flexibilidade do que em C# ou Java. Um recurso interessante é a capacidade de nomear construtores diferentes na mesma classe:

 class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }

Você pode chamar um construtor padrão apenas com o nome da classe: var c = Client();

Existem dois tipos de atalhos para inicializar membros de instância antes que o corpo do construtor seja chamado:

 class Client { String _code; String _name; Client(String this._name) // "this" shorthand for assigning parameter to instance member : _code = _name.toUpper() { // special out-of-body place for initializing // body } }

Construtores podem executar construtores de superclasse e redirecionar para outros construtores na mesma classe:

 Foo.constructor1(int x) : this(x); // redirect to the default ctor in same class; no body allowed Foo.constructor2(int x) : super.plain(x) {...} // call base class named ctor, then run this body Foo.constructor3(int x) : _b = x + 1 : super.plain(x) {...} // initialize _b, then call base class ctor, then run this body

Construtores que chamam outros construtores na mesma classe em Java e C# podem ficar confusos quando ambos têm implementações. No Dart, a limitação de que os construtores de redirecionamento não podem ter um corpo força o programador a tornar as camadas de construtores mais claras.

Há também uma palavra-chave de factory que permite que uma função seja usada como um construtor, mas a implementação é apenas uma função normal. Você pode usá-lo para retornar uma instância em cache ou uma instância de um tipo derivado:

 class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);

6. Modificadores

Em Java e C#, temos modificadores de acesso como private , protected e public . No Dart, isso é drasticamente simplificado: se o nome do membro começar com um sublinhado, ele ficará visível em todos os lugares dentro do pacote (incluindo de outras classes) e oculto de chamadores externos; caso contrário, é visível de todos os lugares. Não há palavras-chave como private para significar visibilidade.

Outro tipo de modificador controla a alterabilidade: as palavras-chave final e const são para esse propósito, mas significam coisas diferentes:

 var a = 1; // a is variable, and can be reassigned later final b = a + 1; // b is a runtime constant, and can only be assigned once const c = 3; // c is a compile-time constant // const d = a + 2; // not allowed because a+2 cannot be resolved at compile time

7. Hierarquia de Classes

A linguagem Dart suporta interfaces, classes e um tipo de herança múltipla. No entanto, não há palavra-chave interface ; em vez disso, todas as classes também são interfaces, então você pode definir uma classe abstract e implementá-la:

 abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }

A herança múltipla é feita com uma linhagem principal usando a palavra-chave extends e outras classes usando a palavra-chave with :

 class Employee extends Person with Salaried implements HasDesk {...}

Nesta declaração, a classe Employee deriva de Person e Salaried , mas Person é a superclasse principal e Salaried é o mixin (a superclasse secundária).

8. Operadores

Existem alguns operadores de dardos divertidos e úteis aos quais não estamos acostumados.

As cascatas permitem que você use um padrão de encadeamento em qualquer coisa:

 emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();

O operador spread permite que uma coleção seja tratada como uma lista de seus elementos em um inicializador:

 var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]

9. Tópicos

O Dart não tem threads, o que permite transpilar para JavaScript. Em vez disso, tem “isolados”, que são mais como processos separados, no sentido de que não podem compartilhar memória. Como a programação multithread é tão propensa a erros, essa segurança é vista como uma das vantagens do Dart. Para se comunicar entre isolados, você precisa transmitir dados entre eles; os objetos recebidos são copiados para o espaço de memória do isolado receptor.

Desenvolva com a linguagem Dart: você pode fazer isso!

Se você é um desenvolvedor C# ou Java, o que você já sabe o ajudará a aprender a linguagem Dart rapidamente, pois ela foi projetada para ser familiar. Para esse fim, reunimos um PDF da folha de dicas do Dart para sua referência, focando especificamente em diferenças importantes dos equivalentes C# e Java:

PDF da folha de dicas da linguagem Dart

As diferenças mostradas neste artigo, combinadas com o seu conhecimento existente, ajudarão você a se tornar produtivo em seu primeiro dia ou dois de Dart. Boa codificação!

Relacionado: Energia Híbrida: Vantagens e Benefícios do Flutter