报错与不报错
场景
有一个账号 account
,账号关联所有者 owner
,而 owner
关联地址 address
,现在要输出这个 address
。
account.owner.address
最理想的状态是 account
、 owner
和 address
都不为 nil
,可以正常输出。
但是如果 account == nil
, 那么account.owner
就会变成 nil.owner
,系统会报 NoMethodError
。如果 account != nil
但是 account.owner == nil
,account.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
或者 &.
方法能让代码更加清晰简洁。
但是,如果是因为代码的问题导致的意外的报错,例如 owner
和 address
之间忘记关联了。这种错误本来是不应该出现的,如果出现了,要及时地「暴露」出来。这种情况,从上面的情况三来看,用 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
方法