最近自分の中でWeb開発はReactJS、モバイルアプリ開発はFlutterが良いという結論がでました。
Flutterでの実装はDart言語で行います。
基本的なモダン文法をサポートしているため、他言語から入ってくる人には入りやすい言語だとは思います。
とはいえ、日本語情報でステップバイステップでDart文法がまとまっているところは少ないので
まとめてみようと思ったのがこの記事です。
Flutterがよい理由
【翻訳記事】なぜFlutterにおいてDartを使用するのか?
・hot reloadが高速(AOT、JITの2段階のコンパイル)
・アイソレートとガベージコレクション最適化による軽快な動作
(Google内でFlutterのチームの近くにDartのチームがいたから・・・という理由もあるらしい)
React Nativeもいじったのですが、
・Webkitのビルドが重い(expoは除く)
・速度的にネイティブに勝てない
・JavaScriptブリッジの問題
・エラーがわかりづらい問題、ネイティブのエラーだとお手上げ問題
・マイグレーションの問題(最新ネイティブ機能の追従がReact Native公式頼み)
のでやはり長期的に運用できるアプリを作るにはFlutterがいいなというのがあります。
React NativeはFacebookかたやFlutterはGoogleがサポートしているのでやはり、餅は餅屋な感じがあります。
この辺の理由はAirbnbがReact Nativeをやめた理由にも書かれています。
【翻訳】React Native at Airbnb: The Technology
Android開発はFlutterでやる方がいい説
・ライフサイクルが単純
・標準でMaterial Design、Cupatino Designがサポートされている
Androidの開発をJavaやkotlinの完全ネイティブで開発すると
Activity、Fragment、Viewと複数のライフライクルを管理することになり、
わかりづらくデバッグしづらいバグが発生します。
この点、Flutterは複雑なライフサイクルがラッピングされているため、
iOSのライフサイクルと同程度にシンプルです。
また、AndroidとiOSで共通のソースコードで実装できるため、大幅に工数が削減できます。
標準のUIライブラリでAndroidとiOS両方対応しているのが嬉しいです。
Flutterのアーキテクチャ
The Engine architecture
・DartVM上で動作
・レンダリングはSkiaエンジン
・4つのタスクランナーで構成されている(Platform、UI、GPU、IO)
C/C++のネイティブのローレベル部分から設計・実装されていることがわかります。
以下の4つのタスクランナーがタスクを分担することでスムーズなアプリケーションの実行を担保してくれています。
Platformタスクランナー
Androidの場合はAndroid MainThreadの実行、iOSの場合はFoundationクラスのMainThreadの実行を行います。
UIタスクランナー
ルートアイソレートというアプリケーションメインの処理を実行します。(Dartコードで書く大半の処理はこのタスクランナーで実行されます。)
Flutterがレンダリングしなければならない各フレームに対して以下の処理を行います。
- ルートアイソレートは、フレームをレンダリングする必要があることをエンジンに伝えなければなりません。
- エンジンはプラットフォームに次のvsyncで通知を受けるように要求します。
- プラットフォームは次のvsyncを待ちます。
- アニメーション補間を更新します。
- レイアウト段階でアプリケーションのウィジェットを再構築します。
- 新しく構築されたウィジェットとウィジェットをレイアウトし、それらをすぐにエンジンに送信されるレイヤーのツリーにペイントします。ここで実際にラスタライズされるものは何もありません。何をペイントする必要があるかの説明だけがペイントフェーズの一部として構築されます。
- 画面上のウィジェットに関するセマンティック情報を含むノードのツリーを構築または更新します。これはプラットフォーム固有のユーザー補助コンポーネントを更新するために使用されます。
- エンジンが最終的にレンダリングするためのフレームを構築することとは別に、ルートアイソレートはプラットフォームプラグインメッセージ、タイマー、マイクロタスク、および非同期I / O(ソケット、ファイルハンドルなどから)に対するすべての応答も実行します。
GPUタスクランナー
GPUタスクランナーは、デバイス上のGPUにアクセスするために必要なタスクを実行します。
UIタスクランナーのDartコードによって作成されたレイヤツリーは、クライアントレンダリングAPIに依存しません。
つまり、OpenGL、Vulkan、ソフトウェア、またはSkia用に設定された他のバックエンドを使用して、
同じレイヤツリーを使用してフレームをレンダリングできます。
GPUタスクランナーのコンポーネントはレイヤツリーを取得して適切なGPUコマンドを作成します。
GPUタスクランナーコンポーネントは、特定のフレームのすべてのGPUリソースを設定する役割も果たします。
これには、プラットフォームと対話してフレームバッファを設定すること、サーフェスライフサイクルを管理すること、および特定のフレームのテクスチャとバッファが完全に準備されていることを確認することが含まれます。
通常、Dartコード上から直接制御することはありません。
IOタスクランナー
これまでに説明したすべてのタスクランナーは、実行できる操作の種類にかなり強い制限があります。
過度に長い時間プラットフォームタスクランナーをブロックすると、
プラットフォームのウォッチドッグがトリガーされる可能性があり、(「アプリが応答していません」などの表示が出る)
UIまたはGPUタスクランナーのいずれかをブロックすると、Flutterアプリケーションに混乱が生じます。(操作に影響が出たり、表示が固まる)
ただし、GPUスレッドには非常に高価な作業を必要とするタスクがいくつかあります。
この高価な作業は、IOタスクランナーで実行されます。
IOタスクランナーの主な機能は、アセットストアから圧縮された画像を読み込み、
それらの画像がGPUタスクランナーでレンダリングできる状態にあることを確認することです。
テクスチャを確実にレンダリングできるようにするには、まずアセットストアから圧縮データ(通常はPNG、JPEGなど)のBLOBとして読み取り、GPUに適した形式に解凍してGPUにアップロードする必要があります。
これらの操作は高価であり、GPUタスクランナーで実行するとジャンクを引き起こすでしょう。
GPUにアクセスできるのはGPUタスクランナーだけなので、
IOタスクランナーコンポーネントはメインGPUタスクランナーコンテキストと同じシェアグループ内にある特別なコンテキストを設定します。
これはエンジンのセットアップ中のごく初期の段階で起こり、IOタスク用の単一のタスクランナーがある理由でもあります。
実際には、圧縮されたバイトの読み取りと解凍はスレッドプールで発生する可能性があります。
コンテキストへのアクセスは特定のスレッドからのみ安全であるため、IOタスクランナーは特別です。
ui.Imageのようなリソースを取得する唯一の方法は、非同期呼び出しを介してです。
これにより、フレームワークはIOランナーと対話して、前述のすべてのテクスチャ操作を非同期的に実行できます。
その場合、GPUスレッドが高価な作業をする必要なく、画像をすぐにフレーム内で使用できます。
Dart文法
サンプル:https://github.com/teradonburi/dart
・参考
Dart公式言語ツアー
Dart公式ライブラリツアー
KotlinとJavaができる人向けDart速習
Dart SDKはここからインストール
無事インストールできれば、dartランタイムで実行できるようになります。
$ dart test.dart
はじめに
- 変数に配置できるものはすべてオブジェクトであり、すべてのオブジェクトはクラスのインスタンスです。プリミティブ、関数、およびnullはオブジェクトです。すべてのオブジェクトはObjectクラスから継承します。
- Dartは強く型付けされていますが、Dartは型を推論できるので型注釈はオプションです。
var number = 1
のようなコードは、numberはint型であると推論されます。型が想定されていないことを明示的に言いたい場合は、特殊な型dynamicを使用してください。 - DartはList (整数のリスト)やList (任意の型のオブジェクトのリスト)のような一般的な型をサポートしています。
- Dartは、トップレベルの関数(main()など)、およびクラスまたはオブジェクトに関連付けられている関数(それぞれ静的メソッドおよびインスタンスメソッド)をサポートしています。関数(入れ子関数またはローカル関数)内に関数を作成することもできます。
- 同様に、Dartはトップレベルの変数と、クラスまたはオブジェクトに関連付けられた変数(静的変数とインスタンス変数)をサポートします。インスタンス変数は、フィールドまたはプロパティとも呼ばれます。
- Javaとは異なり、Dartには、public、protected、およびprivateというキーワードはありません。識別子がアンダースコア(_)で始まる場合は、そのライブラリ専用です。
- 識別子は文字またはアンダースコア(_)で始まり、その後にそれらの文字と数字の任意の組み合わせが続きます。
- Dartには(ランタイム値を持つ)式と(持たない)ステートメントの両方があります。たとえば、条件式
condition ? expr1 : expr2
の値はexpr1またはexpr2です。値を持たないif-elseステートメントと比較してください。文には1つ以上の式が含まれることがよくありますが、式に直接文を含めることはできません。 - Dartツールは、警告とエラーという2種類の問題を報告できます。警告は、コードが機能しない可能性があることを示すだけのものですが、プログラムの実行を妨げるものではありません。コンパイル時エラーが発生すると、コードはまったく実行されません。実行時エラーが発生すると、コードの実行中に例外が発生します。
main関数
dartの処理の実行はmain関数から始まります。
print関数などのよく使われる関数はdart:core
ライブラリにありますが、
dart:core
ライブラリはimport不要で使えます。
//
で1行コメント、/**/
で囲った部分が複数行コメントとなり、実行されません。(他の言語と一緒)
Dartは末尾セミコロン必須です。
// import 'dart:core'; // 省略可
main() {
// コメント
/*
複数行コメント
*/
print('Hello World!!');
}
四則演算
他の言語と一緒です。
main() {
// 四則演算(+, -, *, /, %)
print(1 + 2 - 3 * 4 / 5 % 6);
}
変数定義、変数の埋め込み
Dartは強力な型推論機能を持っているため、
型を明示しなくてもvarで変数定義して値を格納することができます。
print関数に$
指定で変数の値を埋め込むことができます。
main() {
var a = 1; // varで変数定義
print(a);
a = 2;
print('\$で変数の値を埋め込み、$a');
}
基本的なデータ型(built-in)
型注釈はオプションですが、明示的にデータ型を指定することが出来ます。
よく使う型
- 数値:int, double
- 文字列:String
- 真偽:bool
- 配列:List
- セット(ユニークな配列):Set
- Key/Valueペア:Map
main() {
// dartのbuild-in型(明示的な型宣言変数)
int b = 10;
double c = 12.3;
String d = 'abc';
bool e = true;
print('$b, $c, $d, $e');
List f = [1,2,3]; // 配列
f.add(4); // 末尾追加
f.add(4);
print('$f, ${f.length}, ${f[0]}');
Set g = {'a', 'b', 'c'}; // 重複を許さない配列
g.add('d'); // 末尾追加
g.add('d'); // 重複して入ることはない
print('$g, ${g.length}, ${g.toList()[1]}'); // 配列に変換するときはtoListを使う
// Key/Valueペア
Map h = {
// Key: Value
'first': 'one',
'second': 'two',
'third': 'three'
};
h.addAll({'fourth': 'four'});
print('$h, ${h.length}, ${h['first']}'); // Keyでアクセス
// ルーン文字は、文字列のUTF-32コードポイント。\uで文字を指定する
// 絵文字の表示に使える
Runes i = new Runes('\u2665 \u{1f605} \u{1f60e} \u{1f47b} \u{1f596} \u{1f44d}');
print(new String.fromCharCodes(i));
// Symbolオブジェクトは、Dartプログラムで宣言された演算子または識別子を表します。
// コンパイル時にAPI名などは変更されてしまうが、Symbolは変更されないため、API参照などの識別に使える
// ただし、dartライブラリなどの作成者でない限り、ほぼ使う機会はない
// #で始まる
#hogehoge;
// Dartは強く型付けされていますが、Dartは型を推論できるので型注釈はオプションです。
// 次のコードで、数値はint型であると推論されます。型が想定されていないことを明示的に示したい場合は、特殊な型dynamicを使用します。
// any型、スクリプト言語の変数と同じような性質を持つ
dynamic j = 10;
j = 'a'; // dynamicだと途中で別の型のデータも格納できる
print(j);
// varの場合は初期化時の型推論が働くので途中で違う型を入れようとするとエラーになる
// var j = 0;
// j = 'a'; // エラー
// cast、別のデータ型への変換
print(int.parse('42') == 42); // String => int
print(double.parse('42.3') == 42.3); // String => double
print(42.31.toString() == '42.31'); // number => String
}
定数
final、constを使うことで定数を定義できます。
途中で値の変更が許されません。(値が変更されないことが保証される)
main() {
final k = 1; // finalの変数は初期化時のみ代入可能
// k = 2; // 再代入しようとするとエラー
const l = 1; // 定数、再代入はエラーになる(コンパイル時に埋め込まれる)
// l = 2;
List m = const [1, 2, 3]; // 定数配列を代入
// m.add(1); // 定数配列の中身の変更はできない(実行時エラー:Cannot add to an unmodifiable list)
print('$k, $l, $m');
}
finalは変数の性質で、constは変数の性質+値の性質も規定しています(constの方が制限出来る範囲が広義)
例えば、次のようなfinalを使った定数値の指定はできません。
List l = final [1, 2, 3];
finalとconstの詳細な違いはこの記事が参考になります。
制御文
if
, forEach
, for
, for in
, while
, do while
, switch
などの基本的な制御文が使えます。
switch文のみ他の言語と少し違いが有り、連続で実行するためにラベルを明示的に付ける必要があります。
(ラベルをつけさえすれば、次の処理以外も連続で実行できるため、こっちのほうが優れているかもしれない)
main() {
// forEach
var lists = ['l', 'i', 's', 't'];
lists.forEach((value) { print(value); });
var sets = {'s', 'e', 't'};
sets.forEach((value) { print(value); });
var maps = {'k': 'm', 'e': 'a', 'y': 'p'};
maps.forEach((key, value) { print('$key $value'); });
// for
for (var i = 0; i < 3; i++) {
print(i);
}
// for in
for (var i in ['f', 'o', 'r', 'i', 'n']) {
if (i == 'o' || i == 'r') continue;
print(i);
}
for (var i in {'s', 'e', 't'}) {
print(i);
}
// while
var w = 0;
while (true) {
// if文
if (w == 3) {
break;
} else if(w == 1) {
w++;
print('wは$w');
} else {
w++;
print('w=$w');
}
}
// do-while
var dw = 0;
do {
dw++;
print('dw=$dw');
} while (dw < 3);
// switch
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
print('CLOSED');
continue nowClosed; // continueの場合、nowClosedラベルを実行する
nowClosed:
case 'NOW_CLOSED':
print('NOW_CLOSED');
break;
}
}
関数
ある処理をまとめた塊を関数として定義して再利用できます。
Dart特有の機能としては名前付き任意引数と順序付き任意引数があります。
// 関数(処理のまとまりを記述する)
// 戻り値の型を指定しない場合はdynamicになる(あまり推奨されていない)
// 戻り値がいらない場合は戻り値にvoidを指定する
int testFunction(){
const y = 20;
print('$x, $y'); // 関数の外で定義されているxは参照できる
return x + y;
}
main() {
const x = 10;
// print('$y'); // 関数の内部で定義されているyは参照できない(変数のスコープ)
// トップレベルでなくても関数内部でも関数定義できる
// int testFunction(){
// const y = 20;
// print('$x, $y'); // 関数の外で定義されているxは参照できる
// return x + y;
// }
var result = testFunction();
print('$result');
// 一行の場合、ファットアローで省略記法がかける(JavaScriptのアロー関数と同じ)
int oneline(a,b) => a + b;
// この関数と等価
// int oneline(a,b){ return a + b }
print(oneline(1,2));
// {}は名前付き任意引数
void enableFlags({bool bold, bool hidden}) { print('$bold $hidden'); }
// 引数ラベルをつけて呼び出す(記述順序は任意)
// boldにはnull、hiddenにはtrueが渡される
enableFlags(hidden: true);
// []は順序付き任意引数、特定の位置以降の引数を省略可能
// 任意引数にはデフォルト値をもたせることも可能(nullの場合、=の右辺の値が代入される)
String say(String from, String msg, [String device = 'unknown', String mood]) {
// ?? 演算子はnullの場合に右側の値が適応される
return '$from says $msg platform: ${device} mood: ${mood ?? 'unknown'}';
}
}
例外処理(try-catch)
try-catch文を使うことで例外処理の対処を行うことが出来ます。
try文内部で例外が起きた場合にcatch文に飛びます。(特定の型の例外を捕捉したい場合はon データ型 catchを使います)
main() {
void errorFunc() {
try {
// throw Exceptionで意図的に例外を投げる
throw Exception('例外です');
} on Exception catch(e) {
// 捕まえる型を指定するには on ~~ catch を使う
// eはException型
print(e);
// rethrowでtry-catch-finallyブロックの外に例外を投げ直す事ができる(関数の外などでcatchする必要あり)
rethrow;
} finally {
// finallyブロックは例外の有無にかかわらず実行される、省略可。
print('finally');
}
}
// 例外処理:try-catch文
try {
errorFunc();
} catch (e, s) {
// 型を指定しないcatchは、何型かわからない例外全部キャッチする
// catchに仮引数を2つ指定すると、2つ目はStackTraceオブジェクトが入る
print(s);
}
}
クラス
他のオブジェクト指向言語同様、独自のデータ型を定義することが出来ます。
メンバ(変数、関数)を持つことでデータの塊に期待する振る舞いを与えることが出来ます。
Person(this.firstName, this.lastName);
のようにすることで「引数→メンバ変数への代入」の省略表記をすることもできます。
また、C++のクラスライクに初期化子で変数を初期化することもできます。(メンバ変数がfinal、constの場合に使う)
名前付きコンストラクタ、factoryコンストラクタはDart独特の機能だと思います。
他の言語同様extendsで継承もできます。
メンバ関数上書きする際(オーバライド)、対象のメンバ関数にメタデータの@overrideをつけます(ただし、任意です)
同名のメンバ関数で引数違いのメンバ関数定義(オーバーロード)は存在しない(エラーになる)ので、やりたい場合は名前付き引数などを使います。
private定義をするには_
をつけます。ただし、同ライブラリ内だとアクセスできてしまいます。
静的メンバを定義する(クラス共通のメンバ変数、メンバ関数)にはstaticキーワードをつけます。(他の言語と同じ)
他言語同様、子クラスから親クラスにキャストもできます。(ポリモーフィズム)
main() {
// インスタンス生成
var person1 = new Person('Yamada', 'Taro');
print('${person1.firstName} ${person1.lastName}');
// メンバ関数呼び出し
person1.greed();
// 名前付きコンストラクタ呼び出し
var person2 = new Person.origin();
print('${person2.firstName} ${person2.lastName}');
// 静的メンバはインスタンス化しなくても呼び出し可能(クラス共通の関数、変数)
print(Person.capacity);
Person.staticMethod();
// callメソッドの呼び出し
print(person1());
// 実は同ライブラリ内であれば、privateにアクセスできてしまう
person2._member = 'abc';
print(person2._member);
var engineer1 = new Engineer('エン', 'ジニア');
engineer1.greed();
// 親クラスにキャストできる
Person engineer2 = Engineer.instance(true);
// 呼ばれるのは継承クラスのgreed(ポリモーフィズム)
engineer2.greed();
// 明示的に元のクラスにキャストするにはasを使う
(engineer2 as Engineer).greed();
}
class Person {
String firstName;
String lastName;
// this.フィールド名の引数だけで、フィールドに値を代入できる
// コンストラクタのブロック内ではすでに代入された状態で使用できる
// thisを省略すると別の仮引数として扱われてしまう
Person(this.firstName, this.lastName);
// privateメンバは便宜上、_から始まる。ただし、同ライブラリ内であればアクセスしようと思えばアクセスできてしまう。
String _member;
// 名前付きコンストラクタ
// 複数のコンストラクタをもたせたいときに使う
Person.origin() {
this.firstName = '氏';
this.lastName = '名';
// 実はメソッド内はthisを省略してもクラス内のフィールドにアクセスできる
// firstName = '氏';
// lastName = '名';
}
// メンバ関数(インスタンス化したら呼び出し可能)
greed() {
print('Hello ${firstName} ${lastName}');
}
// 同名メソッドは作成できない(メソッドのオーバーロード)
// 引数で処理を分けたい同名メソッドを作りたければ任意引数を使う
// greed(int a) {}
// 静的メンバ変数
static const capacity = 16;
// 静的メンバ関数
static void staticMethod() {
print('Hello');
}
// callは特殊なメンバ関数
// インスタンス名()で呼び出しできる(呼び出し可能なクラス)
call() => '$firstName $lastName';
}
// extendsでクラスの継承(親クラスのメンバが参照できる)
class Engineer extends Person {
final String name;
// 親クラスのコンストラクタを呼ぶには初期化子(:)でsuperを使う
// メンバ変数の初期化も初期化子内で行える
Engineer(String firstName, String lastName) :
name = '',
super(firstName, lastName);
Engineer.origin():
name = 'hello',
super.origin() {
print('${firstName} ${lastName}');
}
// 先頭にfactoryキーワードをつけるとファクトリーコンストラクタとなる
// 自身のインスタンスを戻り値として返すことを明示できる
factory Engineer.instance(bool isEngineer) {
var instance = isEngineer ? new Engineer.origin() : new Person.origin();
return instance;
}
// メソッドの上書き
// @overrideは任意(けど書いたほうがわかりやすい)
@override
greed() {
// super.greed(); // メンバ関数内でコンストラクタ以外の親クラスのメンバ関数はsuper.メンバ関数名で呼び出しできる
print('I am ${firstName}${lastName}');
}
}
不変コンストラクタはメンバ変数が全てfinalの場合に定義できます。
この場合、インスタンス化された変数はコンパイル定数として扱われます。
一部処理を別コンストラクタに移譲するリダイレクトコンストラクタも作成できます。
main() {
const p = Point(1, 2);
print('x=${p.x},y=${p.y}');
var pp = Point.alongXAxis(3);
print('x=${pp.x},y=${pp.y}');
}
class Point {
final int x;
final int y;
// 不変コンストラクタ
// メンバ変数が全部 final 宣言されていると、コンストラクタの頭にconstをつけられる
// この場合、インスタンス化された変数はコンパイル定数として扱われる
const Point(this.x, this.y);
// リダイレクトコンストラクタ
// 別のコンストラクタに一部処理を移譲する
const Point.alongXAxis(int x): this(x, 0);
}
ゲッター、セッター
ゲッターはメンバ関数から値を取得する際に実行されるメンバ関数です。
あたかもメンバ変数かのように参照できます。
セッターは、メンバ変数に代入する際に実行されるメンバ関数です。
あたかもメンバ変数かのように代入できます。
main (){
var rect = Rectangle(3, 4, 20, 15);
print('right=${rect.right}'); // rightを参照するとゲッターが呼ばれて計算結果を取得。left + width
rect.right = 12; // rightを変えるとセッターがよばれて
print('left=${rect.left}'); // leftの結果も変わる
}
class Rectangle {
int left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// ゲッター:rightのパラメータを参照できる。(実態はleft+widthの計算結果を返す)
int get right => left + width;
// セッター:パラメータを代入時にleftを計算する
set right(num value) => left = value - width;
int get bottom => top + height;
set bottom(num value) => top = value - height;
}
抽象クラス
抽象クラスにするにはabstractキーワードをつけます。
抽象クラスはインスタンス化できません。
抽象クラスを継承してメンバ関数を実装します。
main() {
// abstractクラスはインスタンス化できない
// var animal = new Animal();
var cat = new Cat();
cat.hello();
}
// 抽象クラス
// 処理未定義のメンバ関数を持つクラス、インスタンス化できない
abstract class Animal {
void hello();
}
// 抽象クラスを継承して未定義メンバ関数を実装する
class Cat extends Animal {
void hello() {
print("みゃお");
}
}
演算子のオーバロード
C++言語のように演算子(operator)をオーバライドすることができます。
クラス同士の演算を定義でき、強力な反面、使い所を間違えると混乱を招くため
数学的な処理などに限定したほうが良いでしょう。
main (){
var v1 = new Vector(1, 2);
var v2 = new Vector(2, 3);
var v3 = v1 + v2;
print('x=${v3.x},y=${v3.y}');
}
class Vector {
final int x, y;
Vector(this.x, this.y);
// 演算子のオーバーライド(演算子の振る舞いをメソッドの処理に上書きする)
// C++の演算子のオーバーライドと同じ
// この例はベクトルの足し算、引き算を定義できる
// 強力な反面、不用意に使うと混乱を招くのでベクトルや行列計算等の数学的な処理を定義する以外はあまり使わないほうが良い
Vector operator +(Vector v) => Vector(x + v.x, y + v.y); // Vector + Vector
Vector operator -(Vector v) => Vector(x - v.x, y - v.y); // Vector - Vector
}
インタフェース
Dartのクラスはクラスの生成と同時にinterfaceも生成します。(暗黙的なinterface)
interfaceに存在するのはメンバ関数の定義のみで実装をimprementsで行うことが出来ます。(imprements先クラスで実装を保証させる)
main() {
// Masterクラスのインスタンスを作成
var master = new Master('Master');
print(master.commit('ToDo List'));
// Masterインタフェースを継承したBranchクラスのインスタンスを作成
var branch = new Branch();
print(branch.commit('sort'));
var director = Director('Tanaka', 'Saburo');
director.hello();
director.story();
}
// Dartには interfaceキーワードが存在しませんが、クラスを宣言した時点でそのクラスと同じAPIのinterfaceが勝手に作られます(暗黙的なinterface)
// インターフェイスは実装を持たない
// Masterクラスの宣言であり、commit()メソッドを持ったMasterインターフェイスの宣言でもある
class Master {
// privateなものはインターフェイスには含まれない
final _name;
// コンストラクタもインターフェイスには含まれない
Master(this._name);
String commit(String msg) => '${_name} commit ${msg}';
// このメンバ関数の宣言のみインターフェイスに含まれる(実装は含まれない)
// String commit(String msg);
}
// implementsでPersonインターフェイスを実装する
class Branch implements Master {
// privateメンバ変数に関してはゲッターの実装をしないと怒られる
get _name => '';
// commitを実装しないと怒られる
String commit(String msg) => 'Branch commit ${msg}';
}
// extendsの親クラスは1つしか指定できないのに対し、implementsは複数指定できる(Javaと一緒)
// 抽象クラスもimplementsできる
class Director extends Person implements Animal, Point {
// Personのコンストラクタを継承
Director(String firstName, String lastName) : super(firstName, lastName);
// Animalのメンバ関数
@override
void hello() {
print('I am Director');
}
// Pointのゲッター実装
@override
int get x => x;
// Pointのゲッター実装
@override
int get y => y;
// Director独自のメンバ関数
void story() {
print('Yes we can');
}
}
ミックスイン
ミックスインを使用するとmixinの実装を引き継ぐことが出来ます。
ただし、with句で引き継ぐmixinにはコンストラクタを指定することは出来ません。
main() {
// mixin
var musician = new Musician();
// Performerクラスの実装を呼び出す
musician.PerformerMethod();
// Musicalクラスの実装を呼び出す
musician.MusicalMethod();
}
class Performer {
Performer() {
print('Perfomer');
}
void PerformerMethod() {
print('PerformerMethod');
}
}
mixin Musical {
// mixinはコンストラクタは定義できない
void MusicalMethod() {
print('MusicalMethod');
}
}
// ミックスイン、with句でつないだmixinの実装が使える
// 多重継承に似ているが、コンストラクタが定義できるのはextendsしたクラスのみ
class Musician extends Performer with Musical {
Musician(): super() {
print('Musician');
}
}
列挙型
列挙型はデータの識別用に定義しておくと便利です。
indexで0から始まるインデックスにアクセスできます。
main() {
var c = Color.blue;
print(Color.green.index == 1);
// 列挙型はswitchの条件分岐に使える
switch (c) {
case Color.green:
print('green');
break;
case Color.blue:
print('blue');
break;
case Color.red:
print('red');
break;
default:
}
}
// 列挙型
// 列挙子は宣言された順にインデックス(0始まり)が割り振られていて、 index で参照できる。
// enumを継承できなかったり(mixinにも使えない)、enumのインスタンスを自前で生成できない(定数のみしか使えない)
// 実装を持つことが出来ない以外、Javaとほぼ同じ
enum Color { red, green, blue }
ジェネリックス
ジェネリックスはデータの型を利用時に指定することが出来ます。
main() {
// ジェネリックスのインスタンス化は型を指定する
var cache = new Cache<String>();
cache.setByKey('key', 'test');
print('key=${cache.getByKey('key')}');
// メソッドのジェネリックス
// 型の制限をするときは型のextendsを使う
T sum<T extends num>(List<T> list, T init){
T sum = init;
list.forEach((value) {
sum += value;
});
return sum;
}
int r1 = sum<int>([1,2,3], 0);
print(r1);
// 型指定しない場合は左辺の型に暗黙的に型推定される
double r2 = sum([1.1, 2.2, 3.3], 0.0);
print(r2);
}
// 型だけが違う実装をしたクラスを実装したい場合はジェネリックスを使うと便利
// インスタンス化するときTに型を指定する
class Cache<T> {
Map<String, T> store = <String, T>{};
T getByKey(String key) {
return store[key];
}
void setByKey(String key, T value) {
this.store.addAll(<String, T>{key: value});
}
}
typedef
関数に別名(型)を定義することが出来ます。
Dartの場合、typedefにジェネリックスを併用することが出来ます。
main() {
// sort関数をtypedefで定義した型で格納
Compare<int> sortFunc = sort;
print('sort: ${sortFunc(1, 2)}');
}
// typedefで関数の別名(型)を定義できる
typedef Compare<T> = T Function(T a, T b);
int sort(int a, int b) => a - b;
文字列操作
よく使う文字列操作
- 文字列置換
- 正規表現によるパターン抽出
- 改行を含んだ文字列リテラル
- 文字列分解→配列化
main() {
// 文字列置換
'Dart Language'.replaceAll('a', '@'); // == 'D@rt L@ngu@ge'
var address = '東京都港区 1-1-1';
// 正規表現でマッチするすべてを取得する
Iterable<Match> matches = new RegExp('.?区').allMatches(address);
for (Match m in matches) {
print(m.group(0));
}
// 改行を含んだ文字列をリテラルで表現するには'''で囲う
var multiline = '''
改行
しました''';
print(multiline);
}
カスケード記法
特定のインスタンスに対して..
で続けることでそのインスタンスに対する操作(メンバ関数呼び出し)を続けることが出来ます。
main() {
// カスケード記法、..で対象のインスタンスに対するメンバ関数呼び出しの操作を続けられる
var fullString = StringBuffer()
..write('Use a StringBuffer for ')
..writeAll(['efficient', 'string', 'creation'], ' ')
..toString();
print(fullString);
// 次と等価
// var sb = StringBuffer();
// sb.write('Use a StringBuffer for ');
// sb.writeAll(['efficient', 'string', 'creation'], ' ');
// var fullString = sb.toString();
// print(fullString);
}
非同期関数
非同期関数を作成するにはFuture型を返却します。
Futureに関してはJavaScriptのPromiseと似ています。
async/awaitが使えるため、awaitで同期待ちすることができます。
import 'dart:async'; // 非同期処理
main() async {
// 非同期関数(async)、Future<データ型>で戻り値を返却する必要がある
// JavaScriptのPromise関数とほぼ同じ
Future<String> lookUpVersion() async => '1.0.0';
var version = await lookUpVersion(); // awaitで同期待ち(async関数内でのみ使える)
print(version);
}
ジェネレータ
同期のIteratable、非同期のStreamの2種類あります。
JavaScriptのジェネレータに似ています。
呼び出すたびにyieldの値が順次返ります。
Iterableの方はforEachすることですべての値が取得できます。
Streamの方はawait for in
で同期待ちしながらすべての値を取得することが出来ます。
listenを使ってコールバック形式で受け取ることもできます。
import 'dart:async'; // 非同期処理
main() async {
// 同期ジェネレータ、JavaScriptのジェネレータとほぼ同じ、Iterable<データ型>で戻り値を返却する必要がある
// yieldの値が順次返却される
Iterable<int> countIterator(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
var iterator = countIterator(3);
iterator.forEach((value) { print(value); });
print('--------');
// 非同期ジェネレータ、JavaScriptのジェネレータ、Rxのストリームに近い、Stream<データ型>で戻り値を返却する必要がある
// yieldの値が順次返される
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
var stream = countStream(3);
// Streamの同期待ちにはawait forを使う。yieldの結果を順次取り出すイメージ
await for (var value in stream) {
print(value);
}
print('--------');
var stream2 = countStream(3);
// streamの結果を非同期で受け取るにはlistenで結果を待つ
stream2.listen((value){
print(value);
});
}
アイソレータ
高価(高負荷)な処理を別のスレッドに移譲することができます。
正確にはマルチスレッドではなくワーカープロセスなのですが、
JavaScriptのワーカースレッドのような仕組みに処理を移譲することができます。
(ほとんどのコンピュータは、モバイルプラットフォーム上でも、マルチコアCPUを搭載しています。
これらすべてのコアを活用するために、開発者は伝統的に、同時実行されている共有メモリスレッドを使用します。
ただし、共有状態の同時実行はエラーが発生しやすく、コードが複雑になる可能性があります。
Dartはスレッドの代わりに、アイソレータの内部で実行されます。
各アイソレータには独自のメモリヒープがあり、アイソレータの状態には他のアイソレータから直接アクセスできないようになっています。)
ReceivePortクラスで待受ポートを開き、Isolate.spawn関数でアイソレータを実行します。
実行結果はsendで呼び出し元に送信します。
import 'dart:io';
import 'dart:isolate';
main() async {
final receivePort = ReceivePort();
final sendPort = receivePort.sendPort;
// アイソレータ(スレッド処理)を実行
await Isolate.spawn(isolate, sendPort);
// アイソレータの結果待ち
receivePort.listen((msg){
print("message from another Isolate: $msg");
exit(0); // main関数を終了する
});
}
// スレッド処理
void isolate(sendPort) {
for(int i = 0; i< 10; i ++) {
print("waiting: $i");
}
// 呼び出し元に結果を送信する
sendPort.send("finished!");
}