例如,
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的行为,仍然需要定义带有错误键的插入。
虽然我当然可以滚动自己的容器,但这样做很快,即每次调用[]时都不会搜索整个容器。只是想知道是否有人更聪明已经做了我可以偷的东西。
编辑:下面的一些好主意帮助我集中了我的要求:
避免O(n)查询时间
允许多个容器到同一个对象(关联而不是合成)
有不同的数据类型(例如可能使用name
而不是id
)而没有太多重新实现
答案 0 :(得分:2)
您可以自己实施。
查看解决方案草案:
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类(或哈希,如果你真的需要它):
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 :(得分:1)
更改密钥时必须通知容器,否则您必须在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"]]
答案 2 :(得分:1)
虽然Hash对象可能不会按照您希望的方式运行,但您始终可以自定义要插入的对象以特定方式进行哈希处理。
您可以通过向现有类添加两个新方法来实现此目的:
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
可能是一个非常糟糕的主意,因为它可能导致完全混乱和令人兴奋的错误。
答案 3 :(得分:1)
这是一个难题。数据库供应商可以赚钱,因为这是一个难题。您基本上希望实现传统的RDBMS 索引:搜索派生数据,以便快速查找从中派生的数据,同时允许更改数据。如果您想从多个线程访问数据,您将很快遇到所有难以使数据库符合ACID的问题。
我建议将数据放入数据库,添加必要的索引并让数据库(为此目的而优化的应用程序)完成工作。