Dart vs Swift: ein Vergleich

Das Dart-Logo ist unter der Creative Commons Attribution 3.0 Unported-Lizenz lizenziert.

Dart und Swift sind meine beiden Lieblingsprogrammiersprachen. Ich habe sie ausgiebig im kommerziellen und Open-Source-Code verwendet.

Dieser Artikel bietet einen direkten Vergleich zwischen Dart und Swift und zielt darauf ab:

  • Markieren Sie die Unterschiede zwischen den beiden.
  • Eine Referenz für Entwickler sein, die von einer Sprache in die andere wechseln (oder beide verwenden).

Ein Kontext:

  • Dart treibt Flutter an, Googles Framework zum Erstellen nativer Apps aus einer einzigen Codebasis.
  • Swift unterstützt Apples SDKs für iOS, macOS, tvOS und watchOS.

Der folgende Vergleich bezieht sich auf die Hauptfunktionen beider Sprachen (ab Dart 2.1 und Swift 4.2). Da die ausführliche Erörterung der einzelnen Funktionen den Rahmen dieses Artikels sprengt, füge ich gegebenenfalls Verweise zum Lesen hinzu.

Inhaltsverzeichnis

  • Vergleichstabelle
  • Variablen
  • Inferenz eingeben
  • Veränderbare / unveränderliche Variablen
  • Funktionen
  • Benannte und nicht benannte Parameter
  • Optionale und Standardparameter
  • Verschlüsse
  • Tuples
  • Kontrollfluss
  • Sammlungen (Arrays, Sets, Karten)
  • Nullability & Optionals
  • Klassen
  • Erbe
  • Eigenschaften
  • Protokolle / Abstrakte Klassen
  • Mixins
  • Erweiterungen
  • Enums
  • Structs
  • Fehlerbehandlung
  • Generika
  • Zugangskontrolle
  • Asynchrone Programmierung: Futures
  • Asynchrone Programmierung: Streams
  • Speicherverwaltung
  • Zusammenstellung und Ausführung
  • Andere Funktionen nicht abgedeckt
  • Meine Lieblings-Swift-Features fehlen bei Dart
  • Meine Lieblings-Dart-Features fehlen bei Swift
  • Fazit
  • Referenzen & Credits

Vergleichstabelle

Variablen

Die Syntax der Variablendeklaration sieht in Dart folgendermaßen aus:

String name;
int age;
doppelte Höhe;

Und so in Swift:

var name: String
Var Alter: Int
var Höhe: doppelt

Die Variableninitialisierung sieht in Dart folgendermaßen aus:

var name = 'Andrea';
var age = 34;
var height = 1,84;

Und so in Swift:

var name = "Andrea"
var age = 34
var height = 1.84

In diesem Beispiel werden Typanmerkungen nicht benötigt. Dies liegt daran, dass beide Sprachen die Typen aus dem Ausdruck auf der rechten Seite der Zuweisung ableiten können.

Apropos, wovon ...

Inferenz eingeben

Inferenz bedeutet, dass wir Folgendes in Dart schreiben können:

var arguments = {'argA': 'hallo', 'argB': 42}; //  abbilden

Die Art der Argumente wird vom Compiler automatisch aufgelöst.

In Swift kann das Gleiche geschrieben werden wie:

var arguments = ["argA": "Hallo", "argB": 42] // [String: Beliebig]

Noch ein paar Details

Zitieren der Dokumentation für Dart:

Der Analyzer kann Typen für Felder, Methoden, lokale Variablen und die meisten generischen Typargumente ableiten. Wenn der Analysator nicht über genügend Informationen verfügt, um einen bestimmten Typ abzuleiten, wird der dynamische Typ verwendet.

Und für Swift:

Swift verwendet in großem Umfang die Typinferenz, sodass Sie den Typ oder einen Teil des Typs vieler Variablen und Ausdrücke in Ihrem Code weglassen können. Anstatt zum Beispiel var x: Int = 0 zu schreiben, können Sie var x = 0 schreiben und dabei den Typ vollständig weglassen - der Compiler schließt korrekt, dass x einen Wert vom Typ Int nennt.

Dynamische Typen

Eine Variable, die von einem beliebigen Typ sein kann, wird mit dem dynamischen Schlüsselwort in Dart und dem Schlüsselwort Any in Swift deklariert.

Dynamische Typen werden häufig beim Lesen von Daten wie JSON verwendet.

Veränderbare / unveränderliche Variablen

Variablen können als veränderlich oder unveränderlich deklariert werden.

Um veränderbare Variablen zu deklarieren, verwenden beide Sprachen das Schlüsselwort var.

var a = 10; // int (Dart)
a = 20; // okay
var a = 10 // Int (Swift)
a = 20 // ok

Um unveränderliche Variablen zu deklarieren, verwendet Dart final und Swift let.

final a = 10;
a = 20; // 'a': eine letzte Variable, kann nur einmal gesetzt werden.
sei a = 10
a = 20 // Wert kann nicht zugewiesen werden: 'a' ist eine 'let'-Konstante

Hinweis: In der Dart-Dokumentation werden zwei Schlüsselwörter definiert, final und const. Diese funktionieren wie folgt:

Wenn Sie niemals vorhaben, eine Variable zu ändern, verwenden Sie final oder const anstelle von var oder zusätzlich zu einem Typ. Eine endgültige Variable kann nur einmal festgelegt werden. Eine const-Variable ist eine Konstante zur Kompilierungszeit. (Const-Variablen sind implizit final.) Eine finale Variable der obersten Ebene oder Klasse wird bei der ersten Verwendung initialisiert.

Weitere Erklärungen finden Sie in diesem Beitrag auf der Dart-Website:

final bedeutet einmalige Zuordnung. Eine letzte Variable oder ein Feld muss einen Initialisierer haben. Sobald ein Wert zugewiesen wurde, kann der Wert einer endgültigen Variablen nicht mehr geändert werden. final ändert Variablen.

TL; DR: Verwenden Sie final, um unveränderliche Variablen in Dart zu definieren.

In Swift deklarieren wir Konstanten mit let. Zitat:

Eine konstante Deklaration führt einen konstanten benannten Wert in Ihr Programm ein. Konstante Deklarationen werden mit dem Schlüsselwort let deklariert und haben die folgende Form:
Lassen Sie die Konstante name: type = expression
Eine Konstantendeklaration definiert eine unveränderliche Bindung zwischen dem Konstantennamen und dem Wert des Initialisierungsausdrucks. Nachdem der Wert einer Konstante festgelegt wurde, kann er nicht mehr geändert werden.

Lesen Sie mehr: Swift Declarations.

Funktionen

Funktionen sind erstklassige Bürger in Swift und Dart.

Dies bedeutet, dass Funktionen wie Objekte als Argumente übergeben, als Eigenschaften gespeichert oder als Ergebnis zurückgegeben werden können.

Als ersten Vergleich können wir sehen, wie man Funktionen deklariert, die keine Argumente annehmen.

In Dart steht der Rückgabetyp vor dem Methodennamen:

void foo ();
int bar ();

In Swift verwenden wir die -> T-Notation als Suffix. Dies ist nicht erforderlich, wenn kein Rückgabewert (Void) vorliegt:

func foo ()
Funktionsleiste () -> Int

Weiterlesen:

  • Dart-Funktionen
  • Schnelle Funktionen

Benannte und nicht benannte Parameter

Beide Sprachen unterstützen benannte und nicht benannte Parameter.

In Swift werden die Parameter standardmäßig benannt:

func foo (Name: String, Alter: Int, Größe: Double)
foo (Name: "Andrea", Alter: 34, Größe: 1,84)

In Dart definieren wir benannte Parameter mit geschweiften Klammern ({}):

void foo ({String name, int age, double height});
foo (Name: 'Andrea', Alter: 34, Größe: 1,84);

In Swift definieren wir nicht benannte Parameter, indem wir einen Unterstrich (_) als externen Parameter verwenden:

func foo (_ name: String, _ alter: Int, _ höhe: Double)
foo ("Andrea", 34, 1,84)

In Dart definieren wir nicht benannte Parameter, indem wir die geschweiften Klammern ({}) weglassen:

void foo (String name, int age, double height);
foo ("Andrea", 34, 1,84);

Lesen Sie mehr: Funktionsargumentbeschriftungen und Parameternamen in Swift.

Optionale und Standardparameter

Beide Sprachen unterstützen Standardparameter.

In Swift können Sie einen Standardwert für jeden Parameter in einer Funktion definieren, indem Sie dem Parameter nach dem Typ des Parameters einen Wert zuweisen. Wenn ein Standardwert definiert ist, können Sie diesen Parameter beim Aufrufen der Funktion weglassen.
func foo (Name: String, Alter: Int = 0, Höhe: Double = 0.0)
foo (Name: "Andrea", Alter: 34) // Name: "Andrea", Alter: 34, Größe: 0,0

Lesen Sie mehr: Standardparameterwerte in Swift.

In Dart können optionale Parameter entweder positionsbezogen oder benannt sein, jedoch nicht beides.

// positionale optionale Parameter
void foo (String name, [int age = 0, double height = 0.0]);
foo (Andrea, 34); // Name: 'Andrea', Alter: 34, Größe: 0.0
// benannte optionale Parameter
void foo ({String name, int age = 0, double height = 0.0});
foo (Name: 'Andrea', Alter: 34); // Name: 'Andrea', Alter: 34, Größe: 0.0

Lesen Sie mehr: Optionale Parameter in Dart.

Verschlüsse

Als erstklassige Objekte können Funktionen als Argumente an andere Funktionen übergeben oder Variablen zugewiesen werden.

Funktionen werden in diesem Zusammenhang auch als Verschlüsse bezeichnet.

Hier ist ein Dart-Beispiel einer Funktion, die eine Liste von Elementen durchläuft und einen Abschluss verwendet, um den Index und den Inhalt der einzelnen Elemente zu drucken:

letzte Liste = ['Äpfel', 'Bananen', 'Orangen'];
list.forEach ((item) => print ('$ {list.indexOf (item)}: $ item'));

Der Abschluss akzeptiert ein Argument (Element), gibt den Index und den Wert dieses Elements aus und gibt keinen Wert zurück.

Beachten Sie die Verwendung der Pfeilnotation (=>). Dies kann anstelle einer einzelnen return-Anweisung in geschweiften Klammern verwendet werden:

list.forEach ((item) {print ('$ {list.indexOf (item)}: $ item');});

Der gleiche Code in Swift sieht folgendermaßen aus:

let list = ["Äpfel", "Bananen", "Orangen"]
list.forEach ({print ("\ (String (description: list.firstIndex (of: $ 0))) \ ($ 0)"))

In diesem Fall geben wir keinen Namen für das an den Abschluss übergebene Argument an und verwenden stattdessen $ 0 als erstes Argument. Dies ist völlig optional und wir können einen benannten Parameter verwenden, wenn wir es vorziehen:

list.forEach ({item in print ("\ (String (beschreibend: list.firstIndex (von: item)) \ (item)"))

Closures werden in Swift häufig als Abschlussblöcke für asynchronen Code verwendet (siehe Abschnitt über asynchrone Programmierung weiter unten).

Weiterlesen:

  • Dart Anonymous Funktionen
  • Schnelle Schließungen

Tuples

Aus den Swift Docs:

Tupel gruppieren mehrere Werte zu einem einzigen zusammengesetzten Wert. Die Werte in einem Tupel können von einem beliebigen Typ sein und müssen nicht vom gleichen Typ sein.

Diese können als kleine, leichte Typen verwendet werden und sind nützlich, wenn Funktionen mit mehreren Rückgabewerten definiert werden.

So verwenden Sie Tupel in Swift:

let t = ("Andrea", 34, 1,84)
print (t.0) // druckt "Andrea"
print (t.1) // druckt 34
print (t.2) // druckt 1,84

Tupel werden mit einem separaten Paket in Dart unterstützt:

const t = const Tuple3  ('Andrea', 34, 1.84);
print (t.item1); // druckt 'Andrea'
print (t.item2); // druckt 34
print (t.item3); // druckt 1,84

Kontrollfluss

Beide Sprachen bieten eine Vielzahl von Kontrollflussanweisungen.

Beispiele hierfür sind switch-Anweisungen für Bedingungen, for- und while-Schleifen.

Es wäre ziemlich langwierig, diese hier zu behandeln, daher verweise ich auf die offiziellen Dokumente:

  • Schneller Kontrollfluss
  • Dart Control Flow Anweisungen

Sammlungen (Arrays, Sets, Karten)

Arrays / Listen

Arrays sind geordnete Gruppen von Objekten.

Arrays können in Dart als Listen erstellt werden:

var emptyList =  []; // leere Liste
var list = [1, 2, 3]; // Literal auflisten
list.length; // 3
Liste [1]; // 2

Arrays haben in Swift einen eingebauten Typ:

var emptyArray = [Int] () // leeres Array
var array = [1, 2, 3] // Array-Literal
array.count // 3
array [1] // 2

Setzt

Zitieren der Swift-Dokumente:

Eine Menge speichert unterschiedliche Werte desselben Typs in einer Sammlung ohne definierte Reihenfolge. Sie können einen Satz anstelle eines Arrays verwenden, wenn die Reihenfolge der Elemente nicht wichtig ist oder wenn Sie sicherstellen müssen, dass ein Element nur einmal angezeigt wird.

Dies wird mit der Set-Klasse in Dart definiert.

var emptyFruits =  {}; // leeres Set-Literal
var fruits = {'apple', 'banana'}; // setze Literal

Ebenso in Swift:

var emptyFruits = Setze  ()
var fruits = Set  (["Apfel", "Banane"])

Karten / Wörterbücher

Die Swift-Dokumente haben eine gute Definition für eine Karte / ein Wörterbuch:

In einem Wörterbuch werden Zuordnungen zwischen Schlüsseln desselben Typs und Werten desselben Typs in einer Sammlung ohne definierte Reihenfolge gespeichert. Jeder Wert ist einem eindeutigen Schlüssel zugeordnet, der als Kennung für diesen Wert im Wörterbuch fungiert.

Maps werden in Dart wie folgt definiert:

var namesOfIntegers = Map  (); // leere Karte
var airport = {'YYZ': 'Toronto Pearson', 'DUB': 'Dublin'}; // Map-Literal

Karten werden in Swift als Wörterbücher bezeichnet:

var namesOfIntegers = [Int: String] () // leeres Wörterbuch
var airport = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] // Wörterbuchliteral

Weiterlesen:

  • Dart Sammlungen
  • Swift Collections (insbesondere empfehle ich den Abschnitt über Sets).

Nullability & Optionals

In Dart kann jedes Objekt null sein. Der Versuch, auf Methoden oder Variablen von Nullobjekten zuzugreifen, führt zu einer Nullzeigerausnahme. Dies ist eine der häufigsten Fehlerquellen (wenn nicht die häufigste) in Computerprogrammen.

Swift verfügte von Anfang an über optionals, eine integrierte Sprachfunktion, mit der angegeben werden kann, ob Objekte einen Wert haben können oder nicht. Zitieren der Dokumente:

Sie verwenden optionals in Situationen, in denen ein Wert fehlen kann. Ein optionales Objekt bietet zwei Möglichkeiten: Entweder gibt es einen Wert, und Sie können das optionale Objekt auspacken, um auf diesen Wert zuzugreifen, oder es gibt überhaupt keinen Wert.

Im Gegensatz dazu können wir nicht-optionale Variablen verwenden, um sicherzustellen, dass sie immer einen Wert haben:

var x: Int? // Optional
var y: Int = 1 // nicht optional, muss initialisiert werden

Hinweis: Die Angabe, dass eine Swift-Variable optional ist, entspricht in etwa der Angabe, dass eine Dart-Variable null sein kann.

Ohne Sprachunterstützung für optionale Elemente können wir zur Laufzeit nur prüfen, ob eine Variable null ist.

Bei Optionen werden diese Informationen stattdessen zur Kompilierungszeit codiert. Wir können optionals auspacken, um sicher zu überprüfen, ob sie einen Wert enthalten:

func showOptional (x: Int?) {
  // benutze `guard let` anstatt` if let` als Best Practice
  wenn x = x {// optional auspacken lassen
    drucken (x)
  } else {
    print ("kein Wert")
  }
}
showOptional (x: nil) // gibt "kein Wert" aus
showOptional (x: 5) // druckt "5"

Und wenn wir wissen, dass eine Variable einen Wert haben muss, können wir einen nicht optionalen Wert verwenden:

func showNonOptional (x: Int) {
  drucken (x)
}
showNonOptional (x: nil) // [Kompilierungsfehler] Nil ist nicht kompatibel mit dem erwarteten Argumenttyp 'Int'
showNonOptional (x: 5) // druckt "5"

Das erste obige Beispiel kann wie folgt in Dart implementiert werden:

void showOptional (int x) {
  if (x! = null) {
    print (x);
  } else {
    print ('kein Wert');
  }
}
showOptional (null) // gibt "kein Wert" aus
showOptional (5) // druckt "5"

Und die zweite davon:

void showNonOptional (int x) {
  assert (x! = null);
  print (x);
}
showNonOptional (null) // [Laufzeitfehler] Nicht erfasste Ausnahme: Assertion fehlgeschlagen
showNonOptional (5) // druckt "5"

Optionals zu haben bedeutet, dass wir Fehler eher zur Kompilierungszeit als zur Laufzeit abfangen können. Wenn Sie Fehler frühzeitig abfangen, ist der Code sicherer und es treten weniger Fehler auf.

Darts mangelnde Unterstützung für optionale Elemente wird durch die Verwendung von Zusicherungen (und der @ erforderlichen Anmerkung für benannte Parameter) auf irgendeine Weise entschärft.

Diese werden im Flutter SDK häufig verwendet, führen jedoch zu zusätzlichem Code für die Boilerplate.

Für den Datensatz gibt es einen Vorschlag zum Hinzufügen von nicht nullwertfähigen Typen zu Dart.

Es gibt viel mehr über Optionen, als ich hier behandelt habe. Einen guten Überblick finden Sie unter: Optionen in Swift.

Klassen

Klassen sind der Hauptbaustein für das Schreiben von Programmen in objektorientierten Sprachen.

Der Unterricht wird von Dart und Swift mit einigen Unterschieden unterstützt.

Syntax

Hier ist eine Klasse mit einem Initialisierer und drei Mitgliedsvariablen in Swift:

Klasse Person {
  let name: String
  zulassen Alter: Int
  lass die Höhe verdoppeln
  init (Name: String, Alter: Int, Höhe: Double) {
    self.name = name
    self.age = alter
    self.height = height
  }
}

Und das gleiche in Dart:

Klasse Person {
  Person ({this.name, this.age, this.height});
  endgültiger String-Name;
  endgültiges int Alter;
  endgültige doppelte Höhe;
}

Beachten Sie die Verwendung von. [PropertyName] im Dart-Konstruktor. Dies ist syntaktischer Zucker zum Festlegen der Instanzmitgliedsvariablen, bevor der Konstruktor ausgeführt wird.

Fabrikbauer

In Dart ist es möglich, Factory-Konstruktoren zu erstellen. Zitat:

Verwenden Sie das factory-Schlüsselwort, wenn Sie einen Konstruktor implementieren, der nicht immer eine neue Instanz seiner Klasse erstellt.

Ein praktischer Anwendungsfall für Factory-Konstruktoren ist das Erstellen einer Modellklasse aus JSON:

Klasse Person {
  Person ({this.name, this.age, this.height});
  endgültiger String-Name;
  endgültiges int Alter;
  endgültige doppelte Höhe;
  factory Person.fromJSON (Map  json) {
    String name = json ['name'];
    int age = json ['age'];
    doppelte Höhe = json ['Höhe'];
    Rückgabe Person (Name: Name, Alter: Alter, Größe: Größe);
  }
}
var p = Person.fromJSON ({
  'name': 'Andrea',
  "Alter": 34,
  "Höhe": 1,84,
});

Weiterlesen:

  • Dart Klassen
  • Schnelle Strukturen und Klassen

Erbe

Swift verwendet ein Einzelvererbungsmodell, dh, jede Klasse kann nur eine Superklasse haben. Swift-Klassen können mehrere Schnittstellen (auch als Protokolle bezeichnet) implementieren.

Dart-Klassen haben eine Mixin-basierte Vererbung. Zitieren der Dokumente:

Jedes Objekt ist eine Instanz einer Klasse, und alle Klassen stammen von Object ab. Mixin-basierte Vererbung bedeutet, dass obwohl jede Klasse (außer Object) genau eine Oberklasse hat, ein Klassenrumpf in mehreren Klassenhierarchien wiederverwendet werden kann.

Hier ist die Einzelvererbung in Swift in Aktion:

Klasse Fahrzeug {
  let wheelCount: Int
  init (wheelCount: Int) {
    self.wheelCount = wheelCount
  }
}
Klasse Fahrrad: Fahrzeug {
  drin() {
    super.init (wheelCount: 2)
  }
}

Und in Dart:

Klasse Fahrzeug {
  Fahrzeug ({this.wheelCount});
  final int wheelCount;
}
klasse fahrrad erweitert fahrzeug {
  Fahrrad (): super (Radanzahl: 2);
}

Eigenschaften

Diese werden in Dart als Instanzvariablen und in Swift einfach als Eigenschaften bezeichnet.

In Swift wird zwischen gespeicherten und berechneten Eigenschaften unterschieden:

Klasse Circle {
  init (Radius: Double) {
    self.radius = radius
  }
  let radius: Double // gespeicherte Eigenschaft
  var durchmesser: Double {// schreibgeschützte berechnete Eigenschaft
    Rücklaufradius * 2,0
  }
}

In Dart haben wir den gleichen Unterschied:

Klasse Circle {
  Kreis ({this.radius});
  letzter doppelter Radius; // gespeicherte Eigenschaft
  doppelt erhaltener Durchmesser => Radius * 2,0; // berechnete Eigenschaft
}

Zusätzlich zu Gettern für berechnete Eigenschaften können wir auch Setter definieren.

Anhand des obigen Beispiels können wir die Durchmessereigenschaft so umschreiben, dass sie einen Setter enthält:

var Durchmesser: Double {// berechnete Eigenschaft
  bekommen {
    Rücklaufradius * 2,0
  }
  einstellen {
    Radius = newValue / 2.0
  }
}

In Dart können wir einen separaten Setter wie folgt hinzufügen:

eingestellter Durchmesser (doppelter Wert) => Radius = Wert / 2,0;

Immobilienbeobachter

Dies ist eine Besonderheit von Swift. Zitat:

Immobilienbeobachter beobachten und reagieren auf Wertveränderungen einer Immobilie. Eigenschaftsbeobachter werden jedes Mal aufgerufen, wenn der Wert einer Eigenschaft festgelegt wird, auch wenn der neue Wert dem aktuellen Wert der Eigenschaft entspricht.

So können sie eingesetzt werden:

var durchmesser: Double {// schreibgeschützte berechnete Eigenschaft
  willSet (newDiameter) {
    print ("alter Wert: \ (Durchmesser), neuer Wert: \ (neuer Durchmesser)")
  }
  didSet {
    print ("alter Wert: \ (alterWert), neuer Wert: \ (Durchmesser)")
  }
}

Weiterlesen:

  • Dart Instanzvariablen, Getter und Setter
  • Schnelle Eigenschaften

Protokolle / Abstrakte Klassen

Hier geht es um ein Konstrukt, mit dem Methoden und Eigenschaften definiert werden, ohne anzugeben, wie sie implementiert werden. Dies wird in anderen Sprachen als Schnittstelle bezeichnet.

In Swift werden Schnittstellen Protokolle genannt.

Protokollform {
  Funkbereich () -> Double
}
Klasse Square: Shape {
  let side: Double
  init (Seite: Double) {
    self.side = side
  }
  func area () -> Double {
    rückkehrseite * seite
  }
}

Dart hat ein ähnliches Konstrukt, das als abstrakte Klasse bekannt ist. Abstrakte Klassen können nicht instanziiert werden. Sie können jedoch Methoden definieren, die eine Implementierung haben.

Das obige Beispiel kann in Dart so geschrieben werden:

abstrakte Klasse Shape {
  doppelter Bereich ();
}
Klasse Square erweitert Shape {
  Quadrat ({this.side});
  letzte Doppelseite;
  doppelte Fläche () => Seite * Seite;
}

Weiterlesen:

  • Schnelle Protokolle
  • Dart Abstract Klassen

Mixins

In Dart ist ein Mixin nur eine reguläre Klasse, die in mehreren Klassenhierarchien wiederverwendet werden kann.

So können wir die zuvor definierte Person-Klasse mit einem NameExtension-Mixin erweitern:

abstrakte Klasse NameExtension {
  String get name;
  String get uppercaseName => name.toUpperCase ();
  String get lowercaseName => name.toLowerCase ();
}
Klasse Person mit Namenserweiterung {
  Person ({this.name, this.age, this.height});
  endgültiger String-Name;
  endgültiges int Alter;
  endgültige doppelte Höhe;
}
var person = Person (Name: 'Andrea', Alter: 34, Größe: 1,84);
print (person.uppercaseName); // 'ANDREA'

Weiterlesen: Dart Mixins

Erweiterungen

Erweiterungen sind ein Merkmal der Swift-Sprache. Zitieren der Dokumente:

Durch Erweiterungen wird eine vorhandene Klasse, Struktur, Aufzählung oder ein vorhandener Protokolltyp um neue Funktionen erweitert. Dazu gehört die Möglichkeit, Typen zu erweitern, für die Sie keinen Zugriff auf den ursprünglichen Quellcode haben (so genannte rückwirkende Modellierung).

Dies ist mit Mixins in Dart nicht möglich.

Anhand des obigen Beispiels können wir die Person-Klasse folgendermaßen erweitern:

Nebenstelle Person {
  var uppercaseName: String {
    return name.uppercased ()
  }
  var lowercaseName: String {
    return name.lowercased ()
  }
}
var person = Person (Name: "Andrea", Alter: 34, Größe: 1,84)
print (person.uppercaseName) // "ANDREA"

Erweiterungen haben viel mehr zu bieten, als ich hier vorgestellt habe, insbesondere wenn sie in Verbindung mit Protokollen und Generika verwendet werden.

Ein sehr häufiger Anwendungsfall für Erweiterungen ist das Hinzufügen von Protokollkonformität zu vorhandenen Typen. Beispielsweise können wir eine Erweiterung verwenden, um einer vorhandenen Modellklasse Serialisierungsfunktionen hinzuzufügen.

Lesen Sie mehr: Swift Extensions

Enums

Dart hat eine sehr grundlegende Unterstützung für Enums.

Enums in Swift sind sehr mächtig, weil sie assoziierte Typen unterstützen:

enum NetworkResponse {
  Fallerfolg (Körper: Daten)
  Fallversagen (Fehler: Fehler)
}

Dies ermöglicht das Schreiben von Logik wie folgt:

Schalter (Antwort) {
  case .success (let data):
    // mache etwas mit (nicht optionalen) Daten
  case .failure (let error):
    // etwas mit einem (nicht optionalen) Fehler machen
}

Beachten Sie, wie sich die Daten- und Fehlerparameter gegenseitig ausschließen.

In Dart können wir keine zusätzlichen Werte zu Aufzählungen zuordnen, und der obige Code kann folgendermaßen implementiert werden:

Klasse NetworkResponse {
  Netzwerkantwort ({this.data, this.error})
  // Zusicherung, dass sich Daten und Fehler gegenseitig ausschließen
  : assert (data! = null && error == null || data == null && error! = null);
  endgültige Uint8List-Daten;
  endgültiger String-Fehler;
}
var response = NetworkResponse (Daten: Uint8List (0), Fehler: null);
if (response.data! = null) {
  // Daten verwenden
} else {
  // Fehler verwenden
}

Ein paar Notizen:

  • Hier verwenden wir Behauptungen, um die Tatsache zu kompensieren, dass wir keine Optionen haben.
  • Der Compiler kann uns nicht dabei helfen, nach allen möglichen Fällen zu suchen. Dies liegt daran, dass wir keinen Schalter verwenden, um die Antwort zu verarbeiten.

Zusammenfassend sind Swift-Enums viel mächtiger und ausdrucksvoller als in Dart.

Bibliotheken von Drittanbietern wie Dart Sealed Unions bieten ähnliche Funktionen wie Swift Enums und können helfen, diese Lücke zu schließen.

Lesen Sie mehr: Swift Enums.

Structs

In Swift können wir Strukturen und Klassen definieren.

Beide Konstrukte haben viele Gemeinsamkeiten und einige Unterschiede.

Der Hauptunterschied ist, dass:

Klassen sind Referenztypen und Strukturen sind Werttypen

Zitieren der Dokumentation:

Ein Wertetyp ist ein Typ, dessen Wert kopiert wird, wenn er einer Variablen oder Konstanten zugewiesen oder an eine Funktion übergeben wird.
Alle Strukturen und Aufzählungen sind in Swift Werttypen. Dies bedeutet, dass alle von Ihnen erstellten Struktur- und Aufzählungsinstanzen sowie alle Werttypen, die sie als Eigenschaften haben, immer kopiert werden, wenn sie in Ihrem Code weitergegeben werden.
Im Gegensatz zu Werttypen werden Referenztypen nicht kopiert, wenn sie einer Variablen oder Konstanten zugewiesen oder an eine Funktion übergeben werden. Anstelle einer Kopie wird ein Verweis auf dieselbe vorhandene Instanz verwendet.

Um zu sehen, was dies bedeutet, betrachten Sie das folgende Beispiel, in dem die Person-Klasse neu definiert wird, um sie veränderbar zu machen:

Klasse Person {
  var name: String
  Var Alter: Int
  var Höhe: doppelt
  init (Name: String, Alter: Int, Höhe: Double) {
    self.name = name
    self.age = alter
    self.height = height
  }
}
var a = Person (Name: "Andrea", Alter: 34, Größe: 1,84)
var b = a
Alter = 35
print (a.age) // druckt 35

Wenn wir Person als Struktur neu definieren, haben wir Folgendes:

struct Person {
  var name: String
  Var Alter: Int
  var Höhe: doppelt
  init (Name: String, Alter: Int, Höhe: Double) {
    self.name = name
    self.age = alter
    self.height = height
  }
}
var a = Person (Name: "Andrea", Alter: 34, Größe: 1,84)
var b = a
Alter = 35
print (a.age) // druckt 34

Es gibt viel mehr zu Strukturen, als ich hier behandelt habe.

Mit Strukturen können Daten und Modelle in Swift effektiv verarbeitet werden, was zu robustem Code mit weniger Fehlern führt.

Zur besseren Übersicht lesen Sie: Strukturen und Klassen in Swift.

Fehlerbehandlung

Verwenden einer Definition aus den Swift-Dokumenten:

Fehlerbehandlung ist der Prozess des Reagierens auf und Wiederherstellens von Fehlerzuständen in Ihrem Programm.

Sowohl Dart als auch Swift verwenden try / catch als eine Technik zum Behandeln von Fehlern, mit einigen Unterschieden.

In Dart kann jede Methode eine Ausnahme eines beliebigen Typs auslösen.

class BankAccount {
  BankAccount ({this.balance});
  doppeltes Gleichgewicht;
  Stornierung (doppelter Betrag) {
    if (Betrag> Saldo) {
      Exception werfen ('Unzureichende Mittel');
    }
    Saldo - = Betrag;
  }
}

Ausnahmen können mit einem try / catch-Block abgefangen werden:

var account = BankAccount (Kontostand: 100);
Versuchen {
  account.withdraw (50); // okay
  account.withdraw (200); // wirft
} catch (e) {
  print (e); // gibt 'Ausnahme: Unzureichende Mittel' aus
}

In Swift deklarieren wir explizit, wann eine Methode eine Ausnahme auslösen kann. Dies geschieht mit dem Schlüsselwort throws, und alle Fehler müssen dem Fehlerprotokoll entsprechen:

enum AccountError: Fehler {
  Bei unzureichenden Fonds
}
class BankAccount {
  Var balance: Verdoppeln
  init (Kontostand: Double) {
    self.balance = balance
  }
  funcdraw (Anzahl: Double) Würfe {
    Wenn Betrag> Saldo {
      werfen Sie AccountError.insquateFunds
    }
    Saldo - = Betrag
  }
}

Bei der Behandlung von Fehlern verwenden wir ein try-Schlüsselwort in einem do / catch-Block.

var account = BankAccount (Kontostand: 100)
machen {
  versuche account.withdraw (betrag: 50) // ok
  versuch account.withdraw (betrag: 200) // wirft
} catch AccountError.insquateFunds {
  drucken ("Unzureichende Mittel")
}

Beachten Sie, dass das Schlüsselwort try beim Aufrufen von Methoden, die ausgelöst werden können, obligatorisch ist.

Und die Fehler selbst sind stark typisiert, sodass wir mehrere catch-Blöcke haben können, um alle möglichen Fälle abzudecken.

versuchen, versuchen, versuchen!

Swift bietet weniger ausführliche Möglichkeiten für den Umgang mit Fehlern.

Können wir versuchen? ohne do / catch Block. Und dies führt dazu, dass Ausnahmen ignoriert werden:

var account = BankAccount (Kontostand: 100)
Versuchen? account.withdraw (betrag: 50) // ok
Versuchen? account.withdraw (betrag: 200) // schlägt im stillen fehl

Oder wenn wir sicher sind, dass eine Methode nicht funktioniert, können wir try! Verwenden:

var account = BankAccount (Kontostand: 100)
Versuchen! account.withdraw (betrag: 50) // ok
Versuchen! account.withdraw (betrag: 200) // absturz

Das obige Beispiel führt zum Absturz des Programms. Versuchen Sie es daher! wird im Seriencode nicht empfohlen und eignet sich besser zum Schreiben von Tests.

Insgesamt ist die explizite Art der Fehlerbehandlung in Swift beim API-Design von großem Vorteil, da auf diese Weise leicht festgestellt werden kann, ob eine Methode werfen kann oder nicht.

Ebenso lenkt die Verwendung von try-in-Methodenaufrufen die Aufmerksamkeit auf Code, der auslösen kann, und zwingt uns, Fehlerfälle zu berücksichtigen.

Insofern fühlt sich die Fehlerbehandlung sicherer und robuster an als bei Dart.

Weiterlesen:

  • Dart-Ausnahmen
  • Schnelle Fehlerbehandlung

Generika

Zitieren der Swift-Dokumente:

Mit generischem Code können Sie flexible, wiederverwendbare Funktionen und Typen schreiben, die mit jedem Typ arbeiten können, abhängig von den von Ihnen definierten Anforderungen. Sie können Code schreiben, der Duplizierungen vermeidet und seine Absicht klar und abstrahiert zum Ausdruck bringt.

Generika werden von beiden Sprachen unterstützt.

Einer der häufigsten Anwendungsfälle für Generika sind Sammlungen wie Arrays, Sets und Maps.

Und wir können sie verwenden, um unsere eigenen Typen zu definieren. So definieren wir einen generischen Stapeltyp in Swift:

struct Stack  {
  var items = [Element] ()
  mutierender Funk-Push (_ item: Element) {
    items.append (item)
  }
  mutierender Funk-Pop () -> Element {
    return items.removeLast ()
  }
}

In ähnlicher Weise würden wir in Dart schreiben:

class Stack  {
  var items =  []
  void push (Element item) {
    items.add (item)
  }
  void pop () -> Element {
    return items.removeLast ()
  }
}

Generika sind in Swift sehr nützlich und leistungsfähig, wo sie zum Definieren von Typeinschränkungen und zugehörigen Typen in Protokollen verwendet werden können.

Ich empfehle, die Dokumentation für weitere Informationen zu lesen:

  • Schnelle Generika
  • Dart Generics

Zugangskontrolle

Zitieren der Swift-Dokumentation:

Die Zugriffssteuerung beschränkt den Zugriff auf Teile Ihres Codes aus Code in anderen Quelldateien und Modulen. Mit dieser Funktion können Sie die Implementierungsdetails Ihres Codes ausblenden und eine bevorzugte Schnittstelle angeben, über die auf diesen Code zugegriffen und dieser verwendet werden kann.

Swift verfügt über fünf Zugriffsebenen: offen, öffentlich, intern, dateiprivat und privat.

Diese werden im Zusammenhang mit der Arbeit mit Modulen und Quelldateien verwendet. Zitat:

Ein Modul ist eine einzelne Einheit der Codeverteilung - ein Framework oder eine Anwendung, die als einzelne Einheit erstellt und ausgeliefert wird und von einem anderen Modul mit dem Importschlüsselwort von Swift importiert werden kann.

Die Open- und Public-Zugriffsebenen können verwendet werden, um Code außerhalb von Modulen zugänglich zu machen.

Die private und dateiprivate Zugriffsebene kann verwendet werden, um Code außerhalb der Datei, in der er definiert ist, nicht zugänglich zu machen.

Beispiele:

public class SomePublicClass {}
interne Klasse SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private Klasse SomePrivateClass {}

In Dart sind die Zugriffsebenen einfacher und auf öffentlich und privat beschränkt. Zitat:

Im Gegensatz zu Java verfügt Dart nicht über die Schlüsselwörter public, protected und private. Wenn ein Bezeichner mit einem Unterstrich _ beginnt, ist er für seine Bibliothek privat.

Beispiele:

Klasse HomePage erweitert StatefulWidget {// public
  @override
  _HomePageState createState () => _HomePageState ();
}
Klasse _HomePageState erweitert State  {...} // private

Die Zugangskontrolle wurde mit unterschiedlichen Zielen für Dart und Swift entwickelt. Infolgedessen sind die Zugriffsebenen sehr unterschiedlich.

Weiterlesen:

  • Schnelle Zugangskontrolle
  • Dart Libraries und Sichtbarkeit

Asynchrone Programmierung: Futures

Asynchrone Programmierung ist ein Bereich, in dem Dart wirklich glänzt.

Eine Form der asynchronen Programmierung ist erforderlich, wenn Anwendungsfälle behandelt werden:

  • Herunterladen von Inhalten aus dem Web
  • Mit einem Backend-Service sprechen
  • Führen Sie einen Langzeitbetrieb durch

In diesen Fällen ist es am besten, den Hauptthread der Ausführung nicht zu blockieren, da dies dazu führen kann, dass unsere Programme einfrieren.

Zitieren der Dart-Dokumentation:

Durch asynchrone Vorgänge kann Ihr Programm andere Vorgänge abschließen, während es auf den Abschluss eines Vorgangs wartet. Dart verwendet Future-Objekte (Futures), um die Ergebnisse von asynchronen Operationen darzustellen. Um mit Futures zu arbeiten, können Sie entweder async and await oder die Future API verwenden.

Als Beispiel sehen wir uns an, wie wir die asynchrone Programmierung verwenden können, um:

  • Authentifizieren Sie einen Benutzer mit einem Server
  • Speichern Sie das Zugriffstoken, um den Speicher abzusichern
  • Rufen Sie die Benutzerprofilinformationen ab

In Dart kann dies mit async / await in Kombination mit Futures gemacht werden:

Zukünftiges  getUserProfile (UserCredentials-Anmeldeinformationen) async {
  final accessToken = Warten auf networkService.signIn (Anmeldeinformationen);
  warte auf secureStorage.storeToken (accessToken, forUserCredentials: credentials);
  return wait networkService.getProfile (accessToken);
}

In Swift gibt es keine Unterstützung für async / await und wir können dies nur mit Closures (Completion Blocks) erreichen:

func getUserProfile (Anmeldeinformationen: UserCredentials, Abschluss: (_ Ergebnis: UserProfile) -> Void) {
  networkService.signIn (Anmeldeinformationen) {accessToken in
    secureStorage.storeToken (accessToken) {
      networkService.getProfile (accessToken, Vervollständigung: Vervollständigung)
    }
  }
}

Dies führt aufgrund verschachtelter Abschlussblöcke zu einer „Pyramide des Schicksals“. Und die Fehlerbehandlung wird in diesem Szenario sehr schwierig.

In Dart wird die Behandlung von Fehlern im obigen Code einfach durchgeführt, indem der getUserProfile-Methode ein Try / Catch-Block um den Code hinzugefügt wird.

Als Referenz gibt es einen Vorschlag, Swift in Zukunft async / await hinzuzufügen. Dies wird hier ausführlich dokumentiert:

  • Async / Await-Vorschlag für Swift

Bis dies implementiert ist, können Entwickler Bibliotheken von Drittanbietern wie diese Promises-Bibliothek von Google verwenden.

Wie bei Dart kann die ausgezeichnete Dokumentation finden Sie hier:

  • Dart Asynchrone Programmierung: Futures

Asynchrone Programmierung: Streams

Streams werden als Teil der Dart-Kernbibliotheken implementiert, jedoch nicht in Swift.

Zitieren der Dart-Dokumentation:

Ein Stream ist eine Folge von asynchronen Ereignissen.

Streams bilden die Grundlage für reaktive Anwendungen, bei denen sie eine wichtige Rolle beim State Management spielen.

Streams eignen sich beispielsweise hervorragend zum Durchsuchen von Inhalten, bei denen jedes Mal, wenn der Benutzer den Text in einem Suchfeld aktualisiert, eine neue Ergebnismenge ausgegeben wird.

Streams sind nicht in den Swift-Kernbibliotheken enthalten. Bibliotheken von Drittanbietern wie RxSwift bieten Streams-Unterstützung und vieles mehr.

Streams sind ein weites Thema, auf das hier nicht eingegangen wird.

Weiterlesen: Asynchrone Dart-Programmierung: Streams

Speicherverwaltung

Dart verwaltet den Speicher mit einem fortschrittlichen Speicherbereinigungsschema.

Swift verwaltet den Speicher über die automatische Referenzzählung (ARC).

Dies garantiert eine hervorragende Leistung, da der Speicher sofort freigegeben wird, wenn er nicht mehr verwendet wird.

Die Last wird jedoch teilweise vom Compiler auf den Entwickler verlagert.

In Swift müssen wir über den Lebenszyklus und den Besitz von Objekten nachdenken und die entsprechenden Schlüsselwörter (schwach, stark, nicht im Besitz) korrekt verwenden, um Beibehaltungszyklen zu vermeiden.

Lesen Sie mehr: Swift Automatic Reference Counting.

Zusammenstellung und Ausführung

Zunächst ein wichtiger Unterschied zwischen Just-in-Time- (JIT) und AOT-Compilern:

JIT-Compiler

Während der Ausführung des Programms wird ein JIT-Compiler ausgeführt, der im laufenden Betrieb kompiliert.

JIT-Compiler werden in der Regel in dynamischen Sprachen verwendet, in denen Typen nicht im Voraus festgelegt werden. JIT-Programme werden über einen Interpreter oder eine virtuelle Maschine (VM) ausgeführt.

AOT-Compiler

Ein AOT-Compiler wird während der Erstellung des Programms vor der Laufzeit ausgeführt.

AOT-Compiler werden normalerweise mit statischen Sprachen verwendet, die die Datentypen kennen. AOT-Programme werden in systemeigenen Maschinencode kompiliert, der zur Laufzeit direkt von der Hardware ausgeführt wird.

Zitat dieses großartigen Artikels von Wm Leler:

Wenn die AOT-Kompilierung während der Entwicklung durchgeführt wird, führt dies ausnahmslos zu viel langsameren Entwicklungszyklen (die Zeit zwischen dem Ändern eines Programms und dem Ausführen des Programms, um das Ergebnis der Änderung anzuzeigen). Die AOT-Kompilierung führt jedoch zu Programmen, die vorhersehbarer und ohne Pause für die Analyse und Kompilierung zur Laufzeit ausgeführt werden können. AOT-kompilierte Programme werden ebenfalls schneller ausgeführt (da sie bereits kompiliert wurden).
Umgekehrt bietet die JIT-Kompilierung viel schnellere Entwicklungszyklen, kann jedoch zu einer langsameren oder ruckeligeren Ausführung führen. Insbesondere JIT-Compiler haben langsamere Startzeiten, da der JIT-Compiler beim Start des Programms eine Analyse und Kompilierung durchführen muss, bevor der Code ausgeführt werden kann. Studien haben gezeigt, dass viele Benutzer eine App verlassen, wenn die Ausführung länger als ein paar Sekunden dauert.

Als statische Sprache wird Swift vorab kompiliert.

Dart kann sowohl AOT als auch JIT kompiliert werden. Dies bietet erhebliche Vorteile bei der Verwendung mit Flutter. Wieder zitieren:

Die JIT-Kompilierung wird während der Entwicklung mit einem besonders schnellen Compiler verwendet. Sobald eine App zur Veröffentlichung bereit ist, wird sie AOT kompiliert. Folglich kann Dart mithilfe fortschrittlicher Tools und Compiler das Beste aus beiden Welten liefern: extrem schnelle Entwicklungszyklen sowie schnelle Ausführungs- und Startzeiten. - Wm Leler

Mit Dart bekommen wir das Beste aus beiden Welten.

Swift leidet unter dem Hauptnachteil der AOT-Kompilierung. Das heißt, die Kompilierungszeit nimmt mit der Größe der Codebasis zu.

Bei einer mittelgroßen App (zwischen 10.000 und 100.000 Zeilen) kann das Kompilieren einer App problemlos Minuten dauern.

Nicht so bei Flutter-Apps, bei denen wir unabhängig von der Größe der Codebasis ständig ein Hot-Reload von weniger als einer Sekunde durchführen.

Andere Funktionen nicht abgedeckt

Die folgenden Funktionen wurden nicht behandelt, da sie in Dart und Swift recht ähnlich sind:

  • Operatoren (siehe Referenz für Swift und Dart)
  • Saiten (siehe Referenz für Swift und Dart)
  • Optionale Verkettung in Swift (in Dart als bedingter Mitgliederzugriff bezeichnet).

Parallelität

  • Gleichzeitige Programmierung ist mit Isolaten in Dart versehen.
  • Swift verwendet Grand Central Dispatch (GCD) und Dispatch-Warteschlangen.

Meine Lieblings-Swift-Features fehlen bei Dart

  • Structs
  • Aufzählungen mit zugeordneten Typen
  • Optionals

Meine Lieblings-Dart-Features fehlen bei Swift

  • Just-in-Time-Compiler
  • Futures mit Warten / Async (siehe Warten / Async-Vorschlag von Chris Lattner)
  • Streams mit Yield / Async * (RxSwift bietet eine Obermenge von Streams für reaktive Anwendungen)

Fazit

Sowohl Dart als auch Swift sind hervorragende Sprachen, die sich gut zum Erstellen moderner mobiler Apps und darüber hinaus eignen.

Keine Sprache ist überlegen, da beide ihre eigenen Stärken haben.

Wenn ich mir die Entwicklung mobiler Apps und die Tools für beide Sprachen anschaue, habe ich das Gefühl, dass Dart die Oberhand hat. Dies ist auf den JIT-Compiler zurückzuführen, der die Grundlage für das Stateful Hot-Reload in Flutter bildet.

Und Hot-Reload bietet beim Erstellen von Apps einen enormen Produktivitätsgewinn, da der Entwicklungszyklus von Sekunden oder Minuten auf weniger als eine Sekunde verkürzt wird.

Entwicklerzeit ist eine knappere Ressource als Rechenzeit.

Die Optimierung für die Entwicklerzeit ist also ein sehr kluger Schachzug.

Andererseits habe ich das Gefühl, dass Swift ein sehr starkes Typensystem hat. Die Schriftsicherheit ist in alle Sprachfunktionen integriert und führt natürlicher zu robusten Programmen.

Wenn wir einmal die persönlichen Präferenzen beiseite gelegt haben, sind Programmiersprachen nur Werkzeuge. Und es ist unsere Aufgabe als Entwickler, das am besten geeignete Werkzeug für den Job auszuwählen.

In jedem Fall können wir hoffen, dass beide Sprachen im Laufe ihrer Entwicklung die besten Ideen voneinander übernehmen.

Referenzen & Credits

Sowohl Dart als auch Swift haben einen umfangreichen Funktionsumfang und wurden hier nicht vollständig behandelt.

Ich habe diesen Artikel vorbereitet, indem ich Informationen aus der offiziellen Swift and Dart-Dokumentation ausgeliehen habe. Diese finden Sie hier:

  • Die schnelle Programmiersprache
  • Eine Tour durch die Dartsprache

Darüber hinaus ist der Abschnitt über JIT- und AOT-Compiler stark von diesem großartigen Artikel von Wm Leler inspiriert:

  • Warum Flattern Dart benutzt

Habe ich etwas verpasst Lass es mich in den Kommentaren wissen.

Viel Spaß beim Codieren!

UPDATE: Mein Flutter & Firebase Udemy-Kurs ist jetzt für Early Access verfügbar. Verwenden Sie diesen Link, um sich anzumelden (inklusive Rabattcode):

  • Flutter & Firebase: Erstellen Sie eine vollständige App für iOS und Android

Weitere Artikel und Video-Tutorials finden Sie unter Coding With Flutter.

Ich bin @ biz84 auf Twitter. Sie können auch meine GitHub-Seite sehen.