Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

#to_s か #to_str か?Rubyの型変換を徹底解説:明示的キャストと暗黙的型強制の違い

型強制(type coercion)とは、オブジェクトの型を、その値も含めて別の型へ変換することです。たとえば、#to_s を使って Integer を String に変換したり、#to_i を使って Float を Integer に変換したりするのが典型例です。一方、一部のオブジェクトが実装している #to_str#to_int といったメソッドは、一見すると同じ働きをするように見えますが、実際には重要な違いがあります。

本記事では、Rubyにおける「明示的なキャスト」と「暗黙的な型強制」の違いを詳しく掘り下げ、両者がそれぞれどのような場面で使われるのかを解説します。

まずは、Rubyで最もよく使われる明示的なキャストヘルパーから見ていきましょう。

明示的キャストヘルパー(Explicit Casting Helpers)

最も一般的なキャストヘルパーは、#to_s#to_i#to_a#to_h です。これらは明示的なキャストメソッドであり、ある型の値を別の型へ簡単に変換するために使われます。

これらの明示的ヘルパーには、明確な「約束」があります。#to_s がオブジェクトに対して呼ばれた場合、そのオブジェクトが文字列への変換に適していなくても、必ず 文字列を返します。これは、コメディ俳優のマイケル・キートンをバットマン役に起用するようなものです。役柄に必ずしも向いていなくても、とにかく「バットマン」が手に入るわけです。

Rubyでは、標準ライブラリのほぼすべての基本的なオブジェクトに、これらのヘルパーメソッドが用意されています。

:foo.to_s # => "foo"
10.0.to_i # => 10
"10".to_i # => 10

これらのメソッド、特に #to_s は、Rubyのほとんどの基本型に実装されています。キャストはほぼ常に何らかの値を返しますが、その結果が期待どおりになるとは限りません。

"foo10".to_i          # => 0
[1, 2, 3].to_s        # => "[1, 2, 3]"
{ :foo => :bar }.to_s # => "{:foo=>:bar}"
{ :foo => :bar }.to_a # => [[:foo, :bar]]
Object.to_s           # => "Object"
Object.new.to_s       # => "#<Object:0x00007f8e6d053a90>"

#to_s#to_i#to_a#to_h を呼び出すと、どんな値でも選択した型へと強制的に変換されます。値の中身がどうなろうとも、変換先の型の表現が返される点に注意が必要です。

暗黙的型強制メソッド(Implicit Coercion Methods)

変換先の型として振る舞わない値に対してキャストメソッドを呼び出すと、エラーが発生したり、データが失われたりする可能性があります。そこでRubyは、オブジェクトがその型として振る舞える場合にのみ値を返す「暗黙的型強制メソッド」も提供しています。これにより、値が本当に望みの型として扱えることを保証できます。暗黙的型強制メソッドには、#to_str#to_int#to_ary#to_hash があります。

暗黙的型強制は、レナード・ニモイをスポック以外の役にキャストするようなものです。演じるキャラクターがスポックに十分近ければ成立しますが、そうでなければ失敗します。#to_str は文字列への変換を試みますが、オブジェクトがこのメソッドを実装しておらず暗黙的に変換できない場合は、NoMethodError が発生します。

10.to_int                           # => 10
10.0.to_int                         # => 10
require "bigdecimal"
BigDecimal.new("10.0000123").to_int # => 10
 
# 変換に失敗する例
"10".to_int             # => NoMethodError
"foo10".to_int          # => NoMethodError
[1, 2, 3].to_str        # => NoMethodError
{ :foo => :bar }.to_str # => NoMethodError
{ :foo => :bar }.to_ary # => NoMethodError
Object.to_str           # => NoMethodError
Object.new.to_str       # => NoMethodError

このように、Rubyはどの値を要求された型に変換できて、どれができないのかについて、明示的キャストよりも厳格になっています。変換が不可能な場合、その #to_* メソッドはオブジェクトに実装されていないため、呼び出すと NoMethodError が発生します。

暗黙的型強制(たとえば #to_str)を使うということは、「元の型がStringのように振る舞える場合にのみ、Stringオブジェクトを返してほしい」と関数に依頼することを意味します。このため、#to_str はRuby標準ライブラリではStringクラスにのみ実装されているのです。

Ruby自身が暗黙的型強制を使う場面

変換時の意図をより正確に表現できること以外に、暗黙的型強制にはどんな利点があるのでしょうか? 実は、Ruby自体が多くの場面で暗黙的型強制を利用しています。たとえば、+ 演算子でオブジェクトを連結する場合です。

name = "world!"
"Hello " + name # => "Hello world!"
 
# #to_str がない場合
class Name
  def initialize(name)
    @name = name
  end
end
"Hello " + Name.new("world!") # => TypeError: no implicit conversion of Name into String

この例では、Name 型から String への暗黙的な変換ができないため、Rubyが TypeError を発生させています。

しかし、クラスに #to_str を実装すれば、Rubyは Name 型をどのように変換すればよいかを理解できるようになります。

# #to_str がある場合
class Name
  def to_str
    @name
  end
end
"Hello " + Name.new("world!") # => "Hello world!"

同じことは、Arrayと #to_ary の組み合わせでも可能です。

class Options
  def initialize
    @internal = []
  end
 
  def <<(value)
    @internal << value
  end
end
 
options = Options.new
options << :foo
[:some_prefix] + options # => TypeError: no implicit conversion of Options into Array
 
class Options
  def to_ary
    @internal
  end
end
[:some_prefix] + options # => [:some_prefix, :foo]

さらに #to_ary は、配列を複数の変数へ分解(デストラクチャリング)する用途にも使えます。

options = Options.new
options << :first
options << :second
options << :third
first, second, third = options
first  # => :first
second # => :second
third  # => :third

また、ブロック引数へのオブジェクトの変換にも利用されます。

[options].each do |(first, second)|
  first # => :first
  second # => :second
end

暗黙的型強制メソッドが使われる場面は他にもあります。たとえば、** と組み合わせた #to_hash です。次の例では、値が parse_options メソッドに渡される前に、#to_hash によってハッシュへと変換されます。

class Options
  def to_hash
    # Optionsの内部配列からハッシュを生成
    Hash[*@internal]
  end
end
 
def parse_options(opts)
  opts
end
 
options = Options.new
options << :key
options << :value
parse_options(**options) # => {:key=>:value}

型の強制(Enforcing Types)

Rubyには、対象の型が不明な場合に確実に正しい型を取得したいときに使える、より堅牢な変換メソッドも用意されています。基本型ごとに1つずつ存在します(String(...)Integer(...)Float(...)Array(...)Hash(...) など)。

String(self)       # => "main"
String(self.class) # => "Object"
String(123456)     # => "123456"
String(nil)        # => ""
 
Integer(123.999)   # => 123
Integer("0x1b")    # => 27
Integer(Time.new)  # => 1204973019
Integer(nil)       # => TypeError: can't convert nil into Integer

String(...) メソッドは、まず値に対して #to_str を呼び出そうとし、それが失敗した場合に #to_s を呼び出します。すべてのオブジェクトが #to_str を定義しているわけではないため、暗黙的型強制(#to_str)と明示的キャスト(#to_s)の両方を試すことで、文字列変換が成功し、意図した値を得られる可能性が高まります。最初に暗黙的型強制を試すことで、"#<Object:0x00007f8e6d053a90>" のようなオブジェクト表現ではなく、同じ値を持つ変換後の型の結果を得やすくなります。

class MyString
  def initialize(value)
    @value = value
  end
 
  def to_str
    @value
  end
end
 
s = MyString.new("hello world")
s.to_s    # => "#<MyString:0x...>"
s.to_str  # => "hello world"
String(s) # => "hello world"

なお、暗黙的キャストメソッドは、変換先の型として振る舞うオブジェクトにのみ実装すべきです。たとえば、独自のString風クラスには #to_str を実装する、といった具合です。

さらに、String(...) ヘルパーは、暗黙的型強制を最初に試すだけでなく、返された値の型も検証します。#to_str は単なるメソッドなので、String以外の任意の型の値を返せてしまいます。要求された型の値を確実に得るために、型が一致しない場合は String(...)TypeError を発生させます。

class MyString
  def to_str
    nil
  end
end
 
s = MyString.new("hello world")
s.to_s    # => "#<MyString:0x...>"
s.to_str  # => nil
String(s) # => "#<MyString:0x...>"

この例では、#to_str がString型ではない nil を返したため、Rubyはその結果を無視し、代わりに #to_s の結果へフォールバックしていることがわかります。

もし #to_snil を返して正しい型でない場合、String(...)TypeError を発生させます。

class MyString
  def to_str
    nil
  end
 
  def to_s
    nil
  end
end
 
s = MyString.new("hello world")
s.to_s    # => nil
s.to_str  # => nil
String(s) # => TypeError: can't convert MyString to String (MyString#to_s gives NilClass)

このように型の強制においては信頼性が高い反面、String(...)Integer(...) といったキャストヘルパーメソッドは、与えられた値に対してより多くの検証を行う必要があるため、通常はやや処理が遅くなる点に留意してください。

まとめ

オブジェクトを扱う際に正しい型のデータであることを保証したい場合、型強制は非常に有用な仕組みです。本記事では、#to_s#to_i#to_a#to_h といった明示的キャストヘルパーの基礎をおさらいしました。さらに、#to_str#to_int#to_ary#to_hash といった暗黙的ヘルパーが有効な場面や、Ruby自体が内部でそれらをどのように活用しているかについても解説しました。

この型強制の概要と、俳優のキャストにたとえたアナロジーが皆さんのお役に立てば幸いです。取り上げてほしいトピックがあれば、ぜひお知らせください。ご質問やコメントがあれば、@AppSignal までお気軽にご連絡ください。

  1. Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説

    ※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す

  2. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端