🧗‍♂️

RMagick の ImageList の癖が強い

2022/12/17に公開

意図とは異なるコード

本番運用中のコードをリファクタリングしているとこんなのが出てきた。

list = Magick::ImageList.new
list << Magick::Image.read("logo:").first
list << Magick::Image.read("logo:").first
list.destroy!

文脈からするとこのコードは ImageList で管理している画像を一括解放[1]する目的だったらしいのだが、本当に二つの画像は解放されているのだろうか?

実際の挙動

これは list.scene が指す要素を解放するだけなので1つ目は解放されないのだった。

list = Magick::ImageList.new
list << Magick::Image.read("logo:").first
list << Magick::Image.read("logo:").first
list.scene                       # => 1
list.to_a.collect(&:destroyed?)  # => [false, false]
list.destroy!                    # => #<Magick::Image: (destroyed)>
list.to_a.collect(&:destroyed?)  # => [false, true]

method_missing を悪用活用してレシーバが反応しないメソッドは scene が指す位置にあるオブジェクトに委譲するようになっている。だから scene の存在および、今指しているインデックス、そして何にレシーバは反応して、何が委譲されるのかをすべて把握しておかないと(安全に使うのは)難しい。

参照に見える破壊的メソッド

ちなみに ImageList#collect があるにもかかわらず、いったん to_a としているのは collect(&:destroyed?) だと謎のエラーになるから。コードを読むと collect ブロック内では Image のインスタンスを返す前提となっていた。もっと言えば collect は破壊的メソッドだったのである。

他にも参照に見えて破壊的なメソッドが多くあり、あらためて(安全に使うのは)難しいと感じる。

脚注
  1. 昔の RMagick はメモリリークが心配でとにかく自分で解放するのが慣習だった ↩︎

Discussion