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 nilem - 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 nilem, ale dla którego nie jest ona zaimplentowana

  • try w takiej sytuacji - zwróci nil
  • &. - zakończy wykonanie błędem NoMethodError
# 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 nilem 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, natomiast try - ponownie nil
  • 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 na nilu jeżeli jest ona dla niego zaimplementowana
  • safe navigation jest wyraźnie szybsze