У меня есть этот массив в консоли ruby 1.8.6:
arr = [{:foo => "bar"}, {:foo => "bar"}]
оба элемента равны друг другу:
arr[0] == arr[1]
=> true
#just in case there's some "==" vs "===" oddness...
arr[0] === arr[1]
=> true
Но arr.uniq не удаляет дубликаты:
arr.uniq
=> [{:foo=>"bar"}, {:foo=>"bar"}]
Кто-нибудь может сказать мне, что здесь происходит?
РЕДАКТИРОВАТЬ: я могу написать не очень умный uniqifier, который использует include?
следующим образом:
uniqed = []
arr.each do |hash|
unless uniqed.include?(hash)
uniqed << hash
end
end;false
uniqed
=> [{:foo=>"bar"}]
Это дает правильный результат, что делает отказ uniq
еще более загадочным.
РЕДАКТИРОВАТЬ 2: Некоторые заметки о том, что происходит, возможно, просто для моей ясности. Как указывает @Ajedi32 в комментариях, отказ от uniqify происходит из-за того, что два элемента являются разными объектами. Некоторые классы определяют методы eql?
и hash
, используемые для сравнения, означающие, что "действительно это одно и то же, даже если они не являются одним и тем же объектом в памяти". Например, это делает String, поэтому вы можете определить две переменные как «foo», и говорят, что они равны друг другу, даже если они не являются одним и тем же объектом.
Класс Hash не делает этого в Ruby 1.8.6, поэтому, когда .eql?
и .hash
вызываются для хэш-объекта (метод .hash не имеет ничего общего с типом данных Hash — он как хеш контрольной суммы) он возвращается к использованию методов, определенных в базовом классе Object, которые просто говорят: «Это тот же самый объект в памяти».
Операторы ==
и ===
для хеш-объектов уже делают то, что я хочу, то есть говорят, что два хэша одинаковы, если их содержимое одинаково. Я переопределил Hash#eql?
, чтобы использовать их, например:
class Hash
def eql?(other_hash)
self == other_hash
end
end
Но я не знаю, как обращаться с Hash#hash
: то есть я не знаю, как сгенерировать контрольную сумму, которая будет одинаковой для двух хэшей с одинаковым содержимым и всегда разной для двух хэшей с разным содержимым.
@Ajedi32 предложил мне взглянуть на реализацию метода Hash#hash
Рубиниусом здесь https://github.com/rubinius/rubinius/blob/master/core/hash.rb#L589 , и моя версия реализации Rubinius выглядит так:
class Hash
def hash
result = self.size
self.each do |key,value|
result ^= key.hash
result ^= value.hash
end
return result
end
end
и это, похоже, работает, хотя я не знаю, что делает оператор "^=", что меня немного нервирует. Кроме того, он очень медленный — примерно в 50 раз медленнее на основе некоторых примитивных тестов. Это может сделать его слишком медленным в использовании.
РЕДАКТИРОВАТЬ 3: Небольшое исследование показало, что "^" является оператором побитового исключающего ИЛИ. Когда у нас есть два входа, XOR возвращает 1, если входы разные (т.е. он возвращает 0 для 0,0 и 1,1 и 1 для 0,1 и 1,0).
Итак, сначала я подумал, что это означает, что
result ^= key.hash
является сокращением для
result = result ^ key.hash
Другими словами, выполните XOR между текущим значением результата и другим значением, а затем сохраните его в результате. Я все еще не совсем понимаю логику этого, хотя. Я подумал, что, возможно, оператор ^ как-то связан с указателями, потому что его вызов для переменных работает, а вызов для значения переменной не работает: например
var = 1
=> 1
var ^= :foo
=> 14904
1 ^= :foo
SyntaxError: compile error
(irb):11: syntax error, unexpected tOP_ASGN, expecting $end
Итак, это нормально с вызовом ^= для переменной, но не для значения переменной, что заставило меня подумать, что это как-то связано со ссылкой/разыменованием.
Более поздние реализации Ruby также имеют код C для метода Hash#hash, и реализация Rubinius кажется слишком медленной. Немного застрял...
1.8.6
снова был выпущен более 10 лет назад, 9 лет назад была выпущена обновленная1.8.7
версия, а все1.8.x
версии достигли конца жизни более 4 лет назад. Почему тебя это вообще волнует? - person spickermann   schedule 14.11.2017