Il linguaggio Dart: quando Java e C# non sono abbastanza nitidi
Pubblicato: 2022-03-11Nel lontano 2013, la versione 1.0 ufficiale di Dart ha ottenuto un po' di stampa, come con la maggior parte delle offerte di Google, ma non tutti erano ansiosi come i team interni di Google di creare app business-critical con il linguaggio Dart. Con la sua ricostruzione ben ponderata di Dart 2 cinque anni dopo, Google sembrava aver dimostrato il suo impegno per il linguaggio. In effetti, oggi continua a guadagnare terreno tra gli sviluppatori, in particolare i veterani di Java e C#.
Il linguaggio di programmazione Dart è importante per alcuni motivi:
- Ha il meglio di entrambi i mondi: è un linguaggio compilato e indipendente dai tipi (come C# e Java) e un linguaggio di scripting (come Python e JavaScript) allo stesso tempo.
- Traspila in JavaScript per l'uso come front-end web.
- Funziona su tutto e si compila in app mobili native, quindi puoi usarlo per quasi tutto.
- Dart è simile a C# e Java nella sintassi, quindi è veloce da imparare.
Quelli di noi del mondo C# o Java dei sistemi aziendali più grandi sanno già perché la sicurezza dei tipi, gli errori in fase di compilazione e i linter sono importanti. Molti di noi esitano ad adottare un linguaggio "scripty" per paura di perdere tutta la struttura, la velocità, l'accuratezza e la possibilità di debug a cui siamo abituati.
Ma con lo sviluppo di Dart, non dobbiamo rinunciare a nulla di tutto ciò. Possiamo scrivere un'app mobile, un client Web e un back-end nella stessa lingua e ottenere tutte le cose che amiamo ancora di Java e C#!
A tal fine, esaminiamo alcuni esempi chiave del linguaggio Dart che sarebbero nuovi per uno sviluppatore C# o Java, che alla fine riassumeremo in un PDF in linguaggio Dart.
Nota: questo articolo copre solo Dart 2.x. La versione 1.x non era "completamente preparata" - in particolare, il sistema dei tipi era consultivo (come TypeScript) anziché obbligatorio (come C# o Java).
1. Organizzazione del codice
In primo luogo, entreremo in una delle differenze più significative: come sono organizzati e referenziati i file di codice.
File di origine, ambito, spazi dei nomi e importazioni
In C#, una raccolta di classi viene compilata in un assembly. Ogni classe ha uno spazio dei nomi e spesso gli spazi dei nomi riflettono l'organizzazione del codice sorgente nel file system, ma alla fine l'assembly non conserva alcuna informazione sulla posizione del file del codice sorgente.
In Java, i file di origine fanno parte di un pacchetto e gli spazi dei nomi di solito sono conformi alla posizione del file system, ma alla fine un pacchetto è solo una raccolta di classi.
Quindi entrambe le lingue hanno un modo per mantenere il codice sorgente in qualche modo indipendente dal file system.
Al contrario, nel linguaggio Dart, ogni file sorgente deve importare tutto ciò a cui fa riferimento, inclusi gli altri file sorgente e i pacchetti di terze parti. Non ci sono spazi dei nomi allo stesso modo e spesso fai riferimento ai file tramite la loro posizione nel file system. Le variabili e le funzioni possono essere di livello superiore, non solo classi. In questi modi, Dart è più simile a uno script.
Quindi dovrai cambiare il tuo modo di pensare da "una raccolta di classi" a qualcosa di più simile a "una sequenza di file di codice inclusi".
Dart supporta sia l'organizzazione dei pacchetti che l'organizzazione ad hoc senza pacchetti. Iniziamo con un esempio senza pacchetti per illustrare la sequenza dei file inclusi:
// 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 }
Tutto ciò a cui si fa riferimento in un file di origine deve essere dichiarato o importato all'interno di quel file, poiché non esiste un livello di "progetto" né un altro modo per includere altri elementi di origine nell'ambito.
L'unico uso degli spazi dei nomi in Dart è dare un nome alle importazioni e ciò influisce sul modo in cui fai riferimento al codice importato da quel file.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Pacchi
Gli esempi sopra organizzano il codice senza pacchetti. Per utilizzare i pacchetti, il codice viene organizzato in modo più specifico. Ecco un layout di pacchetto di esempio per un pacchetto chiamato apples
:
-
apples/
-
pubspec.yaml
— definisce il nome del pacchetto, le dipendenze e altre cose -
lib/
-
apples.dart
: importazioni ed esportazioni; questo è il file importato dagli eventuali consumatori del pacchetto -
src/
-
seeds.dart
— tutto il resto del codice qui
-
-
-
bin/
-
runapples.dart
—contiene la funzione principale, che è il punto di ingresso (se si tratta di un pacchetto eseguibile o include strumenti eseguibili)
-
-
Quindi puoi importare interi pacchetti anziché singoli file:
import 'package:apples';
Le applicazioni non banali dovrebbero sempre essere organizzate come pacchetti. Ciò allevia molto la necessità di ripetere i percorsi del file system in ogni file di riferimento; inoltre, corrono più veloci. Semplifica anche la condivisione del tuo pacchetto su pub.dev, dove altri sviluppatori possono facilmente prenderlo per uso personale. I pacchetti utilizzati dalla tua app causeranno la copia del codice sorgente nel tuo file system, in modo da poter eseguire il debug in profondità in quei pacchetti come desideri.
2. Tipi di dati
Ci sono importanti differenze nel sistema dei tipi di Dart di cui essere a conoscenza, per quanto riguarda i valori null, i tipi numerici, le raccolte e i tipi dinamici.
Nulli ovunque
Venendo da C# o Java, siamo abituati a tipi primitivi o di valore distinti dai tipi di riferimento o di oggetto . I tipi di valore vengono, in pratica, allocati nello stack o nei registri e le copie del valore vengono inviate come parametri di funzione. I tipi di riferimento vengono invece allocati nell'heap e solo i puntatori all'oggetto vengono inviati come parametri di funzione. Poiché i tipi di valore occupano sempre memoria, una variabile tipizzata di valore non può essere null e tutti i membri di tipo valore devono avere valori inizializzati.
Dart elimina questa distinzione perché tutto è un oggetto; tutti i tipi derivano in definitiva dal tipo Object
. Quindi, questo è legale:
int i = null;
In effetti, tutte le primitive sono implicitamente inizializzate su null
. Ciò significa che non puoi presumere che i valori predefiniti degli interi siano zero come sei abituato in C# o Java e potrebbe essere necessario aggiungere controlli null.
È interessante notare che anche Null
è un tipo e la parola null
si riferisce a un'istanza di Null
:
print(null.runtimeType); // prints Null
Non tanti tipi numerici
A differenza del familiare assortimento di tipi interi da 8 a 64 bit con caratteri con segno e senza segno, il tipo intero principale di Dart è solo int
, un valore a 64 bit. (C'è anche BigInt
per numeri molto grandi.)
Poiché non esiste un array di byte come parte della sintassi del linguaggio, i contenuti dei file binari possono essere elaborati come elenchi di numeri interi, ad esempio List<Int>
.
Se stai pensando che questo deve essere terribilmente inefficiente, i designer ci hanno già pensato. In pratica, esistono diverse rappresentazioni interne a seconda del valore intero effettivo utilizzato in fase di esecuzione. Il runtime non alloca memoria heap per l'oggetto int
se può ottimizzarlo e utilizzare un registro CPU in modalità unboxed. Inoltre, la libreria byte_data
offre UInt8List
e alcune altre rappresentazioni ottimizzate.
Collezioni
Collezioni e generici sono molto simili a quelli a cui siamo abituati. La cosa principale da notare è che non ci sono array di dimensioni fisse: usa semplicemente il tipo di dati List
ovunque tu voglia utilizzare un array.
Inoltre, è disponibile il supporto sintattico per l'inizializzazione di tre tipi di raccolta:
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
Quindi, usa l' List
Dart dove useresti un array Java, ArrayList
o Vector
; o una matrice C# o List
. Usa Set
dove useresti un Java/C# HashSet
. Usa Map
dove useresti Java HashMap
o C# Dictionary
.
3. Digitazione dinamica e statica
In linguaggi dinamici come JavaScript, Ruby e Python, puoi fare riferimento ai membri anche se non esistono. Ecco un esempio 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 lo esegui, person.age
sarà undefined
, ma funziona comunque.
Allo stesso modo, puoi modificare il tipo di una variabile in JavaScript:
var a = 1; // a is a number a = 'one'; // a is now a string
Al contrario, in Java, non puoi scrivere codice come sopra perché il compilatore deve conoscere il tipo e controlla che tutte le operazioni siano legali, anche se usi la parola chiave var:
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java ti consente di codificare solo con tipi statici. (Puoi usare l'introspezione per eseguire un comportamento dinamico, ma non fa direttamente parte della sintassi.) JavaScript e alcuni altri linguaggi puramente dinamici consentono di codificare solo con tipi dinamici.
Il linguaggio Dart consente entrambi:
// 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 ha lo pseudo-tipo dynamic
che fa sì che tutta la logica del tipo venga gestita in fase di esecuzione. Il tentativo di chiamare a.foo()
non disturberà l'analizzatore statico e il codice verrà eseguito, ma fallirà in fase di esecuzione perché non esiste un metodo del genere.
C# era originariamente come Java e in seguito ha aggiunto il supporto dinamico, quindi Dart e C# sono più o meno gli stessi in questo senso.

4. Funzioni
Sintassi della dichiarazione di funzione
La sintassi della funzione in Dart è un po' più leggera e divertente rispetto a C# o Java. La sintassi è una di queste:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
Per esempio:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Passaggio di parametri
Poiché tutto è un oggetto, comprese le primitive come int
e String
, il passaggio dei parametri potrebbe creare confusione. Sebbene non ci siano parametri ref
che passano come in C#, tutto viene passato per riferimento e la funzione non può modificare il riferimento del chiamante. Poiché gli oggetti non vengono clonati quando vengono passati alle funzioni, una funzione può modificare le proprietà dell'oggetto. Tuttavia, quella distinzione per primitive come int e String è effettivamente discutibile poiché quei tipi sono immutabili.
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 opzionali
Se sei nei mondi C# o Java, probabilmente hai maledetto situazioni con metodi sovraccarichi confusi come questi:
// 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
Oppure con i parametri facoltativi C#, c'è un altro tipo di confusione:
// 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# non richiede la denominazione di argomenti facoltativi nei siti di chiamata, quindi il refactoring dei metodi con parametri facoltativi può essere pericoloso. Se alcuni siti di chiamata risultano legali dopo il refactoring, il compilatore non li catturerà.
Dart ha un modo più sicuro e molto flessibile. Innanzitutto, i metodi sovraccaricati non sono supportati. Esistono invece due modi per gestire i parametri facoltativi:
// 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
Non è possibile utilizzare entrambi gli stili nella stessa dichiarazione di funzione.
Posizione parola chiave async
C# ha una posizione confusa per la sua parola chiave async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Ciò implica che la firma della funzione è asincrona, ma in realtà solo l'implementazione della funzione è asincrona. Una delle firme di cui sopra sarebbe un'implementazione valida di questa interfaccia:
interface ICanFoo { Task<int> Foo(); }
Nel linguaggio Dart, async
si trova in una posizione più logica, denotando che l'implementazione è asincrona:
Future<int> foo() async {...}
Ambito e chiusure
Come C# e Java, Dart ha un ambito lessicale. Ciò significa che una variabile dichiarata in un blocco esce dall'ambito alla fine del blocco. Quindi Dart gestisce le chiusure allo stesso modo.
Sintassi della proprietà
Java ha reso popolare il modello get/set della proprietà ma il linguaggio non ha alcuna sintassi speciale per esso:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# ha una sintassi per questo:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart ha una sintassi leggermente diversa che supporta le proprietà:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Costruttori
I costruttori di Dart hanno un po' più di flessibilità rispetto a C# o Java. Una caratteristica interessante è la possibilità di nominare diversi costruttori nella stessa classe:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Puoi chiamare un costruttore predefinito solo con il nome della classe: var c = Client();
Esistono due tipi di scorciatoie per inizializzare i membri dell'istanza prima che venga chiamato il corpo del costruttore:
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 } }
I costruttori possono eseguire costruttori di superclassi e reindirizzare ad altri costruttori nella stessa 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
I costruttori che chiamano altri costruttori nella stessa classe in Java e C# possono creare confusione quando entrambi hanno implementazioni. In Dart, la limitazione che i costruttori di reindirizzamento non possono avere un corpo costringe il programmatore a rendere più chiari i livelli dei costruttori.
C'è anche una parola chiave factory
che consente di utilizzare una funzione come un costruttore, ma l'implementazione è solo una normale funzione. Puoi usarlo per restituire un'istanza memorizzata nella cache o un'istanza di tipo derivato:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Modificatori
In Java e C#, abbiamo modificatori di accesso come private
, protected
e public
. In Dart, questo è drasticamente semplificato: se il nome del membro inizia con un trattino basso, è visibile ovunque all'interno del pacchetto (anche da altre classi) e nascosto dai chiamanti esterni; in caso contrario, è visibile da ogni parte. Non ci sono parole chiave come private
per indicare visibilità.
Un altro tipo di modificatore controlla la modificabilità: le parole chiave final
e const
servono a tale scopo, ma significano cose diverse:
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. Gerarchia di classe
Il linguaggio Dart supporta interfacce, classi e una sorta di eredità multipla. Tuttavia, non esiste una parola chiave per l' interface
; invece, tutte le classi sono anche interfacce, quindi puoi definire una classe abstract
e quindi implementarla:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
L'ereditarietà multipla viene eseguita con un lignaggio principale utilizzando la parola chiave extends
e altre classi utilizzando la parola chiave with
:
class Employee extends Person with Salaried implements HasDesk {...}
In questa dichiarazione, la classe Employee
deriva da Person
e Salaried
, ma Person
è la superclasse principale e Salaried
è la mixin (la superclasse secondaria).
8. Operatori
Ci sono alcuni operatori Dart divertenti e utili a cui non siamo abituati.
Le cascate ti consentono di utilizzare uno schema di concatenamento su qualsiasi cosa:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
L'operatore di diffusione consente di trattare una raccolta come un elenco dei suoi elementi in un inizializzatore:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Fili
Dart non ha thread, il che gli consente di transpilare in JavaScript. Ha invece degli “isolati”, che sono più simili a processi separati, nel senso che non possono condividere la memoria. Poiché la programmazione multi-thread è così soggetta a errori, questa sicurezza è vista come uno dei vantaggi di Dart. Per comunicare tra gli isolati, è necessario trasmettere i dati tra di loro; gli oggetti ricevuti vengono copiati nello spazio di memoria dell'isolato ricevente.
Sviluppa con il linguaggio Dart: puoi farlo!
Se sei uno sviluppatore C# o Java, ciò che già conosci ti aiuterà ad apprendere rapidamente il linguaggio Dart, poiché è stato progettato per essere familiare. A tal fine, abbiamo messo insieme un PDF cheat sheet di Dart come riferimento, concentrandoci in particolare sulle importanti differenze rispetto agli equivalenti C# e Java:
Le differenze mostrate in questo articolo combinate con le tue conoscenze esistenti ti aiuteranno a diventare produttivo entro i tuoi primi giorni o due di Dart. Buona codifica!