Ruby - Setter methods for hash properties

2019-07-29 07:36发布

问题:

I've been tooling around with Ruby by converting a pen and paper RPG to a script.

Right now I have a character's stats kept in a hash, which I would like to be able to set via public method. I got that working with:

class Character
    attr_reader :str, :con, :dex, :wis, :int, :level, :mods, :stats
    def initialize str, con, dex, wis, int, cha, level = 1
        @stats = { :str => str, :con => con, :dex => dex, :wis => wis, :int => int, :cha => cha }
        @mods = {}
        @level = level

        @stats.each_pair do |key, value|
            @mods[key] = ((value / 2 ) -5).floor
        end
    end

    def []=(index, value)
        @stats[index] = value
    end
end

This allows me to instantiate a new character, then update @stats by running newChar.stats[:str] = 12

However, I also seem to be able to modify the @mods using this method as well, which is undesirable. newChar.mods[:str] = 15 will successfully alter the @mods hash, which from my understanding should not be possible with the current setter method.

On a slightly separate note, the iterator I'm using to create my @mods hash seems clunky but I haven't found anything better to accomplish the task.

回答1:

You did not even call your []= method in your example. This would be done like so:

newChar[:str] = 123

instead of

newChar.stats[:str] = 123

so to call newChar.stats[:str] = 123 you do not even need you method definition. The reason is that newChar.stats as well as newChar.mods will both return the actual hash which can then be altered.

One possible workaround is to freeze the @mods variable so it can't be altered any more:

def initialize str, con, dex, wis, int, cha, level = 1
    # omitted ...

    @stats.each_pair do |key, value|
        @mods[key] = ((value / 2 ) -5).floor
    end

    @mods.freeze
end

This is a good solution if you never want to be able to change @mods again. Trying to set a value will result in an error:

newChar.mods[:con] = 123
# RuntimeError: can't modify frozen Hash

Inside your class, you can, however, overwrite @mods entirely.

To summarize, the full class would be:

class Character
    attr_reader :str, :con, :dex, :wis, :int, :level, :mods, :stats
    def initialize str, con, dex, wis, int, cha, level = 1
        @stats = { :str => str, :con => con, :dex => dex, :wis => wis, :int => int, :cha => cha }
        @mods = {}
        @level = level

        @stats.each_pair do |key, value|
            @mods[key] = ((value / 2 ) -5).floor
        end

        @mods.freeze
    end
end


回答2:

If you need a public getter for a hash but you don't want the user to modify the hash –the instance-variable of your class–, you can do it with dup.

class MyClass
  ....
  def my_hash
    @my_hash.dup
  end
end

Where the solution with freeze, as mentioned above, will freeze the hash even for your class, the .dup-solution will let you modify the hash from within your class but not from outside.