Die Dart-Sprache: Wenn Java und C# nicht scharf genug sind
Veröffentlicht: 2022-03-11Vor langer Zeit im Jahr 2013 erhielt die offizielle Version 1.0 von Dart einige Presse – wie bei den meisten Google-Angeboten –, aber nicht alle waren so eifrig wie die internen Teams von Google, geschäftskritische Apps mit der Dart-Sprache zu erstellen. Mit seinem durchdachten Rebuild von Dart 2 fünf Jahre später schien Google sein Bekenntnis zur Sprache bewiesen zu haben. Tatsächlich gewinnt es heute unter Entwicklern immer mehr an Bedeutung – insbesondere unter Java- und C#-Veteranen.
Die Programmiersprache Dart ist aus mehreren Gründen wichtig:
- Es hat das Beste aus beiden Welten: Es ist gleichzeitig eine kompilierte, typsichere Sprache (wie C# und Java) und eine Skriptsprache (wie Python und JavaScript).
- Es wird zur Verwendung als Web-Frontend in JavaScript transpiliert.
- Es läuft auf allem und wird zu nativen mobilen Apps kompiliert, sodass Sie es für fast alles verwenden können.
- Dart ähnelt in der Syntax C# und Java und ist daher schnell zu erlernen.
Diejenigen von uns aus der C#- oder Java-Welt größerer Unternehmenssysteme wissen bereits, warum Typsicherheit, Kompilierungsfehler und Linters wichtig sind. Viele von uns zögern, eine „Skriptsprache“ zu übernehmen, aus Angst, die Struktur, Geschwindigkeit, Genauigkeit und Debugging-Fähigkeit zu verlieren, an die wir gewöhnt sind.
Aber mit der Dart-Entwicklung müssen wir auf nichts davon verzichten. Wir können eine mobile App, einen Webclient und ein Backend in derselben Sprache schreiben – und all die Dinge nutzen, die wir immer noch an Java und C# lieben!
Lassen Sie uns zu diesem Zweck einige wichtige Beispiele der Dart-Sprache durchgehen, die für einen C#- oder Java-Entwickler neu wären, die wir am Ende in einer Dart-Sprache-PDF zusammenfassen.
Hinweis: Dieser Artikel behandelt nur Dart 2.x. Version 1.x war nicht „ausgereift“ – insbesondere war das Typsystem beratend (wie TypeScript) statt erforderlich (wie C# oder Java).
1. Code-Organisation
Zuerst werden wir uns mit einem der wichtigsten Unterschiede befassen: wie Codedateien organisiert und referenziert werden.
Quelldateien, Geltungsbereich, Namespaces und Importe
In C# wird eine Sammlung von Klassen zu einer Assembly kompiliert. Jede Klasse hat einen Namensraum, und oft spiegeln Namensräume die Organisation des Quellcodes im Dateisystem wider – aber letztendlich speichert die Assembly keine Informationen über den Speicherort der Quellcodedatei.
In Java sind Quelldateien Teil eines Pakets und die Namensräume entsprechen normalerweise dem Speicherort des Dateisystems, aber letztendlich ist ein Paket nur eine Sammlung von Klassen.
Beide Sprachen haben also eine Möglichkeit, den Quellcode etwas unabhängig vom Dateisystem zu halten.
Im Gegensatz dazu muss in der Dart-Sprache jede Quelldatei alles importieren, worauf sie sich bezieht, einschließlich Ihrer anderen Quelldateien und Pakete von Drittanbietern. Es gibt keine Namensräume in gleicher Weise, und Sie beziehen sich häufig auf Dateien über ihren Speicherort im Dateisystem. Variablen und Funktionen können oberste Ebene sein, nicht nur Klassen. In dieser Hinsicht ist Dart eher skriptähnlich.
Sie müssen also Ihre Denkweise von „einer Sammlung von Klassen“ zu etwas mehr wie „einer Folge von enthaltenen Codedateien“ ändern.
Dart unterstützt sowohl Paketorganisation als auch Ad-hoc-Organisation ohne Pakete. Beginnen wir mit einem Beispiel ohne Pakete, um die Reihenfolge der enthaltenen Dateien zu veranschaulichen:
// 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 }
Alles, worauf Sie in einer Quelldatei verweisen, muss in dieser Datei deklariert oder importiert werden, da es keine „Projekt“-Ebene und keine andere Möglichkeit gibt, andere Quellelemente in den Geltungsbereich aufzunehmen.
Die einzige Verwendung von Namespaces in Dart besteht darin, Importen einen Namen zu geben, und das wirkt sich darauf aus, wie Sie auf den importierten Code aus dieser Datei verweisen.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Pakete
Die obigen Beispiele organisieren Code ohne Pakete. Um Pakete zu verwenden, wird der Code spezifischer organisiert. Hier ist ein Beispielpaketlayout für ein Paket namens apples
:
-
apples/
-
pubspec.yaml
– definiert den Paketnamen, Abhängigkeiten und einige andere Dinge -
lib/
-
apples.dart
– Importe und Exporte; Dies ist die Datei, die von allen Verbrauchern des Pakets importiert wird -
src/
-
seeds.dart
anderen Codes hier
-
-
-
bin/
-
runapples.dart
– enthält die Hauptfunktion, die der Einstiegspunkt ist (wenn dies ein lauffähiges Paket ist oder lauffähige Tools enthält)
-
-
Dann können Sie anstelle einzelner Dateien ganze Pakete importieren:
import 'package:apples';
Nicht triviale Anwendungen sollten immer als Pakete organisiert werden. Dadurch entfällt die Notwendigkeit, Dateisystempfade in jeder verweisenden Datei zu wiederholen; Außerdem laufen sie schneller. Es macht es auch einfach, Ihr Paket auf pub.dev zu teilen, wo andere Entwickler es sehr einfach für ihren eigenen Gebrauch abrufen können. Von Ihrer App verwendete Pakete führen dazu, dass der Quellcode in Ihr Dateisystem kopiert wird, sodass Sie so tief in diese Pakete debuggen können, wie Sie möchten.
2. Datentypen
Es gibt große Unterschiede im Typsystem von Dart, die es zu beachten gilt, in Bezug auf Nullen, numerische Typen, Sammlungen und dynamische Typen.
Überall Nullen
Von C# oder Java kommend, sind wir daran gewöhnt, primitive oder Werttypen von Referenz- oder Objekttypen zu unterscheiden. Werttypen werden in der Praxis auf dem Stapel oder in Registern zugewiesen, und Kopien des Werts werden als Funktionsparameter gesendet. Referenztypen werden stattdessen auf dem Heap allokiert, und nur Zeiger auf das Objekt werden als Funktionsparameter gesendet. Da Werttypen immer Speicher belegen, kann eine Werttypvariable nicht null sein, und alle Werttypmitglieder müssen initialisierte Werte haben.
Dart beseitigt diese Unterscheidung, weil alles ein Objekt ist; alle Typen leiten sich letztendlich vom Typ Object
ab. Das ist also legal:
int i = null;
Tatsächlich werden alle Primitive implizit auf null
initialisiert. Das bedeutet, dass Sie nicht davon ausgehen können, dass die Standardwerte von Ganzzahlen Null sind, wie Sie es in C# oder Java gewohnt sind, und Sie möglicherweise Nullprüfungen hinzufügen müssen.
Interessanterweise ist sogar Null
ein Typ, und das Wort null
bezieht sich auf eine Instanz von Null
:
print(null.runtimeType); // prints Null
Nicht so viele numerische Typen
Im Gegensatz zu der bekannten Auswahl an Integer-Typen von 8 bis 64 Bit mit vorzeichenbehafteten und vorzeichenlosen Varianten ist der Haupt-Integer-Typ von Dart einfach int
, ein 64-Bit-Wert. (Es gibt auch BigInt
für sehr große Zahlen.)
Da es kein Byte-Array als Teil der Sprachsyntax gibt, können binäre Dateiinhalte als Listen von Ganzzahlen verarbeitet werden, dh List<Int>
.
Wenn Sie denken, dass dies schrecklich ineffizient sein muss, haben die Designer bereits daran gedacht. In der Praxis gibt es unterschiedliche interne Darstellungen, abhängig von dem tatsächlich zur Laufzeit verwendeten ganzzahligen Wert. Die Laufzeit weist dem int
-Objekt keinen Heap-Speicher zu, wenn sie diesen wegoptimieren und ein CPU-Register im Unboxed-Modus verwenden kann. Außerdem bietet die Bibliothek byte_data
UInt8List
und einige andere optimierte Darstellungen.
Sammlungen
Sammlungen und Generika sind dem, was wir gewohnt sind, sehr ähnlich. Beachten Sie vor allem, dass es keine Arrays mit fester Größe gibt: Verwenden Sie einfach den Datentyp List
, wo immer Sie ein Array verwenden würden.
Außerdem gibt es syntaktische Unterstützung für die Initialisierung von drei der Sammlungstypen:
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
Verwenden Sie also die Dart- List
, wo Sie ein Java-Array, ArrayList
oder Vector
verwenden würden; oder ein C#-Array oder List
. Verwenden Sie Set
anstelle von Java/C# HashSet
. Verwenden Sie Map
anstelle von Java HashMap
oder C# Dictionary
.
3. Dynamisches und statisches Schreiben
In dynamischen Sprachen wie JavaScript, Ruby und Python können Sie auf Member verweisen, selbst wenn sie nicht vorhanden sind. Hier ist ein JavaScript-Beispiel:
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 // ... }
Wenn Sie dies ausführen, ist person.age
undefined
, wird aber trotzdem ausgeführt.
Ebenso können Sie den Typ einer Variablen in JavaScript ändern:
var a = 1; // a is a number a = 'one'; // a is now a string
Im Gegensatz dazu können Sie in Java keinen Code wie den obigen schreiben, da der Compiler den Typ kennen muss und prüft, ob alle Operationen zulässig sind – selbst wenn Sie das Schlüsselwort var verwenden:
var b = 1; // a is an int // b = "one"; // not allowed in Java
In Java können Sie nur mit statischen Typen codieren. (Sie können Introspektion verwenden, um etwas dynamisches Verhalten zu erzielen, aber es ist nicht direkt Teil der Syntax.) JavaScript und einige andere rein dynamische Sprachen erlauben Ihnen nur, mit dynamischen Typen zu codieren.
Die Dart-Sprache erlaubt beides:
// 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 hat den Pseudotyp dynamic
, der bewirkt, dass die gesamte Typlogik zur Laufzeit verarbeitet wird. Der Versuch, a.foo()
, stört den statischen Analysator nicht und der Code wird ausgeführt, aber er schlägt zur Laufzeit fehl, da es keine solche Methode gibt.
C# war ursprünglich wie Java und wurde später um dynamische Unterstützung erweitert, sodass Dart und C# in dieser Hinsicht ungefähr gleich sind.

4. Funktionen
Funktionsdeklarationssyntax
Die Funktionssyntax in Dart ist etwas leichter und macht mehr Spaß als in C# oder Java. Die Syntax ist eine von diesen:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
Zum Beispiel:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Parameterübergabe
Da alles ein Objekt ist, einschließlich Primitive wie int
und String
, kann die Parameterübergabe verwirrend sein. Es gibt zwar keinen ref
-Parameter wie in C#, aber alles wird als Referenz übergeben, und die Funktion kann die Referenz des Aufrufers nicht ändern. Da Objekte bei der Übergabe an Funktionen nicht geklont werden, kann eine Funktion Eigenschaften des Objekts ändern. Diese Unterscheidung für Primitive wie int und String ist jedoch praktisch strittig, da diese Typen unveränderlich sind.
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'
Optionale Parameter
Wenn Sie in der C#- oder Java-Welt sind, haben Sie Situationen mit verwirrend überladenen Methoden wie diesen wahrscheinlich verflucht:
// 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
Oder mit optionalen C#-Parametern gibt es eine andere Art von Verwirrung:
// 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# erfordert keine Benennung optionaler Argumente auf Aufrufseiten, daher kann das Refactoring von Methoden mit optionalen Parametern gefährlich sein. Wenn einige Aufrufseiten nach dem Refactoring legal sind, wird der Compiler sie nicht erkennen.
Dart hat einen sichereren und sehr flexiblen Weg. Zunächst einmal werden überladene Methoden nicht unterstützt. Stattdessen gibt es zwei Möglichkeiten, mit optionalen Parametern umzugehen:
// 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
Sie können nicht beide Stile in derselben Funktionsdeklaration verwenden.
async
Keyword-Position
C# hat eine verwirrende Position für sein Schlüsselwort async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Dies impliziert, dass die Funktionssignatur asynchron ist, aber wirklich nur die Funktionsimplementierung asynchron ist. Jede der obigen Signaturen wäre eine gültige Implementierung dieser Schnittstelle:
interface ICanFoo { Task<int> Foo(); }
In der Dart-Sprache befindet sich async
an einer logischeren Stelle, was bedeutet, dass die Implementierung asynchron ist:
Future<int> foo() async {...}
Geltungsbereich und Schließungen
Wie C# und Java ist Dart lexikalisch begrenzt. Dies bedeutet, dass eine in einem Block deklarierte Variable am Ende des Blocks den Geltungsbereich verlässt. Dart handhabt Verschlüsse also auf die gleiche Weise.
Eigenschaftssyntax
Java hat das Get/Set-Muster für Eigenschaften populär gemacht, aber die Sprache hat keine spezielle Syntax dafür:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# hat dafür eine Syntax:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart hat etwas andere syntaxunterstützende Eigenschaften:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Konstrukteure
Dart-Konstruktoren sind wesentlich flexibler als in C# oder Java. Ein nettes Feature ist die Möglichkeit, verschiedene Konstruktoren in derselben Klasse zu benennen:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Sie können einen Standardkonstruktor nur mit dem Klassennamen aufrufen: var c = Client();
Es gibt zwei Arten von Abkürzungen zum Initialisieren von Instanzmitgliedern, bevor der Konstruktorrumpf aufgerufen wird:
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 } }
Konstruktoren können Superklassen-Konstruktoren ausführen und auf andere Konstruktoren in derselben Klasse umleiten:
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
Konstruktoren, die andere Konstruktoren derselben Klasse in Java und C# aufrufen, können verwirrend werden, wenn sie beide Implementierungen haben. In Dart zwingt die Einschränkung, dass umleitende Konstruktoren keinen Körper haben können, den Programmierer dazu, die Ebenen der Konstruktoren klarer zu gestalten.
Es gibt auch ein factory
Schlüsselwort, das es ermöglicht, eine Funktion wie einen Konstruktor zu verwenden, aber die Implementierung ist nur eine reguläre Funktion. Sie können es verwenden, um eine zwischengespeicherte Instanz oder eine Instanz eines abgeleiteten Typs zurückzugeben:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Modifikatoren
In Java und C# haben wir Zugriffsmodifikatoren wie private
, protected
und public
. In Dart wird dies drastisch vereinfacht: Wenn der Mitgliedsname mit einem Unterstrich beginnt, ist er überall im Paket sichtbar (auch von anderen Klassen) und für externe Anrufer verborgen; Andernfalls ist es von überall sichtbar. Es gibt keine Schlüsselwörter wie private
, um Sichtbarkeit zu kennzeichnen.
Eine andere Art von Modifikator steuert die Änderbarkeit: Die Schlüsselwörter final
und const
dienen diesem Zweck, bedeuten aber unterschiedliche Dinge:
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. Klassenhierarchie
Die Sprache Dart unterstützt Schnittstellen, Klassen und eine Art Mehrfachvererbung. Es gibt jedoch kein interface
Schlüsselwort; Stattdessen sind alle Klassen auch Schnittstellen, sodass Sie eine abstract
Klasse definieren und dann implementieren können:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
Mehrfachvererbung erfolgt mit einer Hauptlinie unter Verwendung des Schlüsselworts extends
und anderen Klassen unter Verwendung des Schlüsselworts with
:
class Employee extends Person with Salaried implements HasDesk {...}
In dieser Deklaration leitet sich die Employee
-Klasse von Person
und Salaried
ab, aber Person
ist die Hauptsuperklasse und Salaried
ist das Mixin (die sekundäre Superklasse).
8. Betreiber
Es gibt einige lustige und nützliche Dart-Operatoren, an die wir nicht gewöhnt sind.
Mit Kaskaden können Sie ein Verkettungsmuster für alles verwenden:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
Der Spread-Operator ermöglicht es, eine Sammlung als Liste ihrer Elemente in einem Initialisierer zu behandeln:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Fäden
Dart hat keine Threads, wodurch es in JavaScript transpiliert werden kann. Stattdessen gibt es „Isolate“, die eher separaten Prozessen ähneln, in dem Sinne, dass sie den Speicher nicht teilen können. Da die Multithread-Programmierung so fehleranfällig ist, wird diese Sicherheit als einer der Vorteile von Dart angesehen. Um zwischen Isolaten zu kommunizieren, müssen Sie Daten zwischen ihnen streamen; die empfangenen Objekte werden in den Speicherbereich des empfangenden Isolats kopiert.
Entwickeln mit der Dart-Sprache: Sie können das tun!
Wenn Sie ein C#- oder Java-Entwickler sind, hilft Ihnen das, was Sie bereits wissen, beim schnellen Erlernen der Dart-Sprache, da sie so konzipiert wurde, dass sie vertraut ist. Zu diesem Zweck haben wir ein PDF mit einem Dart-Spickzettel als Referenz zusammengestellt, das sich speziell auf wichtige Unterschiede zu C#- und Java-Äquivalenten konzentriert:
Die in diesem Artikel aufgezeigten Unterschiede in Kombination mit Ihrem vorhandenen Wissen werden Ihnen dabei helfen, innerhalb der ersten ein bis zwei Tage mit Dart produktiv zu werden. Viel Spaß beim Codieren!