Ruby
メソッド
破壊的操作

frozen_string_literalが入って気づいた、メソッド設計の原則

More than 1 year has passed since last update.

frozen_string_literalが入ったことでプログラムを調整していたら、気づいたことがありました。

frozen_string_literalとは

ことは、Ruby 2.1で'str'.freezeとコードに書いたときに、特別な最適化が入るようになったことに遡ります。この最適化を享受しようと、至る所の文字列をfreezeしだすという混沌に陥ったこともあって、「Ruby 3.0からは、文字列リテラルはデフォルトでfreeze状態にする」ということになりました。

ただ、いきなり3.0に移行すれば混乱も避けられないので、Ruby 2.3からは冒頭にコメントを書くことで、この「文字列リテラルのfreeze」の挙動を変えられるようになりました。

  • # frozen_string_literal: true→文字列リテラルはすべてfreezeされた状態になる
  • # frozen_string_literal: false→文字列リテラルはfreezeされない

なお、この指定はファイル単位で有効で、そのファイルに書かれた文字列リテラルだけに有効です。

あぶり出された、おかしなプログラム

文字列でもなんでも、非破壊的な操作を繰り返すとコストが嵩むことがあって、JavaではStringBuilderというString生成用のオブジェクトが用意されていますが、Rubyの場合はString自体が可変なので、別途用意はされていません。

そうなれば勢い、<<で破壊的に連結、ということがよく行われるのですが、frozen_string_literal: trueとしたところでエラーになったのを確認してみたら、なんと引数に与えた文字列<<で破壊的操作を加えていたのでした。

ActiveRecordオブジェクトのように、破壊的操作を行うことが念頭に置かれたオブジェクトもありますが、「文字列引数を渡したら、書き換わって返ってきた」というのは、多くの場合意図するような動作ではないでしょう。引数の文字列に直接<<する、なんていう操作は、(うっかりやらかしているかもしれませんが)基本的に、やったらまずい部類です。

おまけ

Rubyのメソッドでは、最後にsome_method 50, foo: :bar, baz: 1のような形でハッシュを渡すものが多いのですが、これの中で.deleteなどの破壊的操作を行っていることが時折見受けられます。

上の例のようなbare hashであればその場で生成するので問題ないのですが、ふつうのHashを渡せば引数のパースで変わってしまうし、frozenなのを渡せば破壊的に操作できなくてエラーになってしまいます(複数のメソッドに同じハッシュを渡すので、使いまわそうとして気づきました)。

こちらも、dupするなどして対応が必要そうです。