Nie mając pewności, czy obiekt na którym chcemy wywołać metodę istnieje – zdarza się nam posługiwać metodą try
.
Od czasu Ruby’ego 2.3 – pojawiło się również tzw. safe navigation, w postaci &.
Czy i (potencjalnie) czym się różnią, a w jakich sytuacjach działają podobnie?
TRY – &. – cechy wspólne
Podstawowe użycie obu wskazanych metod sprowadza się do tego samego: próba wywołania metody na obiekcie będącym nil
em – zwróci nil
(zamiast błędu NoMethodError
)
nil.try(:foo_bar)
# => nil
nil&.foo_bar
# => nil
nil.foo_bar
# => NoMethodError
TRY – &. – różnice
1. Ruby… niekoniecznie on Rails
Jak wspomnieliśmy we wstępie &.
jest zaimplementowany w Ruby’m (od wersji 2.3) podczas kiedy try
jest metodą Railsową – udostępnioną przez ActiveSupport
2. Kiedy obiekt istnieje, ale “nie chce odpowiadać”
Najbardziej kluczową różnicą pomiędzy omawianymi elementami jest ich zachowanie w przypadku kiedy żądana metoda jest wywołana na obiekcie niebędącym co prawda nil
em, ale dla którego nie jest ona zaimplentowana
try
w takiej sytuacji – zwrócinil
&.
– zakończy wykonanie błędemNoMethodError
# poprawne wykonanie
[['foo', 'bar']].to_h
# => {"foo"=>"bar"}
# wywołanie na obiekcie, który nie ma zaimplementowanej metody
'foo'.to_h
# => NoMethodError
'foo'.try(:to_h)
# => nil
'foo'&.to_h
# => NoMethodError
Warto jednak zwrócić uwagę, że ActiveSupport udostępnia również metodę z bangiem – try!
, która w tej sytuacji zachowa się podobnie:
'foo'.try!(:to_h)
# => NoMethodError
Podobny efekt jesteśmy w stanie uzyskać za pomocą połączenia delegate
+ allow_nil: true
– tu również zabezpieczymy się przed błędem kiedy obiekt jest nil
em ale będziemy oczekiwali błędu kiedy obiekt ten istnieje, a nie ma zaimplementowanej metody.
Dzieje sie tak oczywiście dlatego, że jeżeli obiekt istnieje, to delegacja po prostu spróbuje wykonać na nim wskazaną metodę:
class TestDelegate
attr_reader :str
def initialize(str)
@str = str
end
delegate :upcase, to: :str, allow_nil: true
end
TestDelegate.new('foo').upcase
# => "FOO"
TestDelegate.new(nil).upcase
# => nil
TestDelegate.new(1).upcase
# => NoMethodError
Ważne: jeżeli nil
odpowiada na jakąś metodę (np. to_h
) to w przypadku delegacji zostanie ona na nim wykonana natomiast przy użyciu try
lub &.
– mimo wszystko dostaniemy nil
class TestDelegate
attr_reader :str
def initialize(str)
@str = str
end
delegate :to_h, to: :str, allow_nil: true
def test_try
str.try(:to_h)
end
def test_safe_navigation
str&.try(:to_h)
end
end
TestDelegate.new(nil).to_h
# => {}
TestDelegate.new(nil).test_try
# => nil
TestDelegate.new(nil).test_safe_navigation
# => nil
3. Tempo!
And the last but not the least – try
i &.
różnią się od siebie czasem wykonania:
require "benchmark"
Benchmark.bm do |test|
test.report("try:") do
1_000_000.times { nil.try(:length) }
end
test.report("safe navigation:") do
1_000_000.times { nil&.length }
end
end
user | system | total | real | |
---|---|---|---|---|
try: | 0.179869 | 0.000438 | 0.180307 | 0.180713 |
safe navigation: | 0.034285 | 0.000078 | 0.034363 | 0.034401 |
TL;DR
try
jest metodą Railsową, podczas kiedy&.
to Ruby >= 2.3- oba rozwiązania w przypadku wywołania na
nil
-u zwrócąnil
- dla obiektu, który nie ma zaimplementowanej danej metody –
&.
wypluje błąd, natomiasttry
– ponownienil
- wykorzystanie jednak metody
try!
w takiej sytuacji – również zakończy się błędem - korzystanie z delegacji +
allow_nil: true
spowoduje wykonanie delegowanej metody nawet nanil
u jeżeli jest ona dla niego zaimplementowana - safe navigation jest wyraźnie szybsze