4

问题:

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

例如:

logger&.log("Something important happened...")
  • "Something important happened..."字符串是否在这里评估?
  • 您能否提供一个权威来源,证明或否认这一事实?
  • 或者建议一种如何检查它的方法?

提前致谢。




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

我在整个代码库中都有如下代码:

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

if verbose我的主要目标是在我调用该方法时删除重复检查,log因为它很容易忘记它,并且您根本不会收到有关误用的通知。

Tell启发,不问原则,

我已经在方法实现中移动了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})")

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

但我收到了另一个问题。

"Something important..."string 总是被评估,无论verbosetrue还是false

因此,我完全改变了解决方案:

  • loggernil何时返回。verbose_false
  • Ruby 安全导航运算符应在log调用前使用。
def logger
  @logger ||= Logger.new if verbose
end

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

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

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

所以现在,为了确保“安全导航操作员方法”实际上是解决我的问题的“更好”选择,

我需要确切知道 Ruby 中的安全导航运算符是否在其接收器为nil.

4

4 回答 4

5

引用安全导航运算符的语法文档:

&.,称为“安全导航运算符”,允许在接收者为 时跳过方法调用nil如果跳过调用,它会返回nil并且不评估方法的参数。

因此,log如果您将其调用loggernil

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}" }

在这里,仅当日志级别足够高以至于实际使用消息时才评估块。

于 2020-07-21T20:59:50.537 回答
2

表达式已解析但未执行

logger&.log当 时不评估to 的参数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

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

于 2020-07-21T21:35:46.003 回答
1

它不评估它们:

require 'pry'

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

这将返回:

nil

如果它评估它,那么它将像这个例子一样触发绑定:

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

如果你没有 pry 但有一个现代版本的 Ruby,你可以binding.irbbinding.pry.

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

您可以在如何实现 Ruby 安全导航 (&.)中阅读有关安全导航运算符的更多信息?

于 2020-07-21T20:21:25.143 回答
0

不,而且很容易测试:

$ 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

现在很清楚为什么当它没有被调用时它没有被评估。

于 2020-07-21T20:37:07.173 回答