LoginSignup
31
22

More than 3 years have passed since last update.

Dart vs Swift

Last updated at Posted at 2020-08-08

| 作者 : Andrea Bizzotto
| 原文 : medium
| 翻訳 : jamestong
| 校正 : jamestong

DartとSwiftは私のお気に入りの2つのプログラミング言語です。私は商用およびオープンソースのコードでこれらを幅広く使用してきました。

この記事では、DartとSwiftを並べて比較してみます。

  • 両者の違いを強調する;
  • 開発者の参考として、一方の言語から他方の言語に移行する(または両方を使用する)時の注意点を挙げる。

背景:

  • Dartは、単一のコードベースから美しいネイティブアプリを構築するためのGoogleのフレームワークであるFlutterに対応している;
  • Swiftは、iOS、macOS、tvOS、watchOSにまたがるAppleのSDKをサポートしている。

以下は、両言語の主な機能(Dart 2.1Swift 4.2の時点で)を比較したものです。各機能の詳細な議論はこの記事の範囲を超えているので、必要に応じて両言語の参考文献を参考してください。

目次

  • 対照表
  • 変数
  • 型推論
  • 可変型/不可変型変数
  • 関数
  • 名前付きパラメータと名前なしパラメータ
  • オプションとデフォルトのパラメータ
  • クロージャ
  • タプル
  • 制御フロー
  • コレクション(arrays, sets, maps)
  • Nullability & Optionals
  • クラス
  • 継承
  • プロパティー
  • プロトコル / 抽象クラス
  • Mixins
  • 拡張
  • 列挙型
  • 構造体
  • エラー処理
  • ジェネリクス
  • アクセス制御
  • 非同期プログラミング:Futures
  • 非同期プログラミング:Streams
  • メモリ管理
  • コンパイルと実行
  • その他の機能
  • Dartに欠けている私のお気に入りのSwiftの機能
  • Swiftに欠けている私のお気に入りのDartの機能
  • 結論

対照表

1.png

変数

変数宣言の構文は、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のドキュメントを引用します。

解析器は、フィールド、メソッド、ローカル変数、およびほとんどのジェネリック引数の型を推論することができます。解析器が特定の型を推論するのに十分な情報を持っていない場合は、動的型付けdynamicを使います。

そして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という2つのキーワードが定義されていますが、これらは以下のように動作します。

変数を変更するつもりがない場合は、varの代わりに、または型に加えて、finalまたはconstを使用してください。final変数は一度だけ設定することができ、const変数はコンパイル時の定数です。(const変数は暗黙のうちにfinalになります。) finalのトップレベル変数やクラス変数は、初めて使用されたときに初期化されます。

さらに

finalはシングルアサインメントを意味します。最終的な変数やフィールドにはinitializerが必要です。一度値が代入されると、final変数の値を変更することはできません。

TL;DR: Dartで不変変数を定義するにはfinalを使います。

Swiftではletで定数を宣言します。

定数の宣言は、定数の値をプログラムに導入します。定数はletキーワードを使って宣言され、以下のような形式になります。

let constant name: type = expression

定数の宣言は、定数名とイニシャライザ式の値との間の不変の結合を定義します。
定数の値が設定された後は変更できません。

関数

関数はSwiftとDartの中では一等市民です。

これはオブジェクトと同じように、関数は引数として渡されたり、プロパティとして保存されたり、結果として返されたりすることができることを意味します。

最初の比較として、引数のない関数の宣言方法を見てみましょう。

Dartでは、メソッド名の前にリターンタイプが付きます。

void foo();
int bar();

Swift では、サフィックスとして -> T 記法を使用しています。戻り値がない場合(Void)にはそれを付く必要がありません。

func foo()
func bar() -> Int

名前付きパラメータと名前なしパラメータ

どちらの言語も名前付きと名前なしのパラメータをサポートしています。

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

クロージャ

一級オブジェクトである関数は、他の関数の引数として渡されたり、変数に代入されたりすることができます。

この文脈では、関数はクロージャclosureとしても呼ばれています。

ここでは、各項目のインデックスと内容を表示するためにクロージャを使用して、項目のリストを反復処理する関数のDartの例を示します。

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

クロージャは1つの引数(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ドキュメントより:

タプルは複数の値を1つの複合値にまとめます。タプル内の値は任意の型をとることができ、互いに同じ型である必要はありません。

これらは小型の軽量型として使用することができ、複数の戻り値を持つ関数を定義する際に便利です。

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で配列Arraysを作ることができます。

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 = <String>{}; // empty set literal
var fruits = {'apple', 'banana'}; // set literal

同様に、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を辞書dictionaryと呼びます。

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

Nullability & Optionals

Dartでは、どんなオブジェクトでもnullにすることができます。そして、null オブジェクトのメソッドや変数にアクセスしようとすると、nullポインタ例外が発生します。これは、コンピュータプログラムで最も一般的なエラーの原因の一つです(最も一般的ではないにしても)。

最初から、Swiftにはオブジェクトが値を持てるか持てないかを宣言するための組み込みの言語機能であるオプショナルがありました。ドキュメントを引用します。

値が存在しないかもしれない状況では、オプショナルOptionalを使用します。オプショナルは2つの可能性を表します。値があり、その値にアクセスするためにオプションをアンラップすることができる場合と、値が全く存在しない場合です。

これとは対照的に、非オプショナル変数を使用することで、常に値を持つことを保証できます。

var x: Int? // optional
var y: Int = 1 // non-optional, must be initialized

ノート:Swiftの変数がオプショナルであると言うことは、Dartの変数がnullになる可能性があると言うこととほぼ同じです。

言語レベルでオプショナルをサポートしていない場合、変数がnullであるかどうかを実行時にチェックすることしかできません。

オプショナルを使うと、コンパイル時に情報をエンコードします。オプショナルをアンラップすることで、値を保持しているかどうかを安全にチェックすることができます。

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"

そして2個目はこんな感じ:

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

オプショナルを持つことは、実行時ではなくコンパイル時にエラーをキャッチできることを意味します。そして、エラーを早期にキャッチすることで、より安全でバグの少ないコードを作ることができます。

Dart はオプショナルをサポートしていませんが、アサーション (および名前付きパラメータの @required アノテーション) を使用することでなんとか緩和されています。

これらのアサーションは Flutter SDK で広く使われていますが、結果的には、余計の定型的なコードが生じます。

クラス

クラスはオブジェクト指向言語でプログラムを書くための主要な構成要素です。

クラスは Dart と Swiftが両方ともサポートしていますが、いくつかの違いがあります。

構文

Swiftのinitializerと3つのメンバ変数を持つクラスです。

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] を使用することに注意してください。これは、コンストラクタが実行される前にインスタンスのメンバ変数を設定するための糖衣構文syntactic sugarです。

ファクトリ コンストラクタ

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はシングル継承モデルを使用しており、どのクラスも1つのスーパークラスしか持っていないことを意味します。Swiftのクラスは複数のインターフェイス(プロトコルとも呼ばれる)を実装することができます。

Dartクラスはmixinベースの継承を持っています。ドキュメントを引用します。

すべてのオブジェクトは1つのクラスのインスタンスであり、すべてのクラスはObjectから派生します。mixinベースの継承とは、(Objectを除く)すべてのクラスにはちょうど1つのスーパークラスがありますが、クラス本体は複数のクラス階層で再利用できることを意味します。

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
}

計算されたプロパティのgettersに加えて、settersを定義することもできます。

上の例を使って、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は通常のクラスであり、複数のクラス階層で再利用することができます。

先ほど定義したPersonクラスをNameExtension mixinで拡張する方法を紹介します。

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言語の1つの特徴です。ドキュメントを引用します。

拡張機能は既存のクラス、構造体、列挙、またはプロトコルの型に新しい機能を追加します。これには、元のソースコードにアクセスできない型を拡張する機能が含まれます(レトロアクティブモデリングretroactive modelingとして知られています)。

これは、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
}

dataパラメータとerrorパラメータが相互に排他的であることに注意してください。

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
}

いくつか注意点があります。

  • ここではアサーションを使用して、オプショナルがないという事実を補っています。
  • コンパイラはすべてのケースをチェックすることはできません。これはレスポンスを処理するプロセスでswitchを使わないからです。

まとめると、Swiftの列挙型はDartに比べてかなり強力で表現力があります。

Dart Sealed Unionsのようなサードパーティのライブラリは、Swift列挙型と同様の機能を提供し、ギャップを埋めるのに役立ちます。

構造体

Swiftでは構造体とクラスを定義することができます。

どちらの構造にも多くの共通点があり、いくつかの違いがあります。

主な違いは:

クラスは参照型であり、構造体は値型である。

ドキュメントを引用します。

値型とは、変数や定数に代入されたとき、または関数に渡されたときに値がコピーされる型のことです。

Swiftでは、構造体と列挙はすべて値型です。これは、作成した構造体と列挙のインスタンス、およびそれらがプロパティとして持つ値型は、コード内で渡されるときに常にコピーされることを意味します。

値型とは異なり、参照型は変数や定数に代入されたときや関数に渡されたときにはコピーされません。コピーではなく、同じ既存のインスタンスへの参照が使用されます。

これが何を意味するのかを知るために、次の例を考えてみましょう。

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でデータやモデルを扱うために効果的に使うことができ、バグの少ないロバストなコードrobust codeにつながります。

エラー処理

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")
}

throwできるメソッドを呼び出す際には、tryキーワードが必須であることに注意してください。
また、エラー自体は強く型付けされているので、すべての可能なケースをカバーするために複数のキャッチブロックを持つことができます。

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のドキュメントを引用します。

ジェネリクスコードを使うと、定義した要件に従って、どんな型でも動作する柔軟で再利用可能な関数や型を書くことができます。重複を避け、明確で抽象的な方法で意図を表現するコードを書くことができます。

ジェネリクスについて両言語ともサポートしています。

ジェネリクスの最も一般的な使用例の1つは、配列、集合、マップなどのコレクションです。

そして、これらを使って独自の型を定義することができます。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には5つのアクセスレベルがあります: openpublicinternalfile-privateprivateです。

これらはモジュールやソースファイルを扱う際に使用されます。

モジュールとは、コード配布の単一ユニット、つまりフレームワークやアプリケーションを構築して単一ユニットとして出荷され、Swiftのimportキーワードで別のモジュールからインポートできるものです。

openpublicアクセスのレベルは、モジュールの外部からコードにアクセスできるます。

privateおよびfile-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では異なる目的で設計されました。その結果、アクセスレベルが大きく異なっています。

非同期プログラミング:Futures

非同期プログラミングはDartが最も得意とする分野です。
以下のようなユースケースを扱う場合、何らかの形の非同期プログラミングが必要になります。

  • ウェブからコンテンツをダウンロードする
  • バックエンドとの通信
  • 長時間稼働する業務を行う

これらのケースでは、実行中のメインスレッドをブロックしない方が良いでしょう。

Dartのドキュメントを引用します。

非同期操作は、操作が終わるのを待っている間に他の作業を完了させることができます。Dartでは、非同期操作の結果を表現するためにFutureオブジェクトを使用します。Futureを使用するには、async/awaitまたはFuture 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というAPIを提供しておらず、クロージャ(補完ブロック)でしか実現できません。

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では、上記コードのエラーの処理は、getUserProfileメソッドにtry/catchブロックを追加するだけで済みます。

参考として、将来的にはSwiftにasync/awaitを追加するという提案があります。

これが実装されるまでは、開発者はGoogleのPromisesのようなサードパーティ製のライブラリを使うことができます。

非同期プログラミング:Streams

StreamsはDartのコアライブラリの一部として実装されていますが、Swiftでは実装されていません。

Dartのドキュメントを引用します。

Streamは非同期イベントのシーケンスです。

Streamはリアクティブなアプリケーションの基本であり、状態管理で重要な役割を果たします。

例えば、ユーザーが検索フィールドのテキストを更新するたびに、新しい結果のセットが出力されます。

StreamはSwiftのコアライブラリには含まれていません。RxSwiftのようなサードパーティのライブラリはStreamに対するサポートやその他多くの機能を提供しています。

ストリーム(Stream)は幅広いトピックなので、ここでは詳しく説明しません。

メモリ管理

Dartは高度なガベージコレクション(garbage collection)方式でメモリを管理します。

Swiftは自動参照カウント(ARC)でメモリを管理します。

これは、メモリが使われなくなるとすぐに解放されるので、優れたパフォーマンスを保証します。

しかし、コンパイラから開発者に負担を部分的にシフトしています。

Swiftでは、保持サイクルを回避するためにオブジェクトのライフサイクルと所有権について考慮し、適切なキーワード(weak,strong,unowned)を正しく使う必要があります。

コンパイルと実行

まず最初に、ジャストインタイムjust-in-time(JIT)コンパイラと事前ahead-of-time(AOT)コンパイラの重要な違いを説明します。

JITコンパイラ

JIT コンパイラはプログラムの実行中に実行され、その場でコンパイルを行います。

JIT コンパイラは、型が事前に固定されていない動的な言語で使用されるのが一般的です。JITプログラムはインタプリタや仮想マシン(VM)を介して実行されます。

AOTコンパイラ

AOT コンパイラはプログラムの作成時に実行前に実行されます。

AOT コンパイラは通常、データの型を知っている静的言語で使用されます。AOTプログラムはネイティブのマシンコードにコンパイルされ、実行時にハードウェアによって直接実行されます。

Wm Leler氏のこの素晴らしい記事を引用します。

AOTコンパイルが開発中に行われる場合、開発サイクル(プログラムに変更を加えてからその変更の結果を見るためにプログラムを実行できるようになるまでの時間)が大幅に遅くなります。しかし、AOT コンパイルは、実行時に解析やコンパイルのために一時停止することなく、より予測可能なプログラムを実行することができます。また、AOTコンパイルされたプログラムは、(すでにコンパイルされているため)より速く実行を開始します。

逆に、JITコンパイルは開発サイクルを大幅に短縮しますが、実行が遅くなったり、不安定になったりします。特に、JIT コンパイラはプログラムの実行を開始すると、コードを実行する前に解析とコンパイルを行わなければならないため、起動時間が遅くなります。研究によると、実行開始までに数秒以上かかると、多くの人がアプリを放棄してしまうという結果が出ています。

静的言語として、Swiftは事前にコンパイルされます

DartはAOTJITの両方でコンパイルできます。これはFlutterと併用した場合に大きなメリットがあります。

再度引用します。

開発時には、特に高速なコンパイラを使ってJITコンパイルを行います。そして、アプリのリリース準備が整ったら、AOTコンパイルを行います。その結果、先進的なツールとコンパイラの力を借りて、Dartは、開発サイクルの超高速化と、実行時間及び起動時間の高速化という両世界のベストを実現することができます。-Wm Leler

Dartを使うと、両世界の最高のものを手に入れることができます。

SwiftはAOTコンパイルに主な欠点があります。つまり、コンパイル時間はコードベースのサイズに応じて増加します。

中規模のアプリ(10Kから100K行)では、アプリのコンパイルに数分かかることがあります。

Flutter アプリの場合はそうではなく、コードベースのサイズに関係なく、常に一秒以内のホットリロード(hot-reload)ができます。

その他の機能

以下の機能は、DartとSwiftはかなり似ているので取り上げませんでした。

  • 演算子
  • 文字列
  • SwiftではオプショナルチェイニングOptional chaining(Dartでは条件付きメンバーアクセスとして知られています)

並行性

  • 同時で並行プログラミングはDartのisolateで提供されています。
  • SwiftはGrand Central Dispatch (GCD)とディスパッチキュー(dispatch queue)を使用します。

Dartに欠けている私のお気に入りのSwiftの機能

  • 構造体
  • 関連する型を持つ列挙型
  • オプションナル

Swiftに欠けている私のお気に入りのDartの機能

  • JITコンパイラ
  • Future と await/async
  • Stream と yield/async*(RxSwiftはリアクティブアプリケーション用のストリーム(Stream)のスーパーセットを提供しています)

結論

DartとSwiftはどちらも優れた言語であり、現代のモバイルアプリやそれ以上のものを構築するのに適しています。

どちらの言語も独自の長所を持っているので、どちらが優れているということはありません。

モバイルアプリの開発と2つの言語のツールを見ると、私はDartが優位に立っていると感じます。これは、Flutter のステートフルホットリロードstateful hot-reloadの基盤となっている JIT コンパイラによるものです。

ホットリロードはアプリを構築する際に生産性を大きく向上させます。

開発者の時間は、コンピューティングの時間よりも希少なリソースです。

そのため、開発者の時間を最適化することは非常にスマートな動きです。

一方で、Swiftは非常に強力な型システムを持っていると感じています。型の安全性はすべての言語機能に組み込まれており、より自然にロバスト(頑健)なプログラムにつながります。

個人的な好みはさておき、プログラミング言語は単なるツールに過ぎません。そして、その仕事に最も適したツールを選ぶのが開発者としての私たちの仕事です。

いずれにしても、どちらの言語もお互いに最高のアイデアを借りながら進化していくことを期待しています。

31
22
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
22