5.制御構造
プログラムというものは、最初から最後まで、順番にコードが実行されるだけでは、さっぱり何の役にも立たないのである。とまあ、そこまで言い切ってしまうとあれだが、とてもではないが複雑な処理をすることはできない。ある条件に従って実行する行を変更したり、必要回数同じ処理を繰り返すためにくるくる回ったり、どこか別の場所にあるルーチンの内容を実行したりすることが必要なのだ。
エドガー・ダイクストラというオランダの情報工学者がいた。この学者先生は、ことプログラミングを勉強した人々の間では、非常に有名なのだ。『構造化プログラミング』という概念の提唱者なのである。
『構造化プログラミング』とは、年々大規模複雑化するシステム開発を円滑に進める方法論として、1967年に提唱されたものなのである。既に40年以上の歳月が流れているのだが、現在ただ今でも現役バリバリの理論であり、従来よりのプログラミングの概念を打ち破る革命的な手法が登場しない限り、まあこれ以外にないでしょうというものなのである。
『構造化プログラミング』の概念を実装できるプログラミング言語群は、『構造化プログラミング言語』と呼ばれており、Rubyなどの『オブジェクト指向プログラミング言語』よりほと世代前とされているわけだが、実際のところ、オブジェクト指向プログラミング言語で登場する『クラス』や『オブジェクト』は、その中に構造化されたプログラムロジックを内包、隠蔽しているだけと考えることができる。極論を言うと。
オブジェクト指向プログラミング言語を使用して開発する末端のプログラマこそ、オブジェクト間のメッセージのやり取りだけでアプリケーションを構築できるかもしれないが、クラス設計/実装者は、ダイクストラの提唱した『構造化プログラミング』の概念でもって中身を詰めているのである。
『構造化プログラミング』とは、プログラムをお互いに依存することなく、かつ不変契約(同じ値を渡せば常に同じ結果となることが約束されている)ちいさな部品(
モジュール)を組み合わせて構築するという考え方である。さらに、そのモジュール内部は、構造化定理にのっとって記述されていなければならないとしている。
構造化定理とは、『全てのプログラムは、一つの入口と出口を持つ形(適正プログラム)に等価変形可能であり、適正プログラムでは任意のアルゴリズムを次の三つの基本的な制御構造で記述できる。その基本的な制御構造とは、順次(Seqence)、選択(Selection)、反復(Iteration)である』ということである。
Rubyにも、この三つの制御構造を実現するための命令(コマンド)が用意されているのである。
といっても『順次』構造は、命令が記述された順番に整然と実行されるということなので、とりたてて制御命令は存在しないのだが。
強いて言えば、『Rubyでは、特に明示的に実行順を入れ替えたり分岐させたりする制御命令を記述しなければ、なんと、書いた順番で処理しますよ。すごいでしょ』という言語仕様であろうか。まあ、そんなこと当たり前なのだが。
(1).選択(Selection)
『分岐』と表現している場合もある。ある条件があって、それが成立する場合(真の場合)と、成立しない場合(偽の場合)で、異なる処理を行うのが『選択』である。
Rubyには、選択構造を実現する命令として、次のものが準備されている。
if制御命令
if修飾子
unless制御命令
unless修飾子
case制御命令
(i)if制御命令
if制御命令は、プログラミング言語と名がつけば普通あるだろというほどメジャーなものだ。日本語で表現すると、『もし(if)、なになにがどうならば、これをせよ。でなければ(else)、あれをせよ。おしまい』となる。
だが、プログラミング言語によって書式が微妙に違い、同時期に複数の言語でプログラムを作っていたりすると、わけがわからなくなるときもある。Rubyでの書式は次の通りである。
if 条件式1 then
条件式が真の場合の処理(ステートメント)
elsif 条件式2 then
条件式2が真の場合の処理(ステートメント)
else
条件式2が偽の場合の処理(ステートメント)
end
勿論、偽の場合を省略したいこともあるから、else以降は省略することができる。ただし、『おしまい』にあたるendは省略することができない。構文解析エラーとなってしまう。
厄介なことに、他のプログラミング言語には、この『おしまい』の合図を省略できるものが多いから、Ruby以外の言語をお使いの方は、ついうっかり忘れてしまうことがあるので気をつけよう。
なぜRubyでは endを省略できないのか。それは、『処理ブロックを明確にする』という目的というか、Rubyの設計哲学がある。ここで、プログラミング言語界の古株にして最強の武闘派であるC言語に登場してもらおう。
if ( a>10 ) b=1; else c=1;
仮に、上記のようなif文があったとしよう。このプログラムにある日仕様変更が入り、a>10の条件式が成立しなかった場合、変数dにも1を設定して欲しいということになったのだ。
神様であるお客様の意向には逆らえないので、プログラマ君はしぶしぶプログラムを書き換えたのである。
if ( a>10 ) b=1; else c=1; d=1;
C言語をご存知の方であれば、このコードのアホさ加減はお分かりだと思う。
このコードは全くプログラマ君の意図した動きはせず、a>10の条件式が成立しようがしまいが、d=1の代入式が実行されてしまうのである。なぜならd=1の式は、if文の処理ブロック外にあるからなのである。
C言語の場合、if文の中で実行されるステートメントが1個だけの場合、処理ブロック指定を省略できるのだ。従って2個以上のステートメントを処理したければ、次のように、明示的に{}(中括弧)でブロックを指定してやらなければならない。
if ( a>10 ) b=1; else { c=1; d=1; }
C言語では、改行コードに意味はないから、次のように記述してもなんら変わらない。
if ( a>10 )
b=1;
else
{
c=1;
d=1;
}
次に、異形の怪物言語 VisualBasicを見てみよう。これがまた独特のルールに則っている。さすがは異形の怪物言語だけのことはあるのだ。
if ( a>10 ) b=1; else c=1; d=1;
上記のコードをVisualBasicで書き換えると次のようになる。
If a > 10 Then b = 1 Else c = 1 : d = 1
実はこのコード、なんとプログラマ君の意図した通りに動いてくれるのである。a>10の条件式が成立しなかった場合にのみ、ちゃんとd=1の代入が実行されるのだ。
一体どういうルールに則って動いているんだVisualBasicよ。と言いたくなるではないか。
VisualBasicの場合『同一行に記述されていれば、処理ブロックとしてみなしてあげます』というルールがあるのだ。VisualBasicの場合、改行コードというのが非常に重要な役割を持っているのである。ためしに、C言語のように行をわけて記述しようとすると、いきなり構文エラーになる。
If a > 10 Then
b = 1
Else
c = 1 : d = 1
エラー 'If' の終わりには、対応する 'End If' を
指定しなければなりません
VisualBasicでは、if制御文が複数行に渡るときだけ、『おしまい』の合図、End Ifを指定しなければならないというわけだ。
このように、他の言語においてif制御構文は、よく言えばフレキシブル、悪く言えばよくわからない状態となっているので、Rubyにおいては、『たとえステートメントが1個であろうが、ちゃんとendを記述して、処理ブロックを明確にしようじゃないか』というルールを定めているのである。
なお、これも、他の言語の経験がある方に注意していただきたいのが、elseの場合に、また条件式を指定したい場合だ。
Rubyの場合、elsifという独特な記述方法になる。 elseif でも else if でもないから注意しよう。このelsifという書式は、スクリプト言語の偉大なる先達、Perlと同じだということを参考までに付記しておく。
さて、Rubyのif制御文には、他のプログラミング言語では見かけない特色がある。それは、『if制御文自身が値を保持する』ということなのである。
代入式が値を持つプログラミング言語はあちらこちらで散見されるものの、if制御文自体が値を保持するプログラム言語は少ないのだ。
a = if b<10 then true else false end
『あれ?なあんだ。三項演算子と同じじゃないですか』
まあそうなのだが。
(ii)if修飾子
if制御命令は、プログラミング言語と名がつけば、普通あって当たり前なのだが、このif修飾子については、存在する言語は少ない。先達のPerlがこのif修飾子を持っているので、Perlから強く影響を受けているRubyにもあると言ってもいいかもしれない。
if修飾子は、次のように記述する。
puts "aは10より小さい" if a<10
コードを眺めていれば、自然と納得がいくかと思うが、要するに、”aは10より小さい”という文字列を出力したいわけだが、それを実行するにあたって、コードの後ろに条件式を指定することができるのである。この場合、if a<10 がその条件になる。irbで動作を確認してみよう。
この判定は、普通にif制御命令を使用して記述することもできるのだが、なんとなく回りくどい感じがする。if修飾子は面倒くさがりのプログラマへのRubyからのささやかなプレゼントと言えよう。
(iii)unless制御命令
unlessは、ifの逆である。ifは、条件文が真となったときにステートメントが実行されるが、unlessは偽となったときに実行される。
プログラミングの経験がある皆さんには、なにやら複雑な複合条件でif文を構築して、なんとなく動いたと思ったら、『悪りぃ。あそこの条件だけどさ。逆にしてくんない』といわれ、もうわけがわからん!とやけを起こして、次のような記述にしてしまったことはないだろうか。
(オリジナル)
if ( a>=4 || b<1 ) && ( c>5 && c<10 ) then
処理
end
(修正後)
if ( a>=4 || b<1 ) && ( c>5 && c<10 ) then
#何もしない
else
処理
end
これはちょっと不細工な気がする。明らかに考えることを放棄しているのがバレバレだ。そういうときにunlessを使用すると、少しは体裁がよくなる。
unless ( a>=4 || b<1 ) && ( c>5 && c<10 ) then
処理
end
まあ、条件式を組み立てなおさないという意味では同じなのだが。
unless制御命令にもelseを書くことができる。この場合、else以下は条件式が真の時に実行されることになる。ただし残念ながら、elsifは指定できないし、elunlessなどという記法はない。『もしかしてあるかもね?』と期待された方には申し訳ないが。
(iv)unless修飾子
unless修飾子は、if修飾子と逆の動きをする。
puts "aは10以上である" unless a<10
unlessが存在することにより、条件式の組み立てが非常に楽になるのだが、残念ながらC、C++言語、C#といった中大規模システム構築用のコンパイラ系言語には準備されていない。それらの言語でややこしい条件式を組み立てているときに、ふと『unlessがあればなあ』と思うことがよくあるのだが。
(v).case制御命令
if制御文は、ある条件式があって、それが真か偽かの二者択一であったが、世の中そうなにごとも、真偽で判定できるものではなく、三通り以上の選択肢があることが多いのだ。その場合に重宝するのがこのcase制御命令なのである。
case month
when 2
puts "二月は特別です"
when 4,6,9,11
puts "西向くさむらい小の月です"
else
puts "大の月です"
end
上記は、変数monthの値が、2の場合、4か6か9か11の場合、そして、それ以外の場合で出力するメッセージを切り替えている例だ。
この場合、たまたま評価されるものが変数となっているが、当然演算式でも、代入式でも、比較演算式でも構わない。
when節にある値の指定は、
演算子のところででてきた、範囲演算子が指定できる。
case ji
when 6..11
puts "おはようございます"
when 12..18
puts "こんにちは"
else
puts "こんばんは"
end
それぞれ、6から11、12から18の範囲を持つということになるわけだ。
さらに、case に続く式を省略することもできる。この場合、 when節が順に評価されていき、最初に真となった式が評価される。
case
when (6..11) === ji
puts "おはようございます"
when (12..18) === ji
puts "こんにちは"
else
puts "こんばんは"
end
以上で『選択』の解説は終了だ。
ん?『さらっと流さないでください。===(イコールみっつ)ってなんですか?そんな演算子、解説してないじゃないですか』
実はこの===演算子、普遍的な演算子ではない。Rangeオブジェクトだけに定義されている特別な演算子なのだ。
これを『クラスにおける演算子の多重定義(オーバーローディング)』と呼ぶのだが、詳細はクラスの解説をするときに併せて説明しよう。===は、右辺の値が、左辺のRangeオブジェクトが持つ範囲にあれば真、なければ偽になるという便利な演算子というわけなのであった。
(2).反復(Iteration)
反復を実現する制御命令には、次のものがある。
while 制御命令
while 修飾子
until 制御命令
until 修飾子
for 制御命令
(i)while制御命令
while制御命令の書式は以下の通りだ。
while 式 [do]
...
end
whileは、条件式が真である間、endまでに記述されたステートメントを繰り返し実行する。 whileの条件式は、可能な限りいつか偽となることが保証されるものが望ましい。さもないと、忌むべき永久ループという状態に陥ってしまう。
例として、Rubyリファレンスマニュアルに記載されているサンプルコードを見てみよう。
001 | ary = [0,2,4,8,16,32,64,128,256,512,1024]
002 | i = 0
003 | while i < ary.length
004 | print ary[i]
005 | i += 1
006 | end
これは、配列aryの要素を順番に出力していくものだ。条件式は i<ary.lengh だから、変数iが、配列aryの要素数より小さい間は真になる。そしてこの条件式が偽になるのはいつかというと、5行目のi += 1で、変数iの値がiづつカウントアップされている。ここでいつかはiの値が配列aryの要素数と同じになるときがくるのだ。そのとき、めでたくこの条件式が偽となるのである。
なぜiが配列aryの要素数とイコールになるまでではなく、-1までかというと、変数iがそのまま配列aryの要素番号になっているからだ。
配列の要素番号は 0から要素数-1までであるから。これは、あまたのプログラミング言語仕様においては、最も基礎的なことだ。
さて、このようなコード記述したときに、うっかり忘れてしまいがちなのが、5行目のカウントアップ処理なのである。これがないと、このwhile文は、永久にループしてしまうのだ。であるから、配列の要素数分処理をしたい場合は、イテレータ、すなわち配列オブジェクトが持つeachメソッドを使用するほうが無難だし、簡単だ。
001 | ary = [0,2,4,8,16,32,64,128,256,512,1024]
002 | ary.each { |i|
003 | print i
004 | }
(ii)while修飾子
while にもifやunlessと同様に、修飾子としての書式が存在する。ステートメントの実行にあたって条件があるので、修飾子が存在するのが普通と考えるべきだ。
前出のコードを修飾子を使って書き換えると次のようになる。
001 | ary = [0,2,4,8,16,32,64,128,256,512,1024]
002 | i = 0
003 | begin
004 | print ary[i]
005 | i += 1
006 | end while i<ary.length
なんだ、余計に記述量が増えているではないか。このコードの場合、whileで実行されるステートメントが複数あるから、 begin~endで処理ブロックを作る必要がある。whileで実行するステートメントがひとつである場合に限り、whileに対するendを省略できるから、若干記述量の節約になるのであった。
(iii)until制御命令
while制御命令は、条件式が真である間、処理を実行するものであったが、until制御命令はその逆、即ち条件が真になる間、言い換えれば、条件が偽である間処理を実行するものである。
until 式 [do]
...
end
前出のwhileのコードをuntilで書き換えると次のようになる。
001 | ary = [0,2,4,8,16,32,64,128,256,512,1024]
002 | i = 0
003 | until i>=ary.length
004 | print ary[i]
005 | i += 1
006 | end
(iv)until修飾子
whileにも修飾子としての記述法があるので、当然このuntilにも修飾子としての記述法が存在する。条件が『~の間』と『~になる迄』という相違だけで、この二つの制御構造は一卵性双生児といえるのである。
001 | ary = [0,2,4,8,16,32,64,128,256,512,1024]
002 | i = 0
003 | begin
004 | print ary[i]
005 | i += 1
006 | end until i>=ary.length
while制御構造とuntil制御構造、そしてwhile修飾子とuntil修飾子。先ほど評したように双生児ではなく、こりゃはっきり言って四つ子のようなこれらの命令群をどのような局面で使い分けたらよいのであろうか。
それは、最終的に個人の好みに帰結するのである。
(v)for制御命令
while制御命令、until制御命令とも、実は他のプログラミング言語にも普通に存在し、かつ同様な書式で、同様な動きをするので、ほっと一安心されたかと思う。
その流れから判断すれば、 for制御命令だって他のと同じようなものだろうと甘く見るとノツボにはまるので注意が必要だ。
Rubyのfor制御文は、他の言語とは一味違うのである。
もし皆さんが他のプログラミング言語をご存知であればあれば、 for制御文というと、次のようなものを想起されると思う。
【C、C++、C#、Java】
int i;
for ( i=0; i<10; i++ )
{
...
}
【Visual Basic】
Dim i as Integer
For i=0 To 9
...
next
【pascal】
var
i:Integer;
begin
for i:=0 to 9 do
begin
...
end;
end;
Rubyと関係ないことをだらだら書いて、行を無駄遣いしやがってこの野郎とのご批判もあろうが、要するに、『凡百(?)のプログラミング言語では軒並み、 for文とは、ある変数(カウンタ)があって、それがある決まった値になるまで、増加(または減少)している間、ステートメントが実行されるのだぞ』ということを強調したいのである。
で、最終的に、『ところがどっこいRubyの For文はちょっと違うぞ』ということが言いたいのだ。
Rubyにおいて、for制御命令の書式は次の通りだ。
for 変数 ... in 式 [do]
...
end
【in 式】の式の部分には、以前解説させていただいた『範囲オブジェクト』、または『配列オブジェクト』が指定されるのだ。結果的に『範囲オブジェクト』または、『配列オブジェクト』であればよいわけだから、関数の戻り値でも、クラスメソッドの戻り値でも、値を保持するステートメントでもよいのである。
他のプログラミング言語で例示したコードをRubyの for制御命令で書き換えると、次のようになる。
for i in 0..9
...
end
あれ。これも前回の最後に出てきた配列または範囲オブジェクトのeachメソッドで処理してもいいのでは?と思われた方も多数いらっしゃると思う。なにを隠そうそれは正解だ。
実際に、eachメソッドで処理してもなんら問題ないのである。
(0..9).each { |i|
...
}
ところが、 for制御命令とeachメソッドの呼び出しには、決定的な違いがひとつだけ存在するのだ。Rubyのリファレンスマニュアルには、次のように記してある。
『 do ... endまたは{ }によるブロックは新しいローカル変数の有効範囲を導入するのに対し、 for文はローカル変数のスコープに影響を及ぼさない点が異なるからです』
一読して何のことやらよくわからないかもしれないので、例示して説明しよう。次のコードを見ていただきたい。あまり素敵な例とは言えないのだが。
001 | j=10
002 | for i in 1..10
003 | if i==1 then k=10 end
004 | puts (i+j).to_s
005 | k+= (i+j)
006 | end
007 | puts k.to_s
forループの中で、kという変数が突然誕生しているのがわかると思う(3行目)。その変数kをforループの外側で参照しているのである。
このコードは、無事正常終了する。では同じことをeachメソッドでやってみる。
001 | j=10
002 | (1..10).each { |i|
003 | if i==1 then k=10 end
004 | puts (i+j).to_s
005 | k+= (i+j)
006 | }
007 | puts k.to_s
残念なことに、これは正常に実行できず、エラーとなってしまうのだ。
【NameError: undefined local variable or method
`k' for main:Object】
7行目の変数kなどというものは知らんと怒っているのである。
なぜそのようにRubyが怒るのかというと、変数 kは、each文の{}(中括弧)で閉ざされた内部だけで有効なローカル変数であり、eachの外部からは見えないのだ。要するにスコープが違うのである。
しかし、For文の中であれば、外部と同じスコープであるというわけなのだ。
さて、 for制御命令でも、配列のeachメソッドでもループ処理ができて嬉しいなと喜んでいたところ、なんとRubyには、まだ別の方法が存在する。
サンプルコードの場合、くるくる回る回数は10回であるということがあらかじめわかっている。であれば、次のような記述も可能なのである。
001 | j=10
002 | k=0
003 | 10.times{ |i|
004 | puts (i+1+j).to_s
005 | k += (i+1+j)
006 | }
007 | puts k.to_s
数値オブジェクトの timesメソッドを使用する方法だ。ループ処理ひとつをとってもこれだけたくさんの書法が存在するのである。一体どれを使えばいいのだと悩ましいところであるが、結論から言うと先ほど申し上げたように、最後は実装者個々の好みということになる。
さて、これで反復を実現する制御命令について解説し終わったわけだが、補足として、反復用制御命令と関連して動作する制御命令を解説しておく。
(vi)break
break は、反復処理制御を開始するときに定められた終了条件を満たす以前に、反復処理を抜け出したいときに使用する。反復処理とは、以下のものを指すということは、既にみなさんお分かりだと思う。
while
until
for
イテレータ
コード例を挙げておこう。
001 | ary = [0,2,4,8,16,32,64,128,256,512,1024]
002 | i = 0
003 | while i < ary.length
004 | unless DoSomeThing(ary[i]) then
005 | break
006 | end
007 | print ary[i]
008 | i += 1
009 | end
さてここで、もし貴方がC系のプログラミング言語経験者である場合、ひとつ疑問に思ってほしいことがあるのだ。
それは『case制御命令において、breakは必要ないのか?』ということだ。
C言語において、Rubyのcase制御命令に該当するswitch制御命令の構文は次の通りだ。
001 | switch ( month )
002 | {
003 | case 2:
004 | printf "二月は特別です\n";
005 | break;
006 | case 4:
007 | case 6:
008 | case 9:
009 | case 11:
010 | printf "西向くさむらい小の月です\n";
011 | break;
012 | default:
013 | printf "大の月です\n";
014 | }
このように、 C言語系のswitch制御構文においては、各case節に記述されるステートメントの最後は、breakであることが望ましい。なぜなら、breakを指定しておかないと、下までずるずるっと実行されてしまうからである。
上記のコードで、5行目のbreakを忘れると、変数monthの値が2であった場合、 6行目から10行目までが一気に実行されてしまい、11行目の breakでやっと止まるということになってしまうのである。
変数monthの値が4でも6でも9でも11でもないに関わらずだ。
『なぜそんな変な仕様になっているのですか?』
知らん。
幸いなことにRubyでは、このcaseのずるずる流れ込みが存在しない。
変数の値が2であれば、2の場合に実行するよう記述したステートメントしか実行されないことが保証されている。従ってcase制御構文に breakは必要ないというわけである。
(vii)next
nextは、 for制御構文内、または、イテレータの処理内において、ある条件であれば処理をスキップしたいときに使用する。
001 | #for制御構文の場合
002 | for i in 1..10
003 | next if i==5
004 | puts i.to_s
005 | end
006 |
007 | #イテレータの場合
008 | (1..10).each { |i|
009 | next if i==5
010 | puts i.to_s
011 | }
上記二つのコードは、どちらもiの値が5の場合、処理をスキップする。念のため、irbで動作を確認しておこう。5の表示がすっ飛ばされていることを確認していただきたい。
(viii)redo
redoも、 for制御構文内、または、イテレータの処理内において使用する。ある条件が成立すれば、ループ条件のチェックを行わず、現在の処理を繰り返すものである。
001 | #for制御構文の場合
002 | for i in 1..10
003 | puts i.to_s
004 | redo if i==5
005 | end
006 |
007 | #イテレータの場合
008 | j=10
009 | k=0
010 | (1..10).each { |i|
011 | puts i.to_s
012 | redo if i==5
013 | }
上記二つのコードは、どちらもiの値が5の場合、処理を繰り返す。ご想像の通り、これらコードは、1から5の数字を画面表示した後、そのままループして永遠に5を表示し続けるのだ。実際には、こんなコード書いてはいけないし、実行してもいけない。
(ix)retry
retryも、for制御構文内、または、イテレータの処理内において使用するものだ。 retryは、ある条件が成立すれば、なんとご苦労なことに、ループ処理の最初からもう一度やり直のである。。
001 | #for制御構文の場合
002 | for i in 1..10
003 | puts i.to_s
004 | retry if i==5
005 | end
006 |
007 | #イテレータの場合
008 | j=10
009 | k=0
010 | (1..10).each { |i|
011 | puts i.to_s
012 | retry if i==5
013 | }
上記二つのコードは、どちらもiの値が5の場合、ループの最初に状態を戻す。ご想像の通り、これらのコードは、1から5までの表示を永遠に繰り返すのである。くどいようだが、実際にはこんなコード書いちゃいけないからね。
最終更新:2008年12月04日 12:50