概要
破壊的メソッドは元のオブジェクトも変更してしまいます。
なので当然なのですが、同じ変数に、同じ破壊的メソッドを複数回使ってしまうと、1回目と2回目以降では実行結果が変わってしまいます。
普通の破壊的メソッドだと気を付けられるのですが、OptionsParserクラスのオブジェクトだとあまり意識できなかったので自戒のメモ
コードと実行結果
コード
require 'optparse' def main p load_options p ARGV p load_options p ARGV end def load_options opt = OptionParser.new params = {} opt.on('-l') { |v| params[:l] = v } opt.parse!(ARGV) params end main
実行結果
> sample.rb -l foo {:l=>true} ["foo"] {} ["foo"]
バグの理由
mainメソッド内の、1回目と2回目のp load_options
の実行結果が異なってしまっています。
そもそも、load_options
メソッド内にある、options.parse
は、
opt.on
メソッドのブロックの実行- 配列
ARGV
からオプション値を取り除く
という仕様です。
今回はparse!
という破壊的メソッドを利用しているため、mainメソッド内で、ARGVは下記のように遷移しています。
def main # この時のARGVは、`ARGV = ["-l", "foo"] p load_options # load_options内の、`parse!`メソッドにより、`ARGV = ["foo"]`に遷移 p ARGV # 上記で破壊的メソッドによって、この段階では`ARGV = ["foo"]`のまま! p load_options p ARGV end
となり、今回の実行結果では、
> sample.rb -l foo {:l=>true} ["foo"] {} ["foo"]
のように、2回目のp load_options
では、ARGV =['foo']
を対象に処理を行うので、「オプションは何もないよ!」といった結果になってしまいます。
まとめと解決策
僕みたいな初心者はよく言われますが、『破壊的メソッドは出来るだけ使わない』ように心がけた方が、思わぬバグを減らせるかもしれませんね。
今回はサンプルのコードなので大分簡略化しましたが、実際に実装したコードでは、一度変数に代入をすることで、破壊的メソッドを伴う処理が何度も呼ばれないように修正をしました。
# 修正前 options = load_options.empty? { l: true, w: true} : load_options # 修正後 options = load_options options = options.empty? { l: true, w: true} : options
まぁ良い設計なのかはさておき、『破壊的メソッドを伴う処理は、戻り値を一度変数に入れる』というのも1つの解決策なのだと思います🥰
あとそもそも破壊的メソッド使わなければいいんじゃないか?という話ですが、そうすると今度は逆に、ARGV配列が常に["-l", "foo"]
のまま変化しなくなってしまうので、諸々支障が出ます(割愛)