文書の過去の版を表示しています。
Odinの制御フロー
Odinの制御フローに関する機能は、全て文 (statement)です。 最近の言語では式 (expression)として提供するようになっている傾向がありますが、Odinはそうではありません。 伝統的な立場をとっています。
if文
条件によって処理を分岐するにはif文を使います。 最も基本的なパターンは次のような形式です。
if 条件文 {
// 条件がtrueのとき実行される
} else {
// 条件がfalseのとき実行される
}
elseブランチは必要なければ省略可能です。
else
のあとにif
を書くことで、追加の条件ブランチを書くことができます。
if 条件文1 {
// 条件1がtrueのとき実行される
} else if 条件文2 {
// 条件1がfalseで、条件2がtrueのとき実行される
} else {
// 条件1がfalseで、条件2もfalseのとき実行される
}
C言語のように条件を丸括弧()
で囲む必要はないことに注意してください。
囲んでもエラーではありませんが、冗長なだけで何も良いことはありません。
基本的に、括弧{}
を省略することはできません。
しかし、実行されるブロック本体が1行である場合、括弧{}
の代わりdo
を使うことができます。
if 条件文 do fmt.println("yah") // 条件が真のとき実行される
else {
// ...
}
if
に、初期化文を含めることができます。
if 初期化文; 条件文 {
// ...
}
初期化文で宣言された変数は、if
のブロック本体や、後続のelse if
の条件文、後続のブロック本体でのみ有効になります。
変数のスコープをできるだけ短くすることは良い習慣であるので、可能なら積極的に利用していくのが良いです。
if answer := get_answer(); answer == 42 {
fmt.println("hi")
} else if answer < 0 { // ここでもanswerが使える (また、このifに追加の初期化文を加えることも可能)
fmt.println("oh no!")
} else {
fmt.println("huh? ", answer) // ここでもanswerが使える
}
fmt.println(answer) // コンパイルエラー! 上のifを抜けたらanswerを使うことができない
switch文
条件によって処理を分岐する、もう一つの方法として、switch文があります。 switch文は、条件を判定する対象が、ある一つのもの(主に変数)に限られます。 そのため、柔軟性はif文よりも低いですが、その代わりに記述が簡潔になり、コードの見た目も意図もスッキリして明らかになります。 適用できる場面ではswitch文の方を優先したほうが良いでしょう。 また、C言語のswitch文と比べれば、遥かに要件が緩和されているので、きっと使いやすいと感じられるかと思います。
x := -100 // この値を変えてやれば出力が変わる
switch x {
case -100..<0:
fmt.println("-100を含めてそれより大きく、0より小さい")
case 0:
fmt.println("ゼロに等しい")
case 1, 2, 3:
fmt.println("1か2か3のいずれかに等しい")
case:
fmt.println("条件なしのケースはデフォルトケース")
}
上の例で -100..<0
のところとは注目に値します。
このように、範囲を指定したケースを記述することができます。
1と2と3を同時に扱える点もまた注目に値します。
間違って条件が重複するケースを書いてしまった場合、多くの場合はコンパイラによってコンパイルエラーとして検出されます。
例えば case -100..<0
を case -100..=
としてしまった場合、0の場合が重複するのでコンパイルされません。
これはプログラミングエラーを検出するのに大きな役割を果たしてくれます。
if文と同じように、switch文にも初期化文を含めることができます。 直前の例は、次のように書き換えることができます。
switch x := -100; x {
// ...
}
少し前に、switch文は条件を判定する対象が、ある一つもの(主に変数)に限られる、と言いましたが、これには嘘が含まれています。 switch文の条件は完全に取り除くことが可能です。 この場合、caseに任意の条件を書くことが可能となり、if文と同様の柔軟性が得られます。
x := 10
y := 20
switch {
case x < 10:
fmt.println("x < 10")
case y < 20:
fmt.println("y < 20")
case x == 10 && y == 20:
fmt.println("wow")
}
このように、Odinのswitch文はとても使い勝手が良いです。 一つ、気に留めておいたほうが良いことがあります。 switch文のよくある使い方として、enum型の変数によって処理を振り分けるというのがあります。
State :: enum {
TitleMenu,
Playing,
Paused,
GameOver,
}
まだenumは扱っていませんが、C言語などを知っていれば大体想像される通りのものです。 このStateというenum型の変数について、switch文で処理を振り分けたいとします。 そして、TitleMenuとPlayingのときだけ処理を行い、他は無視したいとします。
current_state := State.Playing
switch current_state {
case .TitleMenu:
// 処理を行う...
case .Playing:
// 処理を行う...
case:
// 他は無視したい...
}
// コンパイルエラー!
これはコンパイルされません。
エラー内容は、対応していないケースがある、言い換えれば、すべてのケースを網羅していないからというものです。
デフォルトケースを記述しているにも関わらずです。
これは奇妙に思われるかもしれません。
enumとswitch文のパターンのルールは少しだけ込み入ったものになっています1)。
簡単にいうと、enumをswitch文で処理する場合、必ず全てのケースを「明示的に」取り扱わないといけません。
(直前の例のように)もしそうでないなら、#partial switch
という特殊なswitchを用いる必要があります。
修正自体は簡単です。
current_state := State.Playing
#partial switch current_state {
case .TitleMenu:
// 処理を行う...
case .Playing:
// 処理を行う...
case:
// 他は無視したい...
}
// OK!
これが必要となるケースは意外と多いです。 しかし、enumのメンバーを追加したときに、switch文のケースを追加せずともコンパイルをパスしてしまうという脆弱さが生まれてしまうので、注意が必要なところでもあります。
for文
Odinでループを表現するには、for文を使います。 従来の多くのC言語族にあるwhile文は存在しません。 forの条件によって、whileと同等のことが可能です。
最も基本的なのは、初期化、条件、更新 のパターンのforです。 C言語のforに相当する、典型的なパターンのfor文です。
for i := 0; i < 5; i += 1 {
fmt.println("loop: ", i)
}
出力は次のようになります。
loop: 0 loop: 1 loop: 2 loop: 3 loop: 4
必要のない部分は空にすることができます。
i := 0
for ; i < 5; {
i += 1
}
fmt.println("after finished loop ", i)
出力:
after finished loop 5
この場合、機能的にはC言語のwhile文と同等です。 しかし、空の文が(セミコロン)が見苦しいです。 これは完全に取り除くことができて、そうすることによって、C言語のwhile文と同等なものとなります。
i := 0
for i < 5 {
i += 1
}
fmt.println("after finished loop ", i)
出力:
after finished loop 5
さらに条件すら取り除いてしまえば無限ループになります。
for {
fmt.println("無限ループ中... Ctrl+C で停止")
}
中途半端に取り除くことはできません。
for i := 0; i < 5 { // コンパイルエラー!
// ...
}
この伝統的なC言語スタイルのfor文は、まだまだ現役であるものの、範囲ベースのforループにかなり多くその役割を受け渡しています。
範囲ベースforループは基本的に次のような形をしています。
for 変数名 in 反復可能なオブジェクト {
// 変数名とした変数を使って処理を行う...
}
先の数値を0から5の一つ手前までインクリメントしながらループする場合、次のようにすることができます。
for i in 0..<5 {
fmt.println("loop ", i)
}
出力:
loop: 0 loop: 1 loop: 2 loop: 3 loop: 4
ここで5が含まれていないことに注意してください。
もし5も含めたい場合 0..=5
とします。
for i in 0..=5 {
fmt.println("loop ", i)
}
出力:
loop: 0 loop: 1 loop: 2 loop: 3 loop: 4 loop: 5
反復可能なオブジェクトには色々とあります。 組み込み型のオブジェクトでは、以下のようになります。
- 文字列
- 配列
- スライス
- 動的配列
- マップ
また、上で「変数名」としたところ「変数名1, 変数名2」のようにすることが可能な、柔軟な反復可能なオブジェクトもあります。
xs := []int{100, 200, 300}
for value, index in xs {
fmt.println("value = ", value, ", index = ", index)
}
出力:
value = 100 , index = 0 value = 200 , index = 1 value = 300 , index = 2
この例は、反復可能なオブジェクトとして、スライスを使ったものです。 C言語スタイルのforループよりも、より間違いにくく、読みやすいので、可能ならば範囲ベースのforを優先して使っていくことになります。
範囲ベースforについて、もう一点付け加えておきます。 「変数名」としたところにバインドされるのは、値のコピーです。 これを参照にしたいことは多くあります。 Odinでもそのようなことは当然考えられていて、参照をバインドしてループすることが可能です。 それによって、反復元のオブジェクトの値を書き換えることが可能です。
xs := []int{100, 200, 300}
for &value in xs {
value += 10
}
fmt.println(xs)
出力:
[110, 210, 310]
範囲ベースforについては以上です。
他の反復可能なオブジェクトについては、少し先で取り扱う予定なのでそれまで先送りしておきます。
do ... while は存在しない
ループの末尾で条件判定を行うループ (C言語の do .. while に相当するもの) はありません。 もし必要なら、break文を利用してなんとかする必要があります。
例:
// "yes"か"no"のいずれかが入力されるまで、ユーザーに入力を求める
answer: string
for {
// もう一度ユーザーから入力を受け付ける
answer = read_user_input(prompt = "続けますか? (yes/no)")
if answer == "yes" || answer == "no" {
break
} else {
fmt.println("yesかnoで答えてください。")
}
}
少し不格好かもしれませんが、たいていの目的は達成できます。
defer文
deferは、文と与えられた処理の実行を、属するスコープの終了後にまで遅延させます。
main :: proc() {
defer fmt.println("a")
fmt.println("b")
{
defer fmt.println("c")
fmt.println("d")
}
fmt.println("e")
}
出力:
b
d
c
e
a
別の言い方すれば、スコープの終了時に必ず処理が実行されることを保証します。 これによって、C++のデストラクタで可能な重要な仕事の一部を担うことができます。 典型的な例としては、ファイルをオープンして、必ずクローズすることを保証するというようなものです。
package main
import "core:fmt"
import "core:os"
main :: proc() {
f, err := os.open("dummy.txt")
if err != os.ERROR_NONE {
fmt.eprintln("cannnot open file dummy.txt")
return
}
defer os.close(f) // 必ずファイルがクローズされることを保証する
// fを使ってファイルを処理する...
}
この例からはさほどメリットは感じられませんが、プロシージャから早期にリターンする条件が増えたりして煩雑になるとありがたみが実感できるようになります。
deferは、if文のブロック全体を受け付けることもできます。
defer if x == 1 {
// ...
} else {
// ...
}
同じスコープに複数のdeferが存在する場合、実行される順序は、スコープ内で出現する順序の逆順になります。
package main
import "core:fmt"
main :: proc() {
defer fmt.println("最後に実行される")
defer fmt.println("2番目に実行される")
defer fmt.println("最初に実行される")
}
出力:
最初に実行される
2番目に実行される
最後に実行される
これもC++のデストラクタと同じ挙動となっています。