Ruby safe navigation operator 是否在 receiver 为 nil 时评估其参数?

Does Ruby safe navigation operator evaluate its parameters when its receiver is nil?

问题:

Ruby 安全导航运算符 (&.) 是否在其接收器为 nil 时评估其参数?

例如:

logger&.log("Something important happened...")

提前致谢。




为什么我要寻找这个问题的答案?

我的代码库中有如下代码:

logger.log("Something important happened. (#{Time.current})") if verbose

我的主要目标是删除每次调用 log 方法时重复的 if verbose 检查,因为它很容易被遗忘,而且您根本不会收到有关误用的通知.

Tell, don't ask原则的启发,

我已经移动了 if verbose 检查内部 log 方法实现。

class Logger
  # ...
  
  def log(message)
    return unless verbose

    # ...
  end
end

def logger
  @logger ||= Logger.new
end

logger.log("Something important happened. (#{Time.current})")

这种方法简化了我的代码,因为我已经解决了我的主要问题 - 我不需要记住在每次调用 log 方法时放置 if verbose

但我又收到了一个问题。

"Something important..." 字符串总是被评估,无论 verbosetrue 还是 false.

因此,我彻底改变了解决方案:

def logger
  @logger ||= Logger.new if verbose
end

logger&.log("Something important happened. (#{Time.current})")

因此,我已将最初记住 if verbose 检查的问题替换为记住 &. 调用。

但是,无论如何,我认为这是一个改进,因为忘记使用安全导航运算符会引发 NoMethodError,换句话说,通知 log 方法误用。

所以现在,为了确保 'safe navigation operator approach' 实际上是我的问题的 'better' 选项,

我需要确切地知道 Ruby 中的安全导航运算符是否在其接收器为 nil 时评估其参数。

它不评估它们:

require 'pry'

logger = nil
logger&.log(binding.pry)

这个returns:

nil

如果它对其进行评估,那么它会像这个示例那样触发绑定:

a = []
a&.push(binding.pry)

如果你没有撬但有现代版本的 Ruby,你可以用 binding.irb 代替 binding.pry

这是否是一个“更好”的解决方案是您应该确定的基准。

您可以在 How is the Ruby safe navigation (&.) implemented?

阅读有关安全导航运算符的更多信息

没有,而且很容易测试:

$ irb
> def test
>   puts 'triggered!'
> end
 => :test 
> def nothing
> end
 => :nothing 
> nothing&.whatever(test)
 => nil
> nothing&.whatever("string_#{test}")
 => nil 

从概念上讲,您可能会认为安全导航运算符是这样的:

x&.test(param) # is "conceptually" equal to

if x.respond_to?(:test)
  x.test(param)
end

# or, as pointed in the comment: 
unless x.nil?
  x.test(param)
end

现在很清楚为什么在未调用时不对其求值了。

引自the syntax documentation安全导航运营商:

&., called “safe navigation operator”, allows to skip method call when receiver is nil. It returns nil and doesn't evaluate method's arguments if the call is skipped.

因此,如果 loggernil,当您将它称为

时,您的 log 方法的参数不会被评估
logger&.log("something happened at #{Time.now}")

话虽如此,请注意 Ruby 核心记录器针对您的确切问题提供了不同的解决方案,即避免在日志级别过高时不得不评估潜在的昂贵参数。

Ruby 核心记录器实现其 add 方法,类似于这样(简化):

class Logger
  attr_accessor :level

  def initialize(level)
    @level = level.to_i
  end

  def add(severity, message = nil)
    return unless severity >= level
    
    message ||= yield
    log_device.write(message)
  end

  def info(message = nil, &block)
    add(1, message, &block)
  end
end

然后您可以将其用作

logger = Logger.new(1)
logger.info { "something happened at #{Time.now}" }

这里,只有当日志级别足够高以至于消息被实际使用时,才会评估该块。

表达式已解析但未执行

logger&.log 的参数在 logger.is_a?(NilClass) == true 时未计算。评估的每个 Ruby 表达式都应该产生影响,因此请考虑:

test = 1
nil&.log(test+=1); test
#=> 1

如果参数由解释器求值,test 将等于 2。因此,虽然解析器肯定 解析 参数中的表达式,但它不会 执行 内部表达式。

您可以通过 Ripper#sexp:

验证解析器看到的内容
require 'ripper'

test = 1
pp Ripper.sexp "nil&.log(test+=1)"; test
[:program,
 [[:method_add_arg,
   [:call,
    [:var_ref, [:@kw, "nil", [1, 0]]],
    [:@op, "&.", [1, 3]],
    [:@ident, "log", [1, 5]]],
   [:arg_paren,
    [:args_add_block,
     [[:opassign,
       [:var_field, [:@ident, "test", [1, 9]]],
       [:@op, "+=", [1, 13]],
       [:@int, "1", [1, 15]]]],
     false]]]]]
#=> 1

这清楚地表明解析器在符号表达式树中看到了递增的赋值。但是,分配从未真正执行过。