Hash

Hash

키-값 쌍(key-value pair)을 저장하고 다루는 클래스예요. Hash는 Ruby에서 아주 자주 쓰이는 핵심 자료구조랍니다.

출처: Ruby 3.3 API

본문

Hash는 각 엔트리(entry)가 고유한 키(key)와 대응하는 값(value)으로 이루어진 사전류 컬렉션이에요. 데이터를 빠르게 조회하고 매핑할 때 사용해요.

간단한 해시 만들기 예시:

sales = {
  January: 10,
  February: 12,
  March: 15
}

엔트리 순서 (Entry Order)

해시에 엔트리가 추가되면, 해시에는 그 엔트리가 마지막에 추가돼요. 반대로 엔트리의 값만 갱신하면 순서는 바뀌지 않아요. 키를 다시 추가하거나 중복 키를 넣으면 마지막 엔트리에 해당하는 값으로 덮여쓰여요.

해시 키 (Hash Keys)

두 개의 해시 키가 같다는 것은 eql?이 참이고 hash가 서로 같다는 뜻이에요. 해시 키가 될 수 있는 객체는 hasheql? 메서드를 구현해야 해요. 기본적으로는 거의 모든 객체가 키가 될 수 있지만, 같은 내용이라도 서로 다른 객체면 다른 키로 취급돼요.

가변 객체를 해시 키로 사용하는 경우 조심해야 해요. 가변 키를 수정하면 해시 인덱스가 손상될 수 있어요:

h = {[:a] => 0}
a0 = [:a]
a1 = [:a]
h[a0] # => 0
h[a1] # => 1  # 같은 배열이지만 다른 객체라서 새 키가 됩
# ... 키를 수정하면
h.include?(a0) # => false  # 인덱스가 깨짐

이럴 땐 rehash 메서드로 인덱스를 복구할 수 있어요:

h.rehash # => {[:bam, :bar]=>0, [:baz, :bat]=>1}

String 키는 항상 안전해요. 언프리즈드 String을 키로 넘기면 복제(frozen)되어 저장되기 때문이에요:

s = 'foo'
h = {s => 0}
h.keys.first.frozen? # => true

사용자 정의 해시 키 (User-Defined Hash Keys)

해시 키로 쓰려면 객체가 hasheql? 메서드를 구현해야 해요. 단, compare_by_identity를 사용하면 해시 값 대신 객체 아이디로 비교하므로 이 요구 사항이 적용되지 않아요.

일반적으로 hash는 객체의 데이터에 기반을 두고, eql?은 재정의한 ==의 별칭으로 만드는 게 흔해요:

class Book
  attr_reader :author, :title

  def initialize(author, title)
    @author = author
    @title = title
  end

  def ==(other)
    self.class === other &&
      other.author == @author &&
      other.title == @title
  end

  alias eql? ==

  def hash
    [self.class, @author, @title].hash
  end
end

book1 = Book.new 'matz', 'Ruby in a Nutshell'
book2 = Book.new 'matz', 'Ruby in a Nutshell'

reviews = {}
reviews[book1] = 'Great reference!'
reviews[book2] = 'Nice and compact!'
reviews.length #=> 1

기본값 (Default Values)

[], values_at, dig는 키에 해당하는 값을 돌려줘요. 키가 없으면 기본 proc(있을 때) 또는 기본값(기본은 nil)으로 결정돼요.

h = Hash.new
h.default # => nil

h = Hash.new(-1)
h.default # => -1
h.default = 0
h.default # => 0

counts = {foo: 42}
counts.default = 0
counts[:bar] # => 0
counts.values_at(:foo, :bar, :baz) # => [42, 0, 0]

기본값은 복제되지 않고 그대로 사용돼요. 그래서 가변 객체를 기본값으로 두는 건 권장하지 않아요:

synonyms = Hash.new([])
synonyms[:hello] << :hi  # 기본값이 변질됨!
synonyms.default # => [:hi]

가변 객체 기본값이 필요하면 기본 proc을 쓰는 걸 추천해요:

synonyms = Hash.new { |hash, key| hash[key] = [] }
synonyms[:hello] << :hi # => [:hi]
synonyms[:world] << :universe
synonyms.keys # => [:hello, :world]

기본 proc은 해시 자신과 누락된 키로 호출돼요. 기본 proc을 설정하면 기본값이 비워지고, 그 반대도 마찬가지예요. 또한 해시를 수정하는 기본 proc은 같은 키에 대해 스레드들이 동시에 호출할 수 있어 스레드에 안전하지 않다는 점을 알아두세요.

여기 있는 것 (What's Here)

클래스 Hash는:

  • 클래스 Object에서 상속받아요.
  • Enumerable 모듈을 포함해서 수십 개의 추가 메서드를 제공해요.

여기서 Hash는 다음을 위한 메서드를 제공해요:

  • 해시 만들기: ::[], ::new, ::try_convert
  • 해시 상태 설정: compare_by_identity, default=, default_proc=, rehash
  • 조회: any?, compare_by_identity?, default, default_proc, empty?, eql?, hash, has_value?, include?, has_key?, member?, key?, length, size, value?
  • 비교: <, <=, ==, >, >=
  • 가져오기: [], assoc, dig, fetch, fetch_values, key, keys, rassoc, values, values_at
  • 할당: []=, store, merge, merge!, update, replace
  • 삭제: clear, compact!, delete, delete_if, filter!, select!, keep_if, reject!, shift (결과 사본: compact, except, filter, select, reject, slice)
  • 순회: each, each_pair, each_key, each_value
  • 변환: inspect, to_s, to_a, to_h, to_hash, to_proc
  • 키·값 변환: transform_keys, transform_keys!, transform_values, transform_values!
  • 기타: flatten, invert

더 알아보기: 공개 클래스 메서드 (Public Class Methods)

Hash[] → new_empty_hash

주어진 객체들로 채워진 새 해시를 돌려줘요.

  • 인자 없이 → 새 빈 해시
  • 인자 하나가 Hash → 해당 엔트리들(기본값·기본 proc 제외)로 채운 새 해시
  • 인자 하나가 2-요소 배열들의 배열 → 각 배열이 키-값 엔트리가 됨
  • 인자 개수가 짝수 → 연속된 두 인자가 키-값 엔트리가 됨
h = {foo: 0, bar: 1, baz: 2}
Hash[h] # => {:foo=>0, :bar=>1, :baz=>2}
Hash[ [ [:foo, 0], [:bar, 1] ] ] # => {:foo=>0, :bar=>1}
Hash[:foo, 0, :bar, 1] # => {:foo=>0, :bar=>1}

new(default_value = nil) → new_hash

새 빈 해시를 돌려줘요. 초기 기본값·기본 proc은 사용한 형식에 따라 달라져요.

h = Hash.new
h.default # => nil
h.default_proc # => nil

h = Hash.new(false)
h.default # => false

h = Hash.new {|hash, key| "Default value for #{key}" }
h.default_proc.class # => Proc
h[:nosuch] # => "Default value for nosuch"

ruby2_keywords_hash(hash) → hash

주어진 해시를 복제하고 ruby2_keywords 플래그를 붙여요. 평소엔 쓰지 말고, 디버깅·연구·인자 직렬화 해제 같은 정말 필요한 경우에만 사용해요.

h = {k: 1}
h = Hash.ruby2_keywords_hash(h)
def foo(k: 42)
  k
end
foo(*[h]) #=> 1 (경고·에러 없음)

ruby2_keywords_hash?(hash) → true or false

주어진 해시가 Module#ruby2_keywords(또는 Proc#ruby2_keywords)로 플래그가 붙었는지 확인해요.

ruby2_keywords def foo(*args)
  Hash.ruby2_keywords_hash?(args.last)
end
foo(k: 1)   #=> true
foo({k: 1}) #=> false

try_convert(obj) → obj, new_hash, or nil

obj가 Hash면 obj를, obj:to_hash에 응답하면 obj.to_hash 결과를, :to_hash에 응답하지 않으면 nil을 돌려줘요. obj.to_hash가 Hash가 아니면 예외를 던져요.

더 알아보기: 공개 인스턴스 메서드 (Public Instance Methods)

hash < other_hash → true or false

hashother_hash의 진부분집합(proper subset)이면 true를 돌려줘요.

h1 = {foo: 0, bar: 1}
h2 = {foo: 0, bar: 1, baz: 2}
h1 < h2 # => true
h2 < h1 # => false
h1 < h1 # => false

hash <= other_hash → true or false

hashother_hash의 부분집합이면 true를 돌려줘요.

h1 <= h2 # => true
h1 <= h1 # => true

hash == object → true or false

다음 조건을 모두 만족하면 true를 돌려줘요:

  • object가 Hash 객체
  • 키가 (순서와 무관하게) 같음
  • 각 키 key에 대해 hash[key] == object[key]
h1 = {foo: 0, bar: 1, baz: 2}
h2 = {foo: 0, bar: 1, baz: 2}
h1 == h2 # => true
h3 = {baz: 2, bar: 1, foo: 0}
h1 == h3 # => true

hash > other_hash → true or false

hashother_hash의 진상위집합(proper superset)이면 true를 돌려줘요.

h1 = {foo: 0, bar: 1, baz: 2}
h2 = {foo: 0, bar: 1}
h1 > h2 # => true
h2 > h1 # => false
h1 > h1 # => false

hash >= other_hash → true or false

hashother_hash의 상위집합이면 true를 돌려줘요.

h1 >= h2 # => true
h1 >= h1 # => true

hash[key] → value

주어진 키와 연결된 값을 돌려줘요. 키가 없으면 기본값을 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h[:foo] # => 0
h[:nosuch] # => nil

hash[key] = value → value · store(key, value)

주어진 키에 값을 연결하고 value를 돌려줘요. 키가 있으면 값을 교체(순서 유지), 없으면 마지막에 엔트리를 추가해요.

h = {foo: 0, bar: 1}
h[:foo] = 2 # => 2
h.store(:baz, 3)
h # => {:foo=>2, :bar=>1, :baz=>3}

any? → true or false

어떤 엔트리가 주어진 기준을 만족하면 true를 돌려줘요. 블록 없이 호출하면 비어 있지 않을 때 true예요.

h = {foo: 0, bar: 1, baz: 2}
h.any?([:bar, 1]) # => true
h.any? {|key, value| value < 3 } # => true
h.any? {|key, value| value > 3 } # => false

관련: Enumerable#any?

assoc(key) → new_array or nil

키를 찾으면 그 키와 값을 가진 2-요소 배열을, 못 찾으면 nil을 돌려줘요.

h.assoc(:bar) # => [:bar, 1]

clear → self

모든 엔트리를 지우고 self를 돌려줘요.

compact → new_hash

nil 값 엔트리를 모두 제거한 해시의 사본을 돌려줘요.

h = {foo: 0, bar: nil, baz: 2}
h.compact # => {:foo=>0, :baz=>2}

compact! → self or nil

제자리에서 nil 값 엔트리를 모두 제거해요. 제거된 게 없으면 nil을 돌려줘요.

compare_by_identity → self

키 비교에서 동일성(identity)만 고려하도록 설정해요. 같은 객체일 때만 같은 키로 취급돼요.

s0 = 'x'
s1 = 'x'
h = {}
h[s0] = 0
h[s1] = 1
h # => {"x"=>1}  # 기본은 같은 키
h.compare_by_identity # => {}
h[s0] = 0
h[s1] = 1
h # => {"x"=>0, "x"=>1}  # 이제 서로 다른 키

compare_by_identity? → true or false

compare_by_identity가 호출됐는지 여부를 돌려줘요.

default → object

기본값을 돌려줘요. 인자 없이 호출하면 현재 기본값, 키를 주면 키 존재와 무관하게 해당 키의 기본값(기본 proc 기준)을 돌려줘요.

h = Hash.new { |hash, key| hash[key] = "No key #{key}"}
h.default(:foo) # => "No key foo"

default = value → object

기본값을 value로 설정하고 돌려줘요.

default_proc → proc or nil

기본 proc을 돌려줘요 (없으면 nil).

default_proc = proc → proc

기본 proc을 설정해요. nil로도 설정 가능해요.

delete(key) → value or nil

주어진 키의 엔트리를 삭제하고 연결된 값을 돌려줘요. 블록을 주면 키가 없을 때 블록을 호출해 그 반환값을 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.delete(:bar) # => 1
h.delete(:nosuch) { |key| "Key #{key} not found" } # => "Key nosuch not found"

delete_if {|key, value| ... } → self

블록이 참을 돌려주는 엔트리를 모두 삭제하고 self를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.delete_if {|key, value| value > 0 } # => {:foo=>0}

dig(key, *identifiers) → object

중첩 객체에서 키·식별자로 지정된 객체를 찾아 돌려줘요.

h = {foo: {bar: {baz: 2}}}
h.dig(:foo, :bar, :baz) # => 2
h.dig(:foo, :bar, :BAZ) # => nil

h = {foo: {bar: [:a, :b, :c]}}
h.dig(:foo, :bar, 2) # => :c

each {|key, value| ... } → self · each_pair

각 키-값 쌍으로 블록을 호출하고 self를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.each_pair {|key, value| puts "#{key}: #{value}"}
# foo: 0
# bar: 1
# baz: 2

each_key {|key| ... } → self

각 키로 블록을 호출하고 self를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

each_value {|value| ... } → self

각 값으로 블록을 호출하고 self를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

empty? → true or false

엔트리가 없으면 true를 돌려줘요.

eql? object → true or false

==와 비슷하되 각 키에 대해 h[key] eql? object[key]인지에 따라 판정해요.

h1 = {foo: 0, bar: 1, baz: 2}
h2 = {baz: 2, bar: 1, foo: 0}
h1.eql? h2 # => true

except(*keys) → a_hash

주어진 키에 대한 엔트리를 제외한 새 해시를 돌려줘요. 없는 키는 무시돼요.

h = { a: 100, b: 200, c: 300 }
h.except(:a) #=> {:b=>200, :c=>300}

fetch(key) → object

주어진 키의 값을 돌려줘요. 키가 없으면 기본값(주어진 경우) 또는 블록 반환값을, 둘 다 없으면 KeyError를 던져요. default·default_proc 값은 사용하지 않아요.

{}.fetch(:nosuch, :default) # => :default
{}.fetch(:nosuch) {|key| "No key #{key}"} # => "No key nosuch"

fetch_values(*keys) → new_array

주어진 키들에 연결된 값들을 담은 새 배열을 돌려줘요. 블록을 주면 누락된 키마다 블록을 호출해 그 반환값을 값으로 삼아요.

h = {foo: 0, bar: 1, baz: 2}
h.fetch_values(:bar, :foo, :bad, :bam) {|key| key.to_s} # => [1, 0, "bad", "bam"]

filter · filter!

select, select!의 별칭이에요.

flatten → new_array

self를 1차원으로 만든 배열을 돌려줘요. 기본적으로 중첩 배열은 펴지지 않아요. level 인자를 주면 재귀 깊이를 제어해요.

h = {foo: 0, bar: [:bat, 3], baz: 2}
h.flatten # => [:foo, 0, :bar, [:bat, 3], :baz, 2]
h.flatten(2) # => [:foo, 0, :bar, :bat, [:baz, [:bat]]]
h.flatten(-1) # => 모든 중첩 배열을 폄
h.flatten(0) # => to_a 와 동일

has_key?(key) → true or false · key? · include? · member?

keyself의 키면 true를 돌려줘요.

has_value?(value) → true or false · value?

valueself의 값이면 true를 돌려줘요.

hash → an_integer

해시의 정수 해시코드를 돌려줘요. 내용이 같은 해시(순서 무관)는 해시코드도 같아요.

initialize_copy

내부용 메서드예요.

inspect → new_string

해시 엔트리를 담은 새 문자열을 돌려줘요.

h.inspect # => "{:foo=>0, :bar=>1, :baz=>2}"

invert → new_hash

각 키-값 쌍을 뒤집은 새 해시를 돌려줘요. 반복되는 새 키는 덮여써요.

h = {foo: 0, bar: 1, baz: 2}
h.invert # => {0=>:foo, 1=>:bar, 2=>:baz}
h = {foo: 0, bar: 0, baz: 0}
h.invert # => {0=>:baz}

keep_if {|key, value| ... } → self

블록이 참을 돌려주는 엔트리만 남기고 self를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.keep_if { |key, value| key.start_with?('b') } # => {:bar=>1, :baz=>2}

key(value) → key or nil

주어진 값과 일치하는 첫 번째 엔트리의 키를 돌려줘요. 없으면 nil.

h = {foo: 0, bar: 2, baz: 2}
h.key(2) # => :bar

keys → new_array

모든 키를 담은 새 배열을 돌려줘요.

h.keys # => [:foo, :bar, :baz]

length → integer · size

엔트리 개수를 돌려줘요.

{foo: 0, bar: 1, baz: 2}.length # => 3

merge → copy_of_self

주어진 해시들을 self의 사본에 병합한 새 해시를 돌려줘요. 새 키는 끝에 추가되고, 중복 키 값은 덮여써요. 블록을 주면 각 중복 키에 대해 키·옛 값·새 값을 인자로 호출하고 그 반환값이 새 값이 돼요.

h = {foo: 0, bar: 1, baz: 2}
h1 = {bat: 3, bar: 4}
h2 = {bam: 5, bat:6}
h.merge(h1, h2) # => {:foo=>0, :bar=>4, :baz=>2, :bat=>6, :bam=>5}
h3 = h.merge(h1, h2) { |key, old_value, new_value| old_value + new_value }
h3 # => {:foo=>0, :bar=>5, :baz=>2, :bat=>9, :bam=>5}

merge! → self · update

각 해시를 self 안으로 병합하고 self를 돌려줘요. 블록 동작은 merge와 같아요.

h = {foo: 0, bar: 1, baz: 2}
h.merge!(h1, h2) # => {:foo=>0, :bar=>4, :baz=>2, :bat=>6, :bam=>5}

rassoc(value) → new_array or nil

value==인 첫 번째 엔트리의 키와 값을 담은 2-요소 배열을 돌려줘요. 없으면 nil.

h = {foo: 0, bar: 1, baz: 1}
h.rassoc(1) # => [:bar, 1]

rehash → self

각 키의 해시 인덱스를 다시 계산해 해시 테이블을 재구성하고 self를 돌려줘요. 엔트리 생성 후 키의 해시 값이 바뀌면 해시 테이블이 유효하지 않게 되는데, 이때 사용해요.

reject {|key, value| ... } → new_hash

블록이 falsenil을 돌려주는 엔트리만 담은 새 해시를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.reject {|key, value| key.start_with?('b') } # => {:foo=>0}

reject! {|key, value| ... } → self or nil

제자리에서 제거하고 self를 돌려줘요. 제거된 게 없으면 nil. 블록이 없으면 새 Enumerator를 돌려줘요.

replace(other_hash) → self

self의 전체 내용을 other_hash의 내용으로 교체하고 self를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.replace({bat: 3, bam: 4}) # => {:bat=>3, :bam=>4}

select {|key, value| ... } → new_hash · filter

블록이 참을 돌려주는 엔트리만 담은 새 해시를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.select {|key, value| value < 2 } # => {:foo=>0, :bar=>1}

select! {|key, value| ... } → self or nil · filter!

제자리에서 걸러내고 self를 돌려줘요. 제거된 게 없으면 nil. 블록이 없으면 새 Enumerator를 돌려줘요.

shift → [key, value] or nil

첫 번째 엔트리를 제거하고 제거된 키와 값의 2-요소 배열을 돌려줘요. 비어 있으면 nil.

h = {foo: 0, bar: 1, baz: 2}
h.shift # => [:foo, 0]
h # => {:bar=>1, :baz=>2}

slice(*keys) → new_hash

주어진 키들에 대한 엔트리만 담은 새 해시를 돌려줘요. 없는 키는 무시돼요.

h = {foo: 0, bar: 1, baz: 2}
h.slice(:baz, :foo) # => {:baz=>2, :foo=>0}

to_a → new_array

각각 키-값 쌍을 담은 2-요소 배열들의 새 배열을 돌려줘요.

h.to_a # => [[:foo, 0], [:bar, 1], [:baz, 2]]

to_h → self or new_hash

Hash 인스턴스면 self를, Hash의 서브클래스면 새 Hash를 돌려줘요. 블록을 주면 키-값 쌍을 담은 2-요소 배열을 반환하도록 해 새 해시를 만들어요.

h = {foo: 0, bar: 1, baz: 2}
h.to_h {|key, value| [value, key] } # => {0=>:foo, 1=>:bar, 2=>:baz}

to_hash → self

self를 돌려줘요.

to_proc → proc

키를 값으로 매핑하는 Proc 객체를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
proc = h.to_proc
proc.call(:foo) # => 0
proc.call(:nosuch) # => nil

to_s

inspect의 별칭이에요.

transform_keys {|key| ... } → new_hash

블록이 만든 키와 self의 값으로 새 해시를 돌려줘요. 선택적 해시 인자로 키 매핑을 줄 수도 있어요. 중복 키는 값을 덮여써요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.transform_keys {|key| key.to_s } # => {"foo"=>0, "bar"=>1, "baz"=>2}
h.transform_keys(foo: :bar, bar: :foo) #=> {bar: 0, foo: 1, baz: 2}
h.transform_keys(foo: :hello, &:to_s) #=> {:hello=>0, "bar"=>1, "baz"=>2}

transform_keys! {|key| ... } → self

Hash#transform_keys와 같되 새 해시 대신 수신자(self)를 제자리에서 바꿔요.

transform_values {|value| ... } → new_hash

self의 키와 블록이 만든 값으로 새 해시를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.transform_values {|value| value * 100} # => {:foo=>0, :bar=>100, :baz=>200}

transform_values! {|value| ... } → self

키는 그대로 두고 블록이 만든 값으로 self를 바꿔요. 블록이 없으면 새 Enumerator를 돌려줘요.

h = {foo: 0, bar: 1, baz: 2}
h.transform_values! {|value| value * 100} # => {:foo=>0, :bar=>100, :baz=>200}

values → new_array

모든 값을 담은 새 배열을 돌려줘요.

h.values # => [0, 1, 2]

values_at(*keys) → new_array

주어진 키들에 대한 값들을 담은 새 배열을 돌려줘요. 없는 키에는 기본값이 돌아와요.

h = {foo: 0, bar: 1, baz: 2}
h.values_at(:baz, :foo) # => [2, 0]
h.values_at(:hello, :foo) # => [nil, 0]