コンテンツへスキップ

制御フロー

プリミティブ型

Nil

最も単純な型はNilです。これには単一の値 nil のみが存在し、実際の値がないことを表します。

前のレッスンでのString#indexを覚えていますか?これは、検索文字列にサブ文字列が存在しない場合、nilを返します。インデックスがないため、インデックス位置は存在しません。

p! "Crystal is awesome".index("aw"),
  "Crystal is awesome".index("xxxx")

Bool

Bool型には、論理とブール代数の真理値を表す truefalse の 2 つの値のみが可能です。

p! true, false

ブール値は、プログラムの制御フローを管理するのに特に役立ちます。

ブール代数

次の例は、ブール値を使用したブール代数を実装するための演算子を示しています。

a = true
b = false

p! a && b, # conjunction (AND)
  a || b,  # disjunction (OR)
  !a,      # negation (NOT)
  a != b,  # inequivalence (XOR)
  a == b   # equivalence

abの値を切り替えて、異なる入力値に対する演算子の動作を確認できます。

真偽性

ただし、ブール代数はブール型だけに限定されるわけではありません。すべての値には暗黙の真偽性があります。nilfalse、および null ポインタ (これは後で説明します) は *偽* です。その他の値 (0 を含む) は *真* です。

上記の例で、truefalse を他の値 (たとえば "foo"nil) に置き換えてみましょう。

a = "foo"
b = nil

p! a && b, # conjunction (AND)
  a || b,  # disjunction (OR)
  !a,      # negation (NOT)
  a != b,  # inequivalence (XOR)
  a == b   # equivalence

AND および OR 演算子は、演算子の真偽値に一致する最初のオペランド値を返します。

p! "foo" && nil,
  "foo" && false,
  false || "foo",
  "bar" || "foo"

NOTXOR、および等価演算子は常にBool値 (true または false) を返します。

制御フロー

プログラムのフローを制御するとは、条件に基づいて異なるパスをたどることを意味します。このチュートリアルのこれまでのすべてのプログラムは、一連の式を順番に実行してきました。今、これが変わろうとしています。

条件

条件付き句は、条件が満たされた場合にのみ開くゲートの後ろにコードの分岐を配置します。

最も基本的な形式では、キーワード if の後に条件として機能する式が続きます。式の戻り値が *真* の場合、条件が満たされます。後続のすべての式は、キーワード end で閉じられるまで、分岐の一部です。

慣例に従い、ネストされた分岐は 2 つのスペースでインデントします。

次の例では、メッセージが Hello で始まるという条件を満たした場合にのみ、メッセージを出力します。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
end

厳密に言うと、このプログラムは依然として定義済みの順序で実行されます。固定メッセージは常に一致し、条件を真にします。しかし、ソースコードでメッセージの値を定義しないと仮定しましょう。たとえば、チャットクライアントのように、ユーザー入力から取得することもできます。

メッセージの値が Hello で始まらない場合、条件付き分岐はスキップされ、プログラムは何も出力しません。

条件式はより複雑になる可能性があります。ブール代数を使用すると、Hello または Hi のいずれかを受け入れる条件を作成できます

message = "Hello World"

if message.starts_with?("Hello") || message.starts_with?("Hi")
  puts "Hey there!"
end

条件を反転させてみましょう。Hello で*ない*場合のみ、メッセージを出力します。これは、前の例からのわずかな逸脱にすぎません。否定演算子 (!) を使用して、条件を反対の式に変えることができます。

message = "Hello World"

if !message.starts_with?("Hello")
  puts "I didn't understand that."
end

別の方法としては、if をキーワード unless に置き換えることです。これは、ちょうど反対の真偽値を期待します。unless xif !x と同等です。

message = "Hello World"

unless message.starts_with?("Hello")
  puts "I didn't understand that."
end

String#index を使用してサブ文字列を検索し、その位置を強調表示する例を見てみましょう。サブ文字列が見つからない場合は nil を返すことを覚えていますか? その場合、何も強調表示できません。そのため、インデックスが nil かどうかを確認する条件を持つ if 句が必要です。.nil? メソッドが最適です。

str = "Crystal is awesome"
index = str.index("aw")

if !index.nil?
  puts str
  puts "#{" " * index}^^"
end

コンパイラは、nil ケースを処理することを強制します。条件を削除するか、条件を true に変更してみてください。型エラーが表示され、その式で Nil 値を使用できないことを説明します。適切な条件を使用すると、コンパイラは index が分岐内で nil になることはなく、数値入力として使用できることを認識します。

ヒント

if !index.nil? の短い形式は if index であり、ほぼ同じです。偽の値が nilfalse かを区別したい場合にのみ違いが生じます。これは、前者条件は false に一致しますが、後者条件は一致しないためです。

Else

プログラムを改良し、メッセージが条件を満たすかどうかに関係なく、両方のケースで反応してみましょう。

これは、否定された条件を持つ 2 つの別々の条件として行うことができます

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
end

if !message.starts_with?("Hello")
  puts "I didn't understand that."
end

これは機能しますが、2 つの欠点があります。条件式 message.starts_with?("Hello") が 2 回評価されます。これは非効率的です。後で、ある場所で条件を変更した場合 (Hi も許可する可能性がある)、他の場所も変更するのを忘れる可能性があります。

条件は複数の分岐を持つことができます。代替の分岐は、キーワード else で示されます。これは、条件が満たされない場合に実行されます。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
else
  puts "I didn't understand that."
end

その他の分岐

プログラムは Hello にしか反応しませんが、より多くのやり取りが必要です。Bye にも応答するための分岐を追加しましょう。同じ条件で異なる条件の分岐を持つことができます。これは、別の統合された if を持つ else のようなものです。したがって、キーワードは elsif です

message = "Bye World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
elsif message.starts_with?("Bye")
  puts "See you later!"
else
  puts "I didn't understand that."
end

else 分岐は、前のどの条件も満たされない場合にのみ実行されます。ただし、常に省略できます。

異なる分岐は相互に排他的であり、条件は上から順に評価されることに注意してください。上記の例では、両方の条件が同時に真になることはないため(メッセージがHelloByeの両方で始まることはない)、これは問題になりません。ただし、これを実証するために、排他的ではない別の条件を追加することができます。

message = "Hello Crystal"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
elsif message.includes?("Crystal")
  puts "Shine bright like a crystal."
end

if message.includes?("Crystal")
  puts "Shine bright like a crystal."
elsif message.starts_with?("Hello")
  puts "Hello to you, too!"
end

両方の節には同じ条件を持つ分岐がありますが、順序が異なり、動作も異なります。最初に一致した条件によって、どの分岐が実行されるかが選択されます。

ループ

このセクションでは、コードの繰り返し実行の基本について説明します。

基本的な機能はwhile節です。構造はif節によく似ています。キーワードwhileが開始を示し、それにループ条件として機能する式が続きます。後続のすべての式は、終了キーワードendまでループの一部です。条件の戻り値がである限り、ループは繰り返し続けます。

1から10まで数える簡単なプログラムを試してみましょう。

counter = 0

while counter < 10
  counter += 1

  puts "Counter: #{counter}"
end

whileendの間のコードは10回実行されます。現在のカウンター値が出力され、1ずつ増加します。10回目の反復後、counterの値は10になり、counter < 10が失敗してループが終了します。

別の方法として、whileをキーワードuntilに置き換えることもできます。これは、ちょうど反対の真理値を期待します。until xwhile !xと同等です。

counter = 0

until counter >= 10
  counter += 1

  puts "Counter: #{counter}"
end

ヒント

これらの式に関する詳細は、言語仕様のwhileおよびuntilを参照してください。

無限ループ

ループを扱う場合、ループ条件がいつかになるように注意することが重要です。そうしないと、永遠に、または外部からプログラムを停止するまで(たとえば、Ctrl+Ckill、プラグを抜く、またはハルマゲドンが来るまで)ループが続行されます。

この例では、カウンターをインクリメントしないことは、次のように記述するのと同じです。

while true
  puts "Counter: #{counter}"
end

または、条件がcounter > 0の場合、すべての値で一致します。値は1からのみ増加するためです。これは技術的には無限ではありません。カウンターが32ビット整数の最大値に達すると、数学エラーで失敗するためです。しかし、概念的には無限ループに似ています。このような論理エラーは見落としやすく、ループ条件を記述するとき、および上記の終了ケースを満たすように注意することが非常に重要です。インデックス変数(この例のcounterなど)の良い習慣は、ループの開始時にインクリメントすることです。これにより、更新を忘れるのが難しくなります。

ヒント

幸いなことに、言語には手動でループを記述する負担を軽減し、有効な終了条件を確保する機能が多数あります。そのうちのいくつかは、後のレッスンで紹介されます。

場合によっては、本当に無限ループを使用することが意図されていることもあります。たとえば、常に接続を待機するサーバーや、ユーザー入力を待機するコマンドプロセッサなどです。その場合は、もちろん明白にする必要があり、複雑で決して失敗しないループ条件に隠すべきではありません。それを表現する最も簡単な方法は、while trueです。条件trueは常に真であるため、ループは無限に繰り返されます。

while true
  puts "Hi, what's your name? (hit Enter when done)"

  # `gets` returns input from the console
  name = gets

  puts "Nice to meet you, #{name}."
  puts "Now, let's repeat."
end

この例は、対話型のプレイグラウンドではありません。プレイグラウンドは自己終了しないプログラムやユーザー入力の処理を処理できないためです。タイムアウトしてエラーを出力するだけです。ただし、ローカルコンパイラでこのコードをコンパイルして実行できます。

プログラムを停止するには、Ctrl+Cを押します。これにより、プロセスに終了を求めるシグナルが送信されます。

スキップと中断

途中のいくつかの反復をスキップしたり、特定の条件で反復を完全に停止したりすると便利な場合があります。

ループ本体内のキーワードnextは、現在の反復に残っている式を無視して、次の反復にスキップします。ループ条件が満たされていない場合、ループは終了し、本体はもう一度実行されません。

counter = 0

while counter < 10
  counter += 1

  if counter % 3 == 0
    next
  end

  puts "Counter: #{counter}"
end

この例は、puts式を条件付きで配置することで、nextなしで簡単に記述できた可能性があります。nextの価値は、スキップする必要のあるメソッド本体に多くの式がある場合に明らかになります。

ループ条件は、たとえば複数のステップが必要な場合や、決定する必要がある入力に依存する場合など、計算が難しい場合があります。このような状況では、ループ条件にすべてのロジックを記述するのはあまり実用的ではありません。キーワードbreakはループ本体のどこでも使用でき、ループ条件に関係なくループから中断するための追加のオプションとして機能します。制御フローは、ループの終了後すぐに継続されます。

counter = 0

while true
  counter += 1

  puts "Counter: #{counter}"

  if counter >= 10
    break
  end
end

puts "done"