Le langage Dart : quand Java et C# ne sont pas assez pointus
Publié: 2022-03-11En 2013, la version officielle 1.0 de Dart a fait l'objet d'une presse, comme pour la plupart des offres Google, mais tout le monde n'était pas aussi désireux que les équipes internes de Google de créer des applications critiques avec le langage Dart. Avec sa reconstruction bien pensée de Dart 2 cinq ans plus tard, Google semblait avoir prouvé son engagement envers le langage. En effet, aujourd'hui, il continue de gagner du terrain parmi les développeurs, en particulier les vétérans de Java et de C#.
Le langage de programmation Dart est important pour plusieurs raisons :
- Il a le meilleur des deux mondes : il s'agit à la fois d'un langage compilé et de type sécurisé (comme C# et Java) et d'un langage de script (comme Python et JavaScript).
- Il se transpile en JavaScript pour être utilisé comme frontal Web.
- Il fonctionne sur tout et se compile en applications mobiles natives, vous pouvez donc l'utiliser pour presque tout.
- Dart est similaire à C # et Java dans la syntaxe, il est donc rapide à apprendre.
Ceux d'entre nous du monde C # ou Java des systèmes d'entreprise plus grands savent déjà pourquoi la sécurité des types, les erreurs de compilation et les linters sont importants. Beaucoup d'entre nous hésitent à adopter un langage «scripty» de peur de perdre toute la structure, la vitesse, la précision et la débogabilité auxquelles nous sommes habitués.
Mais avec le développement de Dart, nous n'avons pas à renoncer à tout cela. Nous pouvons écrire une application mobile, un client Web et un back-end dans le même langage, et obtenir tout ce que nous aimons encore de Java et de C# !
À cette fin, passons en revue quelques exemples clés du langage Dart qui seraient nouveaux pour un développeur C# ou Java, que nous résumerons dans un PDF en langage Dart à la fin.
Remarque : cet article ne couvre que Dart 2.x. La version 1.x n'était pas "complètement préparée" - en particulier, le système de type était consultatif (comme TypeScript) au lieu d'être requis (comme C# ou Java).
1. Organisation des codes
Tout d'abord, nous aborderons l'une des différences les plus importantes : la manière dont les fichiers de code sont organisés et référencés.
Fichiers source, portée, espaces de noms et importations
En C#, une collection de classes est compilée dans un assembly. Chaque classe a un espace de noms, et souvent les espaces de noms reflètent l'organisation du code source dans le système de fichiers, mais au final, l'assembly ne conserve aucune information sur l'emplacement du fichier de code source.
En Java, les fichiers source font partie d'un package et les espaces de noms sont généralement conformes à l'emplacement du système de fichiers, mais au final, un package n'est qu'un ensemble de classes.
Ainsi, les deux langages ont un moyen de garder le code source quelque peu indépendant du système de fichiers.
En revanche, dans le langage Dart, chaque fichier source doit importer tout ce à quoi il fait référence, y compris vos autres fichiers source et packages tiers. Il n'y a pas d'espaces de noms de la même manière et vous vous référez souvent aux fichiers via leur emplacement dans le système de fichiers. Les variables et les fonctions peuvent être de niveau supérieur, pas seulement des classes. De cette manière, Dart ressemble plus à un script.
Vous devrez donc changer votre façon de penser d'une "collection de classes" à quelque chose qui ressemble plus à "une séquence de fichiers de code inclus".
Dart prend en charge à la fois l'organisation de packages et l'organisation ad hoc sans packages. Commençons par un exemple sans packages pour illustrer la séquence des fichiers inclus :
// 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 }
Tout ce à quoi vous faites référence dans un fichier source doit être déclaré ou importé dans ce fichier, car il n'y a pas de niveau "projet" et aucun autre moyen d'inclure d'autres éléments source dans la portée.
La seule utilisation des espaces de noms dans Dart est de donner un nom aux importations, et cela affecte la façon dont vous vous référez au code importé à partir de ce fichier.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Paquets
Les exemples ci-dessus organisent le code sans packages. Afin d'utiliser des packages, le code est organisé de manière plus spécifique. Voici un exemple de disposition de package pour un package nommé apples
:
-
apples/
-
pubspec.yaml
— définit le nom du package, les dépendances et quelques autres choses -
lib/
-
apples.dart
— importations et exportations ; c'est le fichier importé par tous les consommateurs du package -
src/
-
seeds.dart
- tous les autres codes ici
-
-
-
bin/
-
runapples.dart
contient la fonction principale, qui est le point d'entrée (s'il s'agit d'un package exécutable ou inclut des outils exécutables)
-
-
Ensuite, vous pouvez importer des packages entiers au lieu de fichiers individuels :
import 'package:apples';
Les applications non triviales doivent toujours être organisées en packages. Cela évite de devoir répéter les chemins du système de fichiers dans chaque fichier de référence ; en plus, ils courent plus vite. Cela facilite également le partage de votre package sur pub.dev, où d'autres développeurs peuvent très facilement le récupérer pour leur propre usage. Les packages utilisés par votre application entraîneront la copie du code source dans votre système de fichiers, de sorte que vous pourrez déboguer aussi profondément que vous le souhaitez dans ces packages.
2. Types de données
Il existe des différences majeures dans le système de type de Dart à connaître, concernant les valeurs nulles, les types numériques, les collections et les types dynamiques.
Nuls partout
Venant de C# ou Java, nous sommes habitués aux types primitifs ou valeur par opposition aux types référence ou objet . Les types de valeur sont, en pratique, alloués sur la pile ou dans des registres, et des copies de la valeur sont envoyées en tant que paramètres de fonction. Les types de référence sont plutôt alloués sur le tas et seuls les pointeurs vers l'objet sont envoyés en tant que paramètres de fonction. Étant donné que les types valeur occupent toujours de la mémoire, une variable typée valeur ne peut pas être nulle et tous les membres de type valeur doivent avoir des valeurs initialisées.
Dart élimine cette distinction parce que tout est objet ; tous les types dérivent finalement du type Object
. Donc c'est légal :
int i = null;
En fait, toutes les primitives sont implicitement initialisées à null
. Cela signifie que vous ne pouvez pas supposer que les valeurs par défaut des entiers sont nulles comme vous en avez l'habitude en C# ou Java, et vous devrez peut-être ajouter des contrôles nuls.
Fait intéressant, même Null
est un type, et le mot null
fait référence à une instance de Null
:
print(null.runtimeType); // prints Null
Pas autant de types numériques
Contrairement à l'assortiment familier de types entiers de 8 à 64 bits avec des saveurs signées et non signées, le type entier principal de Dart est juste int
, une valeur 64 bits. (Il y a aussi BigInt
pour les très grands nombres.)
Puisqu'il n'y a pas de tableau d'octets dans la syntaxe du langage, le contenu du fichier binaire peut être traité comme des listes d'entiers, c'est-à-dire List<Int>
.
Si vous pensez que cela doit être terriblement inefficace, les concepteurs y ont déjà pensé. En pratique, il existe différentes représentations internes en fonction de la valeur entière réelle utilisée lors de l'exécution. Le runtime n'alloue pas de mémoire de tas pour l'objet int
s'il peut l'optimiser et utiliser un registre CPU en mode non emballé. En outre, la bibliothèque byte_data
propose UInt8List
et d'autres représentations optimisées.
Collections
Les collections et les génériques ressemblent beaucoup à ce à quoi nous sommes habitués. La principale chose à noter est qu'il n'y a pas de tableaux de taille fixe : utilisez simplement le type de données List
partout où vous utiliseriez un tableau.
En outre, il existe un support syntaxique pour initialiser trois des types de collection :
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
Donc, utilisez la List
Dart où vous utiliseriez un tableau Java, ArrayList
ou Vector
; ou un tableau C# ou List
. Utilisez Set
là où vous utiliseriez un Java/C# HashSet
. Utilisez Map
là où vous utiliseriez un Java HashMap
ou C# Dictionary
.
3. Typage dynamique et statique
Dans les langages dynamiques tels que JavaScript, Ruby et Python, vous pouvez référencer des membres même s'ils n'existent pas. Voici un exemple 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 vous l'exécutez, person.age
sera undefined
, mais il s'exécute quand même.
De même, vous pouvez changer le type d'une variable en JavaScript :
var a = 1; // a is a number a = 'one'; // a is now a string
En revanche, en Java, vous ne pouvez pas écrire de code comme ci-dessus car le compilateur doit connaître le type et il vérifie que toutes les opérations sont légales, même si vous utilisez le mot-clé var :
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java vous permet uniquement de coder avec des types statiques. (Vous pouvez utiliser l'introspection pour effectuer un comportement dynamique, mais cela ne fait pas directement partie de la syntaxe.) JavaScript et certains autres langages purement dynamiques ne vous permettent de coder qu'avec des types dynamiques.
Le langage Dart permet à la fois :
// 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 a la dynamic
de pseudo-type qui fait que toute la logique de type est gérée au moment de l'exécution. La tentative d'appel a.foo()
ne dérangera pas l'analyseur statique et le code s'exécutera, mais il échouera à l'exécution car il n'y a pas une telle méthode.

C # était à l'origine comme Java, et plus tard a ajouté un support dynamique, donc Dart et C # sont à peu près les mêmes à cet égard.
4. Fonctions
Syntaxe de déclaration de fonction
La syntaxe des fonctions dans Dart est un peu plus légère et plus amusante qu'en C# ou Java. La syntaxe est l'une de celles-ci :
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
Par exemple:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Passage de paramètres
Étant donné que tout est un objet, y compris les primitives comme int
et String
, le passage de paramètres peut être déroutant. Bien qu'il n'y ait pas de passage de paramètre ref
comme en C#, tout est passé par référence et la fonction ne peut pas modifier la référence de l'appelant. Étant donné que les objets ne sont pas clonés lorsqu'ils sont passés à des fonctions, une fonction peut modifier les propriétés de l'objet. Cependant, cette distinction pour les primitives comme int et String est effectivement sans objet puisque ces types sont immuables.
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'
Paramètres facultatifs
Si vous êtes dans les mondes C# ou Java, vous avez probablement maudit des situations avec des méthodes surchargées comme celles-ci :
// 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 avec les paramètres optionnels C#, il y a un autre type de confusion :
// 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# ne nécessite pas de nommer les arguments facultatifs sur les sites d'appel, de sorte que la refactorisation des méthodes avec des paramètres facultatifs peut être dangereuse. Si certains sites d'appels sont légaux après le refactoring, le compilateur ne les détectera pas.
Dart a une manière plus sûre et très flexible. Tout d'abord, les méthodes surchargées ne sont pas prises en charge. Au lieu de cela, il existe deux manières de gérer les paramètres facultatifs :
// 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
Vous ne pouvez pas utiliser les deux styles dans la même déclaration de fonction.
Position du mot-clé async
C# a une position déroutante pour son mot-clé async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Cela implique que la signature de la fonction est asynchrone, mais en réalité, seule l'implémentation de la fonction est asynchrone. L'une ou l'autre des signatures ci-dessus serait une implémentation valide de cette interface :
interface ICanFoo { Task<int> Foo(); }
Dans le langage Dart, async
est à un endroit plus logique, indiquant que l'implémentation est asynchrone :
Future<int> foo() async {...}
Portée et fermetures
Comme C# et Java, Dart a une portée lexicale. Cela signifie qu'une variable déclarée dans un bloc sort de la portée à la fin du bloc. Ainsi, Dart gère les fermetures de la même manière.
Syntaxe de la propriété
Java a popularisé le modèle de propriété get/set mais le langage n'a pas de syntaxe spéciale pour cela :
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C # a une syntaxe pour cela :
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart a une syntaxe légèrement différente prenant en charge les propriétés :
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Constructeurs
Les constructeurs Dart ont un peu plus de flexibilité qu'en C# ou Java. Une fonctionnalité intéressante est la possibilité de nommer différents constructeurs dans la même classe :
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Vous pouvez appeler un constructeur par défaut avec juste le nom de la classe : var c = Client();
Il existe deux types de raccourcis pour initialiser les membres d'instance avant que le corps du constructeur ne soit appelé :
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 } }
Les constructeurs peuvent exécuter des constructeurs de superclasse et rediriger vers d'autres constructeurs de la même 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
Les constructeurs qui appellent d'autres constructeurs dans la même classe en Java et C# peuvent prêter à confusion lorsqu'ils ont tous les deux des implémentations. Dans Dart, la limitation selon laquelle les constructeurs de redirection ne peuvent pas avoir de corps oblige le programmeur à clarifier les couches de constructeurs.
Il existe également un mot-clé factory
qui permet d'utiliser une fonction comme un constructeur, mais l'implémentation n'est qu'une fonction normale. Vous pouvez l'utiliser pour renvoyer une instance mise en cache ou une instance d'un type dérivé :
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Modificateurs
En Java et C#, nous avons des modificateurs d'accès comme private
, protected
et public
. Dans Dart, cela est considérablement simplifié : si le nom du membre commence par un trait de soulignement, il est visible partout dans le package (y compris depuis les autres classes) et masqué des appelants extérieurs ; sinon, il est visible de partout. Il n'y a pas de mots-clés comme private
pour signifier la visibilité.
Un autre type de modificateur contrôle la possibilité de changement : les mots-clés final
et const
sont là pour cela, mais ils signifient des choses différentes :
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. Hiérarchie des classes
Le langage Dart prend en charge les interfaces, les classes et une sorte d'héritage multiple. Cependant, il n'y a pas de mot-clé d' interface
; à la place, toutes les classes sont également des interfaces, vous pouvez donc définir une classe abstract
puis l'implémenter :
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
L'héritage multiple se fait avec une lignée principale à l'aide du mot-clé extends
, et d'autres classes à l'aide du mot-clé with
:
class Employee extends Person with Salaried implements HasDesk {...}
Dans cette déclaration, la classe Employee
dérive de Person
et Salaried
, mais Person
est la superclasse principale et Salaried
est le mixin (la superclasse secondaire).
8. Opérateurs
Il existe des opérateurs Dart amusants et utiles auxquels nous ne sommes pas habitués.
Cascades vous permet d'utiliser un motif de chaînage sur n'importe quoi :
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
L'opérateur spread permet de traiter une collection comme une liste de ses éléments dans un initialiseur :
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Fils
Dart n'a pas de threads, ce qui lui permet de se transpiler en JavaScript. Il a plutôt des "isolats", qui ressemblent plus à des processus séparés, dans le sens où ils ne peuvent pas partager de mémoire. Étant donné que la programmation multithread est si sujette aux erreurs, cette sécurité est considérée comme l'un des avantages de Dart. Pour communiquer entre les isolats, vous devez diffuser des données entre eux ; les objets reçus sont copiés dans l'espace mémoire de l'isolât récepteur.
Développer avec le langage Dart : vous pouvez le faire !
Si vous êtes un développeur C# ou Java, ce que vous savez déjà vous aidera à apprendre rapidement le langage Dart, car il a été conçu pour être familier. À cette fin, nous avons créé une feuille de triche Dart au format PDF pour votre référence, en nous concentrant spécifiquement sur les différences importantes par rapport aux équivalents C# et Java :
Les différences présentées dans cet article combinées à vos connaissances existantes vous aideront à devenir productif au cours de votre premier jour ou deux de Dart. Bon codage !