Как уточнить, как выбрать ключ для хэша?

#ruby

Вопрос:

Итак, я пытаюсь исправить класс, чтобы он мог использоваться для хэш-ключа. Однако, похоже, я не могу заставить его работать. Вот пример тестового случая:

 class Tmp
 def hash; rand(); end
end

module Patch
 refine Tmp do
  def hash; 1; end
  def eql?(rhs); hash == rhs.hash; end
 end
end

puts "=========================="
module A
 # Using the initial implementation of Tmp
 x = {}
 x[Tmp.new] = 1
 x[Tmp.new] = 2
 puts "#{Tmp.new.hash == Tmp.new.hash}"
 puts "#{Tmp.new.eql? Tmp.new}"
 puts "#{x}"
end

puts "=========================="
module A
 # Enabling the refinement of class Tmp here
 using Patch
 x = {}
 x[Tmp.new] = 1
 x[Tmp.new] = 2
 puts "#{Tmp.new.hash == Tmp.new.hash}"
 puts "#{Tmp.new.eql? Tmp.new}"
 puts "#{x}"
end
 

Это дает результат:

 ==========================
false
false
{#<Tmp:0x0000407e4ae818>=>1, #<Tmp:0x0000407e4ae7a0>=>2}
==========================
true
true
{#<Tmp:0x0000407e4ae188>=>1, #<Tmp:0x0000407e4ae110>=>2}
 

Как вы можете видеть, методы #eql? и #hash работают так, как ожидалось, но когда я пытаюсь использовать их в качестве хэш-ключа, он думает, что это разные ключи. Что я делаю не так?

Редактировать

В качестве отступления, в документах говорится:

Два объекта ссылаются на один и тот же хэш-ключ, если их хэш-значение идентично и два объекта являются eql? друг другу.

Определенный пользователем класс может использоваться в качестве хэш-ключа, если хэш и eql? методы переопределяются, чтобы обеспечить осмысленное поведение.

(Emphasis mine)

It’s interesting that that only #eql? seems to be used to determine key equality:

 class Tmp
 def hash; rand; end
 def eql?(rhs); true; end
end

puts "=========================="
module A
 x = {}
 x[Tmp.new] = 1
 x[Tmp.new] = 2
 puts "#{Tmp.new.hash == Tmp.new.hash}"
 puts "#{Tmp.new.eql? Tmp.new}"
 puts "#{x}"
end
 

Дает:

 ==========================
false
true
{#<Tmp:0x0000407e407b08>=>2}
 

Возвращаемое значение #hash не имеет значения:

 class Tmp
 def hash; 1; end
 def eql?(rhs); false; end
end

puts "=========================="
module A
 x = {}
 x[Tmp.new] = 1
 x[Tmp.new] = 2
 puts "#{Tmp.new.hash == Tmp.new.hash}"
 puts "#{Tmp.new.eql? Tmp.new}"
 puts "#{x}"
end
 

Дает:

 ==========================
true
false
{#<Tmp:0x0000407e40e408>=>1, #<Tmp:0x0000407e40e3b8>=>2}
 

Что, похоже, противоречит документам. Является ли это артефактом использования ruby v2.2.4? (Я использую ruby, который поставляется с Sketchup 2017)

Комментарии:

1. Примечание: Наличие случайного hash значения может вызвать проблемы. Он должен возвращать одно и то же значение каждый раз, когда он вызывается для одного и того же объекта, хранящего одни и те же данные.

2. @tadman я использовал rand только для примера. Обычно я бы так не поступил.

3. Да, просто пытаюсь понять, в чем здесь смысл.

4. Прочитав это несколько раз, я все еще не уверен, в чем заключается вопрос. Этот последний пример работает так, как ожидалось, вы заставили их не быть eql? , поэтому вы получаете два отдельных ключа.

5. @tadman, да, но я ожидал бы, что 2-й последний пример также должен иметь те же результаты, что и последний пример, поскольку #hash не дает постоянного значения, даже если #eql? дает true .

Ответ №1:

Вы реализуете свой класс с явно непоследовательным поведением. Для любого объекта, где a.eql?(b) тогда по определению a.hash == b.hash as #hash должно отражать фактическое содержимое объекта. Если этого не произойдет, вы получите очень странные результаты.

Это похоже на то, как при написании компаратора сортировки, если a < b и b < c тогда a < c должно быть верно. Если по какой-то причине это не так, то ваши операции сортировки приведут к очень странным и противоречивым результатам.

Другими словами:

  • a.eql?(b) должно быть истинно тогда и только тогда, когда a и b представляет собой одно и то же.
  • a.hash == b.hash должно быть правдой, если a и b представлять одно и то же.

Обратите внимание, что a.hash == b.hash это не означает, что они обязательно одно и то же, но это указывает на высокую вероятность того, что это так. Для eql? подтверждения этого всегда необходима дополнительная проверка.

Внутренние хэш-функции могут вызывать или не вызывать обе эти функции. При небольших размерах рубиновый «Хэш» вообще не является хэш — таблицей, это просто крошечный массив. Как таковой, #hash может даже не вызываться до тех пор, пока таблица не достигнет определенного размера, и этот метод имеет значение.

Комментарии:

1.Вы, как видно, упускаете главное. В последней половине (после редактирования) были сделаны выводы, которые не соответствовали документам, а именно это #eql? и #hash должно быть правдой. Учитывая это, в обоих приведенных примерах каждый объект должен рассматриваться как отдельный ключ. Кроме того, это было в стороне. Как раз перед тем, как изменить формулировку фактического вопроса. Чтобы было понятно, почему уточнение используется не для ключа хэша, а при непосредственном использовании методов.

2. Я обновил свой вопрос, чтобы, надеюсь, подчеркнуть соответствующие моменты.

3. Я не уверен, что уточнения будут работать здесь, поскольку он Hash сам вызывает эти методы, что выходит за рамки уточнения.

4. Да, я только что это понял. Он using ограничен областью действия модуля или класса, в котором я его использую, и только с этого момента и до закрытия этого модуля или блока класса. Похоже, то же #to_s самое происходит и при автоматическом преобразовании в строку. Прискорбно. 🙁

5. Да, он чрезвычайно локализован по сути и не влияет на другие исходные файлы.