[CG実習 >  Rubyによるプログラミングの基礎 >  配列とイテレータ]

配列とイテレータ

プログラムでは,しばしば大量のデータを一度に扱います. これまでのプログラムでは, 一つのデータ(オブジェクト)を一つの変数と対応づけていました. すると,たとえば,1000個のデータがあったら, 1000個の変数を用意しなければならないのでしょうか. 1000個の変数に対応づけられた1000個の数値データの平均をとるときには,

  (a1+a2+......a999+a1000)/1000
のように書かないといけないのでしょうか. データが2000個に増えたら,1000個追加して,
  (a1+a2+......a999+a1000+a1001+....+a2000)/2000
と書き換えるしかないのでしょうか.

もちろん,このようなプログラムを書くことは考えられません. 大量のデータを扱うには,配列を使うのが一般的です. また,Rubyでは,配列にイテレータを組み合わせて使うことで, 配列を簡単に処理することができます. ここでは,配列とイテレータについて説明します.

もくじ

  1. 例題
  2. プログラムの仕様
  3. 配列の利用
  4. プログラム
  5. 配列
  6. 例外処理 --- 入力データのチェック
  7. イテレータ
  8. コマンドライン引数を使う

例題

ある金額(円)を硬貨(1円,5円,10円,50円,100円,500円)でおつりがないように支払うとき, できるだけ少ない枚数で支払うには何円玉を何枚出せばよいかを調べる.

プログラムの仕様

プログラムの流れは,おおよそ次の通りになるでしょう.

  1. 金額を入力する.
    1円以上かどうかを確かめる.そうでなければ,すぐに実行終了.
  2. 各硬貨が何枚必要になるか計算して,結果を表示する.
    1. 金額が500円以上であれば,500円玉で支払える枚数を計算して,その枚数を表示する.500円玉で支払う額を差し引いた残額を求める.
    2. 残額が100円以上であれば,100円玉で支払える枚数を計算して,その枚数を表示する.100円玉で支払う額を差し引いた残額を求める.
    3. 残額が50円以上であれば,50円玉で支払える枚数を計算して,その枚数を表示する.50円玉で支払う額を差し引いた残額を求める.
    4. 残額が10円以上であれば,10円玉で支払える枚数を計算して,その枚数を表示する.10円玉で支払う額を差し引いた残額を求める.
    5. 残額が5円以上であれば,5円玉で支払える枚数を計算して,その枚数を表示する.5円玉で支払う額を差し引いた残額を求める.
    6. 残額が1円以上であれば,1円玉で支払える枚数を計算して,その枚数を表示する.

配列の利用

これまでに学んだ範囲の知識で, 上に書いた仕様にそのまま従ってプログラムを作成することはできます. その場合は,次の参考プログラムのようになるでしょう.

しかし,そのようなプログラムは,明らかに非効率になります. 例題で計算と表示を行っている部分は, 各硬貨に対して,明らかに同じ処理を繰り返していることが分かります. これを,前回の「繰り返し」 でうまく表現できないでしょうか.

このようなときに強力な手段を提供してくれるのが,配列です. Rubyでは,さらにイテレータを利用することで, 繰り返しを分かりやすく書くことができます. 配列とイテレータについては, 例題のプログラムの説明の中で詳しく述べます.

プログラム

例題のプログラムをRubyで記述すると, 以下のようになります. なお,各行の左端の数字は説明のために付けた行の番号で, プログラムには,この行番号は含まれません.

coins.rb
[行番号つきプログラムを別のウィンドウで開く] [行番号なしのプログラム]

このプログラム(coins.rb)は次のように実行します.

$ ruby coins.rb
支払う金額(円)を入力して下さい: 1258
1258円は,500円2枚 100円2枚 50円1枚 5円1枚 1円3枚 で支払えます

配列

例題のプログラムでは,3行めで,硬貨を表す配列を用意しています. またこれをCOINSという定数に代入しています.


 2  # 硬貨の定義
 3  COINS=[500,100,50,10,5,1]

配列は,オブジェクトを一列に並べて一つにまとめておくためのオブジェクトです. 配列は,[ ]の中に「,」(コンマ)で区切ったオブジェクトを並べて定義します. COINSは,6個の整数オブジェクトを並べた配列です. 配列の中のオブジェクトを要素といいます. 次の例に示すように配列の要素は,1種類のオブジェクトである必要はありません. さまざまな種類のオブジェクトを一つの配列に入れることができます. したがって,配列の要素がまた配列であることもあり得ます.


  ["KYODAI Taro",MALE,21] # MALEはどこかで定義された定数とします.
  [["id",1],["value",-10.2],["readonly",true]]

要素の取得と要素の設定

配列の要素を取得するには,それが何番目の要素であるかを指定します. 要素は順に,0番目,1番目と数えられます. 先頭は0番目ですので注意が必要です. 先頭は,先頭の要素から0個離れている要素, 次の要素は,先頭の要素から1個離れている要素,のように考えると分かりやすいでしょう. 要素を取得するときには,具体的には次のように記述します.


  配列[インデクス]

インデクス(index;添字)によって, その要素が何番目の要素であるかを示します. インデクスは整数でなければなりません. インデクスは(整数を値とする)式で与えても構いません. 存在しない番号の要素を取得しようとした場合には, nilが得られます.


  MALE=0
  person=["KYODAI Taro",MALE,21]
  p person[0] # ==> "KYODAI Taro"
  p person[1] # ==> 0 (MALEの値)
  p person[2] # ==> 21
  p person[3] # ==> nil
  p person[7] # ==> nil
  i = 2
  p person[i]   # ==> 21 (person[i] == person[2])
  p person[i-1] # ==> 0  (person[i-1] == person[1])

Rubyでは,配列の末尾から数えて何番目という形でも要素を取得できます. また,要素ではなく,配列の一部を取得する方法もあります. これらの詳細については, Rubyのリファレンスマニュアルの「配列」の項を参照して下さい.

要素に新しいオブジェクトを設定するには, やはりインデクスを指定して次のように代入の形で記述します. 新しいオブジェクトを設定すると,もとのオブジェクトは失われます. 今までに存在しなかった番号を指定すると,新たに要素が追加されます. そのような場合,必ずしも末尾に追加する必要はありません. 任意の番号に追加できます.


  person=["KYODAI Taro",MALE,21]
  person[2] = 22 
  p person # ==> ["KYODAI Taro",MALE,22]
  person[3] = 4
  p person # ==> ["KYODAI Taro",MALE,22,4]

なお,末尾に要素を追加する場合には,pushというメソッドが利用できますので, わざわざインデクスを指定する必要はありません.


  p person # ==> ["KYODAI Taro",MALE,22,4]
  person.push("taro@some.univ.edu")
  p person # ==> ["KYODAI Taro",MALE,22,4,"taro@some.univ.edu"]

配列オブジェクトでは,他にもさまざまなメソッドが利用可能です. たとえば,配列の長さ(末尾の要素のインデクス+1)は, sizeというメソッドで取得できます. 詳しくは,Rubyのリファレンスマニュアルの配列の項を参照して下さい.

例外処理 --- 入力データのチェック

プログラムの11〜14行めでは,入力された金額をチェックして, 例外処理を行っています.

データはいつも期待通りに入力されるとは限りません. 期待していないデータが入力されたとき,そのまま処理を続けると, 意味がないばかりか,場合によっては,実行中に異常が発生する場合もあります. これまでは,このようなことは考慮に入れていませんでしたが, 本来,プログラムでは,入力データが適切であるかどうかを調べて, そうでなければ,そのような例外的なケースに対処する処理を行う必要があります.


 5  # 支払う金額を取得する
 6  print "おつりがないように硬貨で支払いを行います\n"
 7  print "支払う金額(円)を入力して下さい: "
 8  amount = gets.to_i
 9  
10  # 金額が0円以下の場合は,エラーメッセージを表示して終了する
11  if(amount <= 0)
12    print "1円以上を指定して下さい\n"
13    exit # プログラムを終了させる
14  end

例題のプログラムでは,金額が0円以下の場合は, 入力が正しくない旨のメッセージを表示して, exitメソッドを起動して, ただちにプログラムを終了させています. exitを起動すると,そこで強制的にプログラムの実行を終了します. その後に何が書いてあっても一切処理は行われません

注1:exitの後は実行されないだけで,文法チェックは行われますので, 何でも自由に書いてよいわけではありません.
注2:Rubyには,例外をもっとスマートに扱う仕組みが用意されていますが, ここでは,そこまで踏みこまないことにします.

参考: プログラムの終了コード

exitメソッドには,整数を引数として渡すことができます. その場合,引数がプログラムの終了コードとなります. 習慣的に,終了コードが0は正常終了,それ以外は,異常終了を意味します. 引数なしでexitを起動した場合の終了コードは0となります. 例題のプログラムの13行めのexitは,異常終了のときに 起動されますので,本来は,0以外の引数を与えておくべきです. なお,最後までプログラムが正常に実行された場合,exitを明示的に書かなくても, 終了コードは0となります.

イテレータ

例題のプログラムでは,19〜25行で, 配列のeachメソッドによるイテレータを利用しています.


19  COINS.each do |coin|         # 各硬貨について(硬貨を変数coinで表す)
20    if rest >= coin            # coin円で1枚以上支払う場合
21      n = rest/coin            # coin円の支払い枚数の計算
22      print coin,"円",n,"枚 "  # coin円の支払い枚数の表示
23      rest = rest - n*coin     # 残額の更新
24    end
25  end

イテレータは, ブロックを伴った,繰り返し処理を行うメソッドです. ブロックは,do〜end(あるいは{ 〜 })で与えられます. ブロックには繰り返す処理の内容を記述します. 配列のeachメソッドの場合, 配列の各要素について,順番にブロックの内容が実行されます. ブロックでは,配列の要素をブロックパラメタで参照します. 上の例では,doのすぐあとの| |の中の変数coinがブロックパラメタです. ブロック内では,この変数coinが配列COINSの各要素に対応することになります. 具体的には,配列COINSは[500,100,50,10,5,1]と定義されていますので, coinに順番に500,100,50,10,5,1が対応づけられて, ブロックの内容が繰り返し実行されます. これによって,目的の処理が正しく行われることを確認して下さい.

次に挙げる別の例を見てみましょう. この例では,ブロックパラメタxに配列の各要素1,2,3が順に対応づけられて, ブロックが3回評価されます. ブロックでは,norm2にxの2乗を加えていっています. したがって,イテレータを実行した後には, norm2は,配列の全要素の2乗の和になっています (もちろん配列の各要素が数値でなければ,このイテレータは意味を持ちません).


  norm2 = 0
  [1,2,3].each do |x|
    norm2 = norm2 + x*x
  end
  p norm2 # ==> 14

配列のeachメソッド以外にもさまざまイテレータが存在します. each_indexメソッドによるイテレータは,配列の各インデクスに関して, ブロックの処理を繰り返します.


  a=[2,0,4,3]
  b=[]
  n=a.length
  a.each_index do |i|
    j = (i+1)%n       # jはi番目の次の要素(ただし最後の要素の次は先頭の要素とする)
    b.push(a[j]-a[i]) # aの各要素とその次の要素との差分をbに集める
  end
  p b # ==> [-2,4,-1,-1]

配列のcollectメソッドによるイテレータは,配列の各要素を ブロックで評価して,評価結果を集めて配列を作ります.


  a = [1,2,3].collect do |x|
    x*x
  end
  p a # ==> [1,4,9]

次は,整数オブジェクトのtimesメソッドによるイテレータです. このイテレータは整数で与えられる回数だけブロックを繰り返し実行します. ブロックパラメタには,0から順に整数が与えられます.


  a = []
  7.times do |i|
    a.push(i)
  end
  p a # ==> [0,1,2,3,4,5,6]

ブロックに複数のオブジェクトが渡される場合もあります. たとえば,配列のeach_with_indexメソッドによるイテレータでは, ブロックに,配列の要素とそのインデクスが渡されます. そのような場合には,通常,ブロックに渡されるオブジェクトの数だけ ブロックパラメタを用意します.


  a = []
  [1,2,3].each_with_index do |x,i| # 要素,インデクスを参照するブロックパラメタ
    a.push(x*i) # 1*0,2*1,3*2
  end
  p a # ==> [0,2,6]

参考: イテレータを利用しない場合のプログラム

例題のプログラムで,敢えて,イテレータを使わないとすると, whileで明示的に繰り返しを書くことになります. その場合は,次の参考プログラムのようになるでしょう.

参考: ブロック付きメソッド

ブロックを伴うメソッドは繰り返し処理を行うイテレータだけではありません. 一般にブロック付きメソッドとは, メソッドに,いわばその引数として「ブロックの中の処理」を渡す仕組みです. ブロックを渡されたメソッドは, 内部で「ブロックの中の処理」をメソッド本体に組み込んで処理を行います. これによって,メソッドで抽象化された処理を書いておいて, その枠組みのもとで, ブロックで渡される処理内容次第で異なる処理を行うということが可能になります.


  a = ["This","is","an","array"]
  p a.sort                               # ==> ["This", "an", "array", "is"]
  p a.sort {|x,y| x <=> y}               # ==> ["This", "an", "array", "is"]
  p a.sort {|x,y| x.upcase <=> y.upcase} # ==> ["an", "array", "is", "This"]
  p a.sort {|x,y| x.length <=> y.length} # ==> ["an", "is", "This", "array"]

配列オブジェクトのメソッドsortは, 配列の要素を(ある基準で)順番に並べ替えます. sortにブロックが与えられた場合は, その中で定められた基準を使って並べ替えを行います. ブロックが与えられない場合には, 要素を「<=>」という演算子で比較します. つまり2番目の例は,1番目の例で行われる処理を明示したものとなっています. これらの場合は,文字列を辞書式順序で並べ替えています. 3番目の例では,文字列を全て大文字に書き直してから比較を行っています. その結果,大文字と小文字を区別しないで辞書式順序に並べ替えることになります. 最後の例では,文字列の長さの短い順に並べ替えています.

これらのsortでは,要素を並べ替える処理は同じです. いずれの場合でも,「与えられた基準」で要素を比較して, 「その基準で小さい」要素から順番に並べていきます. このように「並べ替える基準」をブロックとして与えられるようにしておくことで, さまざまな並べ替えを一つのメソッドで実行できるようになるわけです.

コマンドライン引数を使う

Unixのコマンドを実行する際には,コマンド名とともに引数を与えて実行するのが一般的です. コマンドの記述内容をコマンドライン, コマンドの引数をコマンドライン引数などといいます.

$ cd CG
$ cp coins.rb coins2.rb # ファイルのコピー

Unixコマンドでは,コマンドライン引数を読みこんで, それを利用しているわけです. Rubyでも,コマンドライン引数をプログラム内で参照することができます. コマンドライン引数は, 空白(の列)で区切られた文字列を要素とした配列ARGVに格納されます. 次のような単純な例(argv.rb)を見てみましょう.


  p ARGV
  p ARGV[0]
  # ARGV[0]をtmpに代入する.ARGV[1]以降を一つ前にずらす.
  tmp=ARGV.shift
  p tmp
  p ARGV

$ ruby argv.rb 1.0 foo 100
["1.0", "foo", "100"]
"1.0"
"1.0"
["foo", "100"]

配列オブジェクトのメソッドshiftは, 配列の先頭の要素を取りだして返して,残りの要素を一つずつ前にずらします. なお,さきほども触れましたが,コマンドライン引数は, 空白で区切られて,それぞれ文字列として配列ARGVに格納されます. 文字列で読みこまれるのは,getsでキーボードからデータを読む場合と同じです. コマンドライン引数を文字列以外のオブジェクトとして取得したい場合には, 適切な変換を行う必要があります. たとえば,整数に変換するにはto_iメソッド, 浮動小数点数に変換するにはto_fメソッドを使います.

なお,例題のプログラムをコマンドライン引数を使うように変更した場合は, 次のようになります.



# 硬貨の定義
COINS=[500,100,50,10,5,1]

# 支払う金額をコマンドラインから取得する
amount = ARGV.shift.to_i

# 金額が0円以下の場合は,エラーメッセージを表示して終了する
if(amount <= 0)
  print "1円以上を指定して下さい\n"
  exit # プログラムを終了させる
end

# 各硬貨の支払い枚数の計算と結果の表示
print amount,"円は,"
rest = amount                # 支払残額(最初は支払い額そのもの)
COINS.each do |coin|         # 各硬貨について(硬貨を変数coinで表す)
  if rest >= coin            # coin円で1枚以上支払う場合
    n = rest/coin            # coin円の支払い枚数の計算
    print coin,"円",n,"枚 "  # coin円の支払い枚数の表示
    rest = rest - n*coin     # 残額の更新
  end
end
print "で支払えます\n"


[coins_args.rb]

このプログラム(coins_args.rb)は次のように実行します.

$ ruby coins_args.rb 728
728円は,500円1枚 100円2枚 10円2枚 5円1枚 1円3枚 で支払えます
[CG実習 >  Rubyによるプログラミングの基礎 >  配列とイテレータ]