と思いません?
def foo(x) end foo(1, 2) #=> wrong number of arguments (2 for 1) (ArgumentError)
1.step(10, 0) { } #=> step can't be 0 (ArgumentError)
a = []; a << a
a.flatten #=> tried to flatten recursive array (ArgumentError)
確かにどれも Argument に関する Error ではあるんだけど *1 、全部同じ例外クラスというのは粗すぎですよね。メッセージ読めば意味はわかるからデバッグには困りませんが、ArgumentError の中の特定の例外だけ拾いたいときに困ります。
具体的には、テストです *2 。例えば foo(1, 2) で wrong number of arguments が投げられることをテストしたいとします。以下のテストだと、wrong number of arguments 以外の ArgumentError が投げられる場合でも合格になってしまいます。
assert_raise(ArgumentError) { foo(1, 2) }
ちゃんとやりたければ、例えばこんな感じのコードを書かないとだめかな。
flag = false begin foo(1, 2) rescue ArgumentError => e raise unless ex.message[/\Awrong number of arguments \(\d+ for \d+\)\z/] flag = true end assert(flag)
もちろん実際には assert_raise_with_message みたいなメソッドにくくりだすとしても、メッセージを文字列比較や正規表現で判定しないといけないのはダサいです。
もしも ArgumentError ではなく、wrong number of arguments 専用の例外クラス (ArgumentWrongNumberError とする) を投げてくれれば、こんな風に書けそうです。
assert_raise(ArgumentWrongNumberError) { foo(1, 2) }
引数の数についてもチェックしたいときは、以下のようにかけたらかっこいいよなあ。Test::Unit の修正も必要そうだけど。
# wrong number of arguments (2 for 1) が投げられるテスト assert_raise(ArgumentWrongNumberError.new(2, 1)) { foo(1, 2) }
極端な話、例外にメッセージ文字列を持たせるのではなく、メッセージの種類の数だけ例外クラスがあるべきではないかな *3 。
もし「テストなんかしないから今ので十分」という見解だとすると、それにしては分類が細かすぎると思います。ArgumentError と TypeError とか NameError とか NoMethodError とか、いちいち区別する理由はないような気がします。だって、これらを意識的に rescue し分ける場合ってほとんどなさそうです。実際、TypeError と ArgumentError の区別ってかなりいい加減に決められているように感じます。
# TypeError の方が自然そうな例 Rational([], 0) #=> not an integer (ArgumentError) ObjectSpace.define_finalizer(1, "foo") #=> wrong type argument String (should be callable) (ArgumentError) # ArgumentError でもいい気がする例 1.instance_of?(1) #=> class or module required (TypeError) (1...5.0).max #=> cannot exclude non Integer end value (TypeError)
まあたぶん歴史的経緯 *4 なんだと思いますが、これってどうなんでしょうね。現実問題、逐一例外クラスを設計・命名していくのはめんどすぎかなあ。「提案自体はいいんだけど、例外クラスの名前がしっくりこないからその提案は保留」とか matz に言われたら悲しそうではあります。なにより、作業量のわりにメリットがなさすぎか。うーん。