R 筆記 - try、try! 和 &.

try、try! 和 &.(safe navigation operator)

之前在做 Project 中因為沒有資料導致沒有該物件方法就噴錯,之後知道了 &. 的用法,但也沒有特別理解(反正就只知道如果沒有這個方法會忽略不執行),但最近上班後又遇到一樣的問題,攝影大哥前輩秀了 try 方法,導致我很好奇所以特別查了一下它的用途,也就促使這篇文章的誕生。

來點例子,讓大家能更了解這些方法的使用,希望之後在判斷 nil 值時,能有所幫助~

在解釋之前,我們先從 Ruby 的 tap method 開始說起吧…

tap

其實在寫這篇之前,我也完全不知道這個方法,所以查了文件,來看看 Ruby API 文件的說明:

Yields x to the block, and then returns x.

tap 是透過 yield 某個 object物件 進入 block,再傳回此 object。就如同以下(原始碼):

1
2
3
4
5
6
# activesupport/lib/active_support/core_ext/object/misc.rb

def tap
yield self
self
end

代表著可以用於檢查 method chain,更便於 除錯 以及 簡化程式碼!(什麼意思呢?)

用途一:除錯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(1..5).tap { |x| puts "original: #{x.inspect}" }
#original: 1..5
#1..5
.to_a.tap{ |x| puts "array: #{x.inspect}" }
#original: 1..5
#array: [1, 2, 3, 4, 5]
#[
# [0] 1,
# [1] 2,
# [2] 3,
# [3] 4,
# [4] 5
#]
.select{ |x| x%2 == 0 }.tap{ |x| puts "evens: #{x.inspect}" }
#original: 1..5
#array: [1, 2, 3, 4, 5]
#evens: [2, 4]
#[
# [0] 2,
# [1] 4
#]
.map{ |x| x*x }.tap{ |x| puts "squares: #{x.inspect}" }
#original: 1..5
#array: [1, 2, 3, 4, 5]
#evens: [2, 4]
#squares: [4, 16]
#[
# [0] 4,
# [1] 16
#]

用途二:簡化程式碼

不用先宣告變數~直接塞進去ouo

1
2
[].tap{ |i| i << "abc" }
''.tap{ |i| i << "do_some_thing" }

tap enables you to “tap into” a method chain and perform some tangential function.

&.(safe navigation operator)

safe navigation operator 使用起來跟等等底下要說的 try! 相似,唯一不同的點就是寫法簡潔更多。

1
2
3
4
5
6
7
REGEX = /(ruby) is (\w+)/i
"Ruby is awesome!".match(REGEX).values_at(1, 2)
# => ["Ruby", "awesome"]
"Python is fascinating!".match(REGEX).values_at(1, 2)
# NoMethodError: undefined method `values_at' for nil:NilClass
"Python is fascinating!".match(REGEX)&.values_at(1, 2)
# => nil

上述的方法是在 Ruby 內建的 method,而下方的 try 和 try! 是在 Rails 才有的 method!


try(*a, &b)

在使用方法 try 時,不用擔心前面的對象沒有後面這個方法導致噴錯(Nilclass)。就如同以下(原始碼):

1
2
3
4
5
6
7
8
9
10
11
def try(*a, &b)
if a.empty? && block_given?
yield self
else
public_send(*a, &b) if respond_to?(a.first)
end
end

def try(*args)
nil
end

代表著如果只接受 block 則 yield slef 給 block,否則就執行 public_send ,而 public_send 只會 call public_method(什麼意思呢?)

1
@person.try(:spouse).try(:name)

轉換後的意思其實是

1
@person.spouse.name if @person && @person.spouse

這也如同開頭所說,不用擔心前面的對象沒有後面這個方法導致噴錯,因為它已經是個判斷了!

1
@person.try(:non_existing_method) # => nil

轉換後的意思其實是

1
@person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil

由此可知到,try 的用法就像是 Ruby 中的 Object#send 一樣,可以讓你把 method 當作 argument 傳入object。

但是跟 send 不同的是,當 receiving object(receiver)的該 method 不存在時,不會觸發 NoMethodError,而是回傳 nil 值。

1
2
3
4
user.try(:should_be_error).try(:another_error)
#=> nil
user.send(:should_be_error)
#=> NoMethodError: undefined method `should_be_error' for #<User:0x007fca78bf5848>

try!(*a, &b)

用法跟 try 相同,唯一的區別是,當傳入的 argument(method)不存在時和 receiving object 不是 nil 時,會觸發 NoMethodError exception,而不是 nil;而當 receiving object 是 nil 值時,則會回傳 nil。

1
2
3
4
5
6
7
8
9
user = User.new
user.try(:should_be_error).try(:another_error)
#=> nil
user.try!(:should_be_error).try(:another_error)
#=> NoMethodError: undefined method `account' for #<User:0x007fca7cf91340>
user.try!(:should_be_error).try!(:another_error)
#=> NoMethodError: undefined method `account' for #<User:0x007fca7cf91340>
user.try(:should_be_error).try!(:another_error)
#=> nil

參考資料