Dart Null 安全性 — NullPointerException を撲滅する
Null 安全性は Dart の鎧である — コンパイラが null ポインタ攻撃の 95% をブロックする手助けをしてくれる。
1. 学べること
- 堅牢な Null 安全性の原則:Nullable 型
T?vs Non-nullable 型T - null チェックと型昇格
- late キーワード:遅延初期化と安全性保証
!演算子のユースケースとリスク- Bob のシナリオ:DataPipeline のオプション設定の Null 安全性設計
2. 開発者のリアルな物語
(1) 課題:NullPointerException は最も一般的なランタイムクラッシュ
Bob の DataPipeline はランタイムクラッシュに悩まされており、その 60% が NullPointerException だった。API が null の注文 ID を返し、CSV で任意のフィールドが欠落し、設定オプションが設定なしに使われる — どの状況もプログラムをクラッシュさせ、インシデントごとに平均 5 万件の注文に影響した。
(2) 解決策:堅牢な Null 安全性
Dart 3 の堅牢な Null 安全性は型を nullable T? と non-nullable T に分ける。コンパイラはコンパイル時に non-nullable 型が決して null にならないことを保証する。
// Non-nullable: コンパイラが non-null を保証
String orderId = 'ORD-001'; // null にできない
// orderId = null; // コンパイルエラー!
// Nullable: 使用前にチェックが必要
String? nickname; // null になれる
int length = nickname?.length ?? 0; // 安全なアクセス
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- Null ポインタ例外がランタイムからコンパイル時へと移行し、関連バグが 95% 削減
- non-nullable 型により API コントラクトがより明確になる:どのフィールドが必須かがすぐ分かる
- null チェック昇格によりコードがより簡潔になり、手動の型キャストが不要に
3. 堅牢な Null 安全性の原則
(1) 型システム
flowchart TD
A[変数宣言] --> B{null になれるか?}
B -->|はい| C["T? Nullable 型"]
B -->|いいえ| D["T Non-nullable 型"]
C --> E["使用前にチェック必須"]
E --> F["if (x != null) → T に昇格"]
C --> G["?? デフォルト値提供"]
C --> H["!. 強制アンラップ - 危険"]
D --> I["直接使用 - 安全"]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:Nullable vs Non-nullable 型
void main() {
// Non-nullable 型 - null を保持できない
String name = 'DataPipeline';
int count = 100;
double amount = 1500.0;
// name = null; // コンパイルエラー!
// count = null; // コンパイルエラー!
// Nullable 型 - null を保持できる
String? nickname;
int? maxRetries;
double? discountRate;
print(nickname); // null
print(maxRetries); // null
print(discountRate); // null
// Nullable 型は使用前のチェックが必要
// print(nickname.length); // コンパイルエラー!null の可能性
print(nickname?.length); // null (安全)
print(nickname?.length ?? 0); // 0 (デフォルト付き)
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 型 | null を保持可能 | 使用前のチェック | 例 |
|---|---|---|---|
T |
いいえ | 不要 | String name = 'Bob' |
T? |
はい | 必須 | String? name |
4. Null チェックと型昇格
(1) 型昇格
コンパイラが nullable 変数が null でないと確認したとき、自動的に non-nullable 型に昇格する。
▶ サンプル:if チェックによる昇格
void main() {
String? name = 'Bob';
// チェック前 - nullable
// print(name.length); // エラー!
// null チェック後 - non-nullable に昇格
if (name != null) {
print(name.length); // OK!name は String に昇格
print(name.toUpperCase()); // OK!
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:複数の Null チェック方法
void main() {
String? city;
// 方法 1: if-null チェック
if (city != null) {
print(city.length); // 昇格
}
// 方法 2: null 合体演算子 ??
String safeCity = city ?? 'Unknown';
print(safeCity.length); // 常に安全
// 方法 3: null 認識アクセス ?.
int? length = city?.length;
print(length); // null
// 方法 4: late 初期化
late String resolvedCity;
resolvedCity = city ?? 'Unknown';
print(resolvedCity.length); // 安全
// 方法 5: デバッグモードでの assert
assert(city != null, 'City must not be null');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| チェック方法 | 構文 | 昇格効果 | 安全性 |
|---|---|---|---|
| if-null | if (x != null) |
T に昇格 | 最高 |
| ?? | x ?? default |
T を返す | 高 |
| ?. | x?.method() |
T? を返す | 高 |
| ! | x!.method() |
T として扱う | 低(クラッシュ可能性) |
5. late キーワード
(1) 遅延初期化
▶ サンプル:late の基本的な使い方
class DataPipeline {
// late - 後で初期化されるが、使用前は保証される
late String outputPath;
late final DateTime startTime;
void configure(String path) {
outputPath = path; // 最初の代入
}
void start() {
startTime = DateTime.now(); // late final - 1 回設定
print('Pipeline started at $startTime');
}
}
void main() {
final pipeline = DataPipeline();
pipeline.configure('/tmp/reports');
pipeline.start();
// 初期化式付きの Late 初期化
late final int maxRecords = _loadConfig();
// maxRecords は初回アクセス時にのみ計算される
print('Max records: $maxRecords');
}
int _loadConfig() {
print('Loading config...');
return 1000000;
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:late のリスク
class RiskyPipeline {
late String config;
void process() {
// config が設定されていない場合、LateInitializationError をスロー
print(config); // ランタイムクラッシュの可能性!
}
}
void main() {
final pipeline = RiskyPipeline();
// pipeline.process(); // LateInitializationError!
// 安全なパターン:コンストラクタで初期化
final safePipeline = SafePipeline('/etc/config.yaml');
safePipeline.process(); // OK
}
class SafePipeline {
late String config;
SafePipeline(String configPath) {
config = _loadConfig(configPath);
}
void process() => print('Config: $config');
String _loadConfig(String path) => 'Loaded from $path';
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
late の形式 |
初期化 | 代入回数 | リスク |
|---|---|---|---|
late T |
使用前 | 複数 | 中(初期化前アクセスでクラッシュ) |
late final T |
使用前 | 1 | 中 |
late final T = expr |
初回アクセス時 | 1(自動) | 低 |
6. ! 演算子
(1) 強制アンラップ
! はコンパイラに「この値は null でないと確信している」と伝え、null チェックをスキップする。ただし値が null の場合、TypeError をスローする。
▶ サンプル:! の使用
void main() {
String? name = 'Bob';
// 値が non-null だと知っている場合
print(name!.length); // 3 - name が 'Bob' なので OK
// 危険:null の場合クラッシュ!
String? maybeNull;
// print(maybeNull!.length); // ランタイム TypeError!
// 安全な代替:?? または if-null チェックを使用
print(maybeNull?.length ?? 0); // 0 - 安全
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(2) ! を使うべき場面
▶ サンプル:! の合理的なユースケース
class Order {
final String id;
final double amount;
Customer? customer; // オプション関係
Order({required this.id, required this.amount});
// ビジネスロジックが non-null を保証するときに ! を使用
String get customerName {
// これは危険 - customer は null の可能性
// return customer!.name; // 悪い
// より安全:デフォルト付きセーフアクセス
return customer?.name ?? 'Unknown Customer';
}
}
class Customer {
final String name;
Customer(this.name);
}
void main() {
final order = Order(id: 'ORD-001', amount: 1500.0);
print(order.customerName); // Unknown Customer
// customer 設定後
order.customer = Customer('Alice');
print(order.customerName); // Alice
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| ユースケース | 推奨 | 非推奨 |
|---|---|---|
| nullable 変数 | ?. / ?? / if-null |
! |
| API 戻り値 | null チェック | ! |
コレクションの .first |
.firstOrNull ?? default |
.first! |
| アサーション後 | !(アサーションが non-null を保証) |
— |
7. Null 安全性とコレクション
▶ サンプル:コレクションでの Null 処理
void main() {
// nullable 要素を持つ List
List<String?> names = ['Alice', null, 'Bob', null, 'Charlie'];
// null をフィルタ - 型昇格が機能
final nonNull = names.whereType<String>().toList();
print(nonNull); // [Alice, Bob, Charlie]
// nullable 値を持つ Map
Map<String, double?> revenue = {
'Electronics': 3600.0,
'Books': null,
'Clothing': 890.0,
};
// non-null 値を持つエントリをフィルタ
final validRevenue = Map.fromEntries(
revenue.entries.where((e) => e.value != null),
);
print(validRevenue); // {Electronics: 3600.0, Clothing: 890.0}
// デフォルト付きセーフアクセス
final booksRevenue = revenue['Books'] ?? 0;
print(booksRevenue); // 0
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. Bob のシナリオ:DataPipeline の Null 安全性設計
▶ サンプル:オプション設定の Null 安全性設計
class PipelineConfig {
// 必須フィールド - non-nullable
final String appName;
final String version;
// オプションフィールド - デフォルト付き nullable
final String? outputPath;
final String? logLevel;
final double? customTaxRate;
final int? maxRetries;
// オプションから計算 - non-nullable
String get effectiveOutputPath => outputPath ?? '/tmp/datapipeline/output';
String get effectiveLogLevel => logLevel ?? 'info';
double get effectiveTaxRate => customTaxRate ?? 0.08;
int get effectiveMaxRetries => maxRetries ?? 3;
const PipelineConfig({
required this.appName,
required this.version,
this.outputPath,
this.logLevel,
this.customTaxRate,
this.maxRetries,
});
String get summary => '''
$appName v$version
Output: $effectiveOutputPath
Log: $effectiveLogLevel
Tax: ${(effectiveTaxRate * 100).toStringAsFixed(1)}%
Retries: $effectiveMaxRetries
''';
}
void main() {
// 最小限の設定 - 必須フィールドのみ
final minimal = PipelineConfig(
appName: 'DataPipeline',
version: '1.0.0',
);
print(minimal.summary);
// 完全な設定
final full = PipelineConfig(
appName: 'DataPipeline',
version: '2.0.0',
outputPath: '/data/reports',
logLevel: 'debug',
customTaxRate: 0.10,
maxRetries: 5,
);
print(full.summary);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
9. 完全なサンプル:DataPipeline の Null 安全注文処理
// ============================================
// DataPipeline Null 安全注文処理
// Null 安全性パターンの完全な実演
// ============================================
class Customer {
final String name;
final String? email;
final Address? address;
Customer({required this.name, this.email, this.address});
String get displayName => email ?? name;
String get city => address?.city ?? 'Unknown City';
}
class Address {
final String city;
final String? state;
final String country;
Address({required this.city, this.state, required this.country});
String get fullRegion => state != null ? '$city, $state' : city;
}
class Order {
final String id;
final double amount;
final String status;
final Customer? customer;
final String? discountCode;
final double? discountPercent;
Order({
required this.id,
required this.amount,
required this.status,
this.customer,
this.discountCode,
this.discountPercent,
}) : assert(amount > 0, 'Amount must be positive');
double get effectiveDiscount => discountPercent ?? 0;
double get discountedAmount => amount * (1 - effectiveDiscount);
double get tax => discountedAmount * 0.08;
double get total => discountedAmount + tax;
String get customerName => customer?.name ?? 'Guest';
String get customerCity => customer?.city ?? 'Unknown';
String get formattedTotal =>
'\$${total.toStringAsFixed(2)} USD';
@override
String toString() =>
'Order($id, $customerName, ${formattedTotal}, $status)';
}
class OrderProcessor {
final List<Order> _orders = [];
final List<String> _warnings = [];
void addOrder(Order order) {
_orders.add(order);
// 警告付きの Null 安全チェック
if (order.customer == null) {
_warnings.add('Order ${order.id}: No customer assigned');
}
if (order.discountCode != null && order.discountPercent == null) {
_warnings.add('Order ${order.id}: Discount code without rate');
}
}
double get totalRevenue =>
_orders.fold(0.0, (sum, o) => sum + o.total);
List<Order> get ordersWithCustomers =>
_orders.where((o) => o.customer != null).toList();
Map<String, double> revenueByCity() {
final result = <String, double>{};
for (final order in _orders) {
final city = order.customerCity;
result.update(city, (v) => v + order.total, ifAbsent: () => order.total);
}
return result;
}
void printReport() {
print('=== DataPipeline Order Report ===');
print('Orders: ${_orders.length}');
print('Revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
print('With customers: ${ordersWithCustomers.length}');
print('\nRevenue by City:');
for (final entry in revenueByCity().entries) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
if (_warnings.isNotEmpty) {
print('\nWarnings:');
for (final w in _warnings) {
print(' $w');
}
}
}
}
void main() {
final processor = OrderProcessor();
processor.addOrder(Order(
id: 'ORD-001',
amount: 1500.0,
status: 'completed',
customer: Customer(
name: 'Alice',
email: 'alice@example.com',
address: Address(city: 'New York', state: 'NY', country: 'US'),
),
discountPercent: 0.10,
));
processor.addOrder(Order(
id: 'ORD-002',
amount: 3200.0,
status: 'completed',
// customer なし - nullable フィールド
));
processor.addOrder(Order(
id: 'ORD-003',
amount: 890.0,
status: 'pending',
customer: Customer(
name: 'Bob',
address: Address(city: 'London', country: 'UK'),
),
discountCode: 'SAVE20',
// discountPercent は null - 警告!
));
processor.printReport();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Order Report ===
Orders: 3
Revenue: $5172.36 USD
With customers: 2
Revenue by City:
New York: $1458.00 USD
Unknown: $3456.00 USD
London: $961.20 USD
Warnings:
Order ORD-002: No customer assigned
Order ORD-003: Discount code without rate
❓ よくある質問
Q: 堅牢な Null 安全性と非堅牢の違いは何ですか? A: 堅牢な Null 安全性は、non-nullable 型がプログラム全体で決して null にならないことを保証します(コンパイラがグローバル検証を実行)。非堅牢モード(廃止済み)は、特定のパスがチェックをバイパスすることを許可します。Dart 3 はデフォルトで堅牢です。
Q: nullable 型の代わりにいつ
lateを使うべきですか? A: 使用前に必ず初期化されることが確実で、null にしたくない変数にはlateを使ってください。変数が本当に null になれる場合(意味的にオプション)はT?を使ってください。
Q:
!は完全に避けるべきですか? A: 完全にではありませんが、控えめに使ってください。合理的なシナリオには、アサーションの後、テストコード、またはビジネスロジックが明示的に non-null を保証する場合が含まれます。ほとんどの場合、??または?.の方が安全です。
Q: null チェック昇格はクロージャ内で有効ですか? A: 常にではありません。変数がクロージャ外で変更される可能性がある場合、コンパイラは昇格を保証できません。ローカル
final変数にキャプチャして解決してください。
Q: ジェネリック型パラメータはデフォルトで nullable ですか? A: いいえ。
Tはデフォルトで non-nullable、T?は nullable です。これによりジェネリック型の安全性が保証されます。T?は実際にはTのスーパークラスです。
Q: JSON パースで多くの nullable フィールドを処理する方法は? A:
??を使ってデフォルト値を提供するか、json_serializableパッケージを使ってデフォルト値付きのfromJsonメソッドを自動生成することを推奨します。あらゆる場所で!を使うのは避けてください。
Q:
late finalとfinalの違いは何ですか? A:finalは宣言時またはコンストラクタの初期化リストで代入する必要があります。late finalはコンストラクタ本体または後続のメソッドで最初に代入できます。late finalは代入のタイミングを遅らせます。
📖 まとめ
- 堅牢な Null 安全性は型を T(non-nullable)と T?(nullable)に分け、コンパイラ全体で保証する
- null チェック後、コンパイラは自動的に型昇格を行い、手動の型キャストが不要になる
lateは遅延初期化を可能にするが、初期化前のアクセスはクラッシュを引き起こす;late final+ 初期化式が最も安全!強制アンラップは危険;代替として??または?.を推奨- DataPipeline 設定設計パターン:必須フィールドは non-nullable、オプションフィールドは nullable + ゲッターがデフォルト値を提供
📝 練習問題
- 基礎(難易度 ⭐):5 つの変数(2 つの non-nullable、3 つの nullable)を宣言してください。if-null チェック、
??、?.を使って 3 つの異なる方法で nullable 変数に安全にアクセスし、結果を出力します。 - 中級(難易度 ⭐⭐):
lateを使って 3 つのフィールドの初期化を遅延するConfigクラスを設計し、configure()メソッドでそれらを設定してください。意図的にconfigure()を呼び出さずフィールドにアクセスし、LateInitializationErrorを観察してください。 - 挑戦(難易度 ⭐⭐⭐):すべてのネットワークリクエストが
Result<T?>型を返す NullSafe API クライアントを実装してください。サーバーが null を返す場合、フィールドが欠落している場合、型が一致しない場合の 3 つをすべて!を使わずに正しく処理します。