Subclassing Ruby Hash, object has no methods of Hash?

I'm creating a object of hash in order to write a little script that reads in a file a line at a time, and assigns arrays into my hash class. I get wildly different results depending if I subclass Hash or not, plus using super changes things which I don't' understand.

My main issue is that without subclassing hash ( < Hash) it works perfectly, but I get no methods of Hash (like to iterate over the keys and get things out of it.... Subclassing Hash lets me do those things, but it seems that only the last element of the hashed arrays is ever stored.... so any insight into how you get the methods of a subclass. The Dictionary class is a great example I found on this site, and does exactly what I want, so I'm trying to understand how to use it properly.

filename = 'inputfile.txt.'

# ??? class Dictionary < Hash
class Dictionary
  def initialize()
    @data = Hash.new { |hash, key| hash[key] = [] }
  end
  def [](key)
    @data[key]
  end
  def []=(key,words)
    @data[key] += [words].flatten
    @data[key]
#    super(key,words)
  end
end


listData = Dictionary.new

File.open(filename, 'r').each_line do |line|
  line = line.strip.split(/[^[:alpha:]|@|.]/)
  puts "LIST-> #{line[0]}  SUB->  #{line[1]}  "
  listData[line[0]] = ("#{line[1]}")  
end

puts '====================================='
puts listData.inspect
puts '====================================='
print listData.reduce('') {|s, (k, v)|
  s << "The key is #{k} and the value is #{v}.n"
}

If anyone understands what is going on here subclassing hash, and has some pointers, that would be excellent.

Running without explicit < Hash:

./list.rb:34:in `<main>': undefined method `reduce' for #<Dictionary:0x007fcf0a8879e0> (NoMethodError)

That is the typical error I see when I try and iterate in any way over my hash.

Here is a sample input file:

listA   billg@microsoft.com
listA   ed@apple.com
listA   frank@lotus.com
listB   evanwhite@go.com
listB   joespink@go.com
listB   fredgrey@stop.com

I can't reproduce your problem using your code:

d = Dictionary.new               #=> #<Dictionary:0x007f903a1adef8 @data={}>
d[4] << 5                        #=> [5]
d[5] << 6                        #=> [6]
d                                #=> #<Dictionary:0x007f903a1adef8 @data={4=>[5], 5=>[6]}>
d.instance_variable_get(:@data)  #=> {4=>[5], 5=>[6]}

But of course you won't get reduce if you don't subclass or include a class/module that defines it, or define it yourself!

The way you have implemented Dictionary is bound to have problems. You should call super instead of reimplementing wherever possible. For example, simply this works:

class Dictionary < Hash
  def initialize
    super { |hash, key| hash[key] = [] }
  end
end

d = Dictionary.new  #=> {}
d['answer'] << 42   #=> [42]
d['pi'] << 3.14     #=> [3.14
d                   #=> {"answer"=>[42], "pi"=>[3.14]}

If you want to reimplement how and where the internal hash is stored (ie, using @data ), you'd have to reimplement at least each (since that is what almost all Enumerable methods call to) and getters/setters. Not worth the effort when you can just change one method instead.


While Andrew Marshall's answer already correct, You could also try this alternative below.

Going from your code, We could assume that you want to create an object that act like a Hash, but with a little bit different behaviour. Hence our first code will be like this.

class Dictionary < Hash

Assigning a new value to some key in the dictionary will be done differently in here. From your example above, the assignment won't replace the previous value with a new one, but instead push the new value to the previous or to a new array that initialized with the new value if the key doesn't exist yet.

Here I use the << operator as the shorthand of push method for Array. Also, the method return the value since it's what super do (see the if part)

  def []=(key, value)
    if self[key]
      self[key] << value
      return value # here we mimic what super do
    else
      super(key, [value])
    end
  end

The advantage of using our own class is we could add new method to the class and it will be accessible to all of it instance. Hence we need not to monkeypatch the Hash class that considered dangerous thing.

  def size_of(key)
    return self[key].size if self[key]
    return 0   # the case for non existing key
  end

Now, if we combine all above we will get this code

class Dictionary < Hash
  def []=(key, value)
    if self[key]
      self[key] << value
      return value
    else
      super(key, [value])
    end
  end

  def size_of(key)
    return self[key].size if self[key]
    return 0   # the case for non existing key
  end
end

player_emails = Dictionary.new

player_emails["SAO"] = "kirito@sao.com" # note no << operator needed here
player_emails["ALO"] = "lyfa@alo.com"
player_emails["SAO"] = "lizbeth@sao.com"
player_emails["SAO"] = "asuna@sao.com"

player_emails.size_of("SAO") #=> 3
player_emails.size_of("ALO") #=> 1
player_emails.size_of("GGO") #=> 0

p listData
#=> {"SAO" => ["kirito@sao.com", "lizbeth@sao.com", "asuna@sao.com"],
#=>  "ALO" => ["lyfa@alo.com"] }

But, surely, the class definition could be replaced with this single line

player_emails = Hash.new { [] }
# note that we wont use
#
#     player_emails[key] = value
#
# instead
#
#     player_emails[key] << value
#
# Oh, if you consider the comment,
# it will no longer considered a single line

While the answer are finished, I wanna comment some of your example code:

filename = 'inputfile.txt.'
# Maybe it's better to use ARGF instead,
# so you could supply the filename in the command line
# and, is the filename ended with a dot? O.o;

File.open(filename, 'r').each_line do |line|
# This line open the file anonimously,
# then access each line of the file.
# Please correct me, Is the file will properly closed? I doubt no.

# Saver version:
File.open(filename, 'r') do |file|
  file.each_line do |line|
    # ...
  end
end   # the file will closed when we reach here

# ARGF version:
ARGF.each_line do |line|
  # ...
end

# Inside the each_line block
line = line.strip.split(/[^[:alpha:]|@|.]/)
# I don't know what do you mean by that line,
# but using that regex will result 
#
#     ["listA", "", "", "billg@microsoft.com"]
#
# Hence, your example will fail since
# line[0] == "listA" and line[1] == ""
# also note that your regex mean
#
# any character except:
#   letters, '|', '@', '|', '.'
#
# If you want to split over one or more
# whitespace characters use s+ instead.
# Hence we could replace it with:
line = line.strip.split(/s+/)

puts "LIST-> #{line[0]} SUB-> #{line[1]}   "
# OK, Is this supposed to debug the line?
# Tips: the simplest way to debug is:
#
#     p line
#
# that's all,

listData[line[0]] = ("#{line[1]}")
# why? using (), then "", then #{}
# I suggest:
listData[line[0]] = line[1]

# But to make more simple, actually you could do this instead
key, value = line.strip.split(/s+/)
listData[key] = value

# Outside the block:
puts '====================================='
# OK, that's too loooooooooong...
puts '=' * 30
# or better assign it to a variable since you use it twice
a = '=' * 30
puts a
p listData # better way to debug
puts a

# next:
print listData.reduce('') { |s, (k, v)|
  s << "The key is #{k} and the value is #{v}.n"
}
# why using reduce?
# for debugging you could use `p listData` instead.
# but since you are printing it, why not iterate for
# each element then print each of that.
listData.each do |k, v|
  puts "The key is #{k} and the value is #{v}."
end

OK, sorry for blabbering so much, Hope it help.

链接地址: http://www.djcxy.com/p/60956.html

上一篇: 为什么Hash#select和Hash#reject将一个键传递给一元块?

下一篇: 子类化Ruby哈希,对象没有哈希方法?