フューチャー技術ブログ

Dart入門

この記事はDart/Flutter連載の1記事目です。

TIGの伊藤真彦です。

Dart/Flutter入門に参加します、DartといえばFlutterの話が必ずついてくるものですが、今回は連載1記事目として、敢えてプログラミング言語としてのDartに焦点を絞った記事にします。

Dartとは

dart horizontal logo

DartはGoogleによって開発されたウェブ向けのプログラミング言語です、正式発表された時期は2011年です。

元々はJavaScriptの代替となることを目的に作られましたが、Javascriptのようにブラウザに統合される事なく今日まで至ります。JavaScriptの代替、という概念では競合にあたるTypeScriptが今ではGoogle社内の標準プログラミング言語として承認されています。

しかし、2018年にDart2として再起動、モバイルアプリケーション向けフレームワークであるFlutterの基本ライブラリでDartが採用される事により、近年注目度が上昇しています。今iOS/Androidのクロスプラットフォームでのアプリケーション開発を行うならDartが熱い、という事ですね。

技術的特徴

Dartはクラスベースのオブジェクト指向言語です、単一継承のみがサポートされていますが、Mixinを利用することも可能です。

静的型付け言語としての型アノテーションが存在しつつも、dynamic型と呼ばれる特徴的な型により、動的型付け言語のようにも扱うことが可能です。上記の特徴により、大規模システムでも耐えられる堅牢さ、高パフォーマンスを維持しつつ、時には柔軟性を持つこともできる言語として設計されています。

JavaScriptトランスパイラにより、作成したコードをJavascriptに変換することが可能です。デバッグビルドでのみ動作するassertという構文があるのも特徴です。

Dartのインストール

公式サイトにOSごとのインストールの方法がまとめられています、MACでのインストールが一番簡単です。

インストールに成功したらdart --versionコマンドでバージョンを確認できます。

~$ dart --version
Dart SDK version: 2.12.4 (stable) (Thu Apr 15 12:26:53 2021 +0200) on "macos_x64"

Dartの実行

dart ファイル名で作成したDARTファイルを実行できます。

~$ dart hello.dart
Hello World!

拡張子は.dartが一般的なようですが、dartコマンドに渡す分には他の拡張子でも読み込み、実行できました。

Dartの基礎文法

基本的な文法を紹介する形でDartに触れてみます。

HelloWorld

hello.dart
void main() {
print('Hello World!');
}

Dartはmain関数に実行したい処理を書く形式で単一ファイルでとして実行可能です。

コマンドライン引数

hello.dart
void main(List<String> args) {
print('Hello ${args[0]}!');
}
~ % dart hello.dart Dart
Hello Dart!

main関数に引数を持たせることでコマンドライン引数を受け取ることができます。

Goのflag.Parse()、RubyのARGVのようなコマンドライン引数を取り扱うための独自な手法が無い。引数の書き方が型名 変数名の順番である、などgoに慣れた状態で触れると異文化を感じます。文字列への変数展開はJavaScriptであれば``で囲った文字列である必要があるところを’’でも問題ないあたりも細かい作法が異なりますね。

変数宣言

hello.dart
void main(List<String> args) {
var name = args[0];
print('Hello ${name}!');
}

新しい変数はvar 変数名 = 値の形式で宣言します。

main.dart
void main(List<String> args) {
String name = args[0];
print('Hello ${name}!');
}

変数の型を明示的に指定することも可能です。

main.dart
void main() {
dynamic obj = 1;
print(1 + obj);
obj = "1";
print("1" + obj);
}
~$ dart main.dart
2
11

特定の形を期待しない場合はdynamicを型アノテーションとして付けることができます。
どのような型でも再代入可能になる一方で、dynamic obj = 1;をそのまま"1" + objでString型の文字列と結合することはできません。

スタイルガイドによると、ローカル変数には型アノテーション無しのvarを、公開APIの引数等で型アノテーションを書くようにするような用法が推奨されています。

デフォルト値

main.dart
void main() {
String s;
s = 'Hello Dart!';
print(s);
}
~ % dart main.dart
Hello Dart!

値を決めずに変数を宣言することが可能です。

main.dart
void main() {
String s;
print(s);
}
dart main.dart
main.dart:3:11: Error: Non-nullable variable 's' must be assigned before it can be used.
print(s);

変数のデフォルト値はどのような型であってもnullです、nullを許容しない型の変数を代入しないまま参照するとコンパイルエラーが発生します。

定数

hello.dart
void main() {
const String s = "Hello Dart!";
print(s);
}

constを先頭に付与することで定数として宣言することも可能です。
定数の値を変更しようとするとコンパイルエラーCan't assign to the const variableが発生します。

hello.dart
void main() {
final String s = "Hello Dart!";
print(s);
}

Dartにはfinalという修飾子も存在します。
finalを使って宣言した変数を変更しようとするとコンパイルエラーCan't assign to the final variableが発生します。

使い方が似ていますが、constはコンパイル時に評価され、finalは実行段階で評価されるという違いがあります。

例えばコンパイル段階で計算できない実行時の時刻をconstで宣言することはできません。

hello.dart
void main() {
final now = DateTime.now();
print(now);
}

このコードは正常に動作します。

hello.dart
void main() {
const now = DateTime.now();
print(now);
}

constに置き換えると下記のエラーが発生します。

Error compiling to JavaScript:
Warning: Interpreting this as package URI, 'package:dartpad_sample/main.dart'.
lib/main.dart:2:26:
Error: Cannot invoke a non-'const' constructor where a const expression is expected.
const now = DateTime.now();
^^^
Error: Compilation failed.

このような多様な修飾子の存在はコンパイル速度のパフォーマンスチューニングに貢献しますが、若干難易度が高い印象ですね。

Dart 2.12からlate修飾子も追加されています。

late.dart
late String description;

void main() {
description = 'Feijoada!';
print(description);
}

late修飾子には主に2つのメリットがあります。

  • 変数がnullを許容しない
  • 変数を遅延評価することでパフォーマンスを改善する

条件分岐によっては利用しない値をlate修飾子付きで宣言するような使い方が期待できます。

組み込み型

Dartには下記の組み込み型が用意されています。

  • Numbers (int, double)
  • Strings (String)
  • Booleans (bool)
  • Lists (List, also known as arrays)
  • Sets (Set)
  • Maps (Map)
  • Runes (Runes; often replaced by the characters API)
  • Symbols (Symbol)
  • The value null (Null)

特徴的なものはListSetの違いでしょうか。
Listはお馴染みの配列であるのに対し、Setは重複した値を持たないコレクション型です。

String型の変数はシングルクオート、またはダブルクオート文字列を作成することが可能です。
シングルクオートとダブルクオートでは特殊文字のエスケープのルールが異なります。

string.dart
var s1 = 'Single quotes.';
var s2 = "Double quotes.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

トリプルクオートで複数行の文字列を書くことができるのが特徴的です。

string.dart
var s1 = '''
You can create
multi-line strings like this one.
''';
var s2 = """This is also a
multi-line string.""";

DartにおけるSymbolはコンパイル時常数として扱われる、文字列から生成できるデータ型です。
コンパイル時に難読化なれないため、ライブラリのメタデータの整理などに利用されますが。
ユーザー目線ではほぼ使わないようです。

enum

enum.dart
 enum Color {
red,
blue,
green
}

void main() {
print(Color.red);
print(Color.green.index);
print(Color.values);
}
~$ dart enum.dart
Color.red
2
[Color.red, Color.blue, Color.green]

Dartではバージョン1.8からenumがサポートされています。
比較的素朴な仕組みで、インデックスを1から始めたり飛ばしたり、文字列に変換するような機能はありません。

条件分岐

if

hello.dart
void main(List<String> args) {
if (args[0] == "Dart") {
print("Hello Dart!");
} else if (args[0] == "Flutter") {
print("Welcome Flutter!");
} else {
print("bye");
}
}

if文は特に違和感のないシンプルなスタイルです。
JavaScriptでは比較演算子に===がありましたが、現在のDartでは存在しません。(初期のDartには存在していたようです)

switch

hello.dart
void main(List<String> args) {
switch (args[0]) {
case 'Dart':
print('Hello Dart!');
break;
case 'Flutter':
print('Welcome Flutter!');
break;
default:
print('bye');
}
}

switch文も存在します。
各case毎にbreak文を設置しないと次のcaseが実行されるfall through形式でありつつ、case内部で何かを実行したのにbreakしないとコンパイルエラーが発生するという若干癖のある仕様になっています。

つまり下記のコードはbreakが存在しないためエラーになります。

hello.dart
void main(List<String> args) {
switch (args[0]) {
case 'Dart':
print('Hello Dart!');
case 'Flutter':
print('Welcome Flutter!');
break;
default:
print('bye');
}
}
hello.dart:3:5: Error: Switch case may fall through to the next case.
case 'Dart':
hello.dart
void main(List<String> args) {
switch (args[0]) {
case 'Dart':
case 'Flutter':
print('Hello!');
break;
default:
print('bye');
}
}

空のケースのみfall throughすることで複数条件のケース文を実現するような用途が想定されています。

hello.dart
void main(List<String> args) {
switch (args[0]) {
case 'Dart':
print('Hello Dart!');
continue bye;
case 'Flutter':
print('Hello Flutter!');
break;
bye:
default:
print('bye');
}
}

何か処理を実行してからfall throughすることはラベルを利用することで実現できます。

~$ dart hello.dart Dart
Hello Dart!
bye

ループ処理

for

hello.dart
void main(List<String> args) {
for (int i = 0; i < args.length; i++){
print('Hello ${args[i]}!');
}
}

初期化文、条件式、後処理文の古典的なfor文が利用可能です。

forEach

hello.dart
void main(List<String> args) {
args.forEach((String arg){
print('Hello ${arg}!');
});
}

forEach文でループを処理することも可能です。

item in list

hello.dart
void main(List<String> args) {
for (var arg in args){
print('Hello ${arg}!');
}
}

Dartでは上記2種類に加えPython系のテイストを感じる書き方でもループを回すことが可能です。

while

hello.dart
void main(List<String> args) {
var index = 0;
while(true){
if (index >= args.length){
break;
}
print('Hello ${args[index]}!');
index++;
}
}

do while

hello.dart
void main(List<String> args) {
var index = 0;
do {
print('Hello ${args[index]}!');
index++;
} while(index < args.length);
}

while文、do~while分も存在します。

~$ dart hello.dart Dart Flutter
Hello Dart!
Hello Flutter!

関数

hello.dart
void main() {
hello('Dart');
}

void hello(String name) {
print('Hello ${name}!');
}

関数を定義、実行することが可能です。
関数および関数が受け取る引数の型アノテーションはスタイルガイドで推奨されていますが、必須ではありません。

非同期処理

Dartは非同期処理をサポートしています。

async.dart
Future<String> asyncFunction() {
return Future<String>.value("it is Asynchronous processing");
}

void main() {
final resp = asyncFunction();
resp.then((value) => print(value));
}
~$ dart aync.dart
it is Asynchronous processing

Future<型名>という型アサーションで非同期処理を定義できます。
Future社員としては使いこなすモチベーションが無駄に高まります。

async.dart
Future<String> asyncFunction() {
return Future<String>.value("it is Asynchronous processing");
}

void main() async {
final resp = await asyncFunction();
print(resp);
}

Javascriptのように、asyncawaitの構文も利用可能です。

async.dart
void main() async {
final resp = await new Future((){
String s = 'it is Asynchronous processing';
return s;
});
print(resp);
}

上記のように無名関数をそのまま書いていく事も可能です。

クラス

Dartはオブジェクト指向言語であるため、クラスが存在します。

インスタンス変数、コンストラクタ、メソッドを指定して、クラスを定義することが可能です。Personクラスの定義の内部にPerson()を定義する形でコンストラクタを設定します。
コンストラクタの定義には様々なパターンが存在します。

常数コンストラクタ、ファクトリ・コンストラクタなど高度な定義方法もありますが、ひとまずは基礎的なクラス定義を紹介します。

Generative Constructors

hello.dart
void main() {
var yamada = new Person();
yamada.name = 'yamada';
print(yamada.hello());
}

class Person {
var name;

Person(){
this.name = "unknown";
}

hello() {
return 'Hello, My name is ${this.name}';
}
}

コンストラクタでは引数を受け取らず適当な初期値を入力し、インスタンス生成後にメンバ変数を指定するシンプルな方式です。

hello.dart
void main() {
var yamada = new Person('yamada');
print(yamada.hello());
}

class Person {
var name;

Person(String name){
this.name = name;
}

hello() {
return 'Hello, My name is ${this.name}';
}
}
~$ dart hello.dart
Hello, My name is yamada

コンストラクタに引数を定義し、初期化時に受け取る方式です。

Automatic field initialization

hello.dart
void main() {
var yamada = new Person('yamada');
print(yamada.hello());
}

class Person {
var name;

Person(this.name);

hello() {
return 'Hello, My name is ${this.name}';
}
}
~$ dart hello.dart
Hello, My name is yamada

コンストラクタの記述をシンプルに定義可能な方式です。

Named Constructors

hello.dart
void main() {
var yamada = new Person.asConsultant('yamada');
print(yamada.hello());
}

class Person {
var name;
var job;

Person(this.name, this.job);

Person.asConsultant(String name) {
this.name = name;
this.job = 'IT Consultant';
}

hello() {
return 'Hello, My name is ${this.name}, I am ${this.job}';
}
}
~$ dart hello.dart
Hello, My name is yamada, I am IT Consultant

特定条件付きのコンストラクタを定義可能です。

Redirecting Constructors

hello.dart
void main() {
var yamada = new Person.asConsultant('yamada');
print(yamada.hello());
}

class Person {
var name;
var job;

Person(this.name, this.job);

Person.asConsultant(String name) : this(name, "IT Consultant");

hello() {
return 'Hello, My name is ${this.name}, I am ${this.job}';
}
}
~$ dart hello.dart
Hello, My name is yamada, I am IT Consultant

他のコンストラクタの定義を再利用する方式です。

クラスの継承

hello.dart
void main() {
var yamada = new ITConsultant('yamada');
print(yamada.hello());
}

class Person {
var name;

Person(this.name);

hello() {
return 'Hello, My name is ${this.name}';
}
}

class ITConsultant extends Person {
ITConsultant(String name) : super(name);
hello() {
return 'Hello, My name is ${this.name}, I am IT Consultant';
}
}
~$ dart hello.dart
Hello, My name is yamada, I am IT Consultant

他のオブジェクト指向言語同様クラスは継承することができます。

所感

一通り基礎部分をさらってみましたが、豊富な表現力と適度な硬さの両立を目指そうとしている印象を受けました。

その辺りの思想と肌感覚がマッチすればハマる言語かもしれません。

破壊的な変更により今では動かないシンタックスが検索結果の上位に散見しているので、学習障壁を高めてしまうかなと感じました。
今回の連載で有用な記事を増やして盛り上げていきたいですね。

Dart/Flutter連載の1記事目ででした。次は宮崎さんのFlutter Swagger統合です。