こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 32 回です。Weekly Report については、第 1 回の記事を参照してください。
奈落からの帰還
早期リターンは、関数の主な目的を達成するケース (ハッピーパス) と達成できないケース (アンハッピーパス) を分離し、関数の流れを分かりやすくするために有効なテクニックです。ただし、早期リターンをどのように適用するかは注意深く考える必要があります。
以下のコードでは、 messageData.mainMimeType
が想定外のケースと messageData.content
の保存が失敗したケースで return
していますが、逆に読みにくくしてしまっています。どのように改善したら良いでしょうか。
fun ...(...) {
try {
val messageType = if (messageData.isParsed) {
messageData.messageType
} else {
when (messageData.mainMimeType) {
"image" -> MessageType.IMAGE
"video" -> MessageType.VIDEO
else -> return false
}
}
val saveResult = saveContent(messageData.content, messageType) // IOException を投げる事がある
if (!saveResult.isSuccessful) {
return false
}
return true
} catch (_: IOException) {
return false
}
}
分かりやすい帰り道を作る
先程の関数の return
が分かりにくいのには、以下の 2 つの理由があります。
- ネストの深い場所や、分岐の一部からの
return
がある (例: 最初のreturn false
は、try
・if
・when
の内側にあり、かつ、そこに到達する条件がひと目ではわかりにくい) return
が、ハッピーパスとアンハッピーパスの分離にあまり寄与していない (例:!saveResult.isSuccessful
のreturn false
には後続の動作がない)
早期リターンを使う場合、return
を目立つ場所に置き、条件をわかりやすくし、ハッピーパスが目立つようにする ことを心がける必要があります。
例えば以下のようにすると、「早期リターンが起きるケースは、messageType
が null になるとき」 ということがわかります。また、最後の行を見ると、メッセージの保存がうまく行ったときに true
が返ってくるということもわかりやすくなっています。
fun ...(...) {
val messageType = messageData.messageType.takeIf { messageData.isParsed }
?: MAIN_MIME_TO_TYPE_MAP[messageData.mainMimeType]
?: return false
val saveResult = try {
saveContent(messageData.content, messageType)
} catch (_: IOException) {
SaveResult.Error()
}
return saveResult.isSuccessful
}
companion object {
val MAIN_MIME_TO_TYPE_MAP: Map<String, MessageType> = mapOf(
"image" to MessageType.IMAGE,
"video" to MessageType.VIDEO
)
}
特に、when
や switch
といった多分岐を使うときに、一部の条件だけで return
するコードを書いてしまうと、早期リターンそのものが見落とされかねません。例えば、以下の Foo
を Bar
に変換するコードでは、INVALID
のケースの return
に気づかないかもしれません。
fun ...(foo: Foo) {
val bar = when (foo) {
Foo.FIRST -> Bar.FIRST
Foo.SECOND -> Bar.SECOND
Foo.THIRD -> Bar.THIRD
Foo.INVALID -> return
}
...
}
この問題は、多分岐と早期リターンを分離することで解決できます。以下の改善案では、Foo
から Bar
に変換するコードを関数として抽出しています。
fun ...(foo: Foo) {
val bar = toBar(foo)
?: return
...
}
private fun toBar(foo: Foo): Bar? = when (foo) {
Foo.FIRST -> Bar.FIRST
Foo.SECOND -> Bar.SECOND
Foo.THIRD -> Bar.THIRD
Foo.INVALID -> null
}
こうすることで、「Foo
を Bar
に変換できないケースで早期リターンする」ということを強調できます。
なお、早期リターンについては、読みやすいコードの書き方のプレゼンテーション (slide=39 から slide=48) や、書籍 読みやすいコードのガイドライン の 5 章でも解説しています。併せてご参照ください。
一言まとめ
早期リターンを使う場合は、その存在と条件、およびハッピーパスを目立たせる。
キーワード: early return
, conditional branch
, function flow