Dart vs Swift

知識小集發表於2019-01-31

| 作者:Andrea Bizzotto

| 原文連結:medium.com/coding-with…

Dart 和 Swift 是我最喜歡的程式語言。我在商業和開原始碼中廣泛使用它們。

本文提供了 Dart 和 Swift 之間的比較,旨在:

  • 突出顯示兩者之間的差異;
  • 作為開發人員從一種語言轉移到另一種語言(或使用兩者)的參考。

一些背景:

  • Dart 支援 Flutter,這是 Google 用於從單一程式碼庫構建漂亮的本機應用程式的框架。
  • Swift 通過 iOS,macOS,tvOS 和 watchOS 為 Apple 的 SDK 提供支援。

以下是兩種語言的主要特徵(Dart 2.1Swift 4.2)的比較。由於深入討論每個功能超出了本文的範圍,因此更多的資訊可以參考各自的文件。

目錄

  • 對照表
  • 變數
  • 型別推斷
  • 可變/不可變變數
  • 函式
  • 命名和未命名引數
  • 可選和預設引數
  • 閉包
  • 元組
  • 控制流
  • 集合
  • Nullability & Optionals
  • 繼承
  • 屬性
  • 協議/抽象類
  • Mixins
  • 擴充套件
  • 列舉
  • 結構體
  • 錯誤處理
  • 泛型
  • 訪問控制
  • 非同步程式設計:Future
  • 非同步程式設計:Stream
  • 記憶體管理
  • 編譯和執行
  • 其它未涵蓋功能

對照表

Dart vs Swift

變數

Dart 中變數宣告語法如下:

String name;
int age;
double height;
複製程式碼

Swift 中是如下:

var name: String
var age: Int
var height: Double
複製程式碼

Dart 中變數初始化語法如下:

var name = 'Andrea';
var age = 34;
var height = 1.84;
複製程式碼

Swift 中是如下:

var name = "Andrea"
var age = 34
var height = 1.84
複製程式碼

在此示例中,不需要型別註釋。這是因為兩種語言都可以從賦值右側的表示式推斷出型別。

型別推斷

型別推斷意味著我們可以在 Dart 中編寫以下程式碼:

var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>
複製程式碼

編譯器會自動解析 arguments 的型別。

在 Swift 中,同樣可以寫成:

var arguments = [ "argA": "hello", "argB": 42 ] // [ String : Any ]
複製程式碼

更多細節

Dart 文件有如下描述:

分析器可以推斷欄位、方法、區域性變數和大多數泛型型別引數的型別。當分析器沒有足夠的資訊來推斷特定型別時,將使用動態型別。

Swift 文件中有如下描述:

Swift 廣泛使用型別推斷,允許您省略程式碼中許多變數和表示式的型別或部分型別。例如,不是寫 var x:Int = 0,而是可以寫 var x = 0,完全省略型別 - 編譯器正確地推斷出 x 為 Int 型別的值。

動態型別

可以使用 Dart 中的 dynamic 關鍵字和 Swift 中的 Any 關鍵字宣告可以是任何型別的變數。

在讀取 JSON 等資料時,通常會使用動態型別。

可變/不可變變數

變數可以宣告為可變不可變

為了宣告可變變數,兩種語言都使用 var 關鍵字。

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

var a = 10 // Int (Swift)
a = 20 // ok
複製程式碼

為了宣告不可變變數,Dart 使用 final,Swift 使用 let

final a = 10;
a = 20; // 'a': a final variable, can only be set once.

let a = 10
a = 20 // Cannot assign to value: 'a' is a 'let' constant
複製程式碼

注意:Dart 文件定義了兩個關鍵字 finalconst,其工作方式如下:

如果您不打算更改變數值,請使用 finalconst,而不是 var 或型別。final 變數只能設定一次;const 變數是編譯時常量。(Const 變數是隱式 final。)final 頂層型別變數或類變數在第一次使用時被初始化。

在 Dart 網站上的這篇文章中可以找到進一步的解釋:

final 意味著一次賦值。final 變數或欄位必須具有 initializer。 一旦賦值,就不能改變 final 變數的值。

在 Swift 中,我們用 let 宣告常量。

常量宣告會在程式中引入常量命名值。使用 let 關鍵字宣告常量,並具有以下形式:

let constant name: type = expression
複製程式碼

常量宣告定義常量名稱和初始化表示式值之間的不可變繫結;設定常量值後,無法更改。

函式

函式在 Swift 和 Dart 中都是一等公民。

這意味著就像物件一樣,函式可以作為引數傳遞,儲存為屬性或作為結果返回。

作為初始比較,我們可以看到如何宣告不帶引數的函式。

在 Dart 中,返回型別在方法名稱之前:

void foo();
int bar();
複製程式碼

在 Swift 中,我們使用 -> T 表示法作為字尾。如果沒有返回值(Void),則不需要這樣做:

func foo()
func bar() -> Int
複製程式碼

命名及未命名(un-named)引數

兩種語言都支援命名和未命名的引數。

在 Swift 中,引數預設為命名引數

func foo(name: String, age: Int, height: Double)
foo(name: "Andrea", age: 34, height: 1.84)
複製程式碼

在 Dart 中,我們使用花括號({})定義命名引數:

void foo({String name, int age, double height});
foo(name: 'Andrea', age: 34, height: 1.84);
複製程式碼

在 Swift 中,我們使用下劃線(_) 作為外部引數來定義未命名的引數:

func foo(_ name: String, _ age: Int, _ height: Double)
foo("Andrea", 34, 1.84)
複製程式碼

在 Dart 中,我們通過省略花括號({})來定義未命名的引數:

void foo(String name, int age, double height);
foo('Andrea', 34, 1.84);
複製程式碼

可選和預設引數

兩種語言都支援預設引數。

在 Swift 中,您可以通過在該引數的型別之後為引數賦值來為函式中的任何引數定義預設值。如果定義了預設值,則可以在呼叫函式時省略該引數。

func foo(name: String, age: Int = 0, height: Double = 0.0) 
foo(name: "Andrea", age: 34) // name: "Andrea", age: 34, height: 0.0
複製程式碼

在 Dart 中,可選引數可以是位置引數,也可以是命名引數,但不能同時。

// positional optional parameters
void foo(String name, [int age = 0, double height = 0.0]);
foo('Andrea', 34); // name: 'Andrea', age: 34, height: 0.0
// named optional parameters
void foo({String name, int age = 0, double height = 0.0});
foo(name: 'Andrea', age: 34); // name: 'Andrea', age: 34, height: 0.0
複製程式碼

閉包

作為頂層(first-class)物件,函式可以作為引數傳遞給其他函式,或者分配給變數。

在此上下文中,函式也稱為閉包

這是一個函式的 Dart 示例,它迭代一個 item 列表,使用閉包來列印每個專案的索引和內容:

final list = ['apples', 'bananas', 'oranges'];
list.forEach((item) => print('${list.indexOf(item)}: $item'));
複製程式碼

閉包帶有一個引數(item),列印該項的索引和值,並且不返回任何值。

注意使用箭頭符號(=>)。這可以代替花括號內的單個 return 語句:

list.forEach((item) { print('${list.indexOf(item)}: $item'); });
複製程式碼

Swift 中的相同程式碼如下所示:

let list = ["apples", "bananas", "oranges"]
list.forEach({print("\(String(describing: list.firstIndex(of: $0))) \($0)")})
複製程式碼

在這種情況下,我們不為傳遞給閉包的引數指定名稱,而使用 $0 代替第一個引數。這完全是可選的,我們仍然可以使用命名引數:

list.forEach({ item in print("\(String(describing: list.firstIndex(of: item))) \(item)")})
複製程式碼

閉包通常用作 Swift 中非同步程式碼的完成塊(請參閱下面有關非同步程式設計的部分)。

元組

Swift 文件的描述如下:

元組將多個值分組為單個複合值。元組中的值可以是任何型別,並且不必具有相同的型別。

這些可以用作小型輕量級型別,在定義具有多個返回值的函式時非常有用。

以下是如何在 Swift 中使用元組:

let t = ("Andrea", 34, 1.84)
print(t.0) // prints "Andrea"
print(t.1) // prints 34
print(t.2) // prints 1.84
複製程式碼

Dart 中有一個單獨三方包支援元組:

const t = const Tuple3<String, int, double>('Andrea', 34, 1.84);
print(t.item1); // prints 'Andrea'
print(t.item2); // prints 34
print(t.item3); // prints 1.84
複製程式碼

控制流

兩種語言都提供多種控制流語句。

例如,if、for、while、switch 語句。

在這裡介紹這些將是相當冗長的,所以請參考官方文件。

集合(arrays, sets, maps)

Arrays / Lists

陣列是有序的物件組。

在 Dart 中,使用 List 物件來表示陣列:

var emptyList = <int>[]; // empty list
var list = [1, 2, 3]; // list literal
list.length; // 3
list[1]; // 2
複製程式碼

Swift 中陣列是內建型別:

var emptyArray = [Int]() // empty array
var array = [1, 2, 3] // array literal
array.count // 3
array[1] // 2
複製程式碼

Sets

Swift 文件中的描述:

Set 在集合中儲存相同型別的不同值,沒有定義的順序。當專案的順序不重要時,或者當您需要確保元素僅出現一次時,您可以使用集合而不是陣列。

Dart 中 Set 類的定義:

var emptyFruits = Set<String>();
var fruits = Set<String>.from(['apple', 'banana']); // set from Iterable
複製程式碼

Swift 中的示例:

var emptyFruits = Set<String>()
var fruits = Set<String>(["apple", "banana"])
複製程式碼

Maps / Dictionaries

Swift 文件對 map/dictionary 有一個很好的定義:

字典儲存相同型別的鍵與集合中相同型別的值之間的關聯,而沒有特定的排序。每個值都與唯一鍵相關聯,該唯一鍵充當字典中該值的識別符號。

Dart 中的 map 定義如下:

var namesOfIntegers = Map<Int,String>(); // empty map
var airports = { 'YYZ': 'Toronto Pearson', 'DUB': 'Dublin' }; // map literal
複製程式碼

Swift 中 map 稱為字典:

var namesOfIntegers = [Int: String]() // empty dictionary
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] // dictionary literal
複製程式碼

Nullability & Optionals

在Dart中,任何物件都可以為 null。並且嘗試訪問 null 物件的方法或變數會導致空指標異常。這是計算機程式中最常見的錯誤來源。

從一開始,Swift 就多了一個選擇,一個內建的語言功能,用於宣告物件是否可以有值。看看文件:

您可以在可能缺少值的情況下使用 Optional。Optional 表示兩種可能性:要麼存在值,您可以解開可選項以訪問該值,或者根本沒有值。

與此相反,我們可以使用非 Optional 變數來保證它們始終具有值:

var x: Int? // optional
var y: Int = 1 // non-optional, must be initialized
複製程式碼

注意:說 Swift 變數是可選的與 Dart 變數可以為 null 是大致相同。

如果沒有對選項的語言級支援,我們只能在執行時檢查變數是否為 null

使用 Optional,我們在編譯時對這些資訊進行編碼。我們可以解開 Optional 以安全地檢查它們是否包含值:

func showOptional(x: Int?) {
  // use `guard let` rather than `if let` as best practice
  if let x = x { // unwrap optional
    print(x)
  } else {
    print("no value")
  }
}

showOptional(x: nil) // prints "no value"
showOptional(x: 5) // prints "5"
複製程式碼

如果我們知道變數必須有值,我們可以使用 non-optional 的值:

func showNonOptional(x: Int) {
  print(x)
}
showNonOptional(x: nil) // [compile error] Nil is not compatible with expected argument type 'Int'
showNonOptional(x: 5) // prints "5"
複製程式碼

上面的第一個例子在 Dart 中的實現如下:

void showOptional(int x) {
  if (x != null) {
    print(x);
  } else {
    print('no value');
  }
}
showOptional(null) // prints "no value"
showOptional(5) // prints "5"
複製程式碼

第二個如下實現:

void showNonOptional(int x) {
  assert(x != null);
  print(x); 	
}
showNonOptional(null) // [runtime error] Uncaught exception: Assertion failed
showNonOptional(5) // prints "5"
複製程式碼

有 optional 意味著我們可以在編譯時而不是在執行時捕獲錯誤。及早捕獲錯誤會讓程式碼更安全,錯誤更少。

Dart 缺乏對 optional 的支援在某種程度上通過使用斷言(以及用於命名引數的 @required 註釋)得到緩解。

這些在 Flutter SDK 中廣泛使用,但會產生額外的樣板程式碼。

類是用面嚮物件語言編寫程式的主要構建塊。

Dart 和 Swift 都支援類,但有一些差異。

語法

這裡有一個帶有 initializer 和三個成員變數的 Swift 類:

class Person {
  let name: String
  let age: Int
  let height: Double
  init(name: String, age: Int, height: Double) {
    self.name = name
    self.age = age
    self.height = height
  }
}
複製程式碼

在 Dart 中:

class Person {
  Person({this.name, this.age, this.height});
  final String name;
  final int age;
  final double height;
}
複製程式碼

請注意在 Dart 建構函式中使用的 this.[propertyName]。這是用於在建構函式執行之前設定例項成員變數的語法糖。

工廠建構函式

在 Dart 中,可以使用工廠建構函式。

在實現並不總是建立其類的新例項的建構函式時,請使用 factory 關鍵字。

工廠建構函式的一個實際用例是從 JSON 建立模型類時:

class Person {
  Person({this.name, this.age, this.height});
  final String name;
  final int age;
  final double height;
  factory Person.fromJSON(Map<dynamic, dynamic> json) {
    String name = json['name'];
    int age = json['age'];
    double height = json['height'];
    return Person(name: name, age: age, height: height);
  }
}
var p = Person.fromJSON({
  'name': 'Andrea',
  'age': 34,
  'height': 1.84,
});
複製程式碼

繼承

Swift 使用單繼承模型,這意味著任何類只能有一個超類。Swift類可以實現多個介面(也稱為協議)。

Dart 類具有基於 mixin 的繼承。如文件描述:

每個物件都是一個類的例項,所有類都來自 Object。基於 Mixin 的繼承意味著雖然每個類(除了Object)只有一個超類,但是類體可以在多個類層次結構中重用。

以下是 Swift 中的單繼承:

class Vehicle {
  let wheelCount: Int
  init(wheelCount: Int) {
    self.wheelCount = wheelCount
  }
}
class Bicycle: Vehicle {
  init() {
    super.init(wheelCount: 2)
  }
}
複製程式碼

在 Dart 中:

class Vehicle {
  Vehicle({this.wheelCount});
  final int wheelCount;
}
class Bicycle extends Vehicle {
  Bicycle() : super(wheelCount: 2);
}
複製程式碼

屬性

這些在 Dart 中稱為例項變數,在 Swift 中只是屬性。

在 Swift 中,儲存和計算屬性之間存在區別:

class Circle {
  init(radius: Double) {
    self.radius = radius
  }
  let radius: Double // stored property
  var diameter: Double { // read-only computed property
    return radius * 2.0
  }
}
複製程式碼

在 Dart 中,我們有相同的區分:

class Circle {
  Circle({this.radius});
  final double radius; // stored property
  double get diameter => radius * 2.0; // computed property
}
複製程式碼

除了計算屬性的 getter 之外,我們還可以定義 setter

使用上面的例子,我們可以重寫 diameter 屬性以包含一個 setter

var diameter: Double { // computed property
  get {
    return radius * 2.0
  }
  set {
    radius = newValue / 2.0
  }
}
複製程式碼

在 Dart 中,我們可以像這樣新增一個單獨的 setter

set diameter(double value) => radius = value / 2.0;
複製程式碼

屬性觀察者

這是 Swift 的一個特有功能。如文件描述:

屬性觀察者負責觀察並響應屬性值的變化。每次設定屬性值時都會呼叫屬性觀察者,即使新值與屬性的當前值相同。

這是他們的使用方式:

var diameter: Double { // read-only computed property
  willSet(newDiameter) {
    print("old value: \(diameter), new value: \(newDiameter)")  
  }
  didSet {
    print("old value: \(oldValue), new value: \(diameter)")  
  }
}
複製程式碼

協議/抽象類

這裡我們討論用於定義方法和屬性,而不指定它們的實現方式的結構。這在其他語言中稱為介面。

在 Swift 中,介面稱為協議。

protocol Shape {
  func area() -> Double
}
class Square: Shape {
  let side: Double
  init(side: Double) {
    self.side = side
  }
  func area() -> Double {
    return side * side
  }
}
複製程式碼

Dart有一個類似的結構,稱為抽象類。抽象類無法例項化。但是,他們可以定義具有實現的方法。

上面的例子在 Dart 中可以這樣寫:

abstract class Shape {
  double area();
}
class Square extends Shape {
  Square({this.side});
  final double side;
  double area() => side * side;
}
複製程式碼

Mixins

在 Dart 中,mixin 只是一個常規類,可以在多個類層次結構中重用。

以下程式碼演示了我們使用 NameExtension mixin 擴充套件我們之前定義的 Person 類:

abstract class NameExtension {
  String get name;
  String get uppercaseName => name.toUpperCase();
  String get lowercaseName => name.toLowerCase();
}
class Person with NameExtension {
  Person({this.name, this.age, this.height});
  final String name;
  final int age;
  final double height;	
}
var person = Person(name: 'Andrea', age: 34, height: 1.84);
print(person.uppercaseName); // 'ANDREA'
複製程式碼

擴充套件

擴充套件是 Swift 語言的一個特性。如文件描述:

擴充套件為現有的類,結構,列舉或協議型別新增新功能。這包括擴充套件那些無法訪問原始原始碼的型別的能力(稱為追溯建模)。

在 Dart 中使用 mixins 是無法實現這一點的。

借用上面的例子,我們可以像這樣擴充套件 Person 類:

extension Person {
  var uppercaseName: String {
    return name.uppercased()
  }
  var lowercaseName: String {
    return name.lowercased()
  }
}
var person = Person(name: "Andrea", age: 34, height: 1.84)
print(person.uppercaseName) // "ANDREA"
複製程式碼

擴充套件的內容比我在這裡介紹的要多得多,特別是當它們與協議和泛型一起使用時。

擴充套件的一個非常常見的用例是為現有型別新增協議一致性。例如,我們可以使用擴充套件來為現有模型類新增序列化功能。

列舉

Dart 對列舉有一些非常基本的支援。

而 Swift 中的列舉非常強大,因為它們支援關聯型別:

enum NetworkResponse {
  case success(body: Data) 
  case failure(error: Error)
}
複製程式碼

這使得編寫這樣的邏輯成為可能:

switch (response) {
  case .success(let data):
    // do something with (non-optional) data
  case .failure(let error):
    // do something with (non-optional) error
}
複製程式碼

請注意 dataerror 引數是如何互斥的。

在 Dart 中,我們無法將其他值與列舉相關聯,上面的程式碼可以按以下方式實現:

class NetworkResponse {
  NetworkResponse({this.data, this.error})
  // assertion to make data and error mutually exclusive
  : assert(data != null && error == null || data == null && error != null);
  final Uint8List data;
  final String error;
}
var response = NetworkResponse(data: Uint8List(0), error: null);
if (response.data != null) {
  // use data
} else {
  // use error
}
複製程式碼

幾個注意事項:

  • 在這裡,我們使用斷言來彌補我們沒有 optional 的事實。
  • 編譯器無法幫助我們檢查所有可能的情況。這是因為我們不使用 switch 來處理響應。

總之,Swift 列舉比 Dart 強大且富有表現力。

Dart Sealed Unions 這樣的第三方庫提供了類似於 Swift 列舉的功能,可以幫助填補空白。

結構體

在 Swift 中,我們可以定義結構和類。

這兩種結構都有許多共同點,也有一些不同之處。

主要區別在於:

類是引用型別,結構體是值型別

文件中的描述如下:

值型別是一種型別,其值在被賦值給變數或常量時被複制,或者在傳遞給函式時被複制。 Swift 中所有結構和列舉都是值型別。這意味著您建立的任何結構和列舉例項 - 以及它們所有的值型別的屬性 - 在程式碼中傳遞時始終會被複制。 與值型別不同,引用型別在分配給變數或常量時或者傳遞給函式時不會被複制。而是使用對同一現有例項的引用。

要了解這意味著什麼,請考慮以下示例,其中我們重新使用 Person 類使其變為可變:

class Person {
  var name: String
  var age: Int
  var height: Double
  init(name: String, age: Int, height: Double) {
    self.name = name
    self.age = age
    self.height = height
  }
}
var a = Person(name: "Andrea", age: 34, height: 1.84)
var b = a
b.age = 35
print(a.age) // prints 35
複製程式碼

如果我們將 Person 重新定義為 struct,我們有:

struct Person {
  var name: String
  var age: Int
  var height: Double
  init(name: String, age: Int, height: Double) {
    self.name = name
    self.age = age
    self.height = height
  }
}
var a = Person(name: "Andrea", age: 34, height: 1.84)
var b = a
b.age = 35
print(a.age) // prints 34
複製程式碼

結構體的內容比我在這裡介紹的要多得多。

結構體可用於處理 Swift 中的資料和模型,從而產生具有更少錯誤的強大程式碼。

錯誤處理

使用 Swift 文件中的定義:

錯誤處理是響應程式中的錯誤條件並從中恢復的過程。

Dart 和 Swift 都使用 try/catch 作為處理錯誤的技術,但存在一些差異。

在 Dart 中,任何方法都可以丟擲任何型別的異常。

class BankAccount {
  BankAccount({this.balance});
  double balance;
  void withdraw(double amount) {
    if (amount > balance) {
      throw Exception('Insufficient funds');
    }
    balance -= amount;
  }
}
複製程式碼

可以使用 try/catch 塊捕獲異常:

var account = BankAccount(balance: 100);
try {
  account.withdraw(50); // ok
  account.withdraw(200); // throws
} catch (e) {
  print(e); // prints 'Exception: Insufficient funds'
}
複製程式碼

在 Swift 中,我們顯式宣告方法何時可以丟擲異常。這是通過 throws 關鍵字完成的,並且任何錯誤都必須符合錯誤協議:

enum AccountError: Error {
  case insufficientFunds
}
class BankAccount {
  var balance: Double
  init(balance: Double) {
    self.balance = balance
  }
  func withdraw(amount: Double) throws {
    if amount > balance {
      throw AccountError.insufficientFunds
    }
    balance -= amount
  }
}
複製程式碼

在處理錯誤時,我們在 do/catch 塊內使用 try 關鍵字。

var account = BankAccount(balance: 100)
do {
  try account.withdraw(amount: 50) // ok
  try account.withdraw(amount: 200) // throws
} catch AccountError.insufficientFunds {
  print("Insufficient Funds")
}
複製程式碼

請注意,當呼叫丟擲異常的方法時,try 關鍵字是如何使用的。

錯誤本身是強型別的,所以我們可以有多個 catch 塊來覆蓋所有可能的情況。

try, try?, try!

Swift 提供了一種處理錯誤的不那麼繁瑣的方法。

我們可以使用不帶 do/catch 塊的 try?。這將會忽略任何異常:

var account = BankAccount(balance: 100)
try? account.withdraw(amount: 50) // ok
try? account.withdraw(amount: 200) // fails silently
複製程式碼

或者,如果我們確定某個方法不會丟擲異常,我們可以使用 try!

var account = BankAccount(balance: 100)
try! account.withdraw(amount: 50) // ok
try! account.withdraw(amount: 200) // crash
複製程式碼

上面的示例將導致程式崩潰。所以,在生產程式碼中不建議使用 try!,它更適合編寫測試。

總之,Swift 中錯誤處理的顯式性質在 API 設計中非常有益,因為它可以很容易地知道方法是否可以丟擲。

同樣,在方法呼叫時使用 try 讓我們能關注到可能丟擲錯誤的程式碼,迫使我們考慮錯誤情況。

在這方面,錯誤處理讓 Swift 比 Dart 更安全、更可靠。

泛型

Swift 文件描述:

泛型程式碼使您能夠根據需求編寫可以使用任何型別的靈活的可重用的函式和型別。您可以編寫避免重複的程式碼,並以清晰、抽象的方式表達其意圖。

兩種語言都支援泛型。

泛型的最常見用例之一是集合,例如陣列、集合和對映。

我們可以使用它們來定義我們自己的型別。以下是我們如何在 Swift 中定義通用 Stack 型別:

struct Stack<Element> {
  var items = [Element]()
  mutating func push(_ item: Element) {
    items.append(item)
  }
  mutating func pop() -> Element {
    return items.removeLast()
  }
}
複製程式碼

類似的,在 Dart 中可以這樣寫:

class Stack<Element> {
  var items = <Element>[]
  void push(Element item) {
    items.add(item)
  }
  void pop() -> Element {
    return items.removeLast()
  }
}
複製程式碼

泛型在 Swift 中非常有用非常強大,它們可用於在協議中定義型別約束和相關型別。

訪問控制

Swift 文件描述如下:

訪問控制限制從其他原始檔和模組中的程式碼訪問你的程式碼。此功能可以隱藏程式碼的實現細節,並指定一個首選介面,通過該介面可以訪問和使用該程式碼。

Swift 有五個訪問級別:open, public, internal, file-privateprivate

這些關鍵字用於處理模組和原始檔的上下文中。文件描述如下:

模組是一個程式碼分發單元 - 一個框架或應用程式,它作為一個單元構建和釋出,可以在另一個模組中使用 Swift 的 import 關鍵字匯入。

openpublic 訪問級別可讓程式碼在模組外部訪問。

privatefile-private 訪問級別可讓程式碼無法在其定義的檔案之外訪問。

例如:

public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}
複製程式碼

Dart 中的訪問級別更簡單,僅限於 publicprivate。文件描述如下:

與 Java 不同,Dart 沒有關鍵字 publicprotectedprivate。如果識別符號以下劃線 _ 開頭,則它私有的。

例如:

class HomePage extends StatefulWidget { // public
  @override
  _HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> { ... } // private
複製程式碼

Dart 和 Swift 中訪問控制的設計目標不同。因此,訪問級別非常不同。

非同步程式設計:Future

非同步程式設計是 Dart 中真正閃耀的地方。

在處理任務時需要某種形式的非同步程式設計,例如:

  • 從 Web 下載內容
  • 與後端服務通訊
  • 執行長時間執行的操作

在這些情況下,最好不要阻塞執行的主執行緒,這可能會使我們的程式卡住。

Dart 文件描述如下:

非同步操作可讓您的程式在等待某個任務完成時去執行其它操作。Dart 使用 Future 物件來表示非同步操作的結果。要使用 Future,可以使用 async/awaitFuture API

作為一個例子,讓我們看看我們如何使用非同步程式設計:

  • 使用伺服器驗證使用者
  • 儲存訪問令牌以保護儲存
  • 獲取使用者個人資料資訊

在 Dart 中,這可以通過結合使用 Futureasync/await 來完成:

Future<UserProfile> getUserProfile(UserCredentials credentials) async {
  final accessToken = await networkService.signIn(credentials);
  await secureStorage.storeToken(accessToken, forUserCredentials: credentials);
  return await networkService.getProfile(accessToken);
}
複製程式碼

在 Swift 中,不支援 async/await,我們只能通過閉包來實現這一點:

func getUserProfile(credentials: UserCredentials, completion: (_ result: UserProfile) -> Void) {
  networkService.signIn(credentials) { accessToken in
    secureStorage.storeToken(accessToken) {
      networkService.getProfile(accessToken, completion: completion)
    }
  }
}
複製程式碼

由於巢狀的 completion 塊,這導致了“厄運金字塔(pyramid of doom)”。在這種情況下,錯誤處理變得非常困難。

在 Dart 中,上面程式碼中的處理錯誤只需在程式碼周圍新增一個 try/catch 塊到 getUserProfile 方法即可。

作為參考,有人建議將來向 Swift 中新增 async/await。在下面這個 proposal 中有詳細描述:

在實現之前,開發人員可以使用第三方庫,例如 Google 的 Promises 庫。

非同步程式設計:Stream

Dart 將 Stream 作為核心庫的一部分來實現,但 Swift 沒有。

Dart 文件描述如下:

Stream 是一個非同步事件序列。

Stream 是響應式程式的基礎,它們在狀態管理中發揮著重要作用。

例如,Stream 是搜尋內容的絕佳選擇,每次使用者更新搜尋欄位中的文字時,都會發出一組新結果。

Stream 不包含在 Swift 核心庫中。不過第三方庫(如 RxSwift)提供了對流的支援。

Stream 是一個廣泛的主題,這裡不詳細討論。

記憶體管理

Dart 使用高階垃圾回收(garbage collection)方案管理記憶體。

Swift 通過自動引用計數(ARC)管理記憶體。

這可以保證良好的效能,因為記憶體在不再使用時會立即釋放。

然而,它確實將部分負擔地從編譯器轉移到開發人員。

在 Swift 中,我們需要考慮物件的生命週期和所有權,並正確使用適當的關鍵字(weak, strong, unowned)以避免迴圈引用。

編譯和執行

首先來看看 JITAOT 編譯器之間的重要區別:

JIT

JIT 編譯器在程式執行期間執行,也就是即時編譯

JIT 編譯器通常與動態語言一起使用,其中型別不是提前確定的。JIT 程式通過直譯器或虛擬機器(VM)執行。

AOT

在執行之前,AOT 編譯器在建立程式期間執行。

AOT 編譯器通常與靜態語言一起使用,後者知道資料的型別。AOT 程式被編譯為本機機器程式碼,在執行時由硬體直接執行。

下面引用了 Wm Leler 的這篇文章:

當在開發期間完成 AOT 編譯時,它總是導致更長的開發週期(對程式進行更改和能夠執行程式以檢視更改結果之間的時間)。 但 AOT 編譯讓程式的執行更可預測,而不會在執行時暫停進行分析和編譯。AOT 編譯的程式也可以快速啟動(因為它們已經被編譯)。 相反,JIT 編譯提供了更快的開發週期,但可能導致執行速度變慢或更加笨拙。特別是,JIT 編譯器的啟動時間較慢,因為當程式開始執行時,JIT 編譯器必須在執行程式碼之前進行分析和編譯。研究表明,如果開始執行的時間超過幾秒鐘,很多人都會放棄。

作為一種靜態語言,Swift 是提前編譯的。

Dart 則同時支援 AOTJIT。與 Flutter 一起使用時,這提供了顯著的優勢。看看下面的描述:

在開發過程中使用 JIT 編譯,使用更快的編譯器。然後,當應用程式準備好釋出時,將它編譯為 AOT。因此,藉助先進的工具和編譯器,Dart 可以提供兩全其美的優勢:極快的開發週期,快速的執行和啟動時間。 - Wm Leler

使用 Dart,可以兩全其美。

Swift 有 AOT 編譯的主要缺點。即編譯時間隨著程式碼庫的大小而增加。

對於中型應用程式(10K 到 100K 行之間),編譯應用程式很容易花費幾分鐘。

對於 Flutter 應用程式來說並非如此,無論程式碼庫的大小如何,我們都會不斷進行亞秒級熱載入。

其它未涵蓋功能

本文未涵蓋以下功能,因為它們在 Dart 和 Swift 中非常相似:

  • 運算子
  • 字串
  • Swift 中的可選鏈(在 Dart 中稱為條件成員訪問)。

併發

  • 併發程式設計在 Dart 中通過 isolate 來提供。
  • Swift 使用 Grand Central Dispatch(GCD)和分發佇列。

Dart 中缺失的那些我喜歡的 Swift 特性

  • Structs
  • 帶關聯型別的 Enums
  • Optionals

Swift 中缺失的那些我喜歡的 Dart 特性

  • JIT 編譯器
  • Future 和 await/async
  • Stream 和 yield/async*

結論

Dart 和 Swift 都是出色的語言,非常適合構建現代移動應用程式及其他應用程式。

這兩種語言都有自己獨特的優點。

在比較過移動應用程式開發和兩種語言的工具時,我覺得 Dart 佔了上風。這是由於 JIT 編譯器,它是 Flutter 中有狀態熱載入的基礎。

在構建應用程式時,熱載入可以大大提高生產力,因為它可以將開發週期從幾秒或幾分鐘加速到不到一秒鐘。

開發時間比計算時間更耗費資源。

因此,優化開發人員的時間是一個非常明智的舉措。

另一方面,我覺得 Swift 有一個非常強大的型別系統。型別安全性融入 Swift 的所有語言功能,能更自然地開發出健壯的程式。

一旦我們拋開個人偏好,程式語言就是工具。作為開發人員,我們的任務是為工作選擇最合適的工具。

無論如何,我們可以希望兩種語言在發展過程中互相借鑑最好的想法。

關注我們

歡迎關注我們的公眾號:iOS-Tips,也歡迎加入我們的群組討論問題(加微信 coldlight_hh)。

Dart vs Swift

相關文章