场景

有一个账号 account,账号关联所有者 owner,而 owner 关联地址 address,现在要输出这个 address

account.owner.address

最理想的状态是 accountowneraddress 都不为 nil,可以正常输出。

但是如果 account == nil, 那么account.owner 就会变成 nil.owner,系统会报 NoMethodError。如果 account != nil 但是 account.owner == nilaccount.owner.address 变成 nil.address,系统仍然会报错。

所以,为了避免系统报错,安全的写法应该是

account && account.owner && account.owner.address
# 只有account 和 account.owner 均存在,才会执行最后的 account.owner.address

这个写法实在太啰嗦了,ActiveSupport 提供了一个简便的方法 try,可以写成

account.try(:owner).try(:address)

&.方法

Ruby 2.3.0 版本以后,提供了一个更简洁的方法,&.(Safe Navigation Operator)。可以改写成

account&.owner&.address

区别

但是,这几种方法有一些细微的区别,举例说明

情况一

account = Account.new(owner: nil) # account without an owner

account.owner.address
# => NoMethodError: undefined method `address' for nil:NilClass

account && account.owner && account.owner.address
# => nil

account.try(:owner).try(:address)
# => nil

account&.owner&.address
# => nil

情况二

account = Account.new(owner: false)

account.owner.address
# => NoMethodError: undefined method `address' for false:FalseClass `

account && account.owner && account.owner.address
# => false

account.try(:owner).try(:address)
# => nil

account&.owner&.address
# => undefined method `address' for false:FalseClass`

这段代码说明,&. 方法只会跳过 nil 值,但不会跳过 false 值。也就是说 nil&.address 不会报错,但是 false&.address 就会报错。(其实 false&.address 这种情况几乎不会发生)

情况三

account = Account.new(owner: Object.new)

account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>

account && account.owner && account.owner.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`

account.try(:owner).try(:address)
# => nil

account&.owner&.address
# => NoMethodError: undefined method `address' for #<Object:0x00559996b5bde8>`

这种情况是,owner 存在,但是没有与 address 关联起来(在 Rails 中没有定义 has_many),也就是说 address 本身就没有 .address 这个方法。

&. 方法报错,但是 try 方法依旧没有报错。

也就是说,使用 &. 方法时,会先去检查 owner 是否有 .address 这个方法,如果没有就报错;而 try 则不会返回错误信息。

该不该报错

还是上述的例子,如果在实际的业务逻辑中,有些 account 就是没有 owner 的,在写代码的时候,我们预料到在某些情况下,会出现 account.owner == nil,因而会导致 account.owner.address 出现报错。这种错误是不需要额外处理的,只需要把错误「藏」起来就好。这种情况,用 try 或者 &. 方法能让代码更加清晰简洁。

但是,如果是因为代码的问题导致的意外的报错,例如 owneraddress 之间忘记关联了。这种错误本来是不应该出现的,如果出现了,要及时地「暴露」出来。这种情况,从上面的情况三来看,用 try 就可能会把错误掩盖了,使得除错变得困难;更明智的做法是用 &. 或者 try!

类似地,在创建新 record 的时候,一般做法是

if @record.save
  redirect_to root_path
else
  render :new
end

之所以这么写,是要分别考虑 .save 执行成功和执行不成功的两种情况。如果在业务逻辑中,record 不应该出现执行不成功的情况,那么就应该这么写

@record.save!

如果执行不成功,.save!会就会报错。

数组和哈希的 .dig 方法

如果有这么一个Hash

params = {:account=>{:owner=>{:address=>"abc"}}}

要得到 'abc' 这个值,要这么获取

address = params[:account][:owner][:address]

try 方法来安全获取

address = params[:account].try(:[], :owner).try(:[], :address)

或者用 fetch 方法

address = params[:account].fetch(:owner) .fetch(:address)

其实还有一个更简便的方法,就是 .dig 方法

address = params.dig(:account, :owner, :address)

总结一下

  • 意料中的报错要处理,意料外的报错要暴露
  • &. 方法比 try 方法更严格
  • Hash 可以用 dig 方法