Hash 기본값 (예 : Hash.new ([]))을 사용할 때 이상하고 예상치 못한 동작 (값 사라짐 / 변경)


107

이 코드를 고려하십시오.

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

괜찮습니다.하지만 :

h = Hash.new([])  # Empty array as default value
h[1] <<= 1  #=> {1=>[1]}                  ← Ok
h[2] <<= 2  #=> {1=>[1,2], 2=>[1,2]}      ← Why did `1` change?
h[3] << 3   #=> {1=>[1,2,3], 2=>[1,2,3]}  ← Where is `3`?

이 시점에서 해시가 다음과 같을 것으로 예상합니다.

{1=>[1], 2=>[2], 3=>[3]}

하지만 그것과는 거리가 멀다. 무슨 일이 일어나고 있고 내가 기대하는 행동을 어떻게 얻을 수 있습니까?

답변:


164

먼저이 동작은 배열뿐만 아니라 이후에 변경되는 모든 기본값 (예 : 해시 및 문자열)에 적용됩니다.

TL; DR : Hash.new { |h, k| h[k] = [] }가장 관용적 인 솔루션을 원하고 이유는 신경 쓰지 않는 경우 사용하십시오 .


작동하지 않는 것

Hash.new([])작동하지 않는 이유

Hash.new([])작동하지 않는지 자세히 살펴 보겠습니다 .

h = Hash.new([])
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["a", "b"]
h[1]         #=> ["a", "b"]

h[0].object_id == h[1].object_id  #=> true
h  #=> {}

기본 객체가 재사용되고 변경되는 것을 볼 수 있습니다 (이는 유일한 기본값으로 전달되기 때문입니다. 해시는 새 기본값을 가져올 방법이 없기 때문입니다). 그러나 키나 값이없는 이유는 무엇입니까? h[1]여전히 우리에게 가치를 주면서도 배열에서 ? 다음은 힌트입니다.

h[42]  #=> ["a", "b"]

[]호출에 의해 반환 된 배열 은 기본값 일 뿐이며, 이번에는 변경해 왔으므로 이제 새 값을 포함합니다. <<해시에 할당하지 않기 때문에 ( =현재 없이 루비에서 할당 할 수 없습니다 ), 실제 해시에 아무것도 넣지 않았습니다. 대신 우리는 사용이 <<=(이다 <<+=이다 +) :

h[2] <<= 'c'  #=> ["a", "b", "c"]
h             #=> {2=>["a", "b", "c"]}

이것은 다음과 같습니다.

h[2] = (h[2] << 'c')

Hash.new { [] }작동하지 않는 이유

using Hash.new { [] }은 원래 기본값을 재사용하고 변경하는 문제를 해결하지만 (주어진 블록이 매번 호출되어 새 배열을 반환하므로) 할당 문제는 해결되지 않습니다.

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}

작동하는 것

할당 방법

우리가 항상 사용하는 기억한다면 <<=, 다음 Hash.new { [] } 입니다 가능한 솔루션,하지만 조금 이상한와 (내가 본 적이 비 관용적의 <<=야생에서 사용되지 않습니다). 또한 <<실수로 사용 하면 미묘한 버그가 발생하기 쉽습니다 .

변경 가능한 방법

상태에 대한 문서Hash.new (내 자신을 강조) :

블록이 지정되면 해시 개체와 키로 호출되며 기본값을 반환해야합니다. 필요한 경우 해시에 값을 저장하는 것은 블록의 책임 입니다.

따라서 다음 <<대신 사용하려면 블록 내에서 해시에 기본값을 저장해야합니다 <<=.

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["b"]
h            #=> {0=>["a"], 1=>["b"]}

이렇게하면 개별 호출 (사용 <<=)에서에 전달 된 블록으로 할당을 효과적으로 이동 Hash.new하여 <<.

이 방법과 다른 방법 사이에는 한 가지 기능적 차이가 있습니다.이 방법은 읽을 때 기본값을 할당합니다 (할당은 항상 블록 내부에서 발생하기 때문). 예를 들면 :

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1  #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2  #=> {}

불변의 방법

Hash.new([])잘 작동하는 동안 왜 작동하지 않는지 궁금 할 것 Hash.new(0)입니다. 핵심은 Ruby의 Numerics는 불변이므로 자연스럽게 제자리에서 변경하지 않습니다. 기본값을 불변으로 취급하면 Hash.new([])잘 사용할 수 있습니다 .

h = Hash.new([].freeze)
h[0] += ['a']  #=> ["a"]
h[1] += ['b']  #=> ["b"]
h[2]           #=> []
h              #=> {0=>["a"], 1=>["b"]}

그러나 ([].freeze + [].freeze).frozen? == false. 따라서 불변성이 전체적으로 유지되도록하려면 새 개체를 다시 고정하도록주의해야합니다.


결론

모든 방법 중에서 개인적으로 "불변하는 방법"을 선호합니다. 일반적으로 불변성은 사물에 대한 추론을 훨씬 더 간단하게 만듭니다. 결국 숨겨 지거나 미묘한 예기치 않은 동작의 가능성이없는 유일한 방법입니다. 그러나 가장 일반적이고 관용적 인 방법은 "변동 가능한 방법"입니다.

마지막으로, Hash 기본값의 이러한 동작은 Ruby Koans에 기록되어 있습니다.


이것은 엄격히 사실이 아닙니다. instance_variable_set우회 와 같은 메소드 는 l- 값 =이 동적 일 수 없기 때문에 메타 프로그래밍을 위해 존재해야합니다 .


1
"변경 가능한 방법"을 사용하면 모든 해시 조회가 키 값 쌍을 저장하도록하는 효과가 있으며 (블록에서 할당이 발생하기 때문에) 항상 바람직하지는 않습니다.
johncip

@johncip 모든 조회가 아니라 각 키에 대한 첫 번째 조회입니다. 그러나 나는 당신이 의미하는 바를 알았습니다. 나중에 대답에 추가하겠습니다. 감사!.
Andrew Marshall

엉뚱한 데. 물론 알 수없는 키의 첫 번째 조회입니다. 나는 거의 같은 느낌 { [] }으로 <<=는 실수를 잊고 것은 사실이 아니었다, 가장 적은 놀라움을 가지고 =매우 혼란 디버깅 세션으로 이어질 수 있습니다.
johncip 2015-06-28

차이점에 대한 아주 명확한 설명 기본 값으로 해시 초기화
cisolarix

23

해시의 기본값이 해당 특정 (처음에는 비어있는) 배열에 대한 참조임을 지정합니다.

나는 당신이 원한다고 생각합니다 :

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

그러면 각 키의 기본값이 배열로 설정됩니다.


새 해시마다 별도의 배열 인스턴스를 어떻게 사용할 수 있습니까?
Valentin Vasilyev

5
해당 블록 버전은 Array각 호출에서 새 인스턴스를 제공합니다 . 재치 : h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. 또한 : 당신이하는 블록 버전을 사용하는 경우 설정 값 ( {|hash,key| hash[key] = []})보다는 단순히 하나 생성 값이 ( { [] }), 당신은 필요 <<하지 <<=요소를 추가 할 때.
James A. Rosen

3

+=해당 해시에 적용될 때 연산자 는 예상대로 작동합니다.

[1] pry(main)> foo = Hash.new( [] )
=> {}
[2] pry(main)> foo[1]+=[1]
=> [1]
[3] pry(main)> foo[2]+=[2]
=> [2]
[4] pry(main)> foo
=> {1=>[1], 2=>[2]}
[5] pry(main)> bar = Hash.new { [] }
=> {}
[6] pry(main)> bar[1]+=[1]
=> [1]
[7] pry(main)> bar[2]+=[2]
=> [2]
[8] pry(main)> bar
=> {1=>[1], 2=>[2]}

이 될 수 있기 때문 foo[bar]+=baz에 대한 문법 설탕입니다 의 오른쪽에 가 반환 평가 기본값 객체와 이를 변경하지 않습니다 연산자. 왼손은 기본값을 변경하지 않는 메서드의 구문 설탕입니다 .foo[bar]=foo[bar]+bazfoo[bar]=+[]=

이는에 적용되지 않으며 기본값foo[bar]<<=baz동일하며 변경 될 것 입니다.foo[bar]=foo[bar]<<baz<<

또한, 나는 사이에 아무런 차이를 발견 Hash.new{[]}하고 Hash.new{|hash, key| hash[key]=[];}. 적어도 루비 2.1.2에서.


좋은 설명입니다. 그것은 루비 2.1.1에 보인다 Hash.new{[]}과 동일 Hash.new([])예상의 부족 나를 위해 <<(비록 코스의 행동 Hash.new{|hash, key| hash[key]=[];}작품). 모든 것을 깨는 이상한 작은 것들 : /
butterywombat

1

당신이 쓸 때

h = Hash.new([])

배열의 기본 참조를 해시의 모든 요소에 전달합니다. 해시의 모든 요소가 동일한 배열을 참조하기 때문입니다.

해시의 각 요소가 별도의 배열을 참조하려면 다음을 사용해야합니다.

h = Hash.new{[]} 

루비에서 작동하는 방법에 대한 자세한 내용은 http://ruby-doc.org/core-2.2.0/Array.html#method-c-new 를 참조하십시오.


이것은 잘못되어 작동 Hash.new { [] }하지 않습니다 . 자세한 내용은 내 대답 을 참조하십시오. 이미 다른 답변에서 제안 된 솔루션입니다.
Andrew Marshall
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.