Дивні екземпляри хешів в Ruby

4 хв. читання

Зауважте, що весь код запускався з Ruby MRI 2.4.1, тож не має гарантії, що його поведінка буде ідентичною для інших імплементацій (JRuby, mruby, тощо). Та і навряд ви захочете реалізувати щось подібне на реальному проекті.

Напевно вам, як і кожному користувачу Ruby, доводилося мати справу з вбудованим класом Hash. Я зробив можливим використання моїх власних класів як ключів, і знаючи про існування декількох методів визначення еквівалентності вирішив зробити власний "дивний екземпляр" класу Hash.

Дивний хеш №1: Неунікальні ключі

Що ви можете сказати про хеш, який дозволяє мати тільки одне значення для будь-якого ключа Integer, не дивлячись на його значення? У документації зазначено, що це просто, як зміна поведінки Integer#hash так, щоб він завжди повертав одне й те саме значення. Або ж так само як змусити Integer#eql? повертати true.

class Integer
   def eql?(other)
     true
   end

  def hash
     0
  end
end

table = {}
table[1] = 'one'
table[5] = 'five'
puts table

Результат:
#=> {1=>"one", 5=>"five"}

Таблиця мала два елементи з ключами [1] і [5] навіть я не очікував, що Hash зможе розрізнити ці ключі. Погравшись з деякими з цих модифікованих цілочисельних змінних, можна побачити як вони працюють:

irb(main)> 1 == 2
=> false
irb(main)> 1.eql? 2
=> true
irb(main)> 1.hash
=> 0
irb(main)> 2.hash
=> 0

Я знаю, що я перевизначив eql? та hash у моєму класі, і вони працювали так, як описано у документації Ruby. Але якщо модифікувати таким самим чином клас Array?

class Array
  def eql?(other)
    true
  end

  def hash
    0
  end
end

table = {}
table[[1]] = 'масив з 1'
table[[5]] = 'масив з 5'
puts table

Результат:
#=> {[1]=>"масив з 5"}

Обидва елементи [1] і [5] були оброблені начебто вони мають однаковий ключ. Значення першого елементу масиву table[[1]] = 'масив з 1' було перезаписане останнім table[[5]] = 'масив з 5'. Тож елемент масиву поверне останнє збережене у нього значення table з ключем.

irb(main)> table[[1]]
=> "масив з 5"
irb(main)> table[[5]]
=> "масив з 5"
irb(main)> table[['hello']]
=> "масив з 5"

Пошук істини

Тож чому модифікований клас Array працює по-іншому, у той час як Integer – ні? Деякі тести вказують на те, що інші класи такі, як Symbol і String також вперто продовжують оброблюватися як унікальні навіть після модифікації. Документація не має спеціальних пояснень як вони мають працювати, у випадку їх використання як ключів хешу.

Якщо зазирнути у вихідний код MRI то можна помітити дещо важливе у hash.c:

rb_any_cmp(VALUE a, VALUE b) {
  // ... скорочення
  if (FIXNUM_P(a) && FIXNUM_P(b)) {
    return a != b;
  }
  if (RB_TYPE_P(a, T_STRING) && RBASIC(a)->klass == rb_cString &&
      RB_TYPE_P(b, T_STRING) && RBASIC(b)->klass == rb_cString) {
    return rb_str_hash_cmp(a, b);
  }
  // ... скорочення
  if (SYMBOL_P(a) && SYMBOL_P(b)) {
    return a != b;
  }
  // ... скорочення

Безумовно, це виглядає як оптимізація обробки строкових, числових і символьних ключів. Також цей код схожий на функцію any_hash. Знаючи, що це спеціальні випадки, ви можете впоратися зі створенням "дивних" екземплярів хешу.

Дивний хеш №2: Дубльовані ключі

А що про збереження декількох значень з одним ключем? Якщо об'єкт повертає несумісні значення для hash, тоді його клас не буде розглядати їх як однакові об'єкти:

class Array
  @@last_id = 0

  def eql?(other)
    false
  end

  def hash
    @@last_id = @@last_id + 1
  end
end

table = {}
table[[0]] = 'масив з 0'
table[[0]] = 'ще один масив з 0'

Результат:
puts table
#=> {[0]=>"масив з 0", [0]=>"ще один масив з 0"}
puts table.keys
#=> [[0], [0]]

Масив table має декілька елементів з однаковими ключами:

У будь-якому випадку отримання значень збережених під одним ключем не дасть результатів:

table = {}
table[[0]] = 'масив з 0'

Результат:
puts table[[0]]
#=> nil

Дивний хеш №3: Тимчасові ключі

class Array
  def eql?(other)
    Time.now.to_i < (@@key_expires_at || 0)
  end

  def hash
    @@key_expires_at ||= Time.now.to_i + 3
  end
end

table = {}
table[['expires']] = 'це значення доступне лише 3 секунди'
puts table[['expires']]
Результат:
#=> 'це значення доступне лише 3 секунди'

Результат:
sleep(5)
puts table[['expires']]
#=> nil

Тепер масив може буди використаний для отримання значення з заданим періодом часу. А також, всі ключі масиву стали виглядати як ідентичні у першому прикладі.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 4.7K
Приєднався: 10 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація