Edited at

Safe Navigation Operator で呼ばれるメソッドの引数はレシーバが nil なら評価されない

More than 3 years have passed since last update.

タイトルの通りです。

Safe Navigation Operator に関するコミット を追いかけていると、ドキュメント中に興味深い一文が見つかりました。


You may use &. to designate a receiver, then my_method is not invoked and the result is nil when the receiver is nil. In that case, the arguments of my_method are not evaluated.


これは例えば obj&.my_method(arg) というメソッド呼び出しを行った時、 objnil だったら my_method への引数である arg は評価されない、ということです。


試してみる

早速試してみました。


オブジェクトに対して正しくメソッドを呼び出せたとき


safe1.rb

class Foo

def my_method(s)
"Foo#my_method is called with \"#{s}\""
end
end

def arg
puts "arg is evaluated"
"m"
end

obj = Foo.new
puts obj&.my_method(arg)


$ ruby safe1.rb

arg is evaluated
Foo#my_method is called with "m"

obj.my_method の結果が出力される前に、arg が評価されていることがわかります。

何の違和感もありません。


レシーバのオブジェクトにメソッドが存在しなかったとき


safe2.rb

class Foo

end

def arg
puts "arg is evaluated"
"m"
end

obj = Foo.new
puts obj&.my_method(arg)


$ ruby safe2.rb

arg is evaluated
safe.rb:10:in `<main>': undefined method `my_method' for #<Foo:0x007fe859807020> (NoMethodError)
Did you mean? method

NoMethodError が発生する前に arg が評価されていることがわかります。

これも特に違和感のない挙動です。


レシーバが nil だったとき

そしていよいよドキュメントに記載されているケースです。


safe3.rb

def arg

puts "arg is evaluated"
"m"
end

obj = nil
puts obj&.my_method(arg)


$ ruby safe3.rb

putsnil が渡されることで空行を出力しますが、arg は評価されていないことがわかります。


どういうときにうれしいか

これはやはり引数の評価に時間がかかるような場合、不要な評価を行わない分だけ実行時間を節約できます。

foo&.bar(some_heavy_computation_result)

この some_heavy_computation_result は何か重い処理を行うメソッドだったとして、foonil なら不要であるところのその呼び出しを省略してくれます。

ActiveSupportObject#try! に比べて高速、という記事も書きましたが、これは通常のライブラリでは実現できない最適化です。

逆に言うとレシーバが nil であろうと引数が評価されることを期待していると、そのようには動かないので注意が必要です。