【问题标题】:Ruby Hash whose key is a function of the object?Ruby Hash,其键是对象的函数?
【发布时间】:2012-02-11 07:23:18
【问题描述】:

例如,

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)而无需过多的重新实现

【问题讨论】:

    标签: ruby hash


    【解决方案1】:

    你可以自己实现。

    查看解决方案草案:

    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
    

    【讨论】:

    • 我不确定在寻找匹配项时使用 detect 遍历所有值是解决此问题的一种特别精明的方法。
    • 哦,这就是我们在这里需要哈希的原因。知道了。但问题是当确切对象发生变化等时,hee 需要它动态变化。所以它更复杂,因为每个Stdent 都应该知道他的校园以告诉他id is changed。所以detect 只是最简单的解决方案,但不是最好的
    • 这很好,但希望id 是一个不可变的数据库属性。我添加了一个针对该解决方案进行了优化的答案。
    • 为什么你认为它是不可变的,为什么你认为它是关于数据库的?就在问题作者更改 id :)
    • 无论如何我已经有了一个“更好”版本的楼层答案,它允许您在容器创建时传递一个块而不是硬编码到{|s| s.id == ind},这样我就可以将它用于非学生(我将接受您的回答,因此您可以根据需要为后代更新)。我只是想看看有没有更好的方法,没有抱太大希望。
    【解决方案2】:

    虽然 Hash 对象的行为方式可能不符合您的要求,但您始终可以自定义要插入的对象以特定方式进行哈希处理。

    您可以通过向现有类添加两个新方法来做到这一点:

    class Student
      def hash
        self.id
      end
    
      def eql?(student)
        self.id == student.id
      end
    end
    

    通过定义hash 以返回基于id 的值,哈希将考虑这两个候选对象在哈希中的相同位置。第二个定义声明了具有相同哈希值的任意两个对象之间的“哈希等价”。

    如果您的 id 值适合传统的 32 位 Fixnum 并且不是 64 位 BIGINT 数据库值,这将很好地工作。

    正如 fl00r 所指出的,这仅在您的 id 不可变时才有效。对于大多数数据库来说,情况往往如此。不过,即时更改id 可能是一个非常糟糕的主意,因为它会导致完全混乱和令人震惊的错误。

    【讨论】:

    • 我责怪我选择不当的例子。散列并不意味着是数据库表,也不需要键入唯一标识符。另一个可能是nameseat_number,它们可能会不断变化。
    • 有大量可用的内存数据库。我在一个项目中使用了 Apache Derby 来实现类似的目的。
    【解决方案3】:

    这是一个难题。数据库供应商可以赚钱,因为这是一个难题。您基本上是在寻求实现传统的 RDBMS 索引:搜索派生数据,以提供对派生数据的快速查找,同时允许更改该数据。如果您想从多个线程访问数据,您将很快遇到使数据库难以符合 ACID 的所有问题。

    我建议将数据放入数据库,添加必要的索引,然后让数据库(针对此目的优化的应用程序)完成工作。

    【讨论】:

    • 既然你已经指出了它似乎确实比我自己想解决的要困难得多。我可能会坚持糟糕的 O(n) 解决方案。对于这个内存应用程序来说,使用数据库太慢了。
    【解决方案4】:

    当您的密钥已更改时,必须通知容器,否则您必须在 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"]]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-23
      • 2014-10-13
      • 2011-10-12
      • 1970-01-01
      • 2011-06-16
      • 1970-01-01
      相关资源
      最近更新 更多