DartのDirectoryクラスでよくあるNull Safetyエラーと解決策

2025-05-26

Null safetyの基本的な考え方

  1. Non-nullable by Default(デフォルトで非Null可能): Dartでは、明示的にnullを許可しない限り、すべての型は非Null可能(non-nullable)です。つまり、変数にnullを代入しようとすると、コンパイル時にエラーが発生します。

    int myNumber = 10;
    // myNumber = null; // エラー: A value of type 'Null' can't be assigned to a variable of type 'int'.
    
  2. Nullable Types(Null許容型): 変数にnullを代入できるようにするには、型の後ろに?を付けます。

    String? myName; // myNameはnullまたはStringを保持できる
    myName = "Alice";
    myName = null; // OK
    
  3. Sound Null Safety(健全なNull safety): DartのNull safetyは「健全(Sound)」です。これは、Null許容型ではないと推論された値が、実行時に絶対にnullにならないことを保証します。これにより、コンパイラは不要なNullチェックを省略できるため、より小さく、より高速なコードを生成できます。

DirectoryクラスとNull safety

dart:ioライブラリのDirectoryクラスは、ファイルシステムのディレクトリ(フォルダ)を操作するためのものです。Directoryクラス自体がnullになることはありませんが、Directoryクラスのメソッドが返す値や、そのメソッドに渡す引数でNull safetyが関係してきます。

具体的な例

  1. コンストラクタ: Directoryのコンストラクタはパス(String)を受け取ります。このパスは通常、非Null可能です。

    import 'dart:io';
    
    void main() {
      // 非Null可能なパス
      Directory myDir = Directory('/path/to/my_directory');
    
      // Nullableなパスを渡す場合、事前にnullチェックが必要
      String? nullablePath;
      // Directory otherDir = Directory(nullablePath); // エラー: Argument type 'String?' can't be assigned to the parameter type 'String'.
    
      if (nullablePath != null) {
        Directory otherDir = Directory(nullablePath); // OK
      }
    }
    
  2. existsSync() / createSync() などのメソッド: これらのメソッドはDirectoryインスタンスに対して呼び出されるため、Directoryインスタンスがnullである可能性を心配する必要はありません。

    import 'dart:io';
    
    void main() {
      Directory dir = Directory('my_new_directory');
    
      if (!dir.existsSync()) {
        dir.createSync();
        print('ディレクトリが作成されました: ${dir.path}');
      } else {
        print('ディレクトリは既に存在します: ${dir.path}');
      }
    }
    
  3. list() / listSync() などのリストを返すメソッド: これらのメソッドはFileSystemEntityのリストを返します。リスト自体やリスト内の要素がnullになることは、通常のnull safetyの原則に従っていればありません。

    import 'dart:io';
    
    void main() async {
      Directory currentDir = Directory.current;
      await for (var entity in currentDir.list()) {
        print(entity.path);
      }
    }
    

    ただし、listSync()などでエラーが発生した場合に、特定の例外をスローするなど、エラーハンドリングは別途行う必要があります。

  • パフォーマンスの向上: 健全なNull safetyにより、コンパイラは不要なNullチェックを最適化できるため、生成されるバイナリが小さくなり、実行速度が向上する可能性があります。
  • 開発効率の向上: コンパイラがNull関連のエラーを指摘してくれるため、デバッグ時間が短縮されます。
  • コードの信頼性向上: Nullになる可能性のある場所が明確になり、それらを適切に処理するように強制されるため、より堅牢なコードになります。
  • Null参照エラーの防止: 実行時に最もよく発生するエラーの一つであるNullPointerExceptionを、開発段階で発見し、修正できます。


よくあるエラー

エラー1: NullableなStringDirectoryコンストラクタに渡そうとする

Directoryのコンストラクタは非NullableなString型のパスを期待します。Null許容型のString?を直接渡そうとするとコンパイルエラーになります。

エラーメッセージの例

A value of type 'String?' can't be assigned to a parameter of type 'String' in a constructor invocation.

誤ったコードの例

String? myDirPath; // nullかもしれないパス
// myDirPath = '/path/to/dir'; // ここで初期化されるかもしれない

Directory myDir = Directory(myDirPath); // ここでエラー

エラー2: Null許容型Directory?を使用しているが、Nullチェックを怠っている

Directoryインスタンス自体がNull許容型として宣言されている場合に、Nullチェックなしにメソッドを呼び出そうとするとエラーになります。

エラーメッセージの例

The receiver can't be null, because the method 'existsSync' is unconditional.

誤ったコードの例

Directory? maybeDir; // 何らかの理由でDirectoryがnullになる可能性があると想定
// maybeDir = Directory('/some/path'); // ここで初期化されるかもしれない

if (maybeDir.existsSync()) { // ここでエラー
  // ...
}

エラー3: 非同期処理でのNull許容型の扱い忘れ

Future<Directory?>のような形で非同期的にDirectoryインスタンスを取得する際に、結果がnullになる可能性があることを考慮せずに、直接!演算子(Null assertion operator)で強制的に非Nullとして扱ってしまう。

誤ったコードの例

Future<Directory?> getDirectorySafely(String path) async {
  // 実際には存在しないパスやアクセス権がない場合などにnullを返す可能性がある
  if (path == 'invalid') {
    return null;
  }
  return Directory(path);
}

void main() async {
  Directory myDir = await getDirectorySafely('invalid')!; // ここでNullPointerException(実行時エラー)
  // ! は「絶対にnullではない」とコンパイラに伝えるが、実際にはnullになる可能性があり、実行時にクラッシュする
  if (myDir.existsSync()) {
    print('存在する');
  }
}

解決策1: NullableなStringDirectoryコンストラクタに渡す場合の対応

Null許容型のパスを受け取る場合は、nullチェックを行うか、Null合体演算子(??)を使用してデフォルト値を指定するなどして、非NullableなStringをコンストラクタに渡す必要があります。

修正例

String? myDirPath;
// myDirPath = '/path/to/dir'; // ここで初期化されるかもしれない

if (myDirPath != null) {
  Directory myDir = Directory(myDirPath); // OK
  // ...
} else {
  print('パスが指定されていません。');
}

// あるいは、デフォルトパスを指定する場合
String effectivePath = myDirPath ?? '/default/path';
Directory defaultDir = Directory(effectivePath); // OK

解決策2: Null許容型Directory?のNullチェック

Null許容型のDirectory?を使用する場合は、メソッドを呼び出す前に必ずnullチェックを行う必要があります。

修正例

Directory? maybeDir;
maybeDir = Directory('/some/path'); // ここで初期化される

if (maybeDir != null) { // Nullチェック
  if (maybeDir.existsSync()) { // OK
    print('ディレクトリは存在します。');
  }
} else {
  print('ディレクトリが見つかりませんでした。');
}

// あるいは、Null許容型のチェーン演算子 `?.` を使用する方法
// 存在する場合のみ実行される
if (maybeDir?.existsSync() == true) { // nullの場合はfalseになる
  print('ディレクトリは存在します。');
} else {
  print('ディレクトリが存在しないか、nullです。');
}

解決策3: 非同期処理でのNull許容型の慎重な扱い

非同期処理でDirectory?を返す可能性がある場合は、awaitの結果を慎重に扱い、nullである可能性を考慮したコードを書く必要があります。!演算子を使う場合は、本当にnullにならないという確信がある場合にのみ使用してください。

Future<Directory?> getDirectorySafely(String path) async {
  if (path == 'invalid') {
    return null;
  }
  return Directory(path);
}

void main() async {
  Directory? resultDir = await getDirectorySafely('invalid'); // Directory? 型で受け取る

  if (resultDir != null) { // Nullチェック
    if (resultDir.existsSync()) {
      print('ディレクトリは存在します: ${resultDir.path}');
    } else {
      print('ディレクトリは存在しません: ${resultDir.path}');
    }
  } else {
    print('ディレクトリの取得に失敗しました (nullが返されました)。');
  }

  // あるいは、別な有効なパスで試す
  resultDir = await getDirectorySafely('/valid/path');
  if (resultDir != null) {
    print('ディレクトリが存在します: ${resultDir.path}');
  }
}
  • Null assertion operator (!) の慎重な使用
    !は「この値はnullではないことをコンパイラに保証する」という強い意味を持ちます。本当にnullにならないと確信できる場合(例えば、直前のif (x != null)チェックの後など)にのみ使用してください。誤用すると、Null safetyのメリットを損ない、実行時エラーにつながります。

    Directory? maybeDir = Directory('/some/path');
    
    if (maybeDir != null) {
      // Nullチェックの後なので、ここでは安全に ! を使える
      maybeDir!.createSync(); // この場合、! は冗長だが、使用しても安全
    }
    
  • lateキーワードの利用
    変数を後で初期化することが保証されているが、すぐに初期化できない場合にlateキーワードを使用できます。ただし、初期化される前にアクセスすると実行時エラーになります。

    late Directory myLateDir;
    
    void initializeDir() {
      myLateDir = Directory('/initialized/path');
    }
    
    void main() {
      initializeDir(); // 必ずアクセス前に初期化すること
      if (myLateDir.existsSync()) { // OK
        print('lateディレクトリは存在します。');
      }
    }
    


例1: 基本的なDirectoryの作成とNull safety (パスの扱い)

この例では、非Nullableなパスを使ってディレクトリを作成し、存在しない場合にのみ作成します。Null許容なパスを受け取る場合の処理も示します。

import 'dart:io';

void main() {
  // --- 非Nullableなパスを使用する一般的なケース ---
  print('--- 例1: 非Nullableなパス ---');
  String dirPath = 'my_test_directory';
  Directory myDir = Directory(dirPath); // パスは非Nullable

  if (!myDir.existsSync()) {
    myDir.createSync();
    print('ディレクトリが作成されました: ${myDir.path}');
  } else {
    print('ディレクトリは既に存在します: ${myDir.path}');
  }

  // --- Null許容なパスを受け取る場合の処理 ---
  print('\n--- 例2: Null許容なパスの安全な扱い ---');
  String? nullableDirPath; // nullかもしれないパス

  // nullチェックせずに直接渡すとコンパイルエラー
  // Directory problematicDir = Directory(nullableDirPath); // エラー: A value of type 'String?' can't be assigned to a parameter of type 'String'.

  // 安全な方法1: nullチェック
  if (nullableDirPath != null) {
    Directory safeDir = Directory(nullableDirPath);
    print('nullチェック後、ディレクトリが処理されました: ${safeDir.path}');
  } else {
    print('パスがnullのため、ディレクトリを処理できませんでした。');
  }

  // 安全な方法2: Null合体演算子 (??) を使用してデフォルト値を指定
  String effectivePath = nullableDirPath ?? 'default_directory';
  Directory defaultDir = Directory(effectivePath);
  print('Null合体演算子で処理後、ディレクトリが処理されました: ${defaultDir.path}');
  if (!defaultDir.existsSync()) {
    defaultDir.createSync();
    print('デフォルトディレクトリが作成されました: ${defaultDir.path}');
  } else {
    print('デフォルトディレクトリは既に存在します: ${defaultDir.path}');
  }

  // クリーンアップ (オプション)
  try {
    if (myDir.existsSync()) {
      myDir.deleteSync(recursive: true);
      print('クリーンアップ: ${myDir.path} を削除しました。');
    }
    if (defaultDir.existsSync()) {
      defaultDir.deleteSync(recursive: true);
      print('クリーンアップ: ${defaultDir.path} を削除しました。');
    }
  } catch (e) {
    print('クリーンアップ中にエラーが発生しました: $e');
  }
}

例2: DirectoryインスタンスがNull許容である場合のNull safety

この例では、何らかの理由でDirectoryインスタンスがnullになる可能性がある(例:ファクトリ関数が失敗した場合)と想定し、その場合の安全なアクセス方法を示します。

import 'dart:io';

// ディレクトリを作成し、成功すればDirectory、失敗すればnullを返すファクトリ関数を模倣
Directory? createDirectorySafely(String path) {
  try {
    final dir = Directory(path);
    if (!dir.existsSync()) {
      dir.createSync(recursive: true);
      print('関数内でディレクトリを作成しました: ${dir.path}');
    } else {
      print('関数内でディレクトリは既に存在します: ${dir.path}');
    }
    return dir;
  } catch (e) {
    print('ディレクトリ作成中にエラーが発生しました: $e');
    return null; // エラーが発生した場合はnullを返す
  }
}

void main() {
  print('--- 例2: Null許容なDirectoryインスタンスの扱い ---');

  // 成功するケース
  Directory? successfulDir = createDirectorySafely('successful_dir');
  if (successfulDir != null) { // nullチェックを行う
    print('成功したディレクトリのパス: ${successfulDir.path}');
    // Nullチェック済みなので、安全にメソッドを呼び出せる
    if (successfulDir.existsSync()) {
      print('成功したディレクトリは存在します。');
    }
  } else {
    print('ディレクトリの作成に失敗しました (nullが返されました)。');
  }

  print('\n--- エラーをシミュレートするケース ---');
  // 意図的に失敗させるために、無効なパス(書き込み権限がないルートなど)を渡す
  // ただし、この例は実行環境によって結果が異なる場合があります。
  // 通常は、意図的にnullを返すようにロジックを変更する方が安全です。
  Directory? failedDir = createDirectorySafely('/root/no_permission_dir'); // Linuxなどで書き込み権限がない場所

  if (failedDir != null) { // ここでnullチェックを行う
    print('失敗すると予想されたディレクトリのパス: ${failedDir.path}');
    if (failedDir.existsSync()) {
      print('失敗すると予想されたディレクトリは存在します。');
    }
  } else {
    print('ディレクトリの作成に失敗しました (nullが返されました)。');
  }

  // Null許容型のチェーン演算子 `?.` を使用した安全なアクセス
  print('\n--- Null許容型のチェーン演算子 `?.` の使用 ---');
  Directory? anotherDir = createDirectorySafely('another_dir');

  // `?.` を使用すると、左辺がnullの場合、右辺のメソッド呼び出し全体がnullを返し、エラーにならない
  if (anotherDir?.existsSync() == true) { // anotherDirがnullの場合、existsSync()は呼び出されず、式全体がnullとなり、== trueはfalseになる
    print('anotherDirは存在します。');
  } else {
    print('anotherDirは存在しないか、nullです。');
  }

  // クリーンアップ (オプション)
  try {
    if (successfulDir != null && successfulDir.existsSync()) {
      successfulDir.deleteSync(recursive: true);
      print('クリーンアップ: ${successfulDir.path} を削除しました。');
    }
    if (anotherDir != null && anotherDir.existsSync()) {
      anotherDir.deleteSync(recursive: true);
      print('クリーンアップ: ${anotherDir.path} を削除しました。');
    }
    // failedDirはnullである可能性が高いので、削除を試みる必要はないかもしれません。
  } catch (e) {
    print('クリーンアップ中にエラーが発生しました: $e');
  }
}

ファイル操作はしばしば非同期で行われます。この例では、非同期でDirectoryインスタンスを取得する際に、結果がnullになる可能性を考慮したコードを示します。

import 'dart:io';
import 'dart:async'; // Futureを使うため

// 非同期でディレクトリを取得する関数。失敗した場合はnullを返す可能性あり。
Future<Directory?> getAsyncDirectory(String path, {bool simulateError = false}) async {
  await Future.delayed(Duration(milliseconds: 100)); // 非同期処理を模倣

  if (simulateError) {
    print('非同期関数: エラーをシミュレートしてnullを返します。');
    return null;
  }

  try {
    final dir = Directory(path);
    if (!await dir.exists()) { // 非同期版のexists()を使用
      await dir.create(recursive: true); // 非同期版のcreate()を使用
      print('非同期関数: ディレクトリが作成されました: ${dir.path}');
    } else {
      print('非同期関数: ディレクトリは既に存在します: ${dir.path}');
    }
    return dir;
  } catch (e) {
    print('非同期関数: ディレクトリ処理中にエラーが発生しました: $e');
    return null; // エラー時はnullを返す
  }
}

void main() async {
  print('--- 例3: 非同期処理とNull safety ---');

  // 成功するケース
  Directory? resultDir = await getAsyncDirectory('async_test_dir');

  if (resultDir != null) { // nullチェックを行う
    print('非同期で取得したディレクトリのパス: ${resultDir.path}');
    if (await resultDir.exists()) { // 非同期版のexists()を使用
      print('非同期で取得したディレクトリは存在します。');
    }
  } else {
    print('非同期でディレクトリの取得に失敗しました (nullが返されました)。');
  }

  print('\n--- 非同期でエラーをシミュレートするケース ---');
  Directory? errorDir = await getAsyncDirectory('another_async_dir', simulateError: true);

  if (errorDir != null) {
    print('エラーをシミュレートしたが、ディレクトリが取得できました: ${errorDir.path}');
  } else {
    print('期待通り、非同期でディレクトリの取得に失敗しました (nullが返されました)。');
  }

  // --- Null assertion operator (`!`) の危険な使用例 (非推奨) ---
  // 開発者が「絶対にnullにならない」と誤って確信している場合
  // `getAsyncDirectory('non_existent_path', simulateError: true)!` のようにすると
  // 実行時にNullPointerExceptionが発生する可能性がある
  // `main` 関数が `async` なので、`try-catch` で捕捉できますが、コードは壊れやすくなります。
  try {
    Directory dangerouslyAccessedDir = await getAsyncDirectory('dangerous_dir', simulateError: true)!;
    print('危険なアクセス: ${dangerouslyAccessedDir.path}'); // ここには到達しないはず
  } catch (e) {
    print('危険なアクセスでエラーが発生しました (Null safety違反): $e');
  }


  // クリーンアップ (オプション)
  try {
    if (resultDir != null && await resultDir.exists()) {
      await resultDir.delete(recursive: true);
      print('クリーンアップ: ${resultDir.path} を削除しました。');
    }
  } catch (e) {
    print('クリーンアップ中にエラーが発生しました: $e');
  }
}


path パッケージの活用

path パッケージは、OS に依存しないパス操作を提供します。これにより、パスの構築や結合が Null safety の観点からも安全かつポータブルに行えます。

import 'package:path/path.dart' as p;
import 'dart:io';

void main() {
  String baseDir = Directory.current.path; // 現在のディレクトリのパスを取得
  String? subDirName = 'data'; // Null許容のサブディレクトリ名
  String? fileName = 'config.txt'; // Null許容のファイル名

  // 安全なパスの結合
  // p.joinAll() は Nullable な要素を直接受け付けないため、事前に null チェックやデフォルト値の設定が必要
  String? fullDirPath;
  if (subDirName != null) {
    fullDirPath = p.join(baseDir, subDirName);
  }

  Directory? myDir;
  if (fullDirPath != null) {
    myDir = Directory(fullDirPath);
    if (!myDir.existsSync()) {
      myDir.createSync();
      print('ディレクトリ作成: ${myDir.path}');
    }
  } else {
    print('サブディレクトリ名がnullのため、ディレクトリを作成できませんでした。');
  }

  // ファイルパスの結合
  String? fullFilePath;
  if (myDir != null && fileName != null) {
    fullFilePath = p.join(myDir.path, fileName);
  }

  if (fullFilePath != null) {
    File myFile = File(fullFilePath);
    myFile.writeAsStringSync('Hello, Null Safety!');
    print('ファイル作成: ${myFile.path}');
  } else {
    print('ファイル名またはディレクトリがnullのため、ファイルを作成できませんでした。');
  }

  // クリーンアップ
  if (myDir != null && myDir.existsSync()) {
    myDir.deleteSync(recursive: true);
    print('クリーンアップ: ${myDir.path} を削除しました。');
  }
}

ポイント

  • これにより、パスの構築段階で Null が混入することを防ぎ、Directory コンストラクタに常に有効なパスを渡すことができます。
  • p.join()p.joinAll() は、Null許容型の文字列を直接結合しようとするとコンパイルエラーになります。そのため、結合前にNullチェックを行うか、Null合体演算子 (??) でデフォルト値を指定するなどして、非Null許容の文字列を渡す必要があります。

Result 型(または Either 型)を用いたエラーハンドリング

Null safety は NullPointerException を防ぐのに役立ちますが、null が返されること自体は「エラー」または「意図しない状態」を示す場合があります。このような場合に、null を返す代わりに、操作の成功/失敗とその理由を明示的に示す Result 型(または関数型プログラミングにおける Either 型)を使用することがあります。

これはDartのコアライブラリには含まれていませんが、package:resultpackage:fpdart などの外部パッケージを利用するか、自分でシンプルな Result 型を定義できます。

シンプルな Result 型の例

enum OperationFailure {
  directoryExists,
  permissionDenied,
  invalidPath,
  unknownError,
}

class Result<T, E> {
  final T? value;
  final E? error;
  final bool isSuccess;

  Result.success(this.value) : error = null, isSuccess = true;
  Result.failure(this.error) : value = null, isSuccess = false;
}

Future<Result<Directory, OperationFailure>> createDirectoryRobustly(String path) async {
  await Future.delayed(Duration(milliseconds: 50)); // 非同期処理を模倣
  if (path.isEmpty || path.contains('..')) { // 不正なパスの例
    return Result.failure(OperationFailure.invalidPath);
  }

  try {
    final dir = Directory(path);
    if (await dir.exists()) {
      return Result.failure(OperationFailure.directoryExists);
    }
    await dir.create(recursive: true);
    return Result.success(dir);
  } on PathNotFoundException {
    return Result.failure(OperationFailure.invalidPath);
  } on FileSystemException catch (e) {
    if (e.message.contains('Permission denied')) {
      return Result.failure(OperationFailure.permissionDenied);
    }
    return Result.failure(OperationFailure.unknownError);
  } catch (e) {
    return Result.failure(OperationFailure.unknownError);
  }
}

void main() async {
  print('--- Result 型を用いたエラーハンドリング ---');

  // 成功するケース
  var result1 = await createDirectoryRobustly('my_robust_dir');
  if (result1.isSuccess) {
    print('ディレクトリが正常に作成されました: ${result1.value!.path}');
    // クリーンアップ
    await result1.value!.delete(recursive: true);
  } else {
    print('ディレクトリ作成に失敗しました: ${result1.error}');
  }

  // 既に存在するディレクトリを作成しようとするケース
  var result2 = await createDirectoryRobustly('my_robust_dir'); // 1回目で作成される
  if (result2.isSuccess) {
    print('ディレクトリが正常に作成されました (これは表示されないはず): ${result2.value!.path}');
  } else {
    print('ディレクトリ作成に失敗しました: ${result2.error}'); // OperationFailure.directoryExists
  }

  // 無効なパスのケース
  var result3 = await createDirectoryRobustly('');
  if (result3.isSuccess) {
    print('ディレクトリが正常に作成されました (これは表示されないはず): ${result3.value!.path}');
  } else {
    print('ディレクトリ作成に失敗しました: ${result3.error}'); // OperationFailure.invalidPath
  }

  // 権限エラーのシミュレート(環境によっては実際に発生しない)
  // 意図的にエラーを起こすために、書き込み権限のないシステムパスを試す
  var result4 = await createDirectoryRobustly('/root/protected_dir'); // Linuxの場合など
  if (result4.isSuccess) {
    print('ディレクトリが正常に作成されました (これは表示されないはず): ${result4.value!.path}');
  } else {
    print('ディレクトリ作成に失敗しました: ${result4.error}'); // OperationFailure.permissionDenied または OperationFailure.unknownError
  }
}

ポイント

  • エラーの種類をenumなどで明確にすることで、デバッグが容易になり、より詳細なエラーハンドリングが可能になります。
  • 呼び出し元はisSuccessプロパティで結果を簡単に判断でき、成功した場合はvalueに、失敗した場合はerrorにアクセスできます。
  • nullを返す代わりに、成功時にはResult.success(value)、失敗時にはResult.failure(error)を返します。

Dartのdart:ioライブラリは、ファイル操作に関連する多くのエラーを特定の例外としてスローします。Null safetyはnull参照を防ぐものですが、ファイルが存在しない、権限がないといった「論理的なエラー」は例外として扱われるべきです。

import 'dart:io';

void main() async {
  print('--- try-catch を用いた例外ハンドリング ---');
  String dirPath = 'error_handling_dir';
  Directory dir = Directory(dirPath);

  try {
    // ディレクトリが存在しない場合、createSync() は新しいディレクトリを作成する
    // 既に存在する場合、何もせず続行
    if (!await dir.exists()) {
      await dir.create(recursive: true); // await dir.createSync(recursive: true); でも可
      print('ディレクトリが作成されました: ${dir.path}');
    } else {
      print('ディレクトリは既に存在します: ${dir.path}');
    }

    // 例えば、存在しないファイルを削除しようとすると
    File nonExistentFile = File(p.join(dir.path, 'non_existent_file.txt'));
    await nonExistentFile.delete(); // FileSystemException をスローする可能性がある
    print('ファイルが削除されました (これは表示されないはず)。');

  } on PathNotFoundException catch (e) {
    print('エラー: 指定されたパスが見つかりません。 ${e.path}');
  } on FileSystemException catch (e) {
    // Permission denied や Read-only file system など
    print('ファイルシステムエラー: ${e.message} (パス: ${e.path})');
  } catch (e) {
    print('その他のエラーが発生しました: $e');
  } finally {
    // 最終的にクリーンアップを行う
    if (await dir.exists()) {
      await dir.delete(recursive: true);
      print('クリーンアップ: ${dir.path} を削除しました。');
    }
  }

  // 権限エラーのシミュレーション(環境依存)
  print('\n--- 権限エラーのシミュレーション ---');
  Directory protectedDir = Directory('/root/forbidden_dir'); // Linuxの例

  try {
    if (!await protectedDir.exists()) {
      await protectedDir.create(recursive: true);
      print('protectedDir が作成されました (これは表示されないはず): ${protectedDir.path}');
    }
  } on FileSystemException catch (e) {
    print('権限エラーを捕捉しました: ${e.message} (パス: ${e.path})');
  } catch (e) {
    print('その他のエラー: $e');
  }
}

ポイント

  • finallyブロックは、成功/失敗にかかわらず実行されるため、リソースのクリーンアップ(例:作成したディレクトリの削除)に適しています。
  • それでも発生しうる例外(PathNotFoundException, FileSystemExceptionなど)をtry-catchで捕捉し、エラーの種類に応じて適切な処理を行います。
  • existsSync()exists() などで事前に存在チェックを行うことで、不要な例外スローを減らすことができます。

DartのDirectoryクラスとNull safetyをプログラミングする際の代替アプローチは以下の通りです。

  1. path パッケージ: パスの構築と結合を OS 依存せずに安全に行い、Directory コンストラクタに常に非Null許容の有効なパスを渡すことを保証します。
  2. Result: null を返す代わりに、操作の成功/失敗を明示的な型で表現し、エラーの種類を詳細に伝えることで、より堅牢で読みやすいエラーハンドリングを実現します。
  3. try-catch と明示的な例外ハンドリング: dart:io がスローする具体的な例外を捕捉し、パスの存在、権限、ファイルシステムの状態など、論理的なエラーを適切に処理します。