Dart 演算子 — 算術からカスケード、Null 安全性まで
演算子はコードの動詞である — それを習得すればデータを「動かす」ことができる。
1. 学べること
- 算術 / 関係 / 論理 / ビット単位演算子
- 代入演算子と複合代入演算子
- カスケード演算子
..と?..(メソッドチェーンの強力なツール) - Null 安全性演算子:
?./??/??= - 型テスト演算子:is / is! / as
2. 開発者のリアルな物語
(1) 課題:NullPointerException がバッチタスクをクラッシュさせる
Bob の DataPipeline は、数百万件の注文を処理する際に一部の注文に任意のフィールドが欠けていたため、頻繁にクラッシュした。コードには if (value != null) チェックが散在していたが、1 つが見落とされ、30 万件の処理が停止して完全な再実行が必要になった。
(2) Null 安全性演算子による解決
Dart は 3 つの演算子を提供する — ?.(安全アクセス)、??(null 合体)、??=(null 認識代入)— null 処理を簡潔かつ安全にしてくれる。
// 修正前: 冗長な null チェック
String getCity(Order order) {
if (order.customer != null) {
if (order.customer!.address != null) {
return order.customer!.address!.city;
}
}
return 'Unknown';
}
// 修正後: null 安全演算子
String getCity(Order order) =>
order.customer?.address?.city ?? 'Unknown';
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- コード行数が 60% 削減され、null 処理ロジックが一目で明確になる
- カスケード演算子
..でメソッドチェーンがよりスムーズに - 型テスト演算子が型チェックをより安全にする
3. 算術演算子と関係演算子
(1) 算術演算子
▶ サンプル:基本的な算術
void main() {
int a = 17;
int b = 5;
print(a + b); // 22 - 加算
print(a - b); // 12 - 減算
print(a * b); // 85 - 乗算
print(a / b); // 3.4 - 除算(double を返す)
print(a ~/ b); // 3 - 整数除算
print(a % b); // 2 - 剰余
// 前置・後置インクリメント
int count = 10;
print(count++); // 10(値を返してからインクリメント)
print(count); // 11
print(++count); // 12(インクリメントしてから値を返す)
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 演算子 | 意味 | 例 | 結果 |
|---|---|---|---|
+ |
加算 | 17 + 5 |
22 |
- |
減算 | 17 - 5 |
12 |
* |
乗算 | 17 * 5 |
85 |
/ |
除算(double を返す) | 17 / 5 |
3.4 |
~/ |
整数除算 | 17 ~/ 5 |
3 |
% |
剰余 | 17 % 5 |
2 |
(2) 関係演算子
▶ サンプル:比較演算
void main() {
int orders = 1500;
int threshold = 1000;
print(orders > threshold); // true
print(orders < threshold); // false
print(orders >= 1500); // true
print(orders <= 1500); // true
print(orders == 1500); // true
print(orders != 1000); // true
// 文字列比較は辞書順
print('apple' < 'banana'); // true
print('USD' == 'USD'); // true
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
4. 論理演算子とビット単位演算子
(1) 論理演算子
▶ サンプル:論理演算
void main() {
bool isProduction = true;
bool hasErrors = false;
print(isProduction && !hasErrors); // true - AND + NOT
print(isProduction || hasErrors); // true - OR
print(!isProduction); // false - NOT
// 短絡評価
String? name = null;
// name.length > 0 はクラッシュするが、次は安全:
print(name != null && name.isNotEmpty); // false (安全)
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 演算子 | 意味 | 短絡評価 |
|---|---|---|
&& |
論理 AND | 左が false なら右は評価されない |
|| |
論理 OR | 左が true なら右は評価されない |
! |
論理 NOT | — |
(2) ビット単位演算子
▶ サンプル:ビット単位演算
void main() {
int flags = 0b1010; // 2 進数で 10
int mask = 0b1100; // 2 進数で 12
print(flags & mask); // 8 (0b1000) - AND
print(flags | mask); // 14 (0b1110) - OR
print(flags ^ mask); // 6 (0b0110) - XOR
print(~flags); // -11 (ビット反転) - NOT
print(flags << 2); // 40 (0b101000) - 左シフト
print(flags >> 1); // 5 (0b0101) - 右シフト
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
5. 代入演算子と複合代入演算子
▶ サンプル:複合代入
void main() {
int total = 0;
double revenue = 0.0;
total += 100; // total = total + 100 = 100
total -= 30; // total = 70
total *= 2; // total = 140
total ~/= 3; // total = 46 (整数除算)
revenue += 52500.75; // revenue = 52500.75
// null 認識代入
String? outputPath;
outputPath ??= '/tmp/output'; // null の場合のみ代入
print(outputPath); // /tmp/output
outputPath ??= '/other/path'; // 代入されない(既に non-null)
print(outputPath); // /tmp/output
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 演算子 | 等価な表現 | 例 |
|---|---|---|
+= |
a = a + b |
total += 100 |
-= |
a = a - b |
total -= 30 |
*= |
a = a * b |
total *= 2 |
~/= |
a = a ~/ b |
total ~/= 3 |
??= |
a = a ?? b |
path ??= '/default' |
6. カスケード演算子 .. と ?..
(1) カスケード演算子
カスケード演算子 .. を使うと、同じオブジェクトに対して変数名を繰り返さずに複数の操作を実行できる。
graph TD A[演算子システム] --> B[算術 + 関係 + 論理] A --> C[カスケード ..] A --> D[Null 安全性 ?. ?? ??=] A --> E[型テスト is/as] C --> C1["object..method1()..method2()"]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:カスケード演算子
class DataPipeline {
String name = '';
int batchSize = 0;
bool verbose = false;
final List<String> sources = [];
void start() => print('$name started');
}
void main() {
// カスケードなし - 繰り返し
final pipeline1 = DataPipeline();
pipeline1.name = 'Analytics';
pipeline1.batchSize = 50000;
pipeline1.verbose = true;
pipeline1.sources.add('orders.csv');
pipeline1.start();
// カスケード使用 - 流暢でクリーン
final pipeline2 = DataPipeline()
..name = 'Analytics'
..batchSize = 50000
..verbose = true
..sources.add('orders.csv')
..start();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:null 安全カスケード ?..
class Customer {
String? name;
String? email;
}
void main() {
Customer? customer;
// null 安全カスケード - オブジェクトが null ならスキップ
customer?..name = 'Bob'..email = 'bob@example.com';
print(customer); // null (カスケードがスキップされた)
customer = Customer();
customer?..name = 'Bob'..email = 'bob@example.com';
print(customer?.name); // Bob
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 演算子 | 意味 | オブジェクトが null の場合 |
|---|---|---|
.. |
カスケード操作 | エラー |
?.. |
null 安全カスケード | カスケード全体をスキップ |
7. Null 安全性演算子
▶ サンプル:3 つの Null 安全性演算子
class Address {
final String city;
Address(this.city);
}
class Customer {
final String name;
final Address? address;
Customer(this.name, {this.address});
}
class Order {
final String id;
final Customer? customer;
Order(this.id, {this.customer});
}
void main() {
// ?. - null 安全アクセス
Order order = Order('ORD-001');
print(order.customer?.name); // null (安全)
// print(order.customer.name); // ランタイムエラー!
// ?? - null 合体
String city = order.customer?.address?.city ?? 'Unknown';
print(city); // Unknown
// ??= - null 認識代入
String? outputPath;
outputPath ??= '/tmp/reports';
print(outputPath); // /tmp/reports
outputPath ??= '/other/path';
print(outputPath); // /tmp/reports (変更なし)
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 演算子 | 意味 | 例 | 結果 |
|---|---|---|---|
?. |
安全アクセス | obj?.method() |
null または呼び出し結果 |
?? |
null 合体 | value ?? default |
non-null なら値、null ならデフォルト |
??= |
null 認識代入 | var ??= value |
null の場合のみ代入 |
8. 型テスト演算子
▶ サンプル:is / is! / as
void main() {
Object value = '1,500.00 USD';
// is - 型チェック(bool を返す)
if (value is String) {
print('String length: ${value.length}'); // 型昇格!
}
// is! - 否定型チェック
if (value is! int) {
print('Not an integer');
}
// as - 型キャスト(失敗時は例外)
String text = value as String; // OK
// int number = value as int; // ランタイムエラー!
// 安全なキャストパターン
if (value is String) {
String safe = value; // キャスト不要(昇格)
print(safe.toUpperCase());
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 演算子 | 意味 | 失敗時 |
|---|---|---|
is |
型チェック | false を返す |
is! |
否定型チェック | true を返す |
as |
型キャスト | TypeError をスロー |
9. 完全なサンプル:演算子を使った DataPipeline 注文処理
// ============================================
// 演算子を使った DataPipeline 注文処理
// すべての演算子カテゴリを示す
// ============================================
class Order {
final String id;
final double amount;
String? discountCode;
double? discountPercent;
Order({required this.id, required this.amount});
// 割引計算用の null 安全演算子
double get discountedAmount =>
amount - (amount * (discountPercent ?? 0));
double get tax => discountedAmount * 0.08;
double get total => discountedAmount + tax;
String formatUSD() => '\$${total.toStringAsFixed(2)} USD';
}
class Report {
final List<Order> orders = [];
var totalRevenue = 0.0;
var orderCount = 0;
void addOrder(Order order) {
orders.add(order);
totalRevenue += order.total; // 複合代入
orderCount++;
}
double get averageOrderValue =>
orderCount > 0 ? totalRevenue / orderCount : 0;
bool get isHighVolume => orderCount >= 1000;
String get statusLabel => isHighVolume ? 'High Volume' : 'Normal';
}
void main() {
final report = Report()
..addOrder(Order(id: 'ORD-001', amount: 1500.0)
..discountPercent = 0.1)
..addOrder(Order(id: 'ORD-002', amount: 3250.50)
..discountCode = 'SAVE20'
..discountPercent = 0.2)
..addOrder(Order(id: 'ORD-003', amount: 890.25));
print('=== DataPipeline Order Report ===');
for (final order in report.orders) {
final discount = order.discountPercent != null
? '${(order.discountPercent! * 100).toInt()}%'
: 'None';
print(' ${order.id}: ${order.formatUSD()} (Discount: $discount)');
}
print('\n--- Summary ---');
print('Orders: ${report.orderCount}');
print('Revenue: \$${report.totalRevenue.toStringAsFixed(2)} USD');
print('Average: \$${report.averageOrderValue.toStringAsFixed(2)} USD');
print('Status: ${report.statusLabel}');
// 型テスト
print('\nType check: report is ${report.runtimeType}');
print('Is Report: ${report is Report}');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Order Report ===
ORD-001: $1458.00 USD (Discount: 10%)
ORD-002: $2807.43 USD (Discount: 20%)
ORD-003: $961.47 USD (Discount: None)
--- Summary ---
Orders: 3
Revenue: $5226.90 USD
Average: $1742.30 USD
Status: Normal
Type check: report is Report
Is Report: true
❓ よくある質問
Q:
/と~/の違いは何ですか? A:/は常に double を返します(例:17 / 5 = 3.4)。~/は整数除算の結果を返します(例:17 ~/ 5 = 3)。整数結果が必要なときは~/を使います。
Q: カスケード演算子
..の戻り値は何ですか? A:..は最後のメソッドの戻り値ではなく、オブジェクト自身を返し、チェーンの継続を可能にします。これが通常のメソッド呼び出しとの重要な違いです。
Q:
??=と??の違いは何ですか? A:??は null 合体演算子で、non-null の値を返します。??=は null 認識代入演算子で、変数が null の場合のみ代入し結果を返します。x ??= yはx = x ?? yと同等です。
Q: いつ
asの代わりにisを使うべきですか? A: 型チェックにはisを推奨します。コンパイラが自動型昇格を行うためです。明示的なキャストが必要な場合や型が確実な場合にのみasを使いましょう。誤ったキャストは例外をスローします。
Q:
?..と..はいつ使い分けますか? A: オブジェクトが null の可能性があるときは?..を、確実に non-null なときは..を使います。?..はオブジェクトが null の場合、カスケード全体をスキップします。
Q:
==は参照比較と値比較のどちらですか? A: Dart の==はデフォルトでは参照比較ですが(Java と同様)、多くの組み込み型(String、int、double)は==をオーバーライドして値を比較します。カスタムクラスは==とhashCodeの両方をオーバーライドする必要があります。
Q: ビット単位演算子は実際に頻繁に使われますか? A: 頻繁ではありませんが、権限システム(フラグ)、ネットワークプロトコル解析、暗号化アルゴリズムなどのシナリオでは不可欠です。日々の開発では主に算術演算子と論理演算子が使われます。
📖 まとめ
- 算術演算子
/(double を返す)と~/(整数除算)の違いに注意。 - 論理演算子
&&と||は短絡評価の性質があり、安全アクセスに役立つ。 - カスケード演算子
..はメソッドチェーンを簡素化し、?..はその null 安全版。 - Null 安全性の三本柱:
?.(安全アクセス)、??(null 合体)、??=(null 認識代入)。 - 型テスト:
isは型をチェックして昇格、asは強制キャスト(安全でない場合は例外をスロー)。
📝 練習問題
- 基礎(難易度 ⭐):算術演算子を使って、合計 75,000,000 USD の 1,500,000 件の注文の平均注文額を計算するコードを書いてください。適切な除算演算子を使いましょう。
- 中級(難易度 ⭐⭐):カスケード演算子
..を使ってオブジェクトを作成・設定し、少なくとも 4 つのプロパティを設定して 2 つのメソッドを呼び出してください。その後、カスケードを使わないバージョンと比較しましょう。 - 挑戦(難易度 ⭐⭐⭐):
?.、??、??=演算子を使ってSafeValue<T>クラスを実装し、安全な値アクセス、デフォルト値、遅延代入を提供し、すべての操作で NullPointerException が発生しないことを確認してください。