Dart
DartはGoogleによって開発されたプログラミング言語です。
もともと、JavaScriptを置き換える言語を目指していましたが、Googleがとりあえず諦めてしまったようなので、今はAltJSの選択肢の一つになっています。
なぜ、今Dartなのかというと
- 最近Dartを使っているFlutterが流行っているから
- FlutterでWebアプリが作れる
- FlutterのWindows版が正式版として使える
- Flutterの他のPC版(macOS, Linux)がbetaで動くようになってきてる
- 個人的にDartが好きだから
Dartの言語仕様
Dartはコンパクトな言語仕様らしく仕様書は目次などを含め150ページほどです。(C++17のドラフトは1500ページ弱あります。)
基本形はJavaで、それにJavaScriptの考え方の一部を取り入れた感じです。
影響を受けた言語(from Wikipedia)ではJavaScript, Java, Smalltalk, Erlang, Strongtalk, C#となっていました。
このJava風の言語仕様はすでにWebでJavaScriptを書いてきた人向けというよりは、JavaやSwiftでアプリを書いてたりC++を使って何か書いていたりみたいな人向けなのかもしれません。
DartはネイティブコンパイルやJavaScriptへのトランスパイルができ、DartVMというVM上で動かすこともできます。
コンパイルにはdart compile
というコマンドを使います。
このコマンドはexe
とaot-snapshot
、jit-snapshot
、kernel
、js
をサブコマンドとして取ります。
ネイティブコンパイルする場合はexe
を使用します.このサブコマンドではプラットフォームに依存した実行ファイルを作成します。これには記述したDartのプログラムとDartのランタイムが含まれます。
aot-snapshot
はDartのプログラムをプラットフォームに依存するバイナリを作成しますがDartのランタイムは含まれません。これで作成されたプログラムはdartaotruntime
コマンドを使い実行します。
jit-snapshot
はDartプログラムを中間表現にコンパイルします。実行にはdart run
コマンドを使用します。これで作成されたプログラムはプラットフォームに依存します。
kernel
は中間表現 (カーネルAST) にコンパイルします。これはプラットフォームに依存しない形式です。実行にはdart run
コマンドを使用します。
js
はJavaScript`にコンパイルします。
dart compile exe bin/main.dart
dart compile aot-snapshot bin/main.dart
dart compile jit-snapshot bin/main.dart
dart compile kernel bin/main.dart
dart compile js bin/main.dart
環境
Dartの開発にはいくつかの選択肢がありますが、IntelliJ IDEAを使用するのが一番簡単だと思います。
ここからの流れ
- 型
- 変数
- 関数
- クラス
- パッケージ
- 非同期
の順で説明していきたいと思います。
また、null safetyになってから結構経つので、null safetyであること前提で記述している箇所もありますので、null safety以前のDartをお使いの場合は気を付けてください。
型
基本型
- 数値型は
int
とdouble
(それ以外にも数値を扱う型はあります) - 文字列は
String
型 - リストには
List<T>
型 - 連想配列には
Map<K, V>
型
型の表記
Dartの型の表記にはdynamic
とvar
を使うこともできます。
dynamic
は動的型でList<dynamic>
などのように書きたい時にも使えます。
var
は型推論を行い代入時の型になります。なので、再代入時に別の型の値を入れるとエラーになります。
動的型を乱用することは良いとは思いませんが、動的型を使わないと記述不可能な場面は出てくると思いますので、ここぞという時に使うと良いと思います。
型のTips
Dartの型の多くはObject
型からの派生です。つまり、int
もdouble
もString
も全てObject
から派生しています。
しかし、例外としてnull
の型であるNull
型はObjectから派生していません。
そのため、次のような実行結果になります。
null is Object; // false
int? i1;
i1 is Object; // false
i1 = 0;
i1 is Object; // true
int i2 = 0;
i2 is Object; // true
また、Dartでは暗黙の型変換ができません。例えば一般的には普通にできるint => double
の変換はそのまま代入では不可能ということです。なのでtoInt()
のようなメンバ関数が用意されています。
null safety
Dart 2.12からnull
を許容する型と許容しない型を明示することができるようになりました。
KotlinやSwiftにも同等の機能があります。
この機能によってモダンな感じでコードを書くことができます。
int? // nullable
int // not null
上記の例のように型名?
のような型がnull
になりうる型で、?
が付かない型がnull
を許容しない型です。
ここからはnull
になりうる型をnull許容型、そうでない方を非null型と呼びます。 (勝手にそう呼ぶだけです)
null許容型はnull
が入っているかもしれない型なので、そのまま値を使おうとするとエラーになります。
例えば以下のような場合はエラーになります。
int? a = null;
a.toString(); // aはnullかもしれない
そのため、null許容型を非null型にしなければなりません。
int? a = null;
if (a != null) {
a.toString(); // aはnullかもしれない
}
この場合a.toString()
をする前につまりnull許容型の値を使う前にnull
チェックをしています。
これによりif
文の中ではa
がnull
でないと言えるので普通にa.toString()
を使うことができます。
また、これは昔からある機能ですが、?.
や??
によって、値がnull
かnull
でないかで処理を分けることができます。
int? a = null;
a?.toString(); // a == nullであればnull、a != nullであればa.toString()の値になる
a ?? 0; // a == nullであれば0、a != nullであればaの値になる
変数
Dartの変数は型をJavaの書式とほぼ同じです。
int number = 0;
String str = 'Dart';
var one = 1;
finalとconst
Dartは型の修飾子としてfinal
とconst
が存在しています。
final
はJavaのfinal
と同じ意味です。つまり再代入不可であることを示します。
const
はコンパイル時定数。つまり、「再代入不可 + そのメモリ領域の書き換え不可」を表します。
final int zero = 0;
final one = 1; // 型推論もできる
const int two = 2;
const three = 3; // こっちでも型推論できる
不可視な識別子
Dartはそのファイル内など見える範囲を制限することができます。
ある識別子(変数とか関数とか)を他のソースで見えないようにするには_
で名前を始めます。
int visible = 0;
int _invisible = 0;
なので、Dartにはpublic
とかprotected
とかprivate
とかの予約語がありません。
関数
Dartはもちろん関数があります。
Javaとは違い関数はクラスに属する必要がありません。
JavaやC++, C#などの言語からDartを始めた人が割と戸惑う点としてDartは関数のオーバーロードをサポートしていません。
なので、全ての関数は別の名前がつけられている必要があります。
関数は次のように書きます。
int func(int a, int b) {
return a + b;
}
完全にC系の言語の書き方です。
Dartではさらに返り値の型を省略することもできます。
func(int a, int b) {
return a + b;
}
また、引数の型も省略できます。
func(a, int b) {
return a + b;
}
sync*
Dartは関数の後ろにsync*
をつけることでPythonで言うジェネレータを定義することができます。
ここで出てくるsync
というのは同期 (synchronous) という意味で、後に説明する非同期に対して同期と言っています。
// [0, max)の整数列を返すiota関数
Iterable<int> iota(int max) sync* {
for(int i = 0; i < max; ++i) {
yield i;
}
}
// main
// 実行結果
// 0
// 1
// 2
// 3
// 4
void main() {
for(final n in iota(5)) {
print(n);
}
}
このコード例のyield
は「生む」や「譲る」という意味があります。
yield
を行うと一つ値を「生む」処理を行います。これをfor
文で実行した場合は次のような動作になります。
-
yield
が来た時点で呼び出し元のに実行権を「譲る」処理を行います。 - 呼び出し元の
for
で1回ループが周ります。 - ループが回った後にもう一度
yield
の続きからジェネレータの処理が継続されます。
この処理がジェネレータ関数を抜けるまで繰り返されます。
メソッド
Dartはクラスに属する関数を作ることができます。
一般的には非メンバ関数と同じ書き方ですが、暗黙的に自インスタンスへの参照としてthis
が渡されます。
コンストラクタ
クラスを構築する特殊な関数としてコンストラクタがあります。
コンストラクタにはいくつかの書き方が存在しています。
普通のコンストラクタ
一般的なクラスベースのオブジェクト指向言語と同じくDartの普通のコンストラクタはクラス名と同一の名前をもち返り値の型を書きません。
class Klass {
Klass();
}
名前付きコンストラクタ
Dartは関数のオーバーロードができませんが、この制約はコンストラクタにも付きます。
なので普通のコンストラクタを作ってしまうと、別の引数をとる普通のコンストラクタを作ることができなくなります。
そこで、Dartにはコンストラクタを名前付きで作ることができます。
class Klass {
Klass.fromString(String f);
}
もうお分かりだと思いますが、名前付きコンストラクタも同名では作ることはできません。
ファクトリ
factory
をコンストラクタに指定すると、そのコンストラクタをファクトリとして指定することができます。
ファクトリは一般的なコンストラクタとは違いインスタンスを作成し返します。
インスタンス生成に引数を作るなどの前処理が必要であったり、インスタンス生成後にしかできない処理をする際に便利です。
class Klass {
Klass._private(); // 非公開な名前付きコンストラクタ
factory Klass() {
var instance = Klass._private(); // newは省略できる
return instance;
}
}
定数コンストラクタ
定数オブジェクトを作成することができるコンストラクタです。
メンバ変数は全てfinal
で修飾し、コンストラクタ名の前にconst
をつけます。
class Klass {
final String name;
const Klass(this.name); // this.nameのようにするとnameという引数で受けそのままthis.nameに代入できる
}
特殊な引数
Dartは名前付き引数とオプションの引数をサポートしています。
名前付き引数
名前付き引数は引数の名前を指定して値を渡す引数です。
名前付き引数を使うことで、次のような利点があります。
- 仮引数名が適切であれば読みやすいコードになる
- 引数の順番にこだわる必要がない
名前付き引数は引数を{}
で括ります。
func({first, last});
// 呼び方
func(first: 'f'); // firstだけ指定
func(last: 'l'); // lastだけ指定
func(first: 'f', last: 'l'); // firstとlastを指定
このfirst
とlast
が名前付き引数となります。
名前付き引数にした引数は引数が指定されないということもできてしまいます。
指定されなかった場合はデフォルトでnull
になります。
しかし、名前付き引数にはしたいけど、絶対引数は渡してほしいということはあると思います。
この両方を満たすために使われるのが@required
です。
func({first, @required last});
@required
はmeta
というライブラリに含まれているようで標準ではありませんが、Flutterでは使えるので知っておくと便利だと思います。
Dart 2.12からはnull safetyが標準となり、引数を渡さないとnull
になるという仕様と@required
がバッティングしてしまいます。
Dart 2.12からは@required
ではなくrequired
というキーワードを使い必ず渡す必要があることを明示できます。
func({first, required last});
オプションの引数
オプションの引数は渡さなくても良い引数のことです。
func([first, last]);
デフォルトの引数
名前付き引数やオプションの引数で渡されなかった引数にはnull
が入ります。
これをnull
以外の値にしたいときにデフォルトの引数を使用します。
named({first, last='l'});
opt([first, last='l']);
この例では両方ともlastに引数が指定されなかった場合に'l'
が使用されます。
クラス
DartはJavaScriptとは異なりクラスベースのオブジェクト指向をサポートしています。
クラスの書き方はJavaに似ています。
class Klass {
String _name;
String getName() {
return _name;
}
}
これはDartのコードですがJavaのコードにも見えます。
前述の通りDartはアクセス修飾を変数の名前で示すため、private
やpublic
などの単語を書きません。
この例では_name
がprivateのようなもので、getName
がpublicとなります。
プロパティ
最近の言語はプロパティという仕組みを持っています。
プロパティはゲッターとセッターを扱いやすくしたものです。
class Klass {
String _name; // 外部からは取得だけ許可したい
String get name => _name;
}
この例のname
がプロパティでゲッターです。
セッターは次のように作ります。
class Klass {
String _name;
String get name => _name;
set name(String v) {
_name = v;
}
}
パッケージ
Dartにはパッケージという概念があります。
パッケージはライブラリのようなものでいくつかの機能などをひとまとめにしたものです。
パッケージの作り方はここでは省略し、使い方を紹介します。
import
Dartはパッケージをimport
という単語を使用し読み込みます。
import 'dart:math';
この例ではDartの標準ライブラリのなかのmath
を読み込んでいます。
as
単にimport
をしただけでは中の識別子が名前空間もなく使えるようになります。
しかし、名前の衝突を避けるために名前空間のようなものを指定したいことがあります。
ここで使うのがas
です。
import 'dart:math' as m;
とすることでdart:math
内の識別子がm.???
の形式で使えるようになります。
show
読み込んだ識別子の一部だけ使えるようにするためにはshow
を使います。
pub
pubはDartのパッケージマネージャです。
Dartで外部パッケージを使用する場合に使われます。
pubspec.yamlというファイルにパッケージの情報を書きdart pub get
コマンドを実行することでパッケージがインストールできます。
または,dart pub add <パッケージ名>
とも書けます。このように書いた場合はpubspec.yamlにも同時に書き込まれます。
Flutterの場合はflutter pub get
コマンドです。
パッケージの更新を行う場合はget
をupgrade
にします。
ここらへんはIntelliJ系のIDEであればFlutterプラグインなどでボタン一つで行えるのであまり気にしなくても良いかもしれません。
非同期
Dartは非同期処理をサポートしています。
非同期ではFuture<T>
というクラスを使用します。Future<T>
は非同期処理を管理(?)するためのクラスで終了時に呼ばれる関数などを指定することができます。
Future<T>
かT
を受け取るという意味の型としてFutureOr<T>
もあります。Future
はdart:core
に含まれるためimport
は不要です。FutureOr
はdart:async
に含まれています。
await
await
は非同期な処理の終了を待機することを示す単語です。
await
を使うためには関数をasync
に指定する必要があります。async
に指定した関数は返り値の型がFuture
になります。
Future<void> asyncFunc() async {
// do something
}
// 使う側
Future<void> asyncCaller() async {
return await asyncFunc();
}
async
とasync*
Dartはasync
の代わりにasync*
と書くことで返り値をStream<T>
にすることができます。
async
とasync*
の関係は、普通の関数とsync*
の関係と同じです。
async*
を使うことでyield
を使うことができるようになります。
また、Stream<T>
を手軽に扱うためにDartにはawait for
という機能があります。
これはStream<T>
をfor
と同様の構文で回すための機能です。
Stream<int> getStream() {...}
Future<void> main() async {
await for (final n in getStream()) {
// do something
}
}
終わりに
以上、Dartの基本的な機能をさらっと紹介しました。
Dartを手軽に試す方法としてDartPadがあるので使ってみてください。