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가 서로 같다는 뜻이에요. 해시 키가 될 수 있는 객체는 hash와 eql? 메서드를 구현해야 해요. 기본적으로는 거의 모든 객체가 키가 될 수 있지만, 같은 내용이라도 서로 다른 객체면 다른 키로 취급돼요.
가변 객체를 해시 키로 사용하는 경우 조심해야 해요. 가변 키를 수정하면 해시 인덱스가 손상될 수 있어요:
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)
해시 키로 쓰려면 객체가 hash와 eql? 메서드를 구현해야 해요. 단, 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
hash가 other_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
hash가 other_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
hash가 other_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
hash가 other_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?
key가 self의 키면 true를 돌려줘요.
has_value?(value) → true or false · value?
value가 self의 값이면 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
블록이 false나 nil을 돌려주는 엔트리만 담은 새 해시를 돌려줘요. 블록이 없으면 새 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]