Dart で本来不要な missing_return 警告が出て煩わしい時の対処方法

公開:2020/09/30
更新:2020/10/13
3 min読了の目安(約3000字TECH技術記事
Likes33

注: この記事の例で用いたcaseを網羅したenumでもreturn漏れ扱いになってしまうのは、Dart 2.10以降でnull safetyを有効にすると解消されます

missing_return 警告が出る例

次のような enumAnimal があったとします。

enum Animal {
  dog,
  cat,
}

それぞれの case に応じて何か値を返したい時がよくありますが、ここでは絵文字を返したいとします。

extension AnimalEx on Animal {
  String get emoji {
    switch (this) {
      case Animal.dog:
        return '🐶';
      case Animal.cat:
        return '🐱';
    }
  }
}

enumなので本来これで網羅できていてreturn漏れはありませんが、Dartの場合は次のような警告が発生してしまいます。

warning: This function has a return type of 'String', but doesn't end with a return statement. (missing_return at [flutter_playground] lib/main_enum.dart:12)

なので、この警告を解決するためには、以下のように本来到達しないであろうパターンのreturnを書く必要があります。

extension AnimalEx on Animal {
  String get emoji {
    switch (this) {
      case Animal.dog:
        return '🐶';
      case Animal.cat:
        return '🐱';
    }
    return ''; // あるいは `null` など
  }
}

// ignore: missing_return をメソッドの上に付けるなどでも同じく警告を抑制できますが、そうすると本当にreturn漏れが生じたときに気付けなくなるのでお勧めしません。

上のように、とりあえず適当なreturnを付けるだけでも良いですが、何かの勘違いやミスなどで本当にそこに到達してしまったバグを気付けるように一応 assert を付けるのもありです。

extension AnimalEx on Animal {
  String get emoji {
    switch (this) {
      // 省略
    }
    assert(false, 'Unexpected type: $this');
    return '';
  }
}

Errorを投げるのが最近のお気に入りパターン

わりと当たり前のように上のように書いてましたが、最近は次のように1行で書くのがお気に入りです。もしthrowしている行に達したら必ず例外が投げられるので、return漏れとみなされなくなります。

extension AnimalEx on Animal {
  String get emoji {
    switch (this) {
      // 省略
    }
    throw AssertionError('Unexpected type: $this');
  }
}

さらに以下のように独自のError型を用意して、より簡潔に書くのも良いですね。

class UnexpectedError extends AssertionError {
  UnexpectedError(Object object) : super('Unexpected type: $object');
}

extension AnimalEx on Animal {
  String get emoji {
    switch (this) {
      // 省略
    }
    throw UnexpectedError(this);
  }
}

ちなみに、RiverpodでUnimplementedErrorを使ってこういう書き方をしているのを見て、取り入れました。

assert(false)throw AssertionError() の挙動は違うので注意

assert() は第一引数が false だった場合、開発時(Flutterの場合はDebug Modeの時)のみ AssertionErrorが投げられます。一方、throw AssertionError()と書いた場合は開発時かどうか関係なく、Flutterの場合Release Modeを含めて投げられます。
なので、使い分けとしては次のような感じになります。

  • リリース版では例外が起こらずにしれっと動いてほしい(上の例では空文字でやり過ごす): assert(false)
  • リリース版でも同様に例外が起こってしまっても良い: throw AssertionError()

今回の例のように enum で網羅性が担保されていて到達し得ないだろう場合はどっちにしても大差なく、1行で済むthrow AssertionError()で良い気がします🤔

上の例の場合でも、最後の行に到達してしまうパターン

上で「enum で網羅性が担保されていて到達し得ないだろう場合」と書きましたが、次のように書くと簡単に最後の throw UnexpectedError(this); に到達してしまいます。

void main() {
  const Animal animal = null;
  print(animal.emoji);
}

このように実はちょっとしたミスで到達することがありますが、大抵開発時に容易に気付けるプログラミングミスの類だと思うので、結論としては変わらずassertや例外投げるなどの対処で良いと思います。

また、対応中でFlutterでもそろそろ使えそうなnull安全対応がなされれば、このケースの可能性も潰せます。

最後に、DartPadを貼っておきます。
https://dartpad.dev/5d848bb39e310b620b7e4b0cc91bd32a


記事はすべて無料で書くつもりですが、サポートは歓迎です🐶🙏⬇️