to_int に気をつけろ!

以下の長ったらしい文章の要点は「不用意に既存クラスにメソッドを追加すると、とんでもないところでエラーになるので要注意」ということです。

RailsでWebアプリを作っていたと思ってください。最近はそれしかやってませんから。で、だんだんRubyにも慣れてきて、大胆なことをするようになるわけです。

なにをやったか?

  • "abc123def"みたいな文字列があったとして、"abc123def".to_i とやると当然の如く 0 が返ってくるわけです。ま、それはそれで正しいのですが、ある文字列の数字に見えるところを数字として返してほしいなぁということもあるわけで、ないんだったら作ればいいじゃんと思ったわけです。
  • で、最初は大人しく、def to_number( str ) を定義して、i = to_number("abc123def") とやってたわけですが、文字列に対する操作なのに引数に文字列をとるのはどうなのよ、と思うわけです。
  • それに、to_number はちょっと長いので、to_int はどうかなと。String のリファレンスをみると、to_i はあっても to_int はないようだし、"abc123def".to_int としてみても、undefined method になるので、String クラスに to_int を追加しちゃえばいいじゃん、いい考えじゃんということで、こういう風(↓)にしてみました。
class String
  def to_int
    self.scan(/\d/).join.to_i
  end
end
  • Rails アプリの lib として require してなんら問題なく動いています。全然問題ありません。

なにが問題か?

  • 問題が起きたのは一晩寝て起きた今朝のこと。list.html.erb で<font>指定をしてるんですが、<font>タグは「将来のバージョンアップで廃止される予定」に指定されているので使わない方がいい、というのをググってるときに見つけたので、<font>タグを css へ出さないといけないなと思ったわけです。
  • で、その作業をして、css を書き換えたので、念のため、Mongrel を再起動させたわけです。そうすると、いつものページ(http://localhost:3000/)に css が適用されないじゃないですか。まず、最初に疑ったのは、いまやった list.html.erb と css の書き換え。当然、ひとつ前のバージョンに戻しました。でも、css は適用されません。
  • Mongrel の画面をみると「Error sending file C:/www/2.5-55.jp/public/stylesheets/master.css: Bad file descriptor」と出ています。「file descriptor」がおかしいということはファイルのパーミッション関係のエラーとか「file descriptor」が不足したとかが原因なのかな、と漠然と思います。

こんな調子で物語風に書くといつまでたっても終わらないので

  • Mongrel が「Error sending file C:/www/2.5-55.jp/public/stylesheets/master.css: Bad file descriptor」というエラーを吐くようになった。
  • どうやら、このファイル(C:/www/2.5-55.jp/public/stylesheets/master.css)を送ろうとしてエラーになっているようだ。
  • Mongrel のバージョンが古いからかもしれないと思って「gem update」をやってみたけど現象は変わらず。いままで動いていたんだから、そりゃそうだよね。
  • Mongrel のソースに debugger を仕込んで、エラーを raise しているところをみてみる。
  • 「http_response.rb」の「File.open(path, "rb") {|f| @socket << f.read }」の File.open() でエラーになってる。
  • デバッガで File.open(path, "rb") を実行すると確かにエラーになる。
  • でも1回目は「Errno::ENOENT Exception: No such file or directory」だったけど、2回目以降は「Errno::EINVAL Exception: Invalid argument」になるのはおかしくないか。
  • しかも、File.stat(path) はエラーにならないし、File.open("c:/tmp/test.txt", "rb") もエラーにならない。
  • いろいろやってみた結果がこれ(↓)
(rdb:6) fd = File.open("c:/tmp/2/favicon.ico", "rb")    ← OK
#
(rdb:6) fd = File.open("c:/tmp/25/favicon.ico", "rb")   ← NG
Errno::EINVAL Exception: Invalid argument
(rdb:6) fd = File.open("c:/tmp/2t/favicon.ico", "rb")   ← OK
#
(rdb:6) fd = File.open("c:/tmp/21t/favicon.ico", "rb")   ← NG
Errno::EINVAL Exception: Invalid argument
(rdb:6) fd = File.open("c:/tmp/2t2/favicon.ico", "rb")   ← NG
Errno::EINVAL Exception: Invalid argument
(rdb:6) fd = File.open("c:/tmp/2trrr/favicon.ico", "rb")  ← OK
#
(rdb:6) fd = File.open("c:/tmp/Download/2trrr1/favicon.ico", "rb") ← NG
Errno::EINVAL Exception: Invalid argument
  • 2 が OK で、25 は NG、2t は OK で、21t は NG というのはどういうことか。
  • File.open() の中でエラーが起きてるんだから、ステップ実行で中をみてみれば何かわかるかも。「s」エンター。あれ、自分の定義した to_int メソッドのなかに入ってきたぞ。
  • ああ、わかった。これが原因か。

結局、わかったこと

不用意に String クラスに to_int メソッドを追加したわけですが、Object クラスに to_int というメソッドがすでにあって、整数への暗黙の変換が必要なときはこれが呼ばれるらしいです。で、たぶん File.open() メソッドのなかで path 引数に対して to_int で整数にしているところがあって、それが 自分定義の to_int と置き換わっちゃったため、例外エラーを raise するようになったと。
ちなみに、WEBrick で動かすとこの例外エラーはでなかったので余計にわかりづらかったんですが、WEBrick はFile.open() 周りの処理が違ってるんでしょうね。(この件は未確認です)

参考

  1. リファレンスマニュアルは必須です。今回の件はここ(→プログラミング言語 Ruby リファレンスマニュアル)を参照しました。
  2. <font>タグがなくなるよ、という記述をここ(→http://www5.wisnet.ne.jp/~z-plus/hp/html40/font.html)で見つけなければ、こんな苦労しなくて済んだのに。とは思ってません。早い段階で問題が発覚して助かりました。