Ruby Hash, чей ключ является функцией объекта? - PullRequest
3 голосов
/ 11 февраля 2012

Например,

s1 = Student.new(1, "Bob", "Podunk High")
hash[1] = s1
puts hash[1].name    #produces "Bob"
s1.id = 15
puts hash[15].name   #produces "Bob"
puts hash[1].name    #fails

Это не совсем Hash-подобное поведение, и вставки с неправильным ключом все еще должны быть определены.

Хотя я, конечно, могу свернуть свой собственный контейнер, который ведет себя таким образом, но будет трудно сделать это быстро, то есть не выполнять поиск по всему контейнеру каждый раз, когда вызывается [].Просто интересно, если кто-то умнее уже сделал что-то, что я могу украсть.

РЕДАКТИРОВАТЬ: Некоторые хорошие идеи ниже помогли мне сфокусировать мои требования:

  1. избежать поиска O (n)время

  2. разрешить нескольким контейнерам один и тот же объект (ассоциация, а не композиция)

  3. имеют разные типы данных (например, которые могут использовать nameвместо id) без переопределения

Ответы [ 4 ]

2 голосов
/ 11 февраля 2012

Вы можете реализовать его самостоятельно.

Посмотрите на черновое решение:

class Campus
  attr_reader :students
  def initialize
    @students = []
  end

  def [](ind)
    students.detect{|s| s.id == ind}
  end

  def <<(st)
    raise "Yarrr, not a student"   if st.class != Student
    raise "We already have got one with id #{st.id}" if self[st.id]
    students << st
  end
end

class Student
  attr_accessor :id, :name, :prop
  def initialize(id, name, prop)
    @id, @name, @prop = id, name, prop
  end
end

campus = Campus.new
st1 = Student.new(1, "Pedro", "Math")
st2 = Student.new(2, "Maria", "Opera")
campus << st1
campus << st2
campus[1]
#=> Student...id:1,name:pedro...
campus[2].name
#=> Maria
campus[2].id = 10
campus[2]
#=> error
campus[10].name
#=> Maria

Или вы можете поиграть с классом Array (или Hash, если он вам действительно нужен):

class StrangeArray < Array
  def [](ind)
    self.detect{|v| v.id == ind} || raise "nothing found" # if you really need to raise an error
  end

  def <<(st)
    raise "Looks like a duplicate" if self[st.id]
    self.push(st)
  end
end

campus = StrangeArray.new
campus << Student.new(15, 'Michael', 'Music')
campus << Student.new(40, 'Lisa', 'Medicine')
campus[1]
#=> error 'not found'
campus[15].prop
#=> Music
campus[15].id = 20
campus[20].prop
#=> Music

и т. Д.

И после правильного комментария @ tadman вы можете использовать ссылку на ваш hash прямо в классе ученика:

class Student
  attr_accessor :name, :prop
  attr_reader :id, :campus
  def initialize(id, name, prop, camp=nil)
    @id, @name, @prop = id, name, prop
    self.campus = camp if camp
  end

  def id=(new_id)
    if campus
      rase "this id is already taken in campus" if campus[new_id]
      campus.delete id
      campus[new_id] = self
    end
    @id = new_id
  end

  def campus=(camp)
    rase "this id is already taken in campus" if camp[id]
    @campus = camp
    camp[@id] = self
  end
end

campus = {}
st1 = Student.new(1, "John", "Math")
st2 = Student.new(2, "Lisa", "Math", campus)
# so now in campus is only Lisa
st1.campus = campus
# we've just pushed John in campus
campus[1].name
#=> John
campus[1].id = 10
campus[10].name
#=> John
1 голос
/ 11 февраля 2012

Это сложная проблема. Продавцы баз данных могут зарабатывать деньги, потому что это сложная проблема. В основном вы ищете реализацию традиционных СУБД indexes : поиск по производным данным, чтобы обеспечить быстрый поиск данных, из которых они были получены, и в то же время позволить этим данным изменяться. Если вы хотите получить доступ к данным из нескольких потоков, вы быстро столкнетесь со всеми проблемами, которые затрудняют совместимость базы данных с ACID.

Я предлагаю поместить данные в базу данных, добавить необходимые индексы и позволить базе данных - приложению, оптимизированному для этой конкретной цели - сделать работу.

1 голос
/ 11 февраля 2012

Хотя объект Hash может вести себя не так, как вы этого хотите, вы всегда можете настроить вставляемые объекты так, чтобы они хэшировались определенным образом.

Это можно сделать, добавив два новых метода в существующий.class:

class Student
  def hash
    self.id
  end

  def eql?(student)
    self.id == student.id
  end
end

Определив hash для возврата значения, основанного на id, Hash рассмотрит эти два кандидата на одно и то же место в хэше.Второе определение объявляет «эквивалентность хеша» между любыми двумя объектами, имеющими одинаковое значение хеш-функции.

Это будет работать хорошо, если ваши значения id вписываются в обычный 32-разрядный Fixnum и не являются 64-разряднымиЗначения базы данных BIGINT.

Как указывает fl00r, это будет работать, только если ваш id неизменный.Для большинства баз данных это имеет место.Изменение id на лету, вероятно, является действительно плохой идеей, поскольку оно может привести к полному хаосу и сногсшибательным ошибкам.

1 голос
/ 11 февраля 2012

Контейнер должен быть уведомлен, когда ваш ключ был изменен, в противном случае вы должны искать ключ на лету в lg(n).

Если вы редко меняете ключ и ищите много, просто пересоберите хеш:

def build_hash_on_attribute(objects, attribute)
  Hash[objects.collect { |e| [e.send(method), e] }]
end

s1 = OpenStruct.new id: 1, name: 's1'

h = build_hash_on_attribute([s1], :id)
h[1].name # => 's1'

h[1].id = 15
# rebuild the whole index after any key attribute has been changed
h = build_hash_on_attribute(h.values, :id)
h[1] # => nil
h[15].name # => 's1'

Обновление 02/12 : Добавить решение, используя шаблон наблюдателя

Или вам нужно такое автоматическое построение индекса, вы можете использовать шаблон наблюдателя, как показано ниже, или шаблон декоратора. Но вам нужно использовать обернутые объекты в шаблоне декоратора.

Суть: https://gist.github.com/1807324

module AttrChangeEmitter
  def self.included(base)
    base.extend ClassMethods
    base.send :include, InstanceMethods
  end

  module ClassMethods
    def attr_change_emitter(*attrs)
      attrs.each do |attr|
        class_eval do
          alias_method "#{attr}_without_emitter=", "#{attr}="
          define_method "#{attr}_with_emitter=" do |v|
            previous_value = send("#{attr}")
            send "#{attr}_without_emitter=", v
            attr_change_listeners_on(attr).each do |listener|
              listener.call self, previous_value, v
            end
          end
          alias_method "#{attr}=", "#{attr}_with_emitter="
        end
      end
    end
  end

  module InstanceMethods
    def attr_change_listeners_on(attr)
      @attr_change_listeners_on ||= {}
      @attr_change_listeners_on[attr.to_sym] ||= []
    end

    def add_attr_change_listener_on(attr, block)
      listeners = attr_change_listeners_on(attr)
      listeners << block unless listeners.include?(block)
    end

    def remove_attr_change_listener_on(attr, block)
      attr_change_listeners_on(attr).delete block
    end
  end
end

class AttrChangeAwareHash
  include Enumerable

  def initialize(attr = :id)
    @attr = attr.to_sym
    @hash = {}
  end

  def each(&block)
    @hash.values.each(&block)
  end

  def on_entity_attr_change(e, previous_value, new_value)
    if @hash[previous_value].equal? e
      @hash.delete(previous_value)
      # remove the original one in slot new_value
      delete_by_key(new_value)
      @hash[new_value] = e
    end
  end

  def add(v)
    delete(v)
    v.add_attr_change_listener_on(@attr, self.method(:on_entity_attr_change))
    k = v.send(@attr)
    @hash[k] = v
  end

  alias_method :<<, :add

  def delete(v)
    k = v.send(@attr)
    delete_by_key(k) if @hash[k].equal?(v)
  end

  def delete_by_key(k)
    v = @hash.delete(k)
    v.remove_attr_change_listener_on(@attr, self.method(:on_entity_attr_change)) if v
    v
  end

  def [](k)
    @hash[k]
  end
end

class Student
  include AttrChangeEmitter
  attr_accessor :id, :name
  attr_change_emitter :id, :name

  def initialize(id, name)
    self.id = id
    self.name = name
  end
end

indexByIDA = AttrChangeAwareHash.new(:id)
indexByIDB = AttrChangeAwareHash.new(:id)
indexByName = AttrChangeAwareHash.new(:name)

s1 = Student.new(1, 'John')
s2 = Student.new(2, 'Bill')
s3 = Student.new(3, 'Kate')

indexByIDA << s1
indexByIDA << s3

indexByIDB << s1
indexByIDB << s2

indexByName << s1
indexByName << s2
indexByName << s3

puts indexByIDA[1].name # => John
puts indexByIDB[2].name # => Bill
puts indexByName['John'].id # => 1

s2.id = 15
s2.name = 'Batman'

p indexByIDB[2] # => nil
puts indexByIDB[15].name # => Batman

indexByName.each do |v|
  v.name = v.name.downcase
end

p indexByName['John'] # => nil
puts indexByName['john'].id # => 1

p indexByName.collect { |v| [v.id, v.name] }
# => [[1, "john"], [3, "kate"], [15, "batman"]]

indexByName.delete_by_key 'john'
indexByName.delete(s2)

s2.id = 1 # set batman id to 1 to overwrite john
p indexByIDB.collect { |v| [v.id, v.name] }
# => [[1, "batman"]]

p indexByName.collect { |v| [v.id, v.name] }
# => [[3, "kate"]]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...