そういうこともある

エンジニア的な何かの、プログラミングとかイベントレポートとか読書感想文

Railsのソースコード読んでみる | Active Support instance_values編

f:id:sktktk1230:20171212153754p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

instance_values を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Rails Guidesの日本語ドキュメントを見てみると

instance_valuesメソッドはハッシュを返します。インスタンス変数名から"@"を除いたものがハッシュのキーに、インスタンス変数の値がハッシュの値にマップされます。キーは文字列です。

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end
C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}

引用:RAILS GUIDES:instance_values

インスタンス変数を簡単に取り出して使うメソッドです

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def instance_values を探してみます

f:id:sktktk1230:20180223175611p:plain

2. 該当箇所が1個あったので、それをみてみます

1. activesupport > lib > active_support > core_ext > object > instance_values.rb
# frozen_string_literal: true

class Object
  # Returns a hash with string keys that maps instance variable names without "@" to their
  # corresponding values.
  #
  #   class C
  #     def initialize(x, y)
  #       @x, @y = x, y
  #     end
  #   end
  #
  #   C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
  def instance_values
    Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }]
  end

まず、 Hash[] を調べてみます

Hashクラスのクラスメソッドは、新しいハッシュ(Hashクラスのインスタンス)を返します。の中に[キー1, 値1, キー2, 値2, ...]のようにオブジェクトを並べると、それが新しいハッシュのキーと値になります。

[]内のオブジェクトの数が奇数のときは、例外ArgumentErrorが発生します。

movie = Hash[:title, "Alien", :director, "Ridley Scott", :year, 1979]
puts movie[:title]
puts movie[:year]

Alien

1979

引用:Rubyリファレンス:[]

ハッシュクラスのインスタンスを生成するクラスメソッドになります

次に、どんな値を元にハッシュ生成しているのか見るため instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] } を順を追って見てみたいと思います

まず、instance_variables.map {} です

instance_variables を調べてみると

instance_variablesメソッドは、レシーバのオブジェクトが持っているインスタンス変数の名前を配列に入れて返します。

Ruby 1.9 Ruby 1.8では配列中の変数名は文字列ですが、Ruby 1.9ではシンボルになります。

class Book
  def initialize(title, price)
    @title = title; @price = price
  end
end
book = Book.new("Programming Ruby", 2000)
p book.instance_variables

["@title", "@price"] (Ruby 1.8の場合)

[:@title, :@price] (Ruby 1.9の場合)

引用:Rubyリファレンス:instance_variables

インスタンス変数の名前を配列で取得しています。そしてその配列に対して map を実行しています

mapは

mapメソッドは、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。collectメソッドの別名です。

numbers = ["68", "65", "6C", "6C", "6F"]
p numbers.map {|item| item.to_i(16) }

[104, 101, 108, 108, 111]

引用:Rubyリファレンス:map

ブロックの戻り値を集めて配列にするメソッドです

次にmapに渡しているブロック部分 { |name| [name[1..-1], instance_variable_get(name)] } を見てみます

name[1..-1]インスタンス変数名から部分文字列を取得している処理です

レシーバがStringクラスだった場合は、次のとおりです

文字列の中から部分文字列を取り出すメソッドです。s[2]、s[3,5]、s[2..7]、s[/[0-9]/] のように、いろいろな形で利用できます。配列要素の取り出しのように記述しますが、実際にはメソッド呼び出しです。[]の中はメソッドの引数です。


省略


引数に範囲を指定すると、その範囲に対応する部分文字列を返します。範囲外の位置を指定すると、nilが返ります。

s = "hello, world"
puts s[7..10]   # 7文字目から10文字目まで
puts s[7...10]  # 7文字目から10文字目まで、10文字目は含まない

worl

wor

開始位置と終了位置がマイナスの場合は、文字列の末尾から数えます(-1が末尾から1番目、-2が末尾から2番目、...)。

s = "hello, world"
puts s[-5..-1]  # 末尾から5文字目..末尾から1文字目まで

world

引用:Rubyリファレンス:[] (String)

name[1..-1] は2文字目から末尾1文字目までを取得しています
たとえば、 "@title"であれば title が取得できます

レシーバがシンボルだった場合にも同様です

rangeで指定したインデックスの範囲に含まれる部分文字列を返します。


(self.to_s[range] と同じです。)


[PARAM] range:

取得したい文字列の範囲を示す Range オブジェクトを指定します。

:foo[0..1] # => "fo"

[SEE_ALSO] String#[], String#slice

引用:Ruby2.5.0リファレンス:Symbol

次に、instance_variable_get(name) を見てみます

instance_variable_getメソッドは、レシーバが持っているインスタンス変数の値を返します。引数nameにはインスタンス変数の名前を:@titleや"@title"のようにシンボルか文字列で渡します。

定義されていない変数名を渡すとnilが返ります。:titleのようにインスタンス変数と見なされない名前を渡すと例外NameErrorが発生します。

class Book
  def initialize(title)
    @title = title
  end
end
book = Book.new("Programming Ruby")
p book.instance_variable_get(:@title)
p book.instance_variable_get(:@price)

"Programming Ruby"

nil

引用:Ruby2.5.0リファレンス:Symbol

インスタンス変数名を引数に渡すとその値が取れるという動きです

ブロックの中で行っている処理は、 配列の先頭にインスタンス変数名、次にインスタンス変数の値をセットしている処理です

たとえば、 { |name| [name[1..-1], instance_variable_get(name)] } の name"@title" の場合であれば、 ["title", "@titleに入ってた値"] ということになります

ここまでをまとめて考えてみると、Hash[] の引数に入る値は、

[
  ["インスタンス変数名1", "インスタンス変数の値1"],
  ["インスタンス変数名2", "インスタンス変数の値2"],
  ["インスタンス変数名3", "インスタンス変数の値3"]
]

になります

Hash[] を調べてみると

新しいハッシュを生成します。 引数は必ず偶数個指定しなければなりません。奇数番目がキー、偶数番目が値になります。

このメソッドでは生成するハッシュにデフォルト値を指定することはできません。 Hash.newを使うか、Hash#default=で後から指定してください。

[PARAM] key_and_value:

生成するハッシュのキーと値の組です。必ず偶数個(0を含む)指定しなければいけません。

[EXCEPTION] ArgumentError:

奇数個の引数を与えたときに発生します。

以下は配列からハッシュを生成する方法の例です。


省略


(2) キーと値のペアの配列からハッシュへ

alist = [[1,"a"], [2,"b"], [3,["c"]]]
p Hash[*alist.flatten(1)]  # => {1=>"a", 2=>"b", 3=>["c"]}

引用:Ruby2.5.0リファレンス:Hash.[]

さきほどの配列をHash[] すると

{
  "インスタンス変数名1" => "インスタンス変数の値1",
  "インスタンス変数名2" => "インスタンス変数の値2",
  "インスタンス変数名3" => "インスタンス変数の値3"
}

となります

読んでみて

インスタンス変数も利用がしやすいようにこんなメソッドもあるんだというのが学びでした