初心者向け Dart 入門:これ一つで基本がわかる
はじめに
プログラミングの世界へようこそ!そして、Dartという素晴らしい言語に興味を持っていただき、ありがとうございます。この記事は、「プログラミングは初めて」「Dartって聞いたことあるけど、何ができるの?」「Flutter開発を始めたいけど、まずDartを学びたい」という全くの初心者の方を対象としています。
Dartは、Googleによって開発された、クライアント最適化のためのプログラミング言語です。特に、モバイルアプリ開発フレームワークであるFlutterの公式言語として採用されたことで、近年急速に注目を集めています。Flutterを使えば、一つのコードベースでiOSとAndroidの両方のアプリを開発できるため、Dartの習得は非常に価値があります。
しかし、Dartの魅力はFlutterだけにとどまりません。Webフロントエンド、Webバックエンド、デスクトップアプリケーション、さらにはコマンドラインツールまで、様々なプラットフォームでの開発が可能です。JavaScriptやJava、C#など、他の多くの言語の良い部分を取り入れつつ、独自の強力な機能を備えており、学習しやすい構文を持っています。
この記事では、Dartの環境構築から始め、変数、型、演算子、制御フローといったプログラミングの基本的な要素から、関数、クラス(オブジェクト指向)、非同期処理、例外処理といったより高度な概念まで、Dartの基礎を網羅的に解説します。これを読めば、Dartの基本をしっかりと理解し、次のステップ(例えばFlutterでの開発)に進むための強力な土台を築くことができるでしょう。
さあ、一緒にDartの学習を始めましょう!
なぜDartを学ぶのか?(初心者にとってのメリット)
- Flutterとの連携: Flutter開発をしたいなら、Dartは必須です。FlutterはDartで記述されており、Dartの知識がそのままFlutterの理解につながります。
- 学習しやすい構文: Cスタイル言語(Java, C#, JavaScriptなど)に慣れている人には馴染みやすい構文です。そうでなくても、シンプルで読みやすいコードを書くことができます。
- 効率的な開発: Dartはコンパイルされたコードを生成するため、実行速度が速いです。また、ホットリロード機能(Flutterなど)は開発効率を劇的に向上させます。
- Null安全: DartはNull Safetyを強力にサポートしており、実行時エラーの原因となりやすい「NullPointerException」のような問題をコンパイル時に防ぐことができます。これは、特に初心者にとってバグを減らす上で非常に役立ちます。
- 多様なプラットフォーム: モバイルだけでなく、Web、デスクトップ、サーバーサイドなど、幅広い分野で利用可能です。
環境構築について
Dartのコードを実行するには、Dart SDKが必要です。SDKのインストール方法はオペレーティングシステムによって異なりますが、公式サイト(https://dart.dev/get-dart)に詳細な手順が記載されています。
また、コードを書くための開発環境(IDEやコードエディタ)も必要です。初心者には、以下のいずれかがおすすめです。
- VS Code (Visual Studio Code): DartおよびFlutterの拡張機能をインストールすることで、強力なコード補完、デバッグ、構文強調表示などが利用できます。無料で多機能なので、多くの開発者に利用されています。
- Android Studio / IntelliJ IDEA: GoogleおよびJetBrainsが開発する高機能なIDEです。こちらもDart/Flutterプラグインがあり、統合された開発環境を提供します。特にAndroid StudioはFlutter開発の公式推奨IDEの一つです。
- DartPad: ウェブブラウザ上でDartコードを書いてすぐに実行できるオンラインツールです。https://dartpad.dev/ 環境構築なしで試すことができるため、この記事のコード例を試すのに最適です。
この記事では、DartPadを使うか、ローカル環境にSDKをインストールしてVS Codeなどでdart run
コマンドを使って実行することを想定しています。環境構築の詳細な手順はここでは割愛し、公式サイトなどを参照してください。
Dartの基本
まずはDartの基本的な構成要素から見ていきましょう。
最初のプログラム:Hello, World!
プログラミング学習の伝統として、まずは「Hello, World!」と表示するプログラムから始めましょう。
dart
void main() {
print('Hello, World!');
}
この短いコードが、Dartプログラムの基本構造を示しています。
void
: この関数が何も値を返さないことを示します。main()
: これは特別な関数です。Dartプログラムは、このmain()
関数から実行が開始されます。全ての実行可能なDartプログラムはmain()
関数を持っている必要があります。{ ... }
: これらはブロック(またはボディ)を示します。main
関数の処理内容がこのブロックの中に記述されます。print('Hello, World!');
: これは、指定した文字列をコンソール(またはターミナル、DartPadの出力領域など)に表示するための関数です。'Hello, World!'
: これは文字列リテラルです。シングルクォート('
)またはダブルクォート("
)で囲むことで文字列を表します。;
: これは文(Statement)の終わりを示します。多くの文はセミコロンで終わる必要があります。
このコードをDartPadに貼り付けて「Run」ボタンを押すか、ローカル環境で.dart
ファイルに保存してdart run ファイル名.dart
と実行してみてください。「Hello, World!」と表示されるはずです。
コメントの書き方
コードの中にコメントを書くことは非常に重要です。コメントはプログラムの実行には影響しませんが、コードの意図や動作を説明するために使われ、自分や他の開発者がコードを理解するのを助けます。
Dartには主に2種類のコメントがあります。
-
単一行コメント:
//
の後に続くその行の終わりまでがコメントになります。dart
// これは単一行コメントです
void main() {
print('Hello, World!'); // 行の最後にコメントを書くこともできます
} -
複数行コメント(ブロックコメント):
/*
から始まり*/
で終わるまでの間がコメントになります。複数行にわたるコメントを書くのに便利です。dart
/*
これは
複数行コメントです。
*/
void main() {
print('Hello, World!');
}
また、ドキュメンテーションコメントと呼ばれる特別なコメント形式もありますが、これは少し高度なトピックなので、ここでは基本的なコメントの書き方を押さえておきましょう。
変数の宣言と型
変数とは、値を格納するための「箱」のようなものです。Dartでは、変数を使う前に宣言する必要があります。
Dartは静的型付け言語ですが、変数の型を明示的に指定することも、コンパイラに推論させることも可能です。
var
キーワード
最も一般的な変数の宣言方法の一つに var
キーワードを使う方法があります。var
を使うと、変数の初期値からコンパイラが自動的に型を推論してくれます。一度型が決定されると、その変数に異なる型の値を再代入することはできません。
“`dart
void main() {
var name = ‘Alice’; // コンパイラはString型と推論
var age = 30; // コンパイラはint型と推論
var height = 1.75; // コンパイラはdouble型と推論
print(name);
print(age);
print(height);
// name = 123; // エラー!String型変数にint型を代入しようとしています
}
“`
var
は便利ですが、変数の型が分かりにくくなる場合もあるため、状況に応じて使い分けることが推奨されます。
明示的な型指定
変数の型を明示的に指定して宣言することもできます。これはコードの可読性を高め、意図を明確にする上で役立ちます。
“`dart
void main() {
String name = ‘Bob’;
int age = 25;
double weight = 65.5;
bool isStudent = true;
print(name);
print(age);
print(weight);
print(isStudent);
// age = 25.5; // エラー!int型変数にdouble型を代入しようとしています
}
“`
明示的な型指定は、特にAPIから受け取ったデータや、変数の型を厳密に制御したい場合に有効です。
dynamic
キーワード
dynamic
キーワードを使って変数を宣言すると、その変数にはあらゆる型の値を代入できます。また、実行時にその変数の型が変化する可能性があります。ただし、dynamic
を多用すると、型の恩恵(コンパイル時エラーチェックなど)が受けられなくなり、実行時エラーのリスクが高まるため注意が必要です。
“`dart
void main() {
dynamic value = ‘hello’;
print(value); // 出力: hello
value = 123;
print(value); // 出力: 123
value = true;
print(value); // 出力: true
}
“`
dynamic
は、JSONデータのように事前に型がわからないデータを扱う場合などに限定的に使用するのが一般的です。
組み込み型
Dartにはいくつかの基本的な組み込み型があります。
-
数値 (Numbers):
int
: 整数(小数点以下のない数値)を表します。サイズ制限はありません(プラットフォームによりますが、一般的に非常に大きな値を扱えます)。double
: 浮動小数点数(小数点以下の数値を持つ数値)を表します。IEEE 754標準の64ビット倍精度浮動小数点数です。
dart
int count = 10;
double price = 19.99;
num number = 5; // int または double を格納できる型
number = 10.5; -
文字列 (Strings):
String
: 一連の文字を表します。シングルクォートまたはダブルクォートで囲みます。複数行の文字列はトリプルクォート('''
または"""
)で囲みます。
“`dart
String singleQuote = ‘これはシングルクォートの文字列です。’;
String doubleQuote = “これはダブルクォートの文字列です。”;
String multiLine = ”’
これは
複数行の
文字列です。
”’;// 文字列の連結
String greeting = ‘Hello’ + ‘ World!’; // または ‘Hello’ ‘ World!’
print(greeting);// 文字列補間 (String Interpolation)
String name = ‘Alice’;
int age = 30;
String info = ‘名前: $name, 年齢: $age’; // $変数名 で変数の値を埋め込める
String calculatedInfo = ‘次の誕生日には ${age + 1} 歳になります。’; // ${式} で式の評価結果を埋め込める
print(info);
print(calculatedInfo);
“` -
真偽値 (Booleans):
bool
: 真偽値を表します。値はtrue
またはfalse
のいずれかです。
“`dart
bool isAdult = age >= 20;
bool canDrive = isAdult && true; // 論理演算子については後述print(isAdult);
print(canDrive);
“` -
リスト (Lists):
List
: 複数の値を順序付けして格納できるコレクションです。他の言語では「配列」と呼ばれることもあります。<型>
でリストが保持する要素の型を指定できます(ジェネリクス)。
“`dart
Listnumbers = [1, 2, 3, 4, 5];
Listfruits = [‘Apple’, ‘Banana’, ‘Orange’];
ListmixedList = [1, ‘hello’, true]; // 異なる型の要素を格納 print(numbers[0]); // 最初の要素 (インデックスは0から始まる)
print(fruits.length); // 要素数
fruits.add(‘Grape’); // 要素の追加
print(fruits);
fruits.remove(‘Banana’); // 要素の削除
print(fruits);// リストリテラル
Listlist1 = [1, 2, 3];
Listlist2 = […list1, 4, 5]; // スプレッド演算子 (…) でリストを展開して結合
print(list2);
“` -
マップ (Maps):
Map
: キーと値のペアを格納できるコレクションです。キーは一意である必要があります。<キーの型, 値の型>
で型を指定できます。他の言語では「辞書」や「連想配列」と呼ばれることもあります。
“`dart
Mapcapitals = {
‘Japan’: ‘Tokyo’,
‘USA’: ‘Washington D.C.’,
‘China’: ‘Beijing’,
};Map
user = {
‘name’: ‘Alice’,
‘age’: 30,
‘isStudent’: false,
};print(capitals[‘Japan’]); // キーを指定して値を取得
print(user[‘age’]);user[‘city’] = ‘New York’; // 新しいキーと値を追加または更新
print(user);print(user.containsKey(‘name’)); // キーの存在チェック
print(user.length); // ペアの数
“` -
セット (Sets):
Set
: 重複しない値のコレクションです。順序は保証されません。<型>で要素の型を指定できます。
“`dart
SetuniqueNumbers = {1, 2, 3, 4, 5, 1, 2}; // 重複は無視される
print(uniqueNumbers); // 出力: {1, 2, 3, 4, 5}uniqueNumbers.add(6); // 要素の追加
uniqueNumbers.remove(3); // 要素の削除
print(uniqueNumbers);print(uniqueNumbers.contains(4)); // 要素の存在チェック
``
Set
空のSetを宣言する場合は、emptySet = {}; のように型を指定しないと、空のMap
{}と区別がつかないため注意が必要です。
Set()` コンストラクタを使うこともできます。 -
Null:
null
: 値がないことを示します。DartのNull Safetyによって、Nullを許容するかどうかが厳密に管理されます。
定数 (final
, const
)
変更されることのない値を保持するために、定数を使用します。Dartには final
と const
という2つのキーワードがあります。
-
final
: 変数を一度だけ初期化できることを示します。コンパイル時には値が確定していなくても構いませんが、初めて値が代入された後は変更できません。実行時に決定される定数に使います。“`dart
void main() {
final String playerName = ‘Messi’; // 明示的な型指定
final score = 100; // var のように型推論されるprint(playerName);
print(score);// playerName = ‘Ronaldo’; // エラー!final変数は再代入できません
// score = 200; // エラー!final変数は再代入できません// final変数は実行時に決定される値を保持できる
final DateTime now = DateTime.now(); // プログラム実行時の現在時刻
print(now);
// now = DateTime.now(); // エラー
}
“` -
const
: コンパイル時に値が確定している定数に使います。つまり、リテラル値や、他のconst
定数、またはコンパイル時に評価できる式の結果である必要があります。const
は単なる値だけでなく、const
キーワードで修飾されたコレクション(リストやマップなど)全体も不変にします。“`dart
void main() {
const PI = 3.14159; // 明示的な型指定は省略しても良い
const speedOfLight = 299792458;print(PI);
print(speedOfLight);// PI = 3.0; // エラー!const変数は再代入できません
// const リストやマップは内容も不変
const ListconstantList = [1, 2, 3];
print(constantList);
// constantList.add(4); // エラー!Unmodifiable list// constキーワードは値を不変にするためにも使える
var numbers = const [1, 2, 3]; // numbers変数は再代入可能だが、参照しているリストは不変
print(numbers);
// numbers.add(4); // エラー!Unmodifiable list
numbers = [4, 5, 6]; // numbers変数自体には別のリストを再代入できる
print(numbers);
}
“`
const
はfinal
よりも強い制約で、コンパイル時の最適化などに利用されます。パフォーマンスが重視される場合(特にFlutterのウィジェットなど)によく使われます。初心者のうちは、実行後に値が変わらないものはfinal
、コンパイル時点で値が決まっているものはconst
と覚えておくと良いでしょう。
Null安全 (Null Safety) の基本
Dart 2.12以降、Null Safetyがデフォルトで有効になっています。これは、Null参照による実行時エラー(いわゆる「ヌルポ」)を防ぐための強力な機能です。
Null Safetyの世界では、変数はデフォルトではNullを持つことができません。もしNullを許容したい場合は、型の後ろに ?
をつけて明示的に示す必要があります。
“`dart
void main() {
// String name = null; // エラー!String型はNullを許容しない
String? nullableName = null; // 型名の後ろに ? をつけることでNullを許容
print(nullableName); // 出力: null
nullableName = ‘Alice’; // Null以外の値も代入できる
print(nullableName); // 出力: Alice
String nonNullableName = ‘Bob’;
// nonNullableName = null; // エラー!Nullを許容しない変数にNullは代入できない
}
“`
Null Safetyによって、Nullの可能性がある変数を使う際には、その変数が本当にNullでないかを確認したり、Nullだった場合の代替手段を提供したりすることがコンパイラによって強制されます。
Nullの可能性のある変数を安全に扱うための主な方法:
-
Nullチェック:
if
文などを使って変数がNullでないことを確認します。“`dart
String? message = getMessage(); // Nullを返す可能性がある関数if (message != null) {
print(message.length); // Nullチェックされているので安全にアクセスできる
} else {
print(‘メッセージがありません。’);
}String nonNullableMessage = message!; // 危険な Null assertion operator (!)
// message が Null の場合にここで実行時エラーが発生します。
// message が Null でないことを「絶対に」保証できる場合のみ使用してください。
“` -
Null条件演算子 (
?.
): オブジェクトがNullでない場合にのみメンバーにアクセスします。オブジェクトがNullの場合は、式全体の結果がNullになります。“`dart
String? nullableText = null;
// print(nullableText.length); // Nullの可能性がある変数に直接アクセスするとエラー (あるいは警告)int? length = nullableText?.length; // nullableText が Null なら length は Null になる
print(length); // 出力: nullnullableText = ‘hello’;
length = nullableText?.length;
print(length); // 出力: 5
“` -
Null合体演算子 (
??
): 式がNullだった場合に、代替の値を指定します。“`dart
String? userName = null;
String displayUser = userName ?? ‘ゲスト’; // userName が Null なら ‘ゲスト’ を使う
print(displayUser); // 出力: ゲストuserName = ‘Alice’;
displayUser = userName ?? ‘ゲスト’;
print(displayUser); // 出力: Alice (userName が Null でないのでそのまま使う)
“` -
Null合体代入演算子 (
??=
): 変数がNullの場合にのみ、値を代入します。“`dart
String? serverAddress;
serverAddress ??= ‘192.168.1.1’; // serverAddress が Null なら代入
print(serverAddress); // 出力: 192.168.1.1serverAddress ??= ‘10.0.0.1’; // serverAddress は Null でないので代入されない
print(serverAddress); // 出力: 192.168.1.1
“`
Null Safetyは最初は少し戸惑うかもしれませんが、慣れるとプログラムの堅牢性を大きく高める強力な機能です。Nullableな変数とNon-nullableな変数の区別を意識することが重要です。
演算子
Dartには、値に対して様々な操作を行うための演算子が豊富に用意されています。
算術演算子
数値に対して算術演算を行います。
+
: 加算-
: 減算*
: 乗算/
: 除算(結果はdouble)~/
: 商(整数除算)%
: 剰余(割り算の余り)
“`dart
int a = 10;
int b = 3;
print(a + b); // 13
print(a – b); // 7
print(a * b); // 30
print(a / b); // 3.333… (double)
print(a ~/ b); // 3 (int)
print(a % b); // 1
“`
関係演算子
二つの値を比較し、真偽値 (bool
) を返します。
==
: 等しい!=
: 等しくない>
: より大きい<
: より小さい>=
: より大きいか等しい<=
: より小さいか等しい
“`dart
int x = 10;
int y = 20;
print(x == y); // false
print(x != y); // true
print(x > y); // false
print(x < y); // true
print(x >= 10); // true
print(y <= 20); // true
“`
論理演算子
真偽値に対して論理演算を行い、真偽値を返します。
&&
: 論理AND (両方がtrueならtrue)||
: 論理OR (どちらかがtrueならtrue)!
: 論理NOT (trueならfalse、falseならtrue)
“`dart
bool isSunny = true;
bool isWarm = false;
print(isSunny && isWarm); // false
print(isSunny || isWarm); // true
print(!isSunny); // false
“`
代入演算子
変数に値を代入します。算術演算子と組み合わせて使うことも多いです。
=
: 代入+=
: 加算して代入 (例:x += 1
はx = x + 1
と同じ)-=
: 減算して代入*=
: 乗算して代入/=
: 除算して代入%=
: 剰余を代入??=
: Null合体代入 (上記Null Safetyのセクションで解説)
“`dart
int score = 100;
score += 50; // score は 150 になる
print(score);
score -= 20; // score は 130 になる
print(score);
“`
インクリメント/デクリメント演算子
変数の値を1増やしたり減らしたりします。
++
: インクリメント (1増やす)--
: デクリメント (1減らす)
これらの演算子は、変数の前 (++variable
, --variable
) に置くか、後ろ (variable++
, variable--
) に置くかで評価されるタイミングが異なります。
- 前置 (
++variable
,--variable
): 変数の値が変更されてから、式全体の値としてその新しい値が使われます。 - 後置 (
variable++
,variable--
): 式全体の値としては変更前の変数の値が使われ、その式が評価された後に変数の値が変更されます。
“`dart
int counter = 0;
int result1 = ++counter; // counter は 1 になり、result1 も 1 になる
print(‘counter: $counter, result1: $result1’); // counter: 1, result1: 1
int anotherCounter = 0;
int result2 = anotherCounter++; // anotherCounter は 1 になるが、result2 は 0 になる
print(‘anotherCounter: $anotherCounter, result2: $result2’); // anotherCounter: 1, result2: 0
“`
Null条件演算子 (?.
, ??
, ??=
)
これらの演算子はNull Safetyのセクションで詳しく解説しました。Nullableな値を安全に扱うために非常に重要です。
?.
: Null許容なオブジェクトのメンバーアクセス。Nullなら式全体がNull。??
: Null合体。式がNullなら代替値。??=
: Null合体代入。変数がNullなら代入。
その他の演算子
-
型テスト演算子:
is
: オブジェクトが指定された型であるかチェックします。is!
: オブジェクトが指定された型でないかチェックします。
“`dart
dynamic value = ‘hello’;
print(value is String); // true
print(value is int); // false
print(value is! int); // trueif (value is String) {
// value は String として扱えるようになる(型プロモーション)
print(value.length); // 安全にStringのメンバーにアクセス
}
“` -
型キャスト演算子:
as
: オブジェクトを指定された型にキャストします。キャストできない場合は実行時エラー(CastError
)が発生します。
“`dart
dynamic data = ‘123’;
String strData = data as String; // dataをStringとして扱う
print(strData.length); // 3dynamic anotherData = 123;
// String anotherStrData = anotherData as String; // 実行時エラー!intはStringにキャストできない
``
asは型が保証されている場合に使いますが、保証できない場合は型テスト演算子
is`で確認してから使う方が安全です。 -
カスケード表記 (
..
): 同じオブジェクトに対して複数の操作を連続して行うことができます。メソッドチェーンのように見えますが、メソッドチェーンが各メソッドの戻り値に対して次の操作を行うのに対し、カスケード表記は最初のオブジェクトに対して全ての操作を行います。“`dart
class MyObject {
String name = ”;
int age = 0;void setName(String n) { name = n; }
void setAge(int a) { age = a; }
void display() { print(‘Name: $name, Age: $age’); }
}void main() {
MyObject obj = MyObject();// カスケード表記を使わない場合
// obj.setName(‘Alice’);
// obj.setAge(30);
// obj.display();// カスケード表記を使う場合
obj
..setName(‘Bob’) // objに対してsetName()を呼び出し
..setAge(25) // objに対してsetAge()を呼び出し
..display(); // objに対してdisplay()を呼び出し// コンストラクタと組み合わせる
var anotherObj = MyObject()
..setName(‘Charlie’)
..setAge(20);
anotherObj.display();
}
“`
カスケード表記を使うと、特にBuilderパターンなどで複数のセッターメソッドを呼び出す場合にコードがすっきりします。 -
スプレッド演算子 (
...
): リストやマップの要素を別のリストやマップの中に展開します。Null Safety対応版としてNullを許容するスプレッド演算子...?
もあります。“`dart
Listlist1 = [1, 2];
Listlist2 = [3, 4];
ListcombinedList = […list1, …list2, 5];
print(combinedList); // 出力: [1, 2, 3, 4, 5]Map
map1 = {‘a’: ‘1’, ‘b’: ‘2’};
Mapmap2 = {‘c’: ‘3’, ‘d’: ‘4’};
MapcombinedMap = {…map1, …map2, ‘e’: ‘5’};
print(combinedMap); // 出力: {a: 1, b: 2, c: 3, d: 4, e: 5}List
? nullableList = null;
ListsafeCombinedList = [1, 2, …?nullableList, 3]; // nullableListがNullでもエラーにならない
print(safeCombinedList); // 出力: [1, 2, 3]
“`
FlutterのWidgetツリーを構築する際など、リストの要素を別のリストに追加する場合によく使われます。
制御フロー
プログラムの実行順序を制御するための構文です。
条件分岐 (if
, else if
, else
)
条件が真か偽かによって実行するコードを変えます。
“`dart
int score = 75;
if (score >= 80) {
print(‘素晴らしい成績です!’);
} else if (score >= 60) {
print(‘合格です。’);
} else {
print(‘もう少し頑張りましょう。’);
}
// 条件式はbool型である必要がある
bool isLoggedIn = true;
if (isLoggedIn) {
print(‘ログインしています。’);
}
“`
スイッチ文 (switch
, case
, break
, default
)
一つの変数の値に基づいて、複数の異なる処理の中から一つを実行します。
“`dart
String command = ‘OPEN’;
switch (command) {
case ‘OPEN’:
print(‘ファイルを開いています…’);
break; // 各caseブロックの終わりには break が必要 (明示的にfall-throughしない限り)
case ‘SAVE’:
print(‘ファイルを保存しています…’);
break;
case ‘CLOSE’:
print(‘ファイルを閉じています…’);
break;
default: // どのcaseにも一致しなかった場合
print(‘不明なコマンドです。’);
}
“`
Dart 2.12以降のNull Safety環境では、非Nullのenumやsealedクラスに対してswitchを使うと、全てのケースを網羅しない場合にコンパイラ警告(またはエラー)が出ます。これはコードの安全性を高めるのに役立ちます。
Dart 3からは、より強力なパターンマッチングが導入されました。switch
文もこれに対応し、より柔軟な条件指定や値の抽出が可能になりました。
“`dart
// Dart 3以降のパターンマッチングを利用したswitchの例
Object value = [1, 2, 3];
switch (value) {
case int i: // int型にマッチし、変数iに値をバインド
print(‘整数です: $i’);
case String s: // String型にマッチし、変数sに値をバインド
print(‘文字列です: “$s”‘);
case List
print(‘3つ以上の整数を含むリストです: $list’);
case List
print(‘その他のリストです: $list’);
case {‘name’: String name, ‘age’: int age}: // マップのパターンマッチング
print(‘ユーザーオブジェクトです: 名前 $name, 年齢 $age’);
default:
print(‘不明な型です。’);
}
``
switch`文の使い方を理解しておけば十分です。興味があれば公式ドキュメントなどを参照してください。
パターンマッチングは強力ですが、ここでは基本的な
ループ (for
, while
, do-while
)
同じ処理を繰り返し実行します。
for
ループ
回数が決まっている繰り返しや、リストなどのコレクションを順番に処理する場合によく使われます。
“`dart
// 基本的なforループ
for (int i = 0; i < 5; i++) {
print(‘繰り返し処理 $i’);
}
// コレクションを使ったfor-inループ
List
for (String fruit in fruits) {
print(fruit);
}
“`
while
ループ
条件が真の間、繰り返し処理を実行します。繰り返し回数が事前に決まっていない場合に便利です。
dart
int count = 0;
while (count < 5) {
print('カウント: $count');
count++; // 条件を変化させるのを忘れないように!
}
do-while
ループ
条件を判定する前に、まず一度だけ繰り返し処理を実行します。その後は条件が真の間、繰り返し処理を実行します。
dart
int i = 0;
do {
print('一度は必ず実行されます。');
i++;
} while (i < 0); // 条件は偽だが、一度は実行される
繰り返し処理の制御 (break
, continue
)
ループの実行中に、繰り返し処理のフローを変更することができます。
-
break
: 現在の最も内側のループ(またはswitch
文)を終了し、次の文に移ります。dart
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // iが5になったらループを終了
}
print(i);
}
// 出力: 0, 1, 2, 3, 4 -
continue
: 現在の繰り返し処理の残りの部分をスキップし、次の繰り返し処理に進みます。dart
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // iが偶数ならスキップして次の繰り返しへ
}
print(i);
}
// 出力: 1, 3, 5, 7, 9 (奇数のみ表示)
関数
関数は、特定のタスクを実行するためにコードをまとめたブロックです。関数を使うことで、同じコードを何度も書く必要がなくなり、プログラムの見通しが良くなります。
関数の定義と呼び出し
関数は、戻り値の型、関数名、引数のリスト、そして関数本体(ブロック {}
)で構成されます。
“`dart
// 関数の定義
void sayHello(String name) { // 戻り値の型 void, 関数名 sayHello, 引数 String name
print(‘こんにちは、$nameさん!’);
}
// 関数の呼び出し
void main() {
sayHello(‘山田’);
sayHello(‘佐藤’);
}
“`
引数(必須、任意、デフォルト値)
関数はゼロ個以上の引数を受け取ることができます。
-
必須引数 (Required Parameters): デフォルトでは、引数は必須です。関数を呼び出す際に、定義された順序で全ての必須引数を渡す必要があります。
“`dart
void greet(String greeting, String name) {
print(‘$greeting, $name!’);
}void main() {
greet(‘Hello’, ‘Alice’); // 必須引数は順序通りに渡す
// greet(‘Hi’); // エラー!name引数が不足しています
}
“` -
任意の位置引数 (Optional Positional Parameters): 角括弧
[]
で囲むことで、引数を任意にすることができます。任意の位置引数は、必須引数の後に定義する必要があります。“`dart
void printInfo(String name, [int? age, String? city]) { // ageとcityは任意の位置引数
String info = ‘名前: $name’;
if (age != null) {
info += ‘, 年齢: $age’;
}
if (city != null) {
info += ‘, 街: $city’;
}
print(info);
}void main() {
printInfo(‘Alice’); // 名前のみ
printInfo(‘Bob’, 25); // 名前と年齢
printInfo(‘Charlie’, 30, ‘Tokyo’); // 名前、年齢、街
}
``
int?
任意の位置引数には、Null Safetyを考慮して Nullableな型(,
String?` など)を使うのが一般的です。また、デフォルト値を指定することもできます。 -
任意の名前付き引数 (Optional Named Parameters): 波括弧
{}
で囲むことで、引数を任意かつ名前で渡せるようにします。呼び出し時には引数の順序は関係ありませんが、引数名を使って値を渡す必要があります。“`dart
void printUserDetails({String name = ‘匿名’, int age = 0, String? city}) { // デフォルト値とNull許容を組み合わせ
String info = ‘名前: $name, 年齢: $age’;
if (city != null) {
info += ‘, 街: $city’;
}
print(info);
}void main() {
printUserDetails(name: ‘Alice’, age: 30, city: ‘London’); // 名前付きで渡す
printUserDetails(age: 25, name: ‘Bob’); // 順序は関係ない
printUserDetails(); // 全てデフォルト値を使用
printUserDetails(city: ‘Paris’); // 一部の引数のみ指定
}
``
required` キーワードを使用します(Dart 2.12以降)。
デフォルト値を指定しない名前付き引数は、通常 Nullable にする必要があります。もし名前付き引数を必須にしたい場合は、“`dart
void createProduct({required String name, required double price, String? description}) {
print(‘製品作成: $name (価格: $price)’);
if (description != null) {
print(‘ 説明: $description’);
}
}void main() {
createProduct(name: ‘Widget’, price: 9.99); // nameとpriceは必須
// createProduct(name: ‘Gadget’); // エラー!priceが不足
}
``
required`キーワードが多用されます。
FlutterのWidgetなどでは、名前付き引数と
戻り値
関数は処理結果として値を返すことができます。戻り値がない場合は void
を戻り値の型として指定します。値を返す場合は、その値の型を戻り値の型として指定し、関数内で return
キーワードを使って値を返します。
“`dart
// 整数を返す関数
int add(int a, int b) {
return a + b;
}
// 文字列を返す関数
String createGreeting(String name) {
return ‘こんにちは、$nameさん!’;
}
void main() {
int sum = add(5, 3);
print(sum); // 出力: 8
String greeting = createGreeting(‘花子’);
print(greeting); // 出力: こんにちは、花子さん!
}
“`
アロー関数 (Expression Body)
関数本体が単一の式である場合、波括弧 {}
と return
を省略して、アロー演算子 =>
を使って簡潔に書くことができます。
“`dart
// 通常の関数
int multiply(int a, int b) {
return a * b;
}
// アロー関数を使った場合
int multiplyArrow(int a, int b) => a * b;
void main() {
print(multiply(4, 5)); // 出力: 20
print(multiplyArrow(6, 7)); // 出力: 42
}
“`
短い処理の関数を定義する際にコードがすっきりします。
無名関数(ラムダ式)
関数名をつけずに、その場で一時的に定義して使う関数です。コールバック関数として、他の関数に渡す場合などによく使われます。
“`dart
void main() {
// 無名関数を変数に代入
var greet = (String name) {
print(‘こんにちは、${name}さん!’);
};
greet(‘太郎’); // 無名関数を呼び出し
// ListのforEachメソッドに無名関数を渡す例
List
numbers.forEach((number) { // 各要素に対して実行される無名関数
print(number * 2);
});
// 無名関数をアロー関数形式で書くこともできる
numbers.forEach((number) => print(number * 3));
}
“`
無名関数は、短い処理や一度しか使わないような処理をその場で定義するのに便利です。
スコープ
変数がアクセス可能な範囲をスコープといいます。Dartでは、波括弧 {}
で囲まれたブロックごとにスコープが形成されます(ブロックスコープ)。
“`dart
String globalVar = ‘グローバル’; // グローバルスコープ
void main() {
String localVar = ‘ローカル (main)’; // main関数のローカルスコープ
print(globalVar); // グローバル変数にアクセス可能
print(localVar); // main関数のローカル変数にアクセス可能
if (true) {
String blockVar = ‘ローカル (ifブロック)’; // ifブロックのローカルスコープ
print(localVar); // 外側のスコープの変数にアクセス可能
print(blockVar); // 自身のスコープの変数にアクセス可能
}
// print(blockVar); // エラー!ifブロックの外からはアクセスできない
}
void anotherFunction() {
print(globalVar); // グローバル変数にアクセス可能
// print(localVar); // エラー!main関数のローカル変数にはアクセスできない
}
“`
変数は、自身が定義されたスコープ内およびそれよりも内側のスコープからアクセスできますが、外側のスコープからはアクセスできません。これにより、変数名の衝突を防ぎ、コードの見通しを良くすることができます。
クラスとオブジェクト指向プログラミング (OOP) の基本
Dartはオブジェクト指向プログラミング(OOP)をサポートしています。OOPは、プログラムを「オブジェクト」という単位で考えるプログラミングパラダイムです。オブジェクトは、データ(プロパティ)と、そのデータを操作する手続き(メソッド)をひとまとめにしたものです。
クラスの定義
クラスはオブジェクトの「設計図」です。どのようなプロパティ(データ)を持ち、どのようなメソッド(振る舞い)を持つのかを定義します。
“`dart
class Dog {
// プロパティ(インスタンス変数)
String name;
int age;
String breed;
// コンストラクタ(オブジェクトを生成する際に呼ばれる特殊なメソッド)
// 短縮構文(Syntactic Sugar)を使ったコンストラクタ
Dog(this.name, this.age, this.breed);
// メソッド
void bark() {
print(‘$name がワンワンと吠えています!’);
}
void getInfo() {
print(‘名前: $name, 年齢: $age歳, 犬種: $breed’);
}
}
“`
オブジェクトの生成(インスタンス化)
クラスという設計図をもとに、具体的なオブジェクト(インスタンス)をメモリ上に作成することを「インスタンス化」といいます。インスタンス化するには、クラス名の後に括弧 ()
をつけてコンストラクタを呼び出します。
“`dart
void main() {
// Dogクラスからオブジェクト(インスタンス)を生成
Dog myDog = Dog(‘ポチ’, 3, ‘柴犬’);
// オブジェクトのプロパティにアクセス
print(myDog.name); // 出力: ポチ
print(myDog.age); // 出力: 3
// オブジェクトのメソッドを呼び出し
myDog.bark(); // 出力: ポチ がワンワンと吠えています!
myDog.getInfo(); // 出力: 名前: ポチ, 年齢: 3歳, 犬種: 柴犬
// 別のDogオブジェクトを生成
Dog yourDog = Dog(‘ハル’, 5, ‘プードル’);
yourDog.getInfo();
}
“`
プロパティ(フィールド)とメソッド
- プロパティ (Properties / Fields): オブジェクトが持つデータです。クラスのメンバー変数として定義されます。
- メソッド (Methods): オブジェクトができること(振る舞い)です。クラスの中に定義される関数です。
上記 Dog
クラスの例では、name
, age
, breed
がプロパティ、bark()
, getInfo()
がメソッドです。
コンストラクタ
コンストラクタは、クラスから新しいオブジェクトを生成する際に自動的に呼び出される特殊なメソッドです。オブジェクトの初期化(プロパティに初期値を設定するなど)を行います。
- デフォルトコンストラクタ: クラスにコンストラクタを明示的に定義しない場合、引数を受け取らないデフォルトコンストラクタが自動的に生成されます。
-
カスタムコンストラクタ: 独自のコンストラクタを定義することで、オブジェクト生成時の初期化方法を制御できます。
“`dart
class Point {
int x;
int y;// カスタムコンストラクタ
Point(int x, int y) {
this.x = x; // this は現在のオブジェクト自身を指す
this.y = y;
}// 短縮構文を使ったコンストラクタ (上記Dogクラスで使用)
// Point(this.x, this.y); // 上記のPoint(int x, int y)コンストラクタと等価
}void main() {
Point origin = Point(0, 0);
print(‘(${origin.x}, ${origin.y})’);
}
“` -
名前付きコンストラクタ (Named Constructors): クラス名の後に
.名前
をつけて、複数のコンストラクタを定義できます。オブジェクト生成時にどのコンストラクタを使うかを指定できます。“`dart
class Color {
int red;
int green;
int blue;// デフォルトコンストラクタ (RGB値を指定して生成)
Color(this.red, this.green, this.blue);// 名前付きコンストラクタ (グレーを指定して生成)
Color.grey(int value) : this(value, value, value); // 他のコンストラクタを呼び出すことも可能// 名前付きコンストラクタ (ランダムな色で生成)
Color.random() : this(_generateRandomInt(), _generateRandomInt(), _generateRandomInt());static int _generateRandomInt() {
// 簡単なランダム値生成の例 (実際はdart:mathを使う)
return DateTime.now().millisecond % 256;
}
}void main() {
Color red = Color(255, 0, 0);
Color darkGrey = Color.grey(50);
Color randomColor = Color.random();print(‘Red: ${red.red}’);
print(‘Dark Grey: ${darkGrey.red}’);
print(‘Random Color: ${randomColor.red}, ${randomColor.green}, ${randomColor.blue}’);
}
“`
名前付きコンストラクタは、オブジェクト生成の様々なパターンを提供したい場合に便利です。
ゲッターとセッター
プロパティに直接アクセスする代わりに、ゲッター (Getter) とセッター (Setter) を使うことで、プロパティへのアクセスを制御したり、取得・設定時に追加の処理を行ったりすることができます。
“`dart
class Circle {
double radius;
// コンストラクタ
Circle(this.radius);
// ゲッター (面積を計算して返す)
double get area {
return 3.14159 * radius * radius; // 簡単のため円周率は固定値
}
// セッター (半径を設定する際にバリデーションを行う)
set radius(double value) {
if (value >= 0) {
radius = value;
} else {
print(‘エラー: 半径は負の値にできません。’);
}
}
}
void main() {
Circle myCircle = Circle(5.0);
// ゲッターを使って面積を取得 (プロパティのようにアクセスできる)
print(‘面積: ${myCircle.area}’); // 出力: 面積: 78.53975
// セッターを使って半径を設定 (プロパティのように代入できる)
myCircle.radius = 10.0;
print(‘新しい半径: ${myCircle.radius}’); // 出力: 新しい半径: 10.0
print(‘新しい面積: ${myCircle.area}’); // 出力: 新しい面積: 314.159
myCircle.radius = -2.0; // セッター内のバリデーションが機能
print(‘現在の半径: ${myCircle.radius}’); // 出力: エラー: 半径は負の値にできません。\n現在の半径: 10.0
}
“`
ゲッターとセッターは、外部からはプロパティのように見えますが、実際にはメソッド呼び出しです。これにより、内部の実装を変更しても、外部からのアクセス方法を変える必要がなくなります。
継承 (extends
, super
)
オブジェクト指向の重要な概念の一つに「継承」があります。あるクラス(親クラスまたはスーパークラス)のプロパティやメソッドを、別のクラス(子クラスまたはサブクラス)が引き継ぐことができます。これにより、共通する部分を親クラスにまとめ、コードの再利用性を高めることができます。
子クラスは extends
キーワードを使って親クラスを指定します。子クラスのコンストラクタでは、super()
を使って親クラスのコンストラクタを呼び出すことができます。
“`dart
// 親クラス
class Animal {
String name;
Animal(this.name); // 親クラスのコンストラクタ
void eat() {
print(‘$name が食事をしています。’);
}
void sleep() {
print(‘$name が眠っています。’);
}
}
// 子クラス (Animalクラスを継承)
class Dog extends Animal {
String breed;
// 子クラスのコンストラクタ
Dog(String name, this.breed) : super(name); // super(name)で親クラスのコンストラクタを呼び出す
// 子クラス独自のメソッド
void bark() {
print(‘$name がワンワンと吠えています!’);
}
// 親クラスのメソッドをオーバーライド (上書き)
@override // @override アノテーションは、メソッドが親クラスのメソッドをオーバーライドしていることを示す(必須ではないが推奨)
void eat() {
super.eat(); // superキーワードで親クラスのeatメソッドを呼び出すことも可能
print(‘特にドッグフードを食べています。’);
}
}
void main() {
Dog myDog = Dog(‘ポチ’, ‘柴犬’);
myDog.eat(); // オーバーライドされた子クラスのeatメソッドが呼ばれる
myDog.sleep(); // 親クラスから継承したsleepメソッドが呼ばれる
myDog.bark(); // 子クラス独自のbarkメソッドが呼ばれる
}
“`
継承を使うことで、クラス間に「~は~の一種である」という関係(IS-A関係)を表現できます。(例: DogはAnimalの一種である)
抽象クラス (abstract
)
抽象クラスは、それ自体をインスタンス化することはできず、他のクラスに継承されることを目的としたクラスです。抽象メソッドを持つことができます。抽象メソッドは宣言のみで、実装は子クラスに任されます。
“`dart
abstract class Shape {
// 抽象メソッド(実装を持たない)
double getArea();
// 実装を持つメソッドも持てる
void display() {
print(‘図形です。面積は ${getArea()} です。’);
}
}
// 抽象クラスを継承するクラス
class Circle extends Shape {
double radius;
Circle(this.radius);
// 抽象メソッドの実装が必須
@override
double getArea() {
return 3.14159 * radius * radius;
}
}
class Square extends Shape {
double side;
Square(this.side);
// 抽象メソッドの実装が必須
@override
double getArea() {
return side * side;
}
}
void main() {
// Shape shape = Shape(); // エラー!抽象クラスはインスタンス化できない
Shape myCircle = Circle(5.0); // 抽象クラス型の変数に子クラスのインスタンスを代入可能 (ポリモーフィズム)
Shape mySquare = Square(4.0);
myCircle.display(); // サブクラスで実装されたgetArea()が呼ばれる
mySquare.display();
}
“`
抽象クラスは、共通の振る舞いの枠組みを定義し、具体的な実装を子クラスに委ねる場合に利用します。
インターフェース (implements
)
Dartには「インターフェース」というキーワードはありませんが、全てのクラスが暗黙的にインターフェースとしても機能します。あるクラスをインターフェースとして利用するには、implements
キーワードを使います。
implements
を使うと、実装元のクラスの全てのプロパティとメソッド(コンストラクタを除く)を、実装先のクラスで全てゼロから再実装する必要があります。これは「契約」のようなもので、「このインターフェースを実装するクラスは、これらのメンバーを必ず持たなければならない」ということを示します。
“`dart
// インターフェースとして利用するクラス (通常クラスとして定義)
class Walker {
void walk() {
print(‘歩きます。’);
}
}
class Swimmer {
void swim() {
print(‘泳ぎます。’);
}
}
// 複数のインターフェースを実装
class Human implements Walker, Swimmer {
String name;
Human(this.name);
// Walkerインターフェースのwalkメソッドを実装
@override
void walk() {
print(‘$name が地面を歩いています。’);
}
// Swimmerインターフェースのswimメソッドを実装
@override
void swim() {
print(‘$name が水中を泳いでいます。’);
}
// Human独自のメソッド
void talk() {
print(‘$name が話しています。’);
}
}
void main() {
Human alice = Human(‘Alice’);
alice.walk();
alice.swim();
alice.talk();
// インターフェース型として扱うことも可能
Walker bob = Human(‘Bob’);
bob.walk(); // Walkerインターフェースとしてアクセスできるメソッドのみ呼び出し可能
// bob.swim(); // エラー!Walker型にはswimメソッドがない
}
``
implements` は、継承(IS-A関係)とは異なり、「~の能力を持つ」「~のような振る舞いをする」という関係(HAS-AやCAN-DO関係に似たもの)を表現する場合に使われます。特に、複数の親から機能を継承したいが多重継承は使えない、という場合に有効です。
ミックスイン (with
)
ミックスインは、クラス階層に縛られずに複数のクラス間でコードを再利用するための仕組みです。mixin
キーワードを使って定義し、クラス定義で with
キーワードを使って組み込みます。ミックスインのメンバーは、まるでそのクラス自身が持っているかのように利用できます。
“`dart
// ミックスインの定義
mixin Walkable {
void walk() {
print(‘歩いています。’);
}
}
mixin Swimmable {
void swim() {
print(‘泳いでいます。’);
}
}
// 複数のミックスインを組み合わせ
class Person with Walkable, Swimmable {
String name;
Person(this.name);
void introduce() {
print(‘私の名前は $name です。’);
}
}
class Duck with Walkable, Swimmable {
String name;
Duck(this.name);
void quack() {
print(‘$name がガーガー鳴いています。’);
}
}
void main() {
Person alice = Person(‘Alice’);
alice.introduce();
alice.walk(); // Walkableミックスインのメソッド
alice.swim(); // Swimmableミックスインのメソッド
Duck donald = Duck(‘Donald’);
donald.quack();
donald.walk(); // Walkableミックスインのメソッド
donald.swim(); // Swimmableミックスインのメソッド
}
“`
ミックスインは、特定の機能群を複数の関連性のないクラスに「混ぜ込みたい」場合に非常に強力です。Dartのミックスインは、継承よりも柔軟なコード再利用手段としてよく利用されます。特にFlutterでは、多くのウィジェットがミックスインを活用しています。
静的メンバー (static
)
クラスのメンバー(プロパティやメソッド)に static
キーワードをつけると、それはクラス自身に紐付けられ、オブジェクトのインスタンスには紐付けられなくなります。静的メンバーは、クラス名を介してアクセスします。オブジェクトを生成しなくても利用できます。
“`dart
class MathUtils {
// 静的プロパティ(クラス全体で共有される定数など)
static const double PI = 3.14159;
// 静的メソッド(インスタンスの状態に依存しないユーティリティ関数など)
static double calculateCircleArea(double radius) {
return PI * radius * radius;
}
static int max(int a, int b) {
return a > b ? a : b;
}
}
void main() {
// MathUtils.PI; // エラー!インスタンスからは静的メンバーにアクセスできない (警告になる場合も)
// クラス名を使って静的メンバーにアクセス
print(‘円周率: ${MathUtils.PI}’); // 出力: 円周率: 3.14159
double area = MathUtils.calculateCircleArea(5.0);
print(‘面積: $area’); // 出力: 面積: 78.53975
int maxValue = MathUtils.max(10, 20);
print(‘最大値: $maxValue’); // 出力: 最大値: 20
}
“`
静的メンバーは、特定のインスタンスに依存しないユーティリティ関数や、クラス全体で共有されるデータなどに使われます。
非同期処理
Dartはシングルスレッドで動作しますが、ネットワーク通信やファイルの読み書きなど、時間のかかる処理(I/O処理など)を行う際には、プログラム全体がブロックされて応答しなくなるのを避けるために非同期処理を使用します。非同期処理を使うことで、時間のかかる処理をバックグラウンドで行わせつつ、メインの処理を継続させることができます。
Dartの非同期処理は、主に Future
オブジェクトと async
/await
キーワード、そして Stream
を使って実現されます。
なぜ非同期処理が必要か
同期処理では、ある処理が終わるまで次の処理に進むことができません。例えば、インターネットから画像をダウンロードするのに10秒かかるとすると、そのダウンロードが終わるまでユーザーインターフェースが固まってしまう、といった問題が発生します。
非同期処理では、時間のかかる処理を開始したら、その完了を待たずに次の処理に進みます。時間のかかる処理が完了したら、その結果を通知したり、特定のコールバック関数を実行したりします。
Future
Future
は、非同期処理の結果を表すオブジェクトです。Future
オブジェクトは、まだ結果が得られていない状態か、結果が得られた状態(成功または失敗)のいずれかになります。
- 成功: 値が得られた (
Future<T>
) - 失敗: エラーが発生した (
Future<dynamic>
)
非同期処理を定義する関数は、通常 Future
オブジェクトを返します。
“`dart
// 非同期処理を行う関数 (Futureを返す)
Future
// 擬似的に3秒待機する非同期処理
return Future.delayed(Duration(seconds: 3), () {
// ここでネットワーク通信などの時間のかかる処理を行う
// 処理が成功したら値を返す
return ‘ユーザーデータが取得できました。’;
// 処理が失敗したら throw Exception(…) でエラーを発生させる
});
}
void main() {
print(‘ユーザーデータの取得を開始します…’);
// fetchUserData() は Future を返すため、すぐに完了せず、次の行に進む
Future
// Future の完了を待つための方法 (いくつかあるが、async/awaitが最も一般的で読みやすい)
// その1: then() メソッドを使う
userDataFuture.then((data) {
// 非同期処理が成功して値が取得できた場合に実行される
print(‘then() を使った処理結果: $data’);
}).catchError((error) {
// 非同期処理でエラーが発生した場合に実行される
print(‘then() を使ったエラー処理: $error’);
});
print(‘ユーザーデータの取得とは別の処理をしています…’); // こちらが先に実行される
}
“`
この例を実行すると、「ユーザーデータの取得を開始します…」「ユーザーデータの取得とは別の処理をしています…」というメッセージが先に表示され、3秒後に「then() を使った処理結果: ユーザーデータが取得できました。」が表示されるはずです。これが非同期処理の基本的な動作です。
async
/ await
then()
メソッドを使う方法でも非同期処理の結果を扱えますが、非同期処理が連続する場合などにコードが複雑になりがちです。そこで、Dartでは async
と await
キーワードを使うことで、非同期処理を同期処理を書くのと同じような感覚で読み書きできるようにしています。
async
キーワード: 関数の戻り値の型の手前にasync
をつけます。これは、その関数が非同期処理を含み、Future
を返す可能性があることを示します。await
キーワード:Future
を返す式の前にawait
をつけます。これは、そのFuture
が完了するまで待機し、結果を取得することを指示します。await
はasync
関数の中でしか使えません。
“`dart
// async 関数
Future
print(‘ユーザーデータの取得を開始します (async/await)…’);
// await で Future が完了するまで待機
String data = await fetchUserData(); // fetchUserData() は Future
print(‘ユーザーデータの処理中…’);
// 取得したデータを使った処理
String processedData = data.toUpperCase();
return processedData; // async 関数は値を返すと Future.value(値) を返す
}
void main() async { // main 関数も async にすることができる
print(‘アプリケーション開始…’);
try {
// await で Future が完了するまで待機し、結果を取得
String result = await fetchAndProcessUserData();
print(‘async/await を使った処理結果: $result’);
} catch (e) {
// await した非同期処理でエラーが発生した場合の処理
print(‘async/await を使ったエラー処理: $e’);
}
print(‘アプリケーション終了…’);
}
Future
// 擬似的に3秒待機する非同期処理
return Future.delayed(Duration(seconds: 3), () {
// return ‘ユーザーデータが取得できました。’;
throw Exception(‘ユーザーデータの取得に失敗しました!’); // エラーを発生させる例
});
}
``
fetchAndProcessUserData
この例では、関数は
await fetchUserData()の行で3秒間待機します。待機している間、
main関数の「アプリケーション開始...」の後に続く処理(この例では待機しているだけですが)は中断されません。
fetchUserData()が完了して値を返したら、
awaitの結果としてその値が
data` 変数に代入され、その後の処理が続行されます。
async
/await
を使うことで、非同期処理のフローが同期処理のように上から下に流れるように見えるため、コードが非常に読みやすくなります。ほとんどの場合、非同期処理を扱う際には async
/await
を使うのが良いでしょう。
Stream
(簡単な紹介)
Future
が単一の非同期イベント(一度だけ発生する結果)を扱うのに対し、Stream
は一連の非同期イベント(複数回発生するデータやイベント)を扱います。例えば、ファイルからデータを少しずつ読み込む、ネットワークからパケットを受信する、ユーザーのジェスチャーイベントをリッスンするなど、時間とともに複数の値が発生する場合に使われます。
Streamを扱うには、Stream
オブジェクトを生成し、listen()
メソッドでリスナー(コールバック関数)を登録するか、async*
関数と yield
キーワード、そして await for
ループを使います。
“`dart
// Streamを返す async 関数
Stream
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1)); // 1秒待機
yield i; // Streamにデータを流す
}
}
void main() async {
print(‘Streamからのカウントを開始…’);
// Stream を listen() メソッドで購読する場合
// countStream(3).listen(
// (int value) {
// print(‘Listen: $value’);
// },
// onError: (error) {
// print(‘Listen Error: $error’);
// },
// onDone: () {
// print(‘Listen Done.’);
// }
// );
// async 関数の中で await for ループを使ってStreamを処理する場合
await for (int value in countStream(5)) {
print(‘Await For: $value’);
}
print(‘Streamからのカウントが終了…’);
}
“`
この例を実行すると、1秒ごとに「Await For: 1」「Await For: 2」…「Await For: 5」と表示され、最後に「Streamからのカウントが終了…」と表示されます。
StreamはFutureよりも高度な概念ですが、Flutterでリアルタイムなデータ(例えば、UIの状態変化やFirebaseのデータ変更など)を扱う際によく利用されるため、存在を知っておくと良いでしょう。
例外処理
プログラムの実行中に予期しないエラー(例外)が発生することがあります。例外が発生すると、通常プログラムはそこで停止してしまいます。しかし、例外処理のメカニズムを使うことで、エラーが発生した場合でもプログラムが異常終了するのを防ぎ、回復処理や代替処理を行うことができます。
Dartでは、try
, catch
, on
, finally
, throw
といったキーワードを使って例外処理を行います。
try
: 例外が発生する可能性のあるコードブロックを囲みます。catch
:try
ブロック内で発生した例外を捕捉し、その例外を引数として受け取ります。どのような例外でも捕捉します。on
: 特定の型の例外のみを捕捉したい場合にcatch
の代わりに使用します。finally
:try
ブロックの結果(例外が発生したかどうかにかかわらず)にかかわらず、常に実行されるコードブロックです。リソースの解放などを行います。throw
: 意図的に例外を発生させます。
“`dart
void main() {
print(‘処理開始…’);
try {
// 例外が発生する可能性のあるコード
int result = divide(10, 0); // 0で割ると例外が発生する
print(‘計算結果: $result’); // 例外が発生するとここは実行されない
} on IntegerDivisionByZeroException { // 特定の型の例外を捕捉
print(‘エラー: 0で割ろうとしました!’);
} on FormatException catch (e) { // 別の型の例外を捕捉し、例外オブジェクトも受け取る
print(‘エラー: 入力形式が不正です: $e’);
} catch (e) { // 上記以外のあらゆる例外を捕捉
print(‘予期しないエラーが発生しました: $e’);
} finally {
// 例外の有無にかかわらず、必ず実行されるコード
print(‘例外処理の finally ブロックが実行されました。’);
}
print(‘処理終了…’);
print(‘\n別の例外発生例:’);
try {
validateInput(‘abc’);
// validateInput(”); // エラーを発生させる場合
} catch (e) {
print(‘入力検証エラー: $e’);
}
}
int divide(int a, int b) {
if (b == 0) {
throw IntegerDivisionByZeroException(); // 意図的に例外を発生させる
}
return a ~/ b;
}
void validateInput(String input) {
if (input.isEmpty) {
throw ArgumentError(‘入力文字列が空です。’); // カスタムエラーを発生させる
}
print(‘入力は有効です。’);
}
``
divide(10, 0)
この例では、で
IntegerDivisionByZeroExceptionが発生し、
on IntegerDivisionByZeroExceptionブロックが実行されます。その後の
print(‘計算結果: …’)はスキップされます。
finally` ブロックは例外の有無にかかわらず実行され、最後に「処理終了…」が表示されます。
例外処理は、プログラムをより堅牢にし、ユーザーフレンドリーなエラーハンドリングを実装するために不可欠です。
その他重要な概念
Dartには他にも知っておくと役立つ概念がいくつかあります。
ライブラリとパッケージ (import
)
Dartのコードは「ライブラリ」という単位で管理されます。一つのファイルは一つのライブラリと見なすことができます。他のライブラリで定義されたクラスや関数を利用するには、import
キーワードを使います。
- 組み込みライブラリ: Dart SDKに付属しているライブラリ(例:
dart:core
,dart:math
,dart:io
,dart:async
など)は、import 'dart:ライブラリ名';
の形式でインポートします。 - 外部パッケージ(ライブラリ): Pubパッケージマネージャーを使ってインストールした外部のライブラリは、
import 'package:パッケージ名/ファイルパス.dart';
の形式でインポートします。
“`dart
// dart:math ライブラリから Random クラスをインポート
import ‘dart:math’;
// 外部パッケージ (例: http パッケージをインストールしている場合)
// import ‘package:http/http.dart’ as http; // as でエイリアスをつけることも可能
void main() {
Random random = Random(); // dart:math からインポートした Random クラスを使用
int randomNumber = random.nextInt(100); // 0から99までのランダムな整数を生成
print(‘ランダムな数値: $randomNumber’);
// エイリアスを使った外部パッケージの利用例 (httpパッケージをインストールしている場合のみ有効)
// final url = Uri.https(‘example.com’, ‘/path’);
// final response = await http.get(url);
// if (response.statusCode == 200) {
// print(‘成功!’);
// } else {
// print(‘失敗: ${response.statusCode}’);
// }
}
``
import` は、必要な機能だけを読み込み、名前の衝突を防ぐ役割も果たします。
エクスポート (export
)
あるライブラリで定義されたメンバーを、他のライブラリから import
させる際に、どのメンバーを公開するかを制御できます。export
キーワードを使うと、あるファイルでインポートしたメンバーを、そのファイルをインポートする他のファイルから再エクスポートすることができます。これは、複数の小さなファイルをまとめて一つの大きなライブラリとして公開する場合などに便利です。
“`dart
// lib/utils.dart ファイル
// export ‘string_utils.dart’;
// export ‘number_utils.dart’;
// lib/string_utils.dart ファイル
String reverseString(String s) => s.split(”).reversed.join();
// lib/number_utils.dart ファイル
int sum(int a, int b) => a + b;
// main.dart ファイル
// import ‘package:my_package/utils.dart’; // utils.dart をインポートすれば、string_utils.dart と number_utils.dart のメンバーも利用可能になる
// void main() {
// print(reverseString(‘hello’)); // utils.dart 経由で string_utils.dart の関数を利用
// print(sum(1, 2)); // utils.dart 経由で number_utils.dart の関数を利用
// }
``
export` は、ライブラリの構造を整理し、外部に公開するインターフェースを制御するために使用します。
型エイリアス (typedef
)
既存の関数型や複雑な型に新しい名前(エイリアス)を付けることができます。これにより、コードの可読性が向上します。
“`dart
// int を引数にとり、bool を返す関数型に CheckNumber というエイリアスをつける
typedef CheckNumber = bool Function(int number);
// CheckNumber 型を引数にとる関数
void processNumbers(List
for (int number in numbers) {
if (checker(number)) {
print(‘$number は条件を満たします。’);
}
}
}
void main() {
// エイリアスを使った関数の定義
CheckNumber isEven = (int n) => n % 2 == 0;
List
processNumbers(myNumbers, isEven); // CheckNumber 型の関数を渡す
}
“`
Dart 2.13以降、任意の型にエイリアスを付けられるようになりました。
“`dart
typedef IntList = List
void main() {
IntList numbers = [1, 2, 3];
print(numbers);
}
“`
これにより、複雑な型定義を分かりやすい名前で置き換えることができます。
ジェネリクス (<>
) の基本
ジェネリクスを使うと、型に依存しない柔軟なコードを書くことができます。コレクション型(List, Map, Setなど)では、格納する要素の型を <型>
のように指定するのに使われます。これにより、コンパイル時に型安全性を確保できます。
“`dart
// List
List
// integerList.add(‘hello’); // エラー!Stringはint型ではない
// List
List
// List
List
“`
独自のクラスや関数でもジェネリクスを使うことができます。
“`dart
// ジェネリクスを使ったクラス定義 (Tは任意の型を表すプレースホルダー)
class Box
T value;
Box(this.value);
T getValue() {
return value;
}
}
void main() {
// int型のBoxを作成
Box
int intValue = intBox.getValue(); // 戻り値の型はintとして扱える
print(intValue);
// String型のBoxを作成
Box
String stringValue = stringBox.getValue(); // 戻り値の型はStringとして扱える
print(stringValue);
// Box
}
“`
ジェネリクスを使うことで、コードの再利用性を高めつつ、コンパイル時の型チェックによって安全性を確保できます。
拡張メソッド (Extension Methods) の基本
拡張メソッドを使うと、既存のクラス(自分で定義したクラスだけでなく、Dartの組み込みクラスや外部ライブラリのクラスも含む)に、そのソースコードを編集することなく新しいメソッドを追加することができます。
“`dart
// String クラスを拡張する拡張メソッドを定義
extension StringExtensions on String {
// 文字列を逆順にする拡張ゲッター
String get reversed => split(”).reversed.join();
// 文字列を特定の回数繰り返す拡張メソッド
String repeat(int times) {
StringBuffer buffer = StringBuffer();
for (int i = 0; i < times; i++) {
buffer.write(this); // this は拡張されているオブジェクト(この場合はStringインスタンス)を指す
}
return buffer.toString();
}
}
void main() {
String myString = ‘Dart’;
// Stringクラスのインスタンスに対して、定義した拡張メソッド/ゲッターを呼び出す
print(myString.reversed); // 出力: traD
print(myString.repeat(3)); // 出力: DartDartDart
}
“`
拡張メソッドは、特定のクラスのインスタンスに対して頻繁に行う処理がある場合に、ユーティリティ関数として定義するよりもオブジェクト指向的に書けて便利です。
簡単な実践例:コマンドラインツール
ここまでに学んだDartの基本を使って、簡単なコマンドラインツールを作成してみましょう。ユーザーに名前を入力してもらい、挨拶を返すプログラムです。
“`dart
// dart:io ライブラリをインポートして標準入出力を扱う
import ‘dart:io’;
void main() {
// ユーザーに名前の入力を促す
stdout.write(‘お名前を入力してください: ‘);
// 標準入力から一行読み込む (同期的に待機)
// readLineSync() は Nullable な String? を返す可能性があるため、Null Safetyに注意
String? name = stdin.readLineSync();
// 入力が Null または空文字列でないかチェック
if (name != null && name.isNotEmpty) {
// 名前が入力された場合、挨拶を表示
print(‘こんにちは、$nameさん!’);
} else {
// 入力が無かった場合
print(‘お名前が入力されませんでした。’);
}
}
``
.dart
このコードをファイルとして保存し、ターミナルから
dart run ファイル名.dart` として実行してみてください。プログラムがあなたの名前入力を待ち、入力後に挨拶が表示されるはずです。
この簡単な例でも、import
、変数の宣言、Null Safety (?
, != null
, isNotEmpty
)、制御フロー (if
/else
)、そして入出力 (stdout.write
, stdin.readLineSync
) といった基本的な要素が使われています。
次のステップ
この記事でDartの基本的な文法や主要な概念を学ぶことができました。しかし、これは旅の始まりにすぎません。Dartには、より高度なトピック(アノテーション、アイソレート、メタプログラミングなど)や、豊富なライブラリ、そして何よりもFlutterのような強力なフレームワークがあります。
次に学習を進めるためのリソースや方法をいくつかご紹介します。
- Dart公式ドキュメント: 最も正確で網羅的な情報源です。https://dart.dev/ 公式サイトには、言語ツアー、ライブラリツアー、サンプルコード、APIリファレンスなど、あらゆる情報があります。少し難しいと感じるかもしれませんが、困ったときの辞書として非常に役立ちます。
- Flutter公式ドキュメント: もしFlutterでのアプリ開発を目指しているのであれば、Flutterの公式ドキュメントに進みましょう。https://flutter.dev/ Dartの知識がそのまま活かせます。UI構築の方法、状態管理、様々なプラットフォームへのデプロイなど、Flutter固有の概念を学ぶことになります。
- オンライン学習プラットフォーム: Udemy, Coursera, Udacity, Dart Academyなど、多くのプラットフォームでDartやFlutterのコースが提供されています。動画形式で体系的に学びたい場合に適しています。
- 書籍: 書店やオンラインストアでDartやFlutterに関する書籍を探してみるのも良いでしょう。
- オープンソースプロジェクト: GitHubなどで公開されているDart/Flutterのオープンソースプロジェクトのコードを読んでみることは、実際の開発でどのようにDartが使われているかを学ぶのに非常に有効です。
- コミュニティへの参加: DartやFlutterのコミュニティ(Slack, Discord, フォーラムなど)に参加して、質問したり、他の開発者と交流したりしましょう。
特に、実際に手を動かしてコードを書くことが重要です。簡単なプログラムから始めて、少しずつ複雑なアプリケーションに挑戦してみてください。エラーにぶつかることも多いと思いますが、それらを解決する過程で多くのことを学ぶことができます。
まとめ
この記事では、Dartの初心者向けに、その概要から始まり、以下の基本的な要素と概念を詳しく解説しました。
- Dartの環境構築と最初のプログラム
- コメント、変数、組み込み型、定数、Null安全
- 様々な演算子
- 条件分岐、スイッチ文、ループといった制御フロー
- 関数の定義、引数、戻り値、無名関数、スコープ
- クラス、オブジェクト、コンストラクタ、ゲッター/セッター、継承、抽象クラス、インターフェース、ミックスイン、静的メンバーといったオブジェクト指向の基本
- 非同期処理(Future, async/await, Stream)
- 例外処理(try/catch/finally)
- ライブラリ、パッケージ、ジェネリクス、拡張メソッド
約5000語というボリュームで、Dartの主要な基本要素を網羅したつもりです。この記事で得た知識は、Dartを使ったあらゆる開発の基礎となります。
Dartは活発に開発が進められており、新しい機能が追加されています。常に最新の情報に触れるよう心がけ、学習を続けていくことが大切です。
この記事が、あなたのDartプログラミング学習の第一歩となり、その後の素晴らしい開発体験につながることを願っています。
Happy Coding!