[プログラミング演習(Ruby) >  配列とイテレータ]

配列とイテレータ

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

  (a0+a1+......+a999)/1000
さらにデータが2000個に増えたら,変数を1000個追加して次のように書き換えるしかないのでしょうか.
  (a0+a1+......a999+a1000+......+a1999)/2000

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

もくじ

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

例題

ある金額(円)を硬貨(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枚 で支払えます

配列

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


10  # 硬貨の定義
11  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番ですので注意が必要です. 要素を取得するときには,具体的には次のように記述します.

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

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

Rubyでは配列の末尾から数えて何番目という形でも要素を取得できます. また,要素ではなく配列の一部を取得する方法もあります.

  person[-1]   # ==> 3 (末尾から数えて1番目の要素)
  person[-2]   # ==> 21  (末尾から数えて2番目の要素)
  person[0,2]  # ==> ["KYODAI Taro", 0] (0番目から2要素の配列)
  person[1,3]  # ==> [0, 21, 3] (1番目から3要素の部分配列)
  person[0..2] # ==> ["KYODAI Taro", 0, 21] (0番目から2番目までの要素)

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

  person=["KYODAI Taro",MALE,21]
  person[2] = 22 # person == ["KYODAI Taro", 0, 22, 3]
  person[3] = 4  # person == ["KYODAI Taro", 0, 22, 4]
  person[4] = true # person == ["KYODAI Taro", 0, 22, 4, true]

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

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

配列オブジェクトでは,他にもさまざまなメソッドが利用可能です. たとえば,配列の長さはsizeというメソッドで取得できます.

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

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

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


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

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

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

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

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

イテレータ

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


27  COINS.each do |coin|         # 各硬貨について(硬貨を変数coinで表す)
28    if rest >= coin            # coin円で1枚以上支払う場合
29      n = rest/coin            # coin円の支払い枚数の計算
30      print "#{coin}円#{n}枚 "  # coin円の支払い枚数の表示
31      rest = rest - n*coin     # 残額の更新
32    end
33  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
  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
  b # ==> [-2,4,-1,-1]

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

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

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

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

なおこれは例のための例で,Rubyでは次のように書く方が自然です.

  a = Array.new(7) { |i| i+1 } # a == [1, 2, 3, 4, 5, 6, 7]

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

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

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

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

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

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

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

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

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

ハッシュ

これまでに見てきたように,配列の各要素は,0番目の要素,1番目の要素,2番目の要素というように番号で区別されます. 番号を一つ指定すればそれに対応する一つの要素が決まるわけです. ハッシュ(hash)は配列と似たものですが,ハッシュでは番号だけでなくいろいろなデータを使って要素を指定することができます.

たとえば,日本と世界の都市の時差の一覧をデータとして用意することを考えてみます. もちろん,そのようなデータを配列を使って表すこともできますが, 都市名を使って,ハッシュを利用する方がより分かりやすくなります.

  # 日本(東京)と世界の都市との時差のハッシュ
  time_diff = {"Auckland"=>3,
               "Sydney"=>1,
               "Tokyo"=>0,
               "HongKong"=>-1,
               "Paris"=>-7,
               "London"=>-8,
               "NewYork"=>-13,
               "LosAngeles"=>-16,
               "Honolulu"=>-19}

  # 東京とホノルルの時差
  time_diff["Honolulu"] # ==> -19

  # ロンドンと香港の時差
  time_diff["HongKong"]-time_diff["London"] # ==> 7

  # 東京とモスクワ,東京とカイロの時差を新たに登録する
  time_diff["Moscow"]=-5
  time_diff["Cairo"]=-6

この例で示したように,ハッシュはほとんど配列と同様に操作することができます. ただし,ハッシュに最初にデータを用意する場合は{ }の中に「キー=>値」という形式でデータを並べます. この点は配列とは異なります.

ハッシュにもイテレータがあります. 代表的なイテレータにeachがあります. ハッシュのeachは,配列のeachとは多少異なります.

  # 世界の都市と日本(東京)との時差のハッシュ
  time_diff = {"Auckland"=>3,
               "Sydney"=>1,
               "Tokyo"=>0,
               "HongKong"=>-1,
               "Paris"=>-7,
               "London"=>-8,
               "NewYork"=>-13,
               "LosAngeles"=>-16,
               "Honolulu"=>-19}

  # 東京と各都市との時差を全て表示する.
  time_diff.each do |city,td|
    print "東京と",city,"の時差は,",td,"時間です\n"
  end

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

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

$ 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枚 で支払えます

サンプルプログラム

配列やイテレータを用いたサンプルプログラムをいくつか紹介します.

[プログラミング演習(Ruby) >  配列とイテレータ]