LoginSignup
34
17

More than 5 years have passed since last update.

HashWithIndifferentAccess は Hash と違いキーワード引数に直接渡せない

Last updated at Posted at 2015-05-01

追記

Ruby 2.6 からはキーワード引数にシンボル以外のキーを渡すことができなくなるようです, 自然な流れな気がしています.

Non-Symbol key in keyword arguments hash causes an exception.

https://github.com/ruby/ruby/blob/1692ccaf9f49c001b18de8b7296ede3f68190ab8/NEWS より

HashWithIndifferentAccess

Rails (activesupport) で提供されている Hash のサブクラス
特徴として、 SymbolString を区別せず同一のキーとして取り扱う。

h1 = Hash.new
h1[:foo] = 1
h1[:bar] = 2
h1['bar'] = 3
puts h1[:foo]  # => 1
puts h1['foo'] # => nil
puts h1[:bar]  # => 2
puts h1['bar'] # => 3

h2 = HashWithIndifferentAccess.new
h2[:foo] = 1
h2[:bar] = 2
h2['bar'] = 3
puts h2[:foo]  # => 1
puts h2['foo'] # => 1
puts h2[:bar]  # => 3
puts h2['bar'] # => 3

HashWithIndifferentAccess の内部的な話として、キーに対して Symbol でアクセスが来た際は String に丸めている。

h3 = HashWithIndifferentAccess.new
h3[:foo] = 1
h3['bar'] = 2
h3[:baz] = 3

# Symbol は String に丸められている
h3.keys # => ["foo", "bar", "baz"]

キーワード引数

Ruby 2.0.0 からの機能。メソッドを定義する際、以下のように記述できる。

def test foo: 1, bar: 2, baz: 3
  [foo, bar, baz].join(',')
end

呼び出す際は、以下のような感じ。
以下の、「ハッシュを直接渡すこともできる」の部分が 今回のキモ

# 省略した箇所は、定義中のデフォルトが与えられる
test                           # => '1,2,3'
test foo: 4                    # => '4,2,3'
test foo: 4, bar: 5, baz: 6    # => '4,5,6'

# ハッシュを直接渡すこともできる
arg = {foo: 7, bar: 8, baz: 9}
test arg                       # => '7,8,9'

キーワード引数に HashWithIndifferentAccess を与えてみる

arg = HashWithIndifferentAccess.new({foo: 4, bar: 5, baz: 6})
test arg # => ArgumentError: wrong number of arguments (1 for 0)
  • キーワード引数が受け入れ可能なハッシュは、全てのキーが Symbol であることが前提
    参考 ⇒ class.crb_extract_keywords (と separate_symbol あたり)
  • しかしながら、 HashWithIndifferentAccess はキーを String で保持しているのでキーワード引数の要件を満たさない

ちなみに確認

arg = {foo: 4, bar: 5, "baz" => 6}
test arg # => ArgumentError: wrong number of arguments (1 for 0)

Hash であっても同様で、キーが全て Symbol でなければキーワード引数として受け付けてくれない。

そもそもエラーメッセージの ArgumentError って何言ってんのよ?

追記: 以下の挙動は Ruby 2.5 までのものです.

ArgumentError: wrong number of arguments (1 for 0)
rb_extract_keywordsの説明 より

original_hashで参照されるHashオブジェクトから,Symbolである
キーとその値を新しいHashに取り出します.original_hashの指す
先には,元のHashがSymbol以外のキーを含んでいた場合はそれらが
コピーされた別の新しいHash,そうでなければ0が保存されます.

つまり、キーが全て Symbol でないハッシュを引数に与えると...

arg = {foo: 4, bar: 5, "baz" => 6}
test arg

以下のような呼び出しと扱われる。

test({"baz" => 6}, foo: 4, bar: 5)

(キーワード引数以外の) 引数が 1つ与えられる形になるので
先のエラー (0個引数を与えるべき箇所に1つの引数が渡っている) になる。

# 第一引数を用意しておくと...
def test1 x, foo: 1, bar: 2, baz: 3
  [foo, bar, baz, x].join(',')
end

# 第一引数に arg 全てがわたり、キーワード引数に {} が渡される。
arg = HashWithIndifferentAccess.new({foo: 4, bar: 5, baz: 6})
test1 arg # => "1,2,3,{\"foo\"=>4, \"bar\"=>5, \"baz\"=>6}"
34
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
17