Limbajul Dart: Când Java și C# nu sunt suficient de clare
Publicat: 2022-03-11În 2013, lansarea oficială 1.0 a lui Dart a primit ceva presă – ca și în cazul majorității ofertelor Google – dar nu toată lumea era la fel de dornici ca echipele interne ale Google să creeze aplicații critice pentru afaceri cu limbajul Dart. Cu reconstrucția sa bine gândită a Dart 2 cinci ani mai târziu, Google părea să-și fi dovedit angajamentul față de limbaj. Într-adevăr, astăzi continuă să câștige tracțiune printre dezvoltatori, în special veteranii Java și C#.
Limbajul de programare Dart este important din câteva motive:
- Are tot ce este mai bun din ambele lumi: este un limbaj compilat, sigur de tipare (cum ar fi C# și Java) și un limbaj de scripting (cum ar fi Python și JavaScript) în același timp.
- Se transpilează în JavaScript pentru a fi utilizat ca front-end web.
- Se rulează pe orice și se compilează în aplicații mobile native, astfel încât să îl puteți folosi pentru aproape orice.
- Dart este similar cu C# și Java în sintaxă, așa că este rapid de învățat.
Aceia dintre noi din lumea C# sau Java a sistemelor întreprinderilor mai mari știm deja de ce siguranța tipului, erorile de compilare și linters sunt importante. Mulți dintre noi ezităm să adopte un limbaj „scripty” de teamă să nu pierdem toată structura, viteza, acuratețea și capacitatea de depanare cu care suntem obișnuiți.
Dar odată cu dezvoltarea Dart, nu trebuie să renunțăm la nimic la asta. Putem scrie o aplicație mobilă, un client web și un back-end în aceeași limbă și să obținem toate lucrurile pe care încă ne place la Java și C#!
În acest scop, să trecem prin câteva exemple cheie de limbaj Dart care ar fi noi pentru un dezvoltator C# sau Java, pe care le vom rezuma într-un PDF în limbaj Dart la sfârșit.
Notă: acest articol acoperă numai Dart 2.x. Versiunea 1.x nu a fost „complet gătită” - în special, sistemul de tip a fost consultativ (cum ar fi TypeScript) în loc de obligatoriu (cum ar fi C# sau Java).
1. Cod de organizare
În primul rând, vom intra în una dintre cele mai semnificative diferențe: cum sunt organizate și referite fișierele de cod.
Fișierele sursă, domeniul de aplicare, spațiile de nume și importurile
În C#, o colecție de clase este compilată într-un ansamblu. Fiecare clasă are un spațiu de nume și adesea spațiile de nume reflectă organizarea codului sursă în sistemul de fișiere, dar în cele din urmă, ansamblul nu reține nicio informație despre locația fișierului codului sursă.
În Java, fișierele sursă fac parte dintr-un pachet, iar spațiile de nume se conformează de obicei cu locația sistemului de fișiere, dar în cele din urmă, un pachet este doar o colecție de clase.
Deci ambele limbi au o modalitate de a menține codul sursă oarecum independent de sistemul de fișiere.
În schimb, în limbajul Dart, fiecare fișier sursă trebuie să importe tot ceea ce se referă, inclusiv celelalte fișiere sursă și pachete terțe. Nu există spații de nume în același mod și adesea vă referiți la fișiere prin locația lor în sistemul de fișiere. Variabilele și funcțiile pot fi de nivel superior, nu doar clase. În aceste moduri, Dart este mai asemănător scenariului.
Așa că va trebui să vă schimbați gândirea de la „o colecție de clase” la ceva mai degrabă de genul „o secvență de fișiere de cod incluse”.
Dart acceptă atât organizarea pachetelor, cât și organizarea ad-hoc fără pachete. Să începem cu un exemplu fără pachete pentru a ilustra secvența fișierelor incluse:
// 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 }
Tot ceea ce faceți referire într-un fișier sursă trebuie să fie declarat sau importat în acel fișier, deoarece nu există un nivel de „proiect” și nicio altă modalitate de a include alte elemente sursă în domeniu.
Singura utilizare a spațiilor de nume în Dart este de a da un nume importurilor, iar asta afectează modul în care vă referiți la codul importat din acel fișier.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Pachete
Exemplele de mai sus organizează codul fără pachete. Pentru a utiliza pachetele, codul este organizat într-un mod mai specific. Iată un exemplu de aspect de pachet pentru un pachet numit apples
:
-
apples/
-
pubspec.yaml
— definește numele pachetului, dependențele și alte câteva lucruri -
lib/
-
apples.dart
și exporturi; acesta este fișierul importat de orice consumator al pachetului -
src/
-
seeds.dart
— toate celelalte coduri de aici
-
-
-
bin/
-
runapples.dart
— conține funcția principală, care este punctul de intrare (dacă acesta este un pachet rulabil sau include instrumente rulabile)
-
-
Apoi puteți importa pachete întregi în loc de fișiere individuale:
import 'package:apples';
Aplicațiile netriviale ar trebui să fie întotdeauna organizate ca pachete. Acest lucru ușurează o mulțime de nevoia de a repeta căile sistemului de fișiere în fiecare fișier de referință; in plus, alearga mai repede. De asemenea, facilitează partajarea pachetului pe pub.dev, de unde alți dezvoltatori îl pot prelua foarte ușor pentru uzul lor. Pachetele utilizate de aplicația dvs. vor face ca codul sursă să fie copiat în sistemul dvs. de fișiere, astfel încât să puteți depana atât de adânc în acele pachete cât doriți.
2. Tipuri de date
Există diferențe majore în sistemul de tipuri al lui Dart de care trebuie să fii conștient, în ceea ce privește valorile nule, tipurile numerice, colecțiile și tipurile dinamice.
Nule peste tot
Venind din C# sau Java, suntem obișnuiți cu tipurile primitive sau valorice , diferit de tipurile de referință sau obiecte . Tipurile de valori sunt, în practică, alocate pe stivă sau în registre, iar copiile valorii sunt trimise ca parametri de funcție. Tipurile de referință sunt în schimb alocate pe heap și doar pointerii către obiect sunt trimiși ca parametri de funcție. Deoarece tipurile de valoare ocupă întotdeauna memorie, o variabilă tip valoare nu poate fi nulă și toți membrii tip valoare trebuie să aibă valori inițializate.
Dart elimină această distincție pentru că totul este un obiect; toate tipurile derivă în cele din urmă din tipul Object
. Deci, acesta este legal:
int i = null;
De fapt, toate primitivele sunt implicit inițializate la null
. Aceasta înseamnă că nu puteți presupune că valorile implicite ale numerelor întregi sunt zero, așa cum sunteți obișnuiți cu C# sau Java și ar putea fi necesar să adăugați verificări nule.
Interesant, chiar și Null
este un tip, iar cuvântul null
se referă la o instanță a lui Null
:
print(null.runtimeType); // prints Null
Nu la fel de multe tipuri numerice
Spre deosebire de sortimentul familiar de tipuri de întregi de la 8 la 64 de biți cu arome semnate și nesemnate, tipul întreg principal al lui Dart este doar int
, o valoare de 64 de biți. (Există și BigInt
pentru numere foarte mari.)
Deoarece nu există o matrice de octeți ca parte a sintaxei limbajului, conținutul fișierului binar poate fi procesat ca liste de numere întregi, adică List<Int>
.
Dacă te gândești că acest lucru trebuie să fie teribil de ineficient, designerii s-au gândit deja la asta. În practică, există diferite reprezentări interne în funcție de valoarea efectivă a întregului utilizat în timpul execuției. Timpul de execuție nu alocă memorie heap pentru obiectul int
dacă îl poate optimiza și utiliza un registru CPU în modul unboxed. De asemenea, biblioteca byte_data
oferă UInt8List
și alte reprezentări optimizate.
Colecții
Colecțiile și genericele sunt foarte asemănătoare cu ceea ce suntem obișnuiți. Principalul lucru de remarcat este că nu există matrice de dimensiune fixă: trebuie doar să utilizați tipul de date List
oriunde ați folosi o matrice.
De asemenea, există suport sintactic pentru inițializarea a trei dintre tipurile de colecție:
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
Deci, utilizați List
Dart unde ați folosi o matrice Java, ArrayList
sau Vector
; sau o matrice C# sau List
. Utilizați Set
unde ați folosi un Java/C# HashSet
. Utilizați Map
unde ați folosi un Java HashMap
sau C# Dictionary
.
3. Tastare dinamică și statică
În limbaje dinamice precum JavaScript, Ruby și Python, puteți face referire la membri chiar dacă aceștia nu există. Iată un exemplu 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 // ... }
Dacă rulați acest lucru, person.age
va fi undefined
, dar rulează oricum.
De asemenea, puteți schimba tipul unei variabile în JavaScript:
var a = 1; // a is a number a = 'one'; // a is now a string
În schimb, în Java, nu puteți scrie cod ca cel de mai sus, deoarece compilatorul trebuie să cunoască tipul și verifică dacă toate operațiunile sunt legale, chiar dacă utilizați cuvântul cheie var:
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java vă permite doar să codificați cu tipuri statice. (Puteți folosi introspecția pentru a face un comportament dinamic, dar nu face parte direct din sintaxă.) JavaScript și alte limbaje pur dinamice vă permit să codificați numai cu tipuri dinamice.
Limbajul Dart permite ambele:
// 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 are o dynamic
de pseudo-tip care face ca toată logica de tip să fie gestionată în timpul execuției. Încercarea de a apela a.foo()
nu va deranja analizorul static și codul va rula, dar va eșua în timpul rulării deoarece nu există o astfel de metodă.

C# a fost inițial ca Java, iar mai târziu a adăugat suport dinamic, așa că Dart și C# sunt aproximativ aceleași în acest sens.
4. Funcții
Sintaxa de declarare a funcției
Sintaxa funcției în Dart este puțin mai ușoară și mai distractivă decât în C# sau Java. Sintaxa este oricare dintre acestea:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
De exemplu:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Trecerea parametrilor
Deoarece totul este un obiect, inclusiv primitive precum int
și String
, trecerea parametrilor ar putea fi confuză. Deși nu există niciun parametru ref
care trece ca în C#, totul este transmis prin referință, iar funcția nu poate schimba referința apelantului. Deoarece obiectele nu sunt clonate atunci când sunt transmise la funcții, o funcție poate modifica proprietățile obiectului. Cu toate acestea, această distincție pentru primitive precum int și String este efectiv discutabilă, deoarece acele tipuri sunt imuabile.
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'
Parametri opționali
Dacă vă aflați în lumea C# sau Java, probabil că ați blestemat situații cu metode supraîncărcate confuz precum acestea:
// 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
Sau cu parametrii opționali C#, există un alt tip de confuzie:
// 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# nu necesită denumirea argumentelor opționale la site-urile de apel, așa că metodele de refactorizare cu parametri opționali pot fi periculoase. Dacă unele site-uri de apeluri se întâmplă să fie legale după refactor, compilatorul nu le va prinde.
Dart are un mod mai sigur și foarte flexibil. În primul rând, metodele supraîncărcate nu sunt acceptate. În schimb, există două moduri de a gestiona parametrii opționali:
// 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
Nu puteți utiliza ambele stiluri în aceeași declarație de funcție.
Poziție async
cuvintelor cheie
C# are o poziție confuză pentru cuvântul său cheie async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Aceasta înseamnă că semnătura funcției este asincronă, dar de fapt doar implementarea funcției este asincronă. Oricare dintre semnăturile de mai sus ar fi o implementare validă a acestei interfețe:
interface ICanFoo { Task<int> Foo(); }
În limbajul Dart, async
este într-un loc mai logic, indicând implementarea este asincronă:
Future<int> foo() async {...}
Domeniul de aplicare și închiderile
La fel ca C# și Java, Dart are un scop lexical. Aceasta înseamnă că o variabilă declarată într-un bloc iese din domeniul de aplicare la sfârșitul blocului. Deci Dart se ocupă de închidere în același mod.
Sintaxa proprietății
Java a popularizat proprietatea get/set pattern, dar limbajul nu are nicio sintaxă specială pentru el:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# are sintaxă pentru el:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart are proprietăți de suport de sintaxă ușor diferite:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Constructorii
Constructorii de dart au mai multă flexibilitate decât în C# sau Java. O caracteristică plăcută este capacitatea de a numi diferiți constructori în aceeași clasă:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Puteți apela un constructor implicit doar cu numele clasei: var c = Client();
Există două tipuri de prescurtare pentru inițializarea membrilor instanței înainte ca corpul constructorului să fie apelat:
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 } }
Constructorii pot rula constructori de superclasă și pot redirecționa către alți constructori din aceeași clasă:
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
Constructorii care apelează alți constructori din aceeași clasă în Java și C# pot deveni confuzi atunci când ambii au implementări. În Dart, limitarea că redirecționarea constructorilor nu poate avea un corp obligă programatorul să clarifice straturile de constructori.
Există, de asemenea, un cuvânt cheie factory
care permite ca o funcție să fie utilizată ca un constructor, dar implementarea este doar o funcție obișnuită. Îl puteți folosi pentru a returna o instanță stocată în cache sau o instanță de tip derivat:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Modificatori
În Java și C#, avem modificatori de acces precum private
, protected
și public
. În Dart, acest lucru este simplificat drastic: dacă numele membrului începe cu un caracter de subliniere, este vizibil peste tot în interiorul pachetului (inclusiv din alte clase) și ascuns de apelanții din exterior; in rest, se vede de peste tot. Nu există cuvinte cheie precum private
care să semnifice vizibilitate.
Un alt tip de modificator controlează schimbarea: cuvintele cheie final
și const
sunt în acest scop, dar înseamnă lucruri diferite:
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. Ierarhia claselor
Limbajul Dart acceptă interfețe, clase și un fel de moștenire multiplă. Cu toate acestea, nu există niciun cuvânt cheie interface
; în schimb, toate clasele sunt, de asemenea, interfețe, așa că puteți defini o clasă abstract
și apoi o puteți implementa:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
Moștenirea multiplă se face cu o descendență principală folosind cuvântul cheie extends
și alte clase folosind cuvântul cheie with
:
class Employee extends Person with Salaried implements HasDesk {...}
În această declarație, clasa Employee
derivă din Person
și Salaried
, dar Person
este superclasa principală și Salaried
este mixin (superclasa secundară).
8. Operatori
Există câțiva operatori Dart distrași și utili cu care nu suntem obișnuiți.
Cascadele vă permit să utilizați un model de înlănțuire pe orice:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
Operatorul de răspândire permite ca o colecție să fie tratată ca o listă a elementelor sale într-un inițializator:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Fire
Dart nu are fire, ceea ce îi permite să se transpileze în JavaScript. În schimb, are „izolații”, care seamănă mai mult cu procese separate, în sensul că nu pot împărtăși memoria. Deoarece programarea cu mai multe fire este atât de predispusă la erori, această siguranță este văzută ca unul dintre avantajele Dart. Pentru a comunica între izolate, trebuie să transmiteți date între ele; obiectele primite sunt copiate în spațiul de memorie al izolatului receptor.
Dezvoltați-vă cu limbajul Dart: puteți face asta!
Dacă sunteți un dezvoltator C# sau Java, ceea ce știți deja vă va ajuta să învățați rapid limbajul Dart, deoarece a fost conceput pentru a fi familiar. În acest scop, am alcătuit un PDF de cheat sheet Dart pentru referință, concentrându-ne în special pe diferențele importante față de echivalentele C# și Java:
Diferențele prezentate în acest articol, combinate cu cunoștințele tale existente, te vor ajuta să devii productiv în prima zi sau două de Dart. Codare fericită!