Flutter: assert と throw の使い分け

Flutter: assert と throw の使い分け

July 15, 2024

Dart 言語には assert という機能があります。assert の第一引数が条件式で、こちらが true であれば何も起きず、false であれば例外が throw されます。assert は Flutter のデバッグモードでのみ有効になります。リリースビルドを行った際は assert のコードをツリーシェイキングにより取り除かれ、ビルド後のコードからは除外されます。

var name = "Alice";

assert(name.isEmpty, "Name cannot be empty.")

同じ処理は自分で throw することでも実現可能です。この場合はリリースビルド時も実行されます。

var name = "Alice";

if (name.isEmpty) {
  throw Exception("Name cannot be empty.");
}

実際のアプリケーションでの assert と throw の使い方 #

さて、実際の Flutter アプリケーションで assert を使う場合は、イニシャライザリストのところで用いられることが多いのではないでしょうか。

class MyWidget extends StatelessWidget {
  MyWidget({
    super.key,
    required this.name,
    required this.age,
  })  : assert(name.isEmpty, "Name cannot be empty"),
        assert(age < 0);

  final String name;
  final int age;

  @override
  Widget build(BuildContext context) {
    return Text("Name: $name, Age: $age");
  }
}

これを自分で throw する形にする場合は以下になります。

class MyWidget extends StatelessWidget {
  MyWidget({
    super.key,
    required this.name,
    required this.age,
  }) {
    if (name.isEmpty) {
      throw Exception("Name cannot be empty");
    }
    if (age < 0) {
      throw Exception();
    }
  }

  final String name;
  final int age;

  @override
  Widget build(BuildContext context) {
    return Text("Name: $name, Age: $age");
  }
}

assert と throw の使い分け #

では、assert と throw はどのように使い分ければ良いでしょうか?これは言い換えれば、デバッグモードでしか実行しないデータ検証と、リリースモードでも実行したいデータ検証は、何を持って区別するのか、という質問と言えます。

これに対する私の意見は、すべてのデータ検証はリリースモードでも実行されるべき、です。つまり、assert は使わず throw だけを使うというのが私の考えです。

assert を用いた場合、リリースモードでは実行されない分だけ処理速度が向上するという利点はありますが、これによる速度向上は微々たるものです。それと比較するとデータ検証が無効になるデメリットのほうが圧倒的に大きいというのが私見です。

assert のような throw の関数を作成する #

assert と throw を比較した場合、if 文での分岐による throw よりも assert の方がコードの書きごこちや可読性が高いのは確かでしょう。

ということで、assert のように記述できる関数を自作しましょう。

/// Like assert but is enabled in release mode.
void verify(bool condition, [String? errorMessage]) {
  if (!condition) {
    throw Exception(errorMessage);
  }
}

これを用いれば、assert の利点を保ったまま次のように書くことができます。

class MyWidget extends StatelessWidget {
  MyWidget({
    super.key,
    required this.name,
    required this.age,
  }) {
    verify(name.isEmpty, "Name cannot be empty");
    verify(age < 0);
  }

  final String name;
  final int age;

  @override
  Widget build(BuildContext context) {
    return Text("Name: $name, Age: $age");
  }
}