ActiveSupportのTryとTry!の違い

ActiveSupport::Tryableには`try()`と`try!()`の2つのメソッドが実装されています。
違いがよく分からなかったので、調べました。

結論から言うとソースコードにそのまんま答えが書いてありました。

https://github.com/rails/rails/blob/v5.1.5/activesupport/lib/active_support/core_ext/object/try.rb

## 環境

- Windows 10 Insider Preview Build 17115.rs4_release (Windows Subsystem for Linux)
- rbenv 0.4.0
- Ruby 2.3.6
- Active Support 5.1.5

### 実行環境構築

```
$ rbenv local 2.3.6
$ bundle init
$ echo "gem 'activesupport'" >> Gemfile
$ bundle install --path vendor/bundle
```

## try()とtry!()

### 共通の挙動

[Rails ガイド](https://railsguides.jp/active_support_core_extensions.html#try)によると

>nilでない場合にのみオブジェクトのメソッドを呼び出したい場合、最も単純な方法は条件文を追加することですが、どこか冗長になってしまいます。そこでtryメソッドを使うという手があります。tryはObject#sendと似ていますが、nilに送信された場合にはnilを返す点が異なります。

とありますが、「nilに送信された場合にはnilを返す」挙動は`try()`も`try!()`も同じです。

```ruby:index.rb
require 'active_support/core_ext/object/try'

nil.try(:awesome_method)
nil.try!(:awesome_method)

puts 'Success!!'
```

```
$ bundle exec ruby index.rb
Success!!
```

これは非常に簡単な理由で、`try()`も`try!()`も、`nil`を返すだけという実装がされているからです。

```ruby:active_support/core_ext/object/try.rb
class NilClass
  def try(*args)
    nil
  end
  def try!(*args)
    nil
  end
end
```

### try()とtry!()の違い

[GitHubのTry!()実装箇所](https://github.com/rails/rails/blob/v5.1.5/activesupport/lib/active_support/core_ext/object/try.rb#L91)に、下記のようなコメントがあります。

>Same as #try, but raises a +NoMethodError+ exception if the receiver is not +nil+ and does not implement the tried method.

実際に動かしてみましょう。

```index.rb
require 'active_support/core_ext/object/try'

'awesome_string'.try(:awesome_method)
'awesome_string'.try!(:awesome_method) #=> NoMethodError

puts 'Success!!'
```

```
$ bundle exec ruby index.rb
vendor/bundle/ruby/2.3.0/gems/activesupport-5.1.5/lib/active_support/core_ext/object/try.rb:17:in `public_send': undefined method `awesome_method' for "awesome_string":String (NoMethodError)
        from vendor/bundle/ruby/2.3.0/gems/activesupport-5.1.5/lib/active_support/core_ext/object/try.rb:17:in `try!'
        from index.rb:4:in `<main>'
```

`try!()`のところでNoMethodErrorが発生しました。

`try()`と`try!()`の内部実装を見てみます。

```ruby:active_support/core_ext/object/try.rb
module ActiveSupport
  module Tryable #:nodoc:
    def try(*a, &b)
      try!(*a, &b) if a.empty? || respond_to?(a.first)
    end

    def try!(*a, &b)
      if a.empty? && block_given?
        # (中略)
      else
        public_send(*a, &b)
      end
    end
  end
end

class Object
  include ActiveSupport::Tryable
end
```

`Tryable`から、`a.empty?`のケース(つまりtryの引数が無い場合)と、ブロックが渡されるケースを除いてみます。
すると、非常にシンプルなコードであることがわかります。

```ruby
    def try(*a)
      try!(*a) if respond_to?(a.first)
    end

    def try!(*a)
      public_send(*a)
    end
```

`respond_to?()`は、オブジェクトに対してメソッドをコールできるかどうかを返すメソッドです。`try()`はこのチェックをしていて、`try!()`はしていない、というだけの違いです。

つまり`try()`は「第一引数をメソッドとしてコールできる時だけ`try!()`を実行する」というだけの動きをしています。

## まとめ

- `try()`も `try!()`も、`nil`に対して`nil`を返すのは同じ。
- 存在しないメソッド名を引数にしたとき、`try!()`は`NoMethodError`を吐く点が異なる。

### 参考

- [rails/try\.rb at v5\.1\.5 · rails/rails](https://github.com/rails/rails/blob/v5.1.5/activesupport/lib/active_support/core_ext/object/try.rb)
- [Active Support コア拡張機能 \| Rails ガイド](https://railsguides.jp/active_support_core_extensions.html#try)
- [respond\_to? \(Object\) \- Rubyリファレンス](https://ref.xaio.jp/ruby/classes/object/respond_to)