Kontrollfluss-Ausdrücke

Ruby bietet eine Vielzahl von Möglichkeiten zur Steuerung der Ausführung. Alle hier beschriebenen Ausdrücke geben einen Wert zurück.

Für die Tests in diesen Kontrollfluss-Ausdrücken gelten nil und false als False-Werte und true sowie jedes andere Objekt als True-Werte. In diesem Dokument bedeutet „true“ „True-Wert“ und „false“ bedeutet „False-Wert“.

if-Ausdruck

Der einfachste if-Ausdruck besteht aus zwei Teilen: einem „Test“-Ausdruck und einem „then“-Ausdruck. Wenn der „Test“-Ausdruck zu einem True-Wert ausgewertet wird, wird der „then“-Ausdruck ausgewertet.

Hier ist eine einfache if-Anweisung

if true then
  puts "the test resulted in a true-value"
end

Dies gibt „the test resulted in a true-value“ aus.

Das then ist optional

if true
  puts "the test resulted in a true-value"
end

Dieses Dokument lässt das optionale then bei allen Ausdrücken weg, da dies die häufigste Verwendung von if ist.

Sie können auch einen else-Ausdruck hinzufügen. Wenn der Test nicht zu einem True-Wert ausgewertet wird, wird der else-Ausdruck ausgeführt.

if false
  puts "the test resulted in a true-value"
else
  puts "the test resulted in a false-value"
end

Dies gibt „the test resulted in a false-value“ aus.

Sie können mit elsif eine beliebige Anzahl zusätzlicher Tests zu einem if-Ausdruck hinzufügen. Ein elsif wird ausgeführt, wenn alle Tests oberhalb des elsif false sind.

a = 1

if a == 0
  puts "a is zero"
elsif a == 1
  puts "a is one"
else
  puts "a is some other value"
end

Dies gibt „a is one“ aus, da 1 nicht gleich 0 ist. Da else nur ausgeführt wird, wenn keine Bedingung übereinstimmt.

Sobald eine Bedingung übereinstimmt, entweder die if-Bedingung oder eine elsif-Bedingung, ist der if-Ausdruck abgeschlossen und es werden keine weiteren Tests durchgeführt.

Wie bei einem if kann auf eine elsif-Bedingung ein then folgen.

In diesem Beispiel wird nur „a is one“ ausgegeben.

a = 1

if a == 0
  puts "a is zero"
elsif a == 1
  puts "a is one"
elsif a >= 1
  puts "a is greater than or equal to one"
else
  puts "a is some other value"
end

Die Tests für if und elsif können Seiteneffekte haben. Die häufigste Verwendung von Seiteneffekten ist das Caching eines Wertes in einer lokalen Variablen.

if a = object.some_value
  # do something to a
end

Der Ergebniswert eines if-Ausdrucks ist der zuletzt ausgeführte Wert in dem Ausdruck.

Ternärer if

Sie können einen if-then-else-Ausdruck auch mit ? und : schreiben. Dieser ternäre if

input_type = gets =~ /hello/i ? "greeting" : "other"

Ist derselbe wie dieser if-Ausdruck.

input_type =
  if gets =~ /hello/i
    "greeting"
  else
    "other"
  end

Während der ternäre if viel kürzer zu schreiben ist als die ausführlichere Form, wird zur Lesbarkeit empfohlen, den ternären if nur für einfache Bedingungen zu verwenden. Vermeiden Sie es auch, mehrere ternäre Bedingungen im selben Ausdruck zu verwenden, da dies verwirrend sein kann.

unless-Ausdruck

Der unless-Ausdruck ist das Gegenteil des if-Ausdrucks. Wenn der Wert false ist, wird der „then“-Ausdruck ausgeführt.

unless true
  puts "the value is a false-value"
end

Dies gibt nichts aus, da true kein False-Wert ist.

Sie können ein optionales then mit unless wie bei if verwenden.

Beachten Sie, dass der obige unless-Ausdruck derselbe ist wie

if not true
  puts "the value is a false-value"
end

Wie ein if-Ausdruck können Sie eine else-Bedingung mit unless verwenden.

unless true
  puts "the value is false"
else
  puts "the value is true"
end

Dies gibt „the value is true“ aus der else-Bedingung aus.

Sie können elsif nicht mit einem unless-Ausdruck verwenden.

Der Ergebniswert eines unless-Ausdrucks ist der zuletzt ausgeführte Wert in dem Ausdruck.

Modifikator if und unless

if und unless können auch zur Modifizierung eines Ausdrucks verwendet werden. Wenn sie als Modifikator verwendet werden, ist die linke Seite die „then“-Anweisung und die rechte Seite der „Test“-Ausdruck.

a = 0

a += 1 if a.zero?

p a

Dies gibt 1 aus.

a = 0

a += 1 unless a.zero?

p a

Dies gibt 0 aus.

Während die Modifikator- und Standardversionen sowohl einen „Test“-Ausdruck als auch eine „then“-Anweisung haben, sind sie aufgrund der Parse-Reihenfolge keine exakten Transformationen voneinander. Hier ist ein Beispiel, das den Unterschied zeigt.

p a if a = 0.zero?

Dies löst die NameError „undefined local variable or method ‘a’” aus.

Wenn Ruby diesen Ausdruck parst, stößt es zuerst auf a als Methodenaufruf im „then“-Ausdruck, dann sieht es später die Zuweisung zu a im „Test“-Ausdruck und markiert a als lokale Variable.

Beim Ausführen dieser Zeile wird zuerst der „Test“-Ausdruck a = 0.zero? ausgeführt.

Da der Test true ist, wird der „then“-Ausdruck p a ausgeführt. Da das a im Körper als nicht vorhandene Methode aufgezeichnet wurde, wird die NameError ausgelöst.

Das Gleiche gilt für unless.

case-Ausdruck

Der case-Ausdruck kann auf zwei Arten verwendet werden.

Die gebräuchlichste Art ist der Vergleich eines Objekts mit mehreren Mustern. Die Muster werden mit der Methode === abgeglichen, die auf Object als == aliasiert ist. Andere Klassen müssen sie überschreiben, um sinnvolles Verhalten zu erzielen. Siehe Module#=== und Regexp#=== für Beispiele.

Hier ist ein Beispiel für die Verwendung von case zum Vergleich eines String mit einem Muster.

case "12345"
when /^1/
  puts "the string starts with one"
else
  puts "I don't know what the string starts with"
end

Hier wird der String "12345" mit /^1/ verglichen, indem /^1/ === "12345" aufgerufen wird, was true zurückgibt. Wie beim if-Ausdruck wird der erste übereinstimmende when ausgeführt und alle anderen Übereinstimmungen werden ignoriert.

Wenn keine Übereinstimmungen gefunden werden, wird else ausgeführt.

Das else und then sind optional. Dieser case-Ausdruck liefert dasselbe Ergebnis wie der obige.

case "12345"
when /^1/
  puts "the string starts with one"
end

Sie können mehrere Bedingungen auf demselben when platzieren.

case "2"
when /^1/, "2"
  puts "the string starts with one or is '2'"
end

Ruby versucht jede Bedingung nacheinander. Zuerst gibt /^1/ === "2" false zurück, dann gibt "2" === "2" true zurück, sodass „the string starts with one or is ‘2’” ausgegeben wird.

Sie können then nach der when-Bedingung verwenden. Dies wird am häufigsten verwendet, um den Körper von when in einer einzigen Zeile zu platzieren.

case a
when 1, 2 then puts "a is one or two"
when 3    then puts "a is three"
else           puts "I don't know what a is"
end

Die andere Möglichkeit, einen case-Ausdruck zu verwenden, ist wie ein if-elsif-Ausdruck.

a = 2

case
when a == 1, a == 2
  puts "a is one or two"
when a == 3
  puts "a is three"
else
  puts "I don't know what a is"
end

Auch hier sind then und else optional.

Der Ergebniswert eines case-Ausdrucks ist der zuletzt ausgeführte Wert in dem Ausdruck.

Seit Ruby 2.7 bieten case-Ausdrücke auch eine leistungsfähigere Mustervergleichsfunktion über das Schlüsselwort in.

case {a: 1, b: 2, c: 3}
in a: Integer => m
  "matched: #{m}"
else
  "not matched"
end
# => "matched: 1"

Die Syntax des Mustervergleichs wird auf einer eigenen Seite beschrieben.

while-Schleife

Die while-Schleife wird ausgeführt, solange eine Bedingung true ist.

a = 0

while a < 10 do
  p a
  a += 1
end

p a

Gibt die Zahlen 0 bis 10 aus. Die Bedingung a < 10 wird geprüft, bevor die Schleife betreten wird, dann wird der Körper ausgeführt, dann wird die Bedingung erneut geprüft. Wenn die Bedingung false ergibt, wird die Schleife beendet.

Das Schlüsselwort do ist optional. Die folgende Schleife ist äquivalent zur obigen Schleife.

while a < 10
  p a
  a += 1
end

Das Ergebnis einer while-Schleife ist nil, es sei denn, break wird verwendet, um einen Wert anzugeben.

until-Schleife

Die until-Schleife wird ausgeführt, solange eine Bedingung false ist.

a = 0

until a > 10 do
  p a
  a += 1
end

p a

Dies gibt die Zahlen 0 bis 11 aus. Wie bei einer while-Schleife wird die Bedingung a > 10 beim Betreten der Schleife und jedes Mal, wenn der Schleifenkörper ausgeführt wird, geprüft. Wenn die Bedingung false ist, wird die Schleife weiter ausgeführt.

Wie bei einer while-Schleife ist do optional.

Wie bei einer while-Schleife ist das Ergebnis einer until-Schleife nil, es sei denn, break wird verwendet.

for-Schleife

Die for-Schleife besteht aus for, gefolgt von einer Variablen, die das Iterationsargument aufnehmen soll, gefolgt von in und dem Wert, über den iteriert werden soll, mittels each. Das do ist optional.

for value in [1, 2, 3] do
  puts value
end

Gibt 1, 2 und 3 aus.

Wie while und until ist do optional.

Die for-Schleife ähnelt der Verwendung von each, erstellt jedoch keinen neuen Variablenbereich.

Der Ergebniswert einer for-Schleife ist der iterierte Wert, es sei denn, break wird verwendet.

Die for-Schleife wird in modernen Ruby-Programmen selten verwendet.

Modifikator while und until

Wie if und unless können while und until als Modifikatoren verwendet werden.

a = 0

a += 1 while a < 10

p a # prints 10

until als Modifikator verwendet.

a = 0

a += 1 until a > 10

p a # prints 11

Sie können begin und end verwenden, um eine while-Schleife zu erstellen, die den Körper einmal vor der Bedingung ausführt.

a = 0

begin
  a += 1
end while a < 10

p a # prints 10

Wenn Sie rescue oder ensure nicht verwenden, optimiert Ruby jeden Overhead für die Ausnahmebehandlung weg.

break-Anweisung

Verwenden Sie break, um einen Block frühzeitig zu verlassen. Dies stoppt die Iteration über die Elemente in values, wenn eines davon gerade ist.

values.each do |value|
  break if value.even?

  # ...
end

Sie können eine while-Schleife auch mit break beenden.

a = 0

while true do
  p a
  a += 1

  break if a < 10
end

p a

Dies gibt die Zahlen 0 und 1 aus.

break akzeptiert einen Wert, der das Ergebnis des Ausdrucks liefert, aus dem es „ausbricht“.

result = [1, 2, 3].each do |value|
  break value * 2 if value.even?
end

p result # prints 4

next-Anweisung

Verwenden Sie next, um den Rest der aktuellen Iteration zu überspringen.

result = [1, 2, 3].map do |value|
  next if value.even?

  value * 2
end

p result # prints [2, nil, 6]

next akzeptiert ein Argument, das als Ergebnis der aktuellen Blockiteration verwendet werden kann.

result = [1, 2, 3].map do |value|
  next value if value.even?

  value * 2
end

p result # prints [2, 2, 6]

redo-Anweisung

Verwenden Sie redo, um die aktuelle Iteration erneut auszuführen.

result = []

while result.length < 10 do
  result << result.length

  redo if result.last.even?

  result << result.length + 1
end

p result

Dies gibt [0, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11] aus.

In Ruby 1.8 konnten Sie auch retry dort verwenden, wo Sie redo verwendeten. Dies ist nicht mehr der Fall. Sie erhalten nun eine SyntaxError, wenn Sie retry außerhalb eines rescue-Blocks verwenden. Siehe Exceptions für die korrekte Verwendung von retry.

Modifikator-Anweisungen

Rubys Grammatik unterscheidet zwischen Anweisungen und Ausdrücken. Alle Ausdrücke sind Anweisungen (ein Ausdruck ist eine Art von Anweisung), aber nicht alle Anweisungen sind Ausdrücke. Einige Teile der Grammatik akzeptieren Ausdrücke und keine anderen Arten von Anweisungen, was dazu führt, dass Code, der ähnlich aussieht, unterschiedlich geparst wird.

Zum Beispiel sind if, else, while, until und begin, wenn sie nicht als Modifikatoren verwendet werden, Ausdrücke (und auch Anweisungen). Wenn sie jedoch als Modifikatoren verwendet werden, sind if, else, while, until und rescue Anweisungen, aber keine Ausdrücke.

if true; 1 end # expression (and therefore statement)
1 if true      # statement (not expression)

Anweisungen, die keine Ausdrücke sind, können nicht in Kontexten verwendet werden, in denen ein Ausdruck erwartet wird, wie z. B. bei Methodenargumenten.

puts( 1 if true )      #=> SyntaxError

Sie können eine Anweisung in Klammern setzen, um einen Ausdruck zu erstellen.

puts((1 if true))      #=> 1

Wenn Sie ein Leerzeichen zwischen dem Methodennamen und der öffnenden Klammer setzen, benötigen Sie keine zwei Klammerpaare.

puts (1 if true)       #=> 1, because of optional parentheses for method

Das liegt daran, dass dies ähnlich wie ein Methodenaufruf ohne Klammern geparst wird. Es ist äquivalent zum folgenden Code, ohne die Erstellung einer lokalen Variable.

x = (1 if true)
p x

In einer Modifikator-Anweisung muss die linke Seite eine Anweisung und die rechte Seite ein Ausdruck sein.

Daher wird in a if b rescue c, weil b rescue c eine Anweisung ist, die kein Ausdruck ist und daher nicht als rechte Seite der if-Modifikator-Anweisung zulässig ist, der Code notwendigerweise als (a if b) rescue c geparst.

Dies interagiert mit der Operatorrangfolge so, dass

stmt if v = expr rescue x
stmt if v = expr unless x

geparst werden als

stmt if v = (expr rescue x)
(stmt if v = expr) unless x

Dies liegt daran, dass Modifikator rescue eine höhere Priorität als = hat und Modifikator if eine niedrigere Priorität als = hat.

Flip-Flop

Der Flip-Flop ist ein etwas spezieller bedingter Ausdruck. Eine seiner typischen Verwendungen ist die Verarbeitung von Text aus Ruby-Einzeilenprogrammen, die mit ruby -n oder ruby -p verwendet werden.

Die Form des Flip-Flops ist ein Ausdruck, der angibt, wann der Flip-Flop eingeschaltet wird, .. (oder ...), dann ein Ausdruck, der angibt, wann der Flip-Flop ausgeschaltet wird. Solange der Flip-Flop eingeschaltet ist, wird er weiterhin zu true ausgewertet, und false, wenn er ausgeschaltet ist.

Hier ist ein Beispiel

selected = []

0.upto 10 do |value|
  selected << value if value==2..value==8
end

p selected # prints [2, 3, 4, 5, 6, 7, 8]

Im obigen Beispiel ist die „Ein“-Bedingung n==2. Der Flip-Flop ist anfänglich für 0 und 1 „Aus“ (false), wird aber für 2 „Ein“ (true) und bleibt bis 8 „Ein“. Nach 8 schaltet er sich aus und bleibt für 9 und 10 „Aus“.

Der Flip-Flop muss innerhalb einer Bedingung wie !, ? :, not, if, while, unless, until usw. verwendet werden, einschließlich der Modifikatorformen.

Wenn Sie einen inklusiven Bereich (..) verwenden, wird die „Aus“-Bedingung ausgewertet, wenn sich die „Ein“-Bedingung ändert.

selected = []

0.upto 5 do |value|
  selected << value if value==2..value==2
end

p selected # prints [2]

Hier werden beide Seiten des Flip-Flops ausgewertet, sodass der Flip-Flop nur dann ein- und ausgeschaltet wird, wenn value gleich 2 ist. Da sich der Flip-Flop in der Iteration eingeschaltet hat, gibt er true zurück.

Wenn Sie einen exklusiven Bereich (...) verwenden, wird die „Aus“-Bedingung in der nächsten Iteration ausgewertet.

selected = []

0.upto 5 do |value|
  selected << value if value==2...value==2
end

p selected # prints [2, 3, 4, 5]

Hier schaltet sich der Flip-Flop ein, wenn value gleich 2 ist, aber er schaltet sich nicht in derselben Iteration aus. Die „Aus“-Bedingung wird erst in der nächsten Iteration ausgewertet und value wird niemals wieder zwei sein.

throw/catch

throw und catch werden verwendet, um nicht-lokalen Kontrollfluss in Ruby zu implementieren. Sie funktionieren ähnlich wie Ausnahmen und ermöglichen es, dass die Kontrolle direkt von der Stelle, an der throw aufgerufen wird, zu der Stelle übergeht, an der das übereinstimmende catch aufgerufen wird. Der Hauptunterschied zwischen throw/catch und der Verwendung von Ausnahmen besteht darin, dass throw/catch für erwartete nicht-lokale Kontrollflussänderungen konzipiert sind, während Ausnahmen für außergewöhnliche Kontrollflusssituationen, wie die Behandlung unerwarteter Fehler, konzipiert sind.

Wenn Sie throw verwenden, übergeben Sie 1-2 Argumente. Das erste Argument ist der Wert für das übereinstimmende catch. Das zweite Argument ist optional (standardmäßig nil) und ist der Wert, den catch zurückgibt, wenn ein übereinstimmendes throw innerhalb des catch-Blocks vorhanden ist. Wenn keine übereinstimmende throw-Methode innerhalb eines catch-Blocks aufgerufen wird, gibt die catch-Methode den Rückgabewert des an sie übergebenen Blocks zurück.

def a(n)
  throw :d, :a if n == 0
  b(n)
end

def b(n)
  throw :d, :b if n == 1
  c(n)
end

def c(n)
  throw :d if n == 2
end

4.times.map do |i|
  catch(:d) do
    a(i)
    :default
  end
end
# => [:a, :b, nil, :default]

Wenn das erste Argument, das Sie an throw übergeben, nicht von einem übereinstimmenden catch behandelt wird, wird eine UncaughtThrowError-Ausnahme ausgelöst. Dies liegt daran, dass throw/catch nur für erwartete Kontrollflussänderungen verwendet werden sollten, daher ist die Verwendung eines Wertes, der nicht bereits erwartet wurde, ein Fehler.

throw/catch werden als Kernel-Methoden (Kernel#throw und Kernel#catch) implementiert, nicht als Schlüsselwörter. Daher sind sie in einem BasicObject-Kontext nicht direkt verwendbar. In diesem Fall können Sie Kernel.throw und Kernel.catch verwenden.

BasicObject.new.instance_exec do
  def a
    b
  end

  def b
    c
  end

  def c
    ::Kernel.throw :d, :e
  end

  result = ::Kernel.catch(:d) do
    a
  end
  result # => :e
end