Javaのswitch文の場合
問題。以下のJavaコードの main(...)
を呼び出したときの countOnes(int[])
の結果は何でしょう(筆者としては引っかけようという気持ちはあまりないので、まぁお気軽に)?
public class SwitchSample {
public static int countOnes(int[] numbers) {
int counter = 0;
for(int number : numbers) {
switch(number) {
case 0:
System.out.println("ZERO");
break;
case 1:
counter++;
default:
}
}
System.out.println("ONEs: " + counter);
return counter;
}
public static void main(String[] args) {
System.out.println("count: " + countOnes(new int[] { 1, 2, 3, 2, 1, 0, 3, 2, 1, 0, 4}));
}
}
配列中の 1
の数を数えている途中で 0
に当たって break;
ループを抜け出すので、答えは2、と考えた方は、残念ながら罠にはまってしまいました。
case 0:
の2行後の break;
は switch文から脱出する もので、forループからは脱出しません。
したがって配列中に 0
が現れてもループ処理に影響は及ぼさないため、 countOnes(int[])
は配列のすべての 1
の数を数えるので、答えは3です。
仮に case 0:
の後の break;
を省くと、switch文は「フォールスルー」して case 1:
の後も実行され、1だけでなくゼロの数も数えてしまいます。
case 1:
の後にも「フォールスルー」を防ぐため break;
が書かれるべきだ、という批判はありえますが、この例ではなくても支障はありません。
この例を見て、「なんでループから脱出する文とswitch文から脱出する文が同じなんだ?」と思われないでしょうか? 筆者は思います。
ともあれ気になるのは、「ではこの場合、ループから脱出するコードはどう書けばいいのか?」という点です。
以下のようにコードを修正すれば、switch文は case 0:
でループから脱出するようになり、冒頭の問題の答えは2に変わります。
public class SwitchSample {
public static int countOnesBeforeZero(int[] numbers) {
int counter = 0;
loop: for(int number : numbers) {
switch(number) {
case 0:
System.out.println("ZERO");
break loop;
case 1:
counter++;
default:
}
}
System.out.println("ONEs: " + counter);
return counter;
}
}
違いは、for文のブロックにラベル (loop) をつけ、break文で指定するようにした点だけです。
簡単ですが、ブロックラベルは必要とするケースが少ないため、「書いたことない」「文法を知らない」というJavaプログラマも結構いるのではないかと思います。かくいう筆者も文法の記憶があやふやで、書くたびにネットで検索したりしていました。
フォールスルー?
そもそも、switch文内にbreak文を書かないと次のcaseラベル以降の文に「フォールスルー」して実行される、というC言語由来のswitch文の仕様は、あまりにも「単独のcaseラベルの文のみが実行される」というプログラマの錯覚を引き起こしやすく、break文の書き忘れによる大量のバグを生み出しました。このため、現代では「フォールスルー」自体がプログラミングのアンチパターンとみなされ、近年になって普及したプログラミング言語の大半は「フォールスルー」を起こすswitch文を言語仕様に採用しなくなっています。今ではJavaScriptとPHPがフォールスルーするプログラミング言語のほとんど最後の世代で、次に古いのがJava、という感じになっています。
「なんでループから脱出する文とswitch文から脱出する文が同じなんだ?」と少し上に書きましたが、これはC言語においてswitch文はwhileなどのループと位置づけが近いフロー制御を行う文であり、脱出にbreakを充てるのが自然、という考え方をJavaにも受け継がせたことが理由と思われます。しかし一方で、C言語では多重ループから一気に抜け出すのにgoto文が使われていたのに対し、「構造化プログラミング」の考え方が普及した時代に設計されたJavaにはgoto文を加えるわけにいかず、代わりに「ラベルつきブロック」の概念を生み出し、breakでラベルを指定することでブロックから脱出することを可能にしました(と思われます)。これによってJavaではbreak文の機能が変わっているので、switch文からの脱出にはループ脱出と同じ予約語を採用するべきではなかったのでは、と筆者は思います。
Kotlinのwhen式の場合
時は流れ、Java仮想機械 (Java Virtual Machine, JVM) 向け言語としてKotlinが開発されました。前世代のJVM向け言語であるScalaですら採用されなかった、フォールスルーするswitch文がKotlinで採用されるわけもなく、改良版switchとして、Rubyのcase-whenのように「式」としても使えるようにしたwhen式が導入されました。
when式は複数のブロックを実行することはなく、その動作は if(...) { ... } else if(...) else if(...) { ... } else { ... } と書くのとほぼ同じことになりました(Kotlinでは、if文も式として使えるif式です)。
when式はもはやフォールスルーしないので、when式の仕様にはもはやbreakはなく、ループから脱出するbreak文との混同の心配もありません。
したがって、「1の数を数える。ただしゼロに当たったらループを脱出して処理を終了する」というコードは、以下のように書けます。
object SwitchSample {
fun countOnesBeforeZero(numbers: IntArray): Int {
var counter = 0
for(number in numbers) {
when(number) {
0 -> {
println("ZERO")
break
}
1 -> {
counter++
}
else -> {}
}
}
println("ONEs: $counter")
return counter
}
@JvmStatic
fun main(args: Array<String>) {
println("count: " + countOnes(intArrayOf(1, 2, 3, 2, 1, 0, 3, 2, 1, 0, 4)))
}
}
つい先日安定版がリリースされたKotlin1.4で正常にビルドできます。
ただし、Kotlin1.3以下では
'break' and 'continue' are not allowed in 'when' statements. Consider using labels to continue/break from the outer loop
というエラーメッセージが出てビルドに失敗します。
「when文の中ではbreakとcontinueは使えません。ループのcontinue/breakにはラベルを使うことを検討してください」
???
慌てる必要はありません。
もし、あなたの開発環境が何らかの事情でKotlin1.4にアップデートできない場合は、以下のように書いてください。
object SwitchSample {
fun countOnesBeforeZero(numbers: IntArray): Int {
var counter = 0
loop@ for(number in numbers) {
when(number) {
0 -> {
println("ZERO")
break@loop
}
1 -> {
counter++
}
else -> {}
}
}
println("ONEs: $counter")
return counter
}
}
Java編の解決策と同じく、脱出したいブロックにラベルをつけ、break文で指定すれば、正常にビルドされます。
で? この記事は何が言いたいの?
本稿中の実用的な情報は、
- Javaのswitch文中にループ制御のbreakを使いたい場合は、ラベルが必須
- Kotlinのwhen式中にループ制御のbreak/continueを使いたい場合は、Kotlin1.3以下の場合のみラベルが必須
だけですね(苦笑)。
Kotlin1.3以下のwhenでのbreak/continueがラベルなしで使えない理由の解説がこちらにあります(ただし、筆者には充分な説明とは思えませんでしたが)。
whenを使わずif〜else if〜elseで同じロジックを書いた場合にはこの制約はなく、Kotlin1.3以下でもラベルなしでbreak/continueが使えます。
試しに、Android Studioの「if式をwhen式に変換する」機能を用いて、if式内でラベルのないbreak文を使ってロジックを書いた後、if式をwhen式に変換してみました。
すると、ループブロックに自動的にラベルが付与され、そのラベルのついたbreak文が現れました。
ラベルが必須でないはずのKotlin1.4のAndroidプラグインでこの変換を試した場合にもラベルが付与されました。Kotlin1.4で言語仕様が変わっても変換処理はとりあえず変わっていないようです。
さまざまなプログラミング言語のswitchオルタナティブについて
最近の筆者はJavaのコードを書くことが激減し、Kotlinが使えればよい、という状況に近くなっているため、Kotlin1.3以下のwhen文の中でbreakにラベルを付け忘れたときはエラーが出てくれてそれを訂正すればよさそうです。しかし、Javaのswitch文中のbreakに必要なラベルを省略した場合はエラーとはならず、意味が大きく変わってしまいますので、Javaのコードを書いているプログラマは注意しましょう。
「ループ中のswitch文でbreakを使ったとき、それはどの文に対するbreakか」という問題はいろいろなプログラミング言語で生じそうな問題だ、と思ったのが、この記事を書いてみたきっかけです。そのためJava、Kotlin以外のプログラミング言語の話題もいくつか書きましたが、筆者はプログラミング言語マニアではありませんので、とりあえずJavaとKotlinだけを主担当としておき、その他の言語については他の著者におまかせしたいと思います。
もっとも、ネット検索してみると、ループ中のswitch文中のbreakで悩んだ、というコンテンツは全然見つけられませんでした。その意味ではこの記事はオリジナリティーが高いのかもしれません(笑)が、もしかして、ほとんどのプログラマはswitch文中のbreakを見たとき、それがループ内であっても、「このbreakはループから脱出するためのbreakかもしれない」と誤読することはほとんどなく、問題にならないのかもしれません。そこに思い至ると、筆者自身もこの問題で悩んだ記憶がほとんどないことに気づきました。なので、冒頭の問題の罠にはまる人が続出することはほとんど期待できないでしょう(苦笑)。
参考文献
What's New in Kotlin 1.4.0 - Using break and continue inside when expressions included in loops
switch文 - Wikipedia