El lenguaje Dart: cuando Java y C# no son lo suficientemente nítidos
Publicado: 2022-03-11Allá por 2013, el lanzamiento oficial de Dart 1.0 recibió algo de prensa, como con la mayoría de las ofertas de Google, pero no todos estaban tan ansiosos como los equipos internos de Google por crear aplicaciones críticas para el negocio con el lenguaje Dart. Con su reconstrucción bien pensada de Dart 2 cinco años después, Google parecía haber demostrado su compromiso con el lenguaje. De hecho, hoy en día continúa ganando terreno entre los desarrolladores, especialmente los veteranos de Java y C#.
El lenguaje de programación Dart es importante por varias razones:
- Tiene lo mejor de ambos mundos: es un lenguaje compilado con seguridad de tipos (como C# y Java) y un lenguaje de secuencias de comandos (como Python y JavaScript) al mismo tiempo.
- Se transpila a JavaScript para su uso como front-end web.
- Se ejecuta en todo y se compila en aplicaciones móviles nativas, por lo que puede usarlo para casi cualquier cosa.
- Dart es similar a C# y Java en sintaxis, por lo que es rápido de aprender.
Aquellos de nosotros del mundo C# o Java de los sistemas empresariales más grandes ya sabemos por qué la seguridad de tipos, los errores en tiempo de compilación y los linters son importantes. Muchos de nosotros dudamos en adoptar un lenguaje "scripty" por temor a perder toda la estructura, velocidad, precisión y capacidad de depuración a las que estamos acostumbrados.
Pero con el desarrollo de Dart, no tenemos que renunciar a nada de eso. Podemos escribir una aplicación móvil, un cliente web y un back-end en el mismo idioma, ¡y obtener todas las cosas que todavía amamos de Java y C#!
Con ese fin, repasemos algunos ejemplos clave del lenguaje Dart que serían nuevos para un desarrollador de C# o Java, que resumiremos en un PDF del lenguaje Dart al final.
Nota: Este artículo solo cubre Dart 2.x. La versión 1.x no estaba "completamente cocinada"; en particular, el sistema de tipos era de asesoramiento (como TypeScript) en lugar de obligatorio (como C# o Java).
1. Organización del Código
Primero, entraremos en una de las diferencias más significativas: cómo se organizan y se referencian los archivos de código.
Archivos de origen, ámbito, espacios de nombres e importaciones
En C#, una colección de clases se compila en un ensamblaje. Cada clase tiene un espacio de nombres y, a menudo, los espacios de nombres reflejan la organización del código fuente en el sistema de archivos, pero al final, el ensamblado no retiene ninguna información sobre la ubicación del archivo de código fuente.
En Java, los archivos fuente son parte de un paquete y los espacios de nombres generalmente se ajustan a la ubicación del sistema de archivos, pero al final, un paquete es solo una colección de clases.
Así que ambos lenguajes tienen una forma de mantener el código fuente algo independiente del sistema de archivos.
Por el contrario, en el lenguaje Dart, cada archivo fuente debe importar todo lo que hace referencia, incluidos sus otros archivos fuente y paquetes de terceros. No hay espacios de nombres de la misma manera y, a menudo, hace referencia a los archivos a través de su ubicación en el sistema de archivos. Las variables y funciones pueden ser de nivel superior, no solo clases. De esta manera, Dart es más parecido a un guión.
Por lo tanto, deberá cambiar su forma de pensar de "una colección de clases" a algo más parecido a "una secuencia de archivos de código incluidos".
Dart admite tanto la organización de paquetes como la organización ad-hoc sin paquetes. Comencemos con un ejemplo sin paquetes para ilustrar la secuencia de archivos incluidos:
// 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 }
Todo lo que hace referencia en un archivo fuente debe declararse o importarse dentro de ese archivo, ya que no hay un nivel de "proyecto" ni otra forma de incluir otros elementos fuente en el alcance.
El único uso de los espacios de nombres en Dart es dar un nombre a las importaciones, y eso afecta la forma en que se refiere al código importado de ese archivo.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Paquetes
Los ejemplos anteriores organizan el código sin paquetes. Para usar paquetes, el código se organiza de una manera más específica. Aquí hay un diseño de paquete de ejemplo para un paquete llamado apples
:
-
apples/
-
pubspec.yaml
el nombre del paquete, las dependencias y algunas otras cosas -
lib/
-
apples.dart
—importaciones y exportaciones; este es el archivo importado por cualquier consumidor del paquete -
src/
-
seeds.dart
otro código aquí
-
-
-
bin/
-
runapples.dart
contiene la función principal, que es el punto de entrada (si se trata de un paquete ejecutable o incluye herramientas ejecutables)
-
-
Luego puede importar paquetes completos en lugar de archivos individuales:
import 'package:apples';
Las aplicaciones no triviales siempre deben organizarse como paquetes. Esto alivia mucho tener que repetir las rutas del sistema de archivos en cada archivo de referencia; además, corren más rápido. También facilita compartir su paquete en pub.dev, donde otros desarrolladores pueden tomarlo fácilmente para su propio uso. Los paquetes utilizados por su aplicación harán que el código fuente se copie en su sistema de archivos, por lo que puede depurar esos paquetes tan profundamente como desee.
2. Tipos de datos
Hay diferencias importantes en el sistema de tipos de Dart a tener en cuenta, con respecto a nulos, tipos numéricos, colecciones y tipos dinámicos.
nulos en todas partes
Viniendo de C# o Java, estamos acostumbrados a los tipos primitivos o de valor a diferencia de los tipos de referencia o de objeto . En la práctica, los tipos de valor se asignan en la pila o en los registros, y se envían copias del valor como parámetros de función. En cambio, los tipos de referencia se asignan en el montón y solo los punteros al objeto se envían como parámetros de función. Dado que los tipos de valor siempre ocupan memoria, una variable de tipo de valor no puede ser nula y todos los miembros de tipo de valor deben tener valores inicializados.
Dart elimina esa distinción porque todo es un objeto; todos los tipos derivan en última instancia del tipo Object
. Entonces, esto es legal:
int i = null;
De hecho, todas las primitivas se inicializan implícitamente en null
. Esto significa que no puede asumir que los valores predeterminados de los números enteros son cero como está acostumbrado en C# o Java, y es posible que deba agregar comprobaciones nulas.
Curiosamente, incluso Null
es un tipo, y la palabra null
se refiere a una instancia de Null
:
print(null.runtimeType); // prints Null
No tantos tipos numéricos
A diferencia de la variedad familiar de tipos de enteros de 8 a 64 bits con sabores firmados y sin firmar, el tipo de entero principal de Dart es simplemente int
, un valor de 64 bits. (También hay BigInt
para números muy grandes).
Dado que no existe una matriz de bytes como parte de la sintaxis del lenguaje, los contenidos de los archivos binarios se pueden procesar como listas de enteros, es decir, List<Int>
.
Si está pensando que esto debe ser terriblemente ineficiente, los diseñadores ya pensaron en eso. En la práctica, existen diferentes representaciones internas según el valor entero real utilizado en tiempo de ejecución. El tiempo de ejecución no asigna memoria de almacenamiento dinámico para el objeto int
si puede optimizarlo y usar un registro de CPU en modo no empaquetado. Además, la biblioteca byte_data
ofrece UInt8List
y algunas otras representaciones optimizadas.
Colecciones
Las colecciones y los genéricos se parecen mucho a lo que estamos acostumbrados. Lo principal a tener en cuenta es que no hay arreglos de tamaño fijo: simplemente use el tipo de datos List
donde quiera que use un arreglo.
Además, hay soporte sintáctico para inicializar tres de los tipos de colección:
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
Entonces, use Dart List
donde usaría una matriz Java, ArrayList
o Vector
; o una matriz C# o List
. Use Set
donde usaría un Java/C# HashSet
. Use Map
donde usaría Java HashMap
o C# Dictionary
.
3. Escritura dinámica y estática
En lenguajes dinámicos como JavaScript, Ruby y Python, puede hacer referencia a miembros incluso si no existen. Aquí hay un ejemplo 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 // ... }
Si ejecuta esto, person.age
no estará undefined
, pero se ejecuta de todos modos.
Asimismo, puede cambiar el tipo de una variable en JavaScript:
var a = 1; // a is a number a = 'one'; // a is now a string
Por el contrario, en Java, no puede escribir código como el anterior porque el compilador necesita saber el tipo y verifica que todas las operaciones sean legales, incluso si usa la palabra clave var:
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java solo le permite codificar con tipos estáticos. (Puede usar la introspección para realizar un comportamiento dinámico, pero no es directamente parte de la sintaxis). JavaScript y algunos otros lenguajes puramente dinámicos solo le permiten codificar con tipos dinámicos.
El lenguaje Dart permite ambos:
// 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 tiene la dynamic
de pseudotipo que hace que toda la lógica de tipos se maneje en tiempo de ejecución. El intento de llamar a.foo()
no molestará al analizador estático y el código se ejecutará, pero fallará en tiempo de ejecución porque no existe tal método.

C# era originalmente como Java, y luego agregó soporte dinámico, por lo que Dart y C# son casi iguales en este sentido.
4. Funciones
Sintaxis de declaración de función
La sintaxis de la función en Dart es un poco más ligera y divertida que en C# o Java. La sintaxis es cualquiera de estas:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
Por ejemplo:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Paso de parámetros
Dado que todo es un objeto, incluidas las primitivas como int
y String
, el paso de parámetros puede resultar confuso. Si bien no hay paso de parámetro ref
como en C#, todo se pasa por referencia y la función no puede cambiar la referencia de la persona que llama. Dado que los objetos no se clonan cuando se pasan a funciones, una función puede cambiar las propiedades del objeto. Sin embargo, esa distinción para primitivas como int y String es discutible, ya que esos tipos son inmutables.
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 opcionales
Si estás en el mundo de C# o Java, probablemente hayas maldecido en situaciones con métodos confusos y sobrecargados como estos:
// 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
O con los parámetros opcionales de C#, existe otro tipo de confusión:
// 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# no requiere nombrar argumentos opcionales en los sitios de llamada, por lo que la refactorización de métodos con parámetros opcionales puede ser peligrosa. Si algunos sitios de llamadas resultan ser legales después de la refactorización, el compilador no los detectará.
Dart tiene una forma más segura y muy flexible. En primer lugar, los métodos sobrecargados no son compatibles. En cambio, hay dos formas de manejar parámetros opcionales:
// 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
No puede usar ambos estilos en la misma declaración de función.
Posición de la palabra clave async
C# tiene una posición confusa para su palabra clave async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Esto implica que la firma de la función es asíncrona, pero en realidad solo la implementación de la función es asíncrona. Cualquiera de las firmas anteriores sería una implementación válida de esta interfaz:
interface ICanFoo { Task<int> Foo(); }
En el lenguaje Dart, async
está en un lugar más lógico, lo que indica que la implementación es asíncrona:
Future<int> foo() async {...}
Alcance y Cierres
Al igual que C# y Java, Dart tiene un alcance léxico. Esto significa que una variable declarada en un bloque queda fuera del alcance al final del bloque. Entonces Dart maneja los cierres de la misma manera.
Sintaxis de propiedad
Java popularizó el patrón de propiedad get/set pero el lenguaje no tiene ninguna sintaxis especial para ello:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# tiene una sintaxis para ello:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart tiene propiedades de soporte de sintaxis ligeramente diferentes:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Constructores
Los constructores de Dart tienen bastante más flexibilidad que en C# o Java. Una buena característica es la capacidad de nombrar diferentes constructores en la misma clase:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Puede llamar a un constructor predeterminado con solo el nombre de la clase: var c = Client();
Hay dos tipos de abreviaturas para inicializar los miembros de la instancia antes de que se llame al cuerpo del constructor:
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 } }
Los constructores pueden ejecutar constructores de superclase y redirigir a otros constructores de la misma clase:
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
Los constructores que llaman a otros constructores de la misma clase en Java y C# pueden resultar confusos cuando ambos tienen implementaciones. En Dart, la limitación de que los constructores de redirección no pueden tener un cuerpo obliga al programador a aclarar las capas de constructores.
También hay una palabra clave de factory
que permite usar una función como un constructor, pero la implementación es solo una función normal. Puede usarlo para devolver una instancia almacenada en caché o una instancia de un tipo derivado:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Modificadores
En Java y C#, tenemos modificadores de acceso como private
, protected
y public
. En Dart, esto se simplifica drásticamente: si el nombre del miembro comienza con un guión bajo, es visible en todas partes dentro del paquete (incluso de otras clases) y oculto para las personas que llaman; de lo contrario, es visible desde todas partes. No hay palabras clave como private
para significar visibilidad.
Otro tipo de modificador controla la capacidad de cambio: las palabras clave final
y const
tienen ese propósito, pero significan cosas 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. Jerarquía de clases
El lenguaje Dart admite interfaces, clases y una especie de herencia múltiple. Sin embargo, no hay una palabra clave interface
; en cambio, todas las clases también son interfaces, por lo que puede definir una clase abstract
y luego implementarla:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
La herencia múltiple se realiza con un linaje principal usando la palabra clave extends
y otras clases usando la palabra clave with
:
class Employee extends Person with Salaried implements HasDesk {...}
En esta declaración, la clase Employee
se deriva de Person
y Salaried
, pero Person
es la superclase principal y Salaried
es la mezcla (la superclase secundaria).
8. Operadores
Hay algunos operadores Dart divertidos y útiles a los que no estamos acostumbrados.
Las cascadas le permiten usar un patrón de encadenamiento en cualquier cosa:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
El operador de propagación permite tratar una colección como una lista de sus elementos en un inicializador:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Hilos
Dart no tiene subprocesos, lo que le permite transpilar a JavaScript. En cambio, tiene "aislados", que son más como procesos separados, en el sentido de que no pueden compartir la memoria. Dado que la programación de subprocesos múltiples es tan propensa a errores, esta seguridad se considera una de las ventajas de Dart. Para comunicarse entre aislamientos, debe transmitir datos entre ellos; los objetos recibidos se copian en el espacio de memoria del aislado receptor.
Desarrolle con Dart Language: ¡usted puede hacer esto!
Si es un desarrollador de C# o Java, lo que ya sabe lo ayudará a aprender el lenguaje Dart rápidamente, ya que fue diseñado para que le resulte familiar. Con ese fin, hemos creado una hoja de trucos de Dart en PDF para su referencia, centrándonos específicamente en las diferencias importantes con respecto a los equivalentes de C# y Java:
Las diferencias que se muestran en este artículo, combinadas con su conocimiento existente, lo ayudarán a ser productivo en su primer o segundo día de Dart. ¡Feliz codificación!