当接收器为nil时,Ruby安全导航操作符是否评估其参数?

f0brbegy  于 11个月前  发布在  Ruby
关注(0)|答案(5)|浏览(111)

问题:

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

举例来说:

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

字符串

  • "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
因此,我完全改变了解决方案:

  • verbosefalse时,logger返回nil
  • Ruby安全导航操作符应该用在log调用前面。
def logger
  @logger ||= Logger.new if verbose
end

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


因此,我将最初的记住if verbose检查的问题替换为记住&.调用。
但是,无论如何,我认为这是一个改进,因为忘记使用安全导航操作符会引发NoMethodError,换句话说,会通知log方法的误用。
所以现在,为了确保“安全导航操作员方法”实际上是解决我的问题的“更好”选择,
我需要确切地知道Ruby中的安全导航操作符是否在其接收器为nil时评估其参数。

avwztpqn

avwztpqn1#

引用安全导航操作符的语法文档:
&.,称为“安全导航操作符”,允许在接收方为nil时跳过方法调用。如果跳过调用,则返回nil,并且不评估方法的参数。
因此,当您将log方法调用为nil时,如果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}" }


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

ijnw1ujt

ijnw1ujt2#

解析表达式但未执行

logger.is_a?(NilClass) == true时,logger&.log的参数不会被计算。每个被计算的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

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

6ojccjat

6ojccjat3#

它不评估它们:

require 'pry'

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

字符串
这将返回:

nil


如果它计算了它,那么它将触发绑定,就像这个例子一样:

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


如果你没有pry,但有一个现代版本的Ruby,你可以用binding.irb代替binding.pry
这是否是一个“更好”的解决方案是您应该确定的基准。
您可以在How is the Ruby safe navigation (&.) implemented?上阅读更多关于安全导航操作员的信息

azpvetkf

azpvetkf4#

不,这很容易测试:

$ 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


现在很清楚为什么它在没有被调用的时候没有被求值。

7y4bm7vi

7y4bm7vi5#

这不是对具体问题的回答,因为这个问题已经得到了回答,而是根据所述目标提出的一种替代办法。
Ruby的Logger已经有了一个块语法,其中块只在 * 当 * 日志级别将打印语句时才被计算。

logger.debug { puts "Do not evaluate this!"; "My String" }

字符串
如果日志级别为debug或更高,则将对该块进行评估,并记录它返回的字符串。如果日志级别低于debug,则根本不会对该语句进行评估。
TL;DR:将昂贵的日志语句传递给块内的日志记录器。

相关问题