例外処理

プログラムの不具合には様々な段階がある。 文法的なエラーなどプログラムの実行そのものができない単純なもの, 実行はできるが問題解決法が違うなど論理的な誤りのものについては, いずれもプログラムを作成している場所で判断して修正することができる。 ところがそれらの問題がないプログラムでも,実際に実行するときの 外界条件によって予期した動きを取れなくなるような不具合がある。 たとえば,データをファイルに保存しようとしたのに ディスク容量不足などが原因で書き込みができないような状況では プログラムは目的を達成できない。

このように,特定の条件によって想定した処理を進められないような 事態のことを例外 という。プログラム実行時にそうした 事態が生じたときには例外を意味する Exception が発生される。

例外発生

文法的にはエラーのない以下のプログラムを動かしてみよ。

openerr.rb

#!/usr/bin/env ruby

file = ARGV.shift || "nonexistent.txt"
open(file, "r") do |f|
  while line = f.gets
    print line
  end
end

実行すると例外が発生する。

openerr.rb:4:in `initialize': No such file or directory @ rb_sysopen -
 nonexistent.txt (Errno::ENOENT)
        from openerr.rb:4:in `open'
        from openerr.rb:4:in `<main>'

begin〜rescue〜end

おみくじプログラムの例に戻る。 改良すべき2つの点,

にはそれぞれ落し穴がある。

既存の結果ファイル fortune.txt をバックアップファイル fortune.bak に移動できるかの確認は何を確かめればよいのだろう。 場合によってはバックアップファイルである fortune.bak が存在するかや,そのファイルに書き込みできるかを確かめたくなるが, 実際にはこれは「バックアップファイルを作れるかどうか」には関係ない。 以下の実験で,移動先のファイルを書き込み禁止にして移動を行なってみる。

mkdir dirtest
cd dirtest
echo This is foo > foo
echo This is backup file > foo.bak     (foo.bakがバックアップファイル)
chmod -w foo.bak                       (書き込み禁止にする)
ls -lF
total 1
-rw-r--r--  1 yuuji  wheel  12 Mar 15 12:59 foo
-r--r--r--  1 yuuji  wheel  15 Mar 15 12:59 foo.bak
echo hoge > foo.bak
zsh: permission denied: foo.bak
(直接ファイルに書こうとするとエラーになるが...)
mv foo foo.bak
override r--r--r--  yuuji/wheel for foo.bak? n
(mvの場合は警告が出るがnと答える)
irb
irb> File.rename("foo", "foo.bak")
=> 0
irb> exit
ls -lF
total 1
-rw-r--r--  1 yuuji  wheel  12 Mar 15 12:59 foo.bak
(12バイトなので元々fooだったものがfoo.bakになっている)

ファイルをrenameする場合は, 移動先ファイルを修正するのではないので移動先ファイルのパーミッションは 関係ない。元のファイルを消してそのファイルを作る権利があるかが 意味を持つので,ディレクトリに書き込み権があるかが意味を持つ。

touch foo foo.bak
chmod -w .             (ディレクトリを書き込み禁止にする)
irb
irb> File.rename("foo", "foo.bak")
Errno::EACCES: Permission denied @ sys_fail2 - (foo, foo.bak)
        from (irb):1:in `rename'
        from (irb):1
        from /usr/local/bin/irb21:11:in `<main>'

これは,ファイルを新規作成するときも同様の要件となる。

以上のことから,おみくじの例での改良2点,

は,ディレクトリへの書き込み権限を検査する必要があることが分かる。

ft1.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
#Foretune teller - version 1
ftune	= %w,大吉 吉 中吉 小吉 半吉 末吉 小凶 凶,
outfile	= "fortune.txt"
srand
result = ftune[rand(ftune.length)]

if test(?w, ".") then		# ディレクトリの書き込み権限を確認
  if test(?s, outfile) then
    ext = File.extname(outfile)
    bak = File.basename(outfile, ext)+".bak"
    File.rename(outfile, bak)
  end
  open(outfile, "w") do |f|
    f.printf("今日の運勢は %s\n", result)
  end
  STDERR.puts "#{outfile}を見よ。"
end

しかしこれでも不十分で,ファイルシステムが溢れるなど, 書き込みに失敗する要因は他にいくらでもある。 だからといって,それらに対する事前予想を if で書き連ねてもプログラムの本筋が見づらくなる。

このような場合begin〜rescue〜end 構文を用いる。 エラーが起きない理想的な場合の処理のみをまず書き,エラーが起きた 場合の処理をまとめて別箇所に書くことで,処理の流れがはっきりし, エラーが起きた場合の対処ももれなく書くことが容易になる。

ft2.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
#Foretune teller - version 2
ftune	= %w,大吉 吉 中吉 小吉 半吉 末吉 小凶 凶,
outfile	= "fortune.txt"
srand
result = ftune[rand(ftune.length)]

begin
  if test(?s, outfile) then
    ext = File.extname(outfile)
    bak = File.basename(outfile, ext)+".bak"
   # File.rename(outfile, bak)
  end
  open(outfile, "w") do |f|
    f.printf("今日の運勢は %s\n", result)
  end
  STDERR.puts "#{outfile}を見よ。"
rescue
  abort(<<MSG)
書き込みに失敗しました。ディレクトリへの書き込み権限を確認してください。
MSG
end

この場合は例外発生時に復旧せず,異常終了している。 そのためのメソッドが abort である。


本日の目次

yuuji@e.koeki-u.ac.jp