2014-02-28: Ruby 2.1.1 で Hash#reject のデグレ

Ruby 2.1.0 まで、Hash を継承したクラスの reject メソッドの挙動は継承クラスのオブジェクトを返す挙動をしてました。 しかし、 Ruby 2.1.1 で意図せずその挙動が変更され、必ず Hash クラスのオブジェクトが返されるようになりました。

class SubHash < Hash; end
p SubHash.new.reject{}.class #=> 2.1.0: SubHash 2.1.1: Hash
p Hash.new.reject{}.class #=> 2.1.0: Hash 2.1.1: Hash

(厳密には ivar 等その他の属性もコピーされなくなっています。 また、Ruby 2.1 からはバージョニングポリシーの変更により、2.0.0 までのパッチリリースにあたるリリース時で TEENY が増えます。詳細は今月の WEB+DB で!)

この挙動変更で具体的に影響を受けるのは、Rails の HashWithIndifferentAccess, OrderedHash です。壊れます (reject メソッドが必ず Hash オブジェクトを返してくるようになった)。 https://github.com/rails/rails/issues/14188

何故起きたか

そもそもこの挙動の変更は Ruby 2.1.1 では意図的ではないと思っています。事故です…。 これは bugs.r-l.o#9223 で昨年末 (2.1 リリース前に) 議論されていた変更ですが、この時期からの仕様変更はできないと Ruby 2.1.0 では見送られています。 そのため Ruby 2.1.0 では deprecated warning に留められました。 (それも --verbose, -v がついてないと表示されない物って…というのは今は置いておく。)

この変更の周辺コミットは https://gist.github.com/sorah/9265008 の通りです。詳細は←の gist を読んでください。 Ruby 2.1.0 の段階で、#ifdef により定数で警告+従来の挙動、あるいは警告せず新しい挙動 (必ず Hash オブジェクトを返す) という 2つ の挙動を切り替えられるようになっていました。尚、0 で新挙動、1 で従来の挙動になっています。

Ruby 2.1.0 リリースブランチが切られた後、trunk では r44358 で定数名が変更され、2.1.1 ブランチにバックポートされています。が、このコミットで一箇所 #ifdef 側の名前に変更漏れがあり、r44370 で修正されています。この r44370 がバックポートされず、必ず #ifdef が 0 と評価されて 2.1.1 では新挙動になってしまったという訳です。

事故ですね。

つきましては、r44370 をバックポートした Ruby 2.1.1p77 をビルドするか (たぶん大丈夫)、HashWithIndifferentAccess, OrderedHash にモンキーパッチで対処 する事をオススメします。

尚 r44370 のバックポートはされたので、2.1.2 では元の挙動に戻るでしょう。 https://bugs.ruby-lang.org/issues/9576

しかし、 2.2.0 では 2.1.1、つまり Hash クラスのオブジェクトが帰ってくる挙動に変更される ので、将来の事を考えたら 2.1.0 以前の挙動を期待するのはやめたほうが無難です。

Published at 2014-02-28 21:00:00 +0900