😸

Flutter開発で知っておくべきDart知識「JavaScriptを習得した私の差分理解」初見でも初めて会った感じがしない、つまり気が合う

2024/10/25に公開

はじめに

みなさん、こんにちは。
今回はDartの基本文法についてまとめています。
他の言語を習得済みの方の差分理解の手助けになれば幸いです。
(特にJavaScript習得をした方)

DartPadで練習できる

Dartの練習を手軽に行えるDartPadというページがあります。

Dartの特徴

概要

  • 静的型付け言語
  • クラスベースのオブジェクト指向言語
  • null安全
  • 型推論が効く
  • Dartでは1つのファイルをライブラリと呼ぶ
  • Dartの言語仕様は他の多くの言語と似ている
  • Dart3で様々な機能が登場し難しい機能も増えてきた

Dartは静的型付け言語であり、型を活用しております。さらにnull安全にもなっており安全なプログラミングが行えます。型は推論されるため全てを自分で入力する必要はありません。

Dartはクラスベースのオブジェクト指向言語であり、かつ関数の仕組みも持っています。JavaとJavaScriptを合体させたような雰囲気の言語です。
Dartは他の言語と似ているので初見でも理解しやすいですが、Dart3から難しい機能も増えてきており理解するのに時間がかかる言語仕様も出てきました。

Dartでは1つのファイルをライブラリと呼ぶようです。

変数・基本データ型

概要

  • final, constで定数宣言
  • $変数名${式}で文字列に値の埋め込み
  • nullの許可は変数名?
  • nullの可能性がある場合はnullチェックや変数名!で変数を利用
  • nullの可能性がある場合は変数名?.プロパティ(やメソッド())で利用
  • 論理値はbool型

宣言

変数宣言はvarキーワードで行います。Dartには型推論の機能があるので、型を明示的に指定しない場合は値から推論されます。

変数宣言は定数とするとが推奨されています。定数はfinalキーワードで宣言します。

final title = 'abc';
// final String title = 'abc';と同じ

文字列への値の埋め込み

文字列の表現は’(シングルクォート)”(ダブルクォート)の二つがあります。$変数名で変数の値を埋め込むことができます。${式}で演算結果や関数呼び出しの戻り値を埋め込むことができます。

final title = 'abc';
final sentence = 'Title is $title.';
print(sentence); // -> Title is abc.
  
print('Length is ${title.length}.'); // -> Length is 3.
print('Upper is ${title.toUpperCase()}.'); // -> Upper is ABC.
print('1 + 1 =  ${1 + 1}'); // -> 1 + 1 =  2

dynamic型は型チェックされない

TypeScriptにおけるany型のように「なんでも代入可能」を示すのがdynamic型です。コンパイラにチェックされないためどんな値でも代入できますが、その分、型によるエラーに気づかなかったりエディタの補完が効かなかったり不都合でもあります。

// 型チェックされない
dynamic dynamicValue = 'どんな型でも';

jsonDecodeメソッドなどは戻り値にdynamic型が用いられています。その場合は別の型に変換した上で利用すると安全です。またdynamic型はObject?型と相互に変換可能です。

Object? objectValue = 'Hello';
dynamic dynamicValue = objectValue;

nullの代入

Dartはnull安全であるためデフォルトでnullの代入は許可されてません。変数にnullを代入する場合は?マークを型名の後ろに記述します。

処理の戻り値が、想定した値とnullのいずれかになっていることがよくあります。受け取る値がnullかもしれない場合は?マークをつけておきます。

String? nullAble = null;

nullチェックと非nullアサーション

null安全な言語では、nullの可能性がある変数はそのままでは利用できません。if文を使いnullではないことを確認するnullチェックを行うか、変数名の後ろに!をつけてコンパイラにnullではないことを明示する非nullアサーションを指定します。

nullの可能性がある変数からプロパティやメソッドを呼び出す場合は?.で呼び出すことができます。nullじゃない場合は正常にプロパティやメソッドが呼び出され、nullの場合はnullが値として返ってきます。

int? price = 500;
print(price + 10); // コンパイルエラー(it is potentially null)
print(price! + 10); // ! で非nullアサーション

// nullではないことをチェック
if (price != null) {
  print(price + 10);
}

print(price?.isEven); // -> priceがnullならnull、500ならtrue

制御構文

概要

  • for-in文で要素数分のループ
  • switch式で定数やパターンに応じて値を取り出せる

for-in文

Dartの繰り返し構文は通常のfor文ももちろんあり、要素数を基準に繰り返すfor-in文も用意されています。

// Listの宣言
final colors = ['red', 'blue', 'green'];

// 要素数分ループ
for (final color in colors) {
  print(color);
}

switch式

Dart3(2023/5)からswitch式が登場しました。switch文のように値を基準に判断し、該当するケースの値を返却します。値の返却があるというのがポイントですね。まるでReactにおけるReducer関数のようですね。あちらはActionTypeに応じてStateを返しているので、なんだか似ている気がしてきます。

書き方は従来のswitchに似ています。各ケースにのパターンには値を指定し、合致する場合に返す値は=> の右側に値そのものか式として記述します。_をパターンに指定するとデフォルトケースとして扱われます。

final 変数 = switch(判断に使う値){=> 返す値や式,=> 返す値や式,
 _ => デフォルトの値
};
final value = 1;
final result = switch(value){
    1 => value * 10,
    2 => value - 10,
    _ => value
};

String型の値を扱う例です。デフォルトケースを配置しないとコンパイルエラーになります。

final item = 'pencil';

// 引数に渡された値にマッチする値を返す
final price = switch (item) {
  'pencil' || 'pen' => 120, // or(やand)も指定可能
  'notebook' => 200,
  _ => 0, // デフォルトケース
};
print(price); // -> 120

enumの値を扱う例です。全てのenum定数を網羅しないとコンパイルエラーになります。

enum Title {
  titleA, titleB, titleC
}

void main() {
  final selected = Title.titleA;

  // 全ての列挙定数を網羅していないとコンパイルエラー
  final price = switch (selected) {
    Title.titleA => 1000,
    Title.titleB => 2000,
    Title.titleC => 3000,
  };
  print(price); // -> 1000
}

sealedクラスのサブクラスを扱う例です。全てのサブクラスを網羅しないとコンパイルエラーになります。

sealed class Food {}
class Rice extends Food {}
class Blead extends Food {}

void main() {
  final selected = Blead();

  // 全てのsealedクラスのサブクラスを網羅していないとコンパイルエラー
  final price = switch (selected) {
    Rice() => 1000,
    Blead() => 2000,
  };
  print(price); // -> 2000
}

リスト

概要

  • 配列に相当するListクラスがある
  • 要素の型は推論され、異なる型を入れようとするとコンパイルエラー
  • []で宣言、addで追加
  • whereメソッドで抽出
  • mapメソッドで加工・変換
  • forEachメソッドで繰り返し
  • mapメソッド、whereメソッドは戻り値がIterable型
  • toListでList化

Dartには配列に相当するList型があります。Listには要素を操作するためのメソッドが多数用意されていますが、よく使いそうなものをピックアップします。

宣言

まず宣言は[]で行うことができます。要素の型は推論され型の違う値は混在できないようになります。なおListを生成するには他にも様々なコンストラクタが利用できます。

// 宣言
final items = [5, 10, 15];

追加(addメソッド)

要素の追加はaddメソッドで行います。引数の要素をListの末尾に追加します。追加する要素の型がListの要素と異なる場合はコンパイルエラーになります。

// add
items.add(20);
print(items); // -> [5, 10, 15, 20]
//   items.add('abc'); // 型不一致でコンパイルエラー

抽出(whereメソッド)

where メソッドで条件に合う要素のみを抽出できます。引数に渡した処理の結果がtrueとなる要素を抽出します。結果はIterable型でList型ではありません。

// where
final evenList = items.where((item) => item.isEven);
print(evenList); // -> (10, 20)

加工・変換(mapメソッド)

map メソッドで要素を加工・変換することができます。引数に渡した処理の結果が加工後の要素です。このメソッドも結果はIterable型でList型ではありません。

// map
final doubleList = items.map((item) => item * 2);
print(doubleList); // -> (10, 20, 30, 40)

繰り返し(forEachメソッド)

forEachメソッドは要素を順番に扱って繰り返し処理します。mapとの違いは戻り値がvoidで結果を返さないことです。よって引数に渡す処理も戻り値のない処理を指定します。

// forEach
items.forEach((item) => print(item)); // -> 5 10 15 20

メソッドチェーンで記述(where map forEach)

where メソッドやmapメソッドは戻り値にIterable型の値を返します。そしてそのIterable型からメソッドを繋げて呼び出すことができます。

// where map forEachを繋げて
items
    .where((item) => item.isEven)
    .map((item) => item * 2)
    .forEach((item) => print(item));

Iterable型をList型に変換(toListメソッド)

where メソッドやmapメソッドは戻り値がIterable型です。FlutterのWidgetには引数にList型を要求するものがありますが、これらのメソッドで変換したIterable型をそのまま渡せません。そこでtoListメソッドを使うことでList型に変換することができます。

// IterableをList型に(doubleListはmapの戻り値を代入した変数)
doubleList.toList();

マップ

概要

  • key-value形式のデータのまとまりの表現
  • JSのオブジェクトリテラルに相当
  • key-valueの型は推論され、異なる型を入れようとするとコンパイルエラー
  • {}で宣言
  • [key]を指定し参照と追加・更新
  • JSONと相互変換できるメソッドがある
  • 通信で扱うデータ(JSON)をMapに変換して扱う

key-value形式のデータのまとまりを表すMap型があります。key-valueの型は値から推論され、異なる型の値を入れようとするとコンパイルエラーになります。

宣言

DartにはJavaScriptのようなオブジェクトリテラルはありませんが、Mapがその代わりになります。JavaScriptのオブジェクトのようにkey-valueのvalueに様々な型の値を扱いたい場合は、Map<String, Object>型とすることで可能です。

宣言は{}で行います。キーと値は:で区切ります。またコンストラクタも多数用意されており、それらを使っての宣言も可能です。

// 宣言 Map<String, Object>
final item = {
  'name': 'abc',
  'price': 2000
};

追加や取得

要素の追加や取得は[キー]で行います。値を代入すれば更新になります。存在しないキーを指定して値を代入すると値を追加できます。

// 追加
item['title'] = 'def';

// 取得
print(item['name']); // -> abc

JSONとの相互変換

Map は通信で扱うJSONデータと相互変換して扱うことがあります。jsonEncodeメソッドはMapからJSONに変換します。jsonDecodeは逆にJSONをMapに変換します。これらのメソッドは標準ライブラリのdart:convertにあります。

// JSONの変換機能を持つ標準ライブラリ
import 'dart:convert';

// MapからJSONに変換
final json = jsonEncode(item);

// JSONからMapに変換
final data = jsonDecode(json);

サンプルコード全体像

// JSONの変換機能を持つ標準ライブラリ
import 'dart:convert';

void main() {
  
  // 宣言 Map<String, Object>
  final item = {
    'name': 'abc',
    'price': 2000
  };
  
  // 追加
  item['title'] = 'def';
  
  // 取得
  print(item['name']); // -> abc
  
  // MapからJSONに変換
  final json = jsonEncode(item);
  
  // JSONからMapに変換
  final data = jsonDecode(json);
}

関数

概要

  • トップレベルで関数を宣言できる
  • 関数は値として扱うことができる
  • アロー演算子を使った省略表現が存在する
  • 引数の仕様(省略可能引数、名前付き引数と必須化)

宣言

Dartは関数を宣言でき、JavaScriptと同じように関数を値として扱うことができます。宣言時に引数の型は省略することができますが、記述しておいた方が安全でしょう。

// 関数宣言
String createTitle(String name) {
    return name + 'def';
}

// 呼び出し
print(createTitle('abc')); // -> abcdef

省略表現

関数宣言には=>を使った省略表現があります。処理が1行のみの場合は{}returnを省略することができます。

// 関数宣言(省略表現)
void showData() => print('data');
int calc(int x) => x + 5;

省略可能引数

Dartの関数には「省略可能引数」という仕様があります。引数の宣言を[]で囲むことで省略可能となります。省略した場合はnullが渡されるので、型名に?を付与しnull許容にします。引数が渡されなかった場合にnullではなくデフォルト値を設定することもできます。

// 省略可能引数
void doSome1(String name, [int? age]) => print('did');

// 省略可能引数(デフォルト値)
void doSome2(String name, [int age = 20]) => print('did');

名前付き引数

引数の仕様に「名前付き引数」という表現もあります。実行時に名前を指定して引数を渡すことができるので可読性の向上に役立ちます。{}で囲って引数リストの末尾に宣言します。

名前付き引数はデフォルトで省略可能引数という扱いになります。必須にするにはrequiredキーワードをつけます。

呼び出す際は、名前付き引数は順番を変えても問題ありません。

// 名前付き引数(省略可能)
void doSome3(String name, {int? age}) => print('did');
doSome3('abc',age: 20); // age引数は名前付きで値を設定
doSome3('abc'); // age引数は省略可能(nullになる)

// 名前付き引数(必須)
void doSome4(String name, {required int age}) => print('did');
doSome4('abc',age: 20); // age引数は必須
doSome4(age: 20,'abc'); // 引数の順番を変えてもOK

デバッグ

概要

  • print関数、debugPrint関数でコンソール出力
  • runtimeTypeで型を確認
  • assert関数で中断

デバッグで使える機能をいくつかピックアップしました。

print関数

コンソール出力はprint関数を使います。

print('abc');  // -> abc

debugPrint関数

FlutterではdebugPrint関数という機能もあります。こちらは長いメッセージでも欠損なく表示できます。一方でprint関数はあまりに長いメッセージは欠損してしまうようです。

import 'package:flutter/foundation.dart';

debugPrint('def'); // -> def

型情報を取得するruntimeType

値の型情報を取得するにはruntimeTypeを参照します。

print('abc'.runtimeType);  // -> String

デバッグ時に処理を中断する機能としてassert関数があります。引数がfalseだった場合に処理を中断します。

assert(false); // 中断

おわりに

Dartの言語使用や基本文法は割ととっつき易いと思いました。
一方でここに掲載していない複雑な文法も存在しています。そういった複雑だったり新しめな文法はあまり活用されていないのかなあという印象を持ちましたが、引き続き学習をしていきます。

Discussion