如何动态定义哈希中所有键的 setter 方法

How to define setter methods for all keys in a hash dynamically

提问人:PKP 提问时间:1/4/2023 最后编辑:PKP 更新时间:1/5/2023 访问量:118

问:

我正在编写一个具有哈希数据成员的类。哈希被初始化为在构造函数中具有一组具有默认值的键。

然后,如何生成与哈希中所有键相对应的 setter 方法?

请考虑以下示例:

class MyClass
    def initialize
        @hash = {
            "key1" => "N/A",
            "key2" => "N/A",
            "key3" => "N/A",
        }
    end
end

c = MyClass.new
c.key1 = "value1" # <--- How do I make something like this possible?

我想象使用方法,然后使用..each:define_method

我看过这个问题这个问题,但我无法从他们那里推断出我的问题的解决方案。

Ruby 方法 构造函数 HashTable

评论

0赞 steenslag 1/4/2023
怎么样 ?然后你可以做MyClass = Struct.new(:key1, :key2, :key3)c = MyClass.new; c.key1 = "value1"
0赞 PKP 1/4/2023
我想保留哈希数据成员。这样,它就可以转换为具有特定键名称的 json 对象。我需要它服从其他地方的特定接口。
0赞 steenslag 1/4/2023
FWIW 将返回哈希值。如果你,你也会有一个直接的方法。c.to_hrequire 'json'c.to_json
0赞 PKP 1/4/2023
明白了。我可能会考虑这是否是一个更好的方法。正如你可能猜到的那样,我对 Ruby 很陌生。感谢您的指导。

答:

2赞 Konstantin Strukov 1/4/2023 #1

有几种方法可以解决问题,但利弊各异。

  1. 您可以按照您的建议使用,(几乎)(此解决方案有一个严重的缺点,我稍后会回到它):define_singleton_method
class MyClass
  def initialize
    @hash = {
        "key1" => "N/A",
        "key2" => "N/A",
        "key3" => "N/A",
    }

    @hash.each do |key, value|
      self.define_singleton_method "#{key}=" do |value|
        @hash[key] = value
      end
    end
  end
end

c = MyClass.new #=> #<MyClass:0x00000001451b76c8 @hash={"key1"=>"N/A", "key2"=>"N/A", "key3"=>"N/A"}>
c.key1 = "foo"
c #=> #<MyClass:0x00000001451b76c8 @hash={"key1"=>"foo", "key2"=>"N/A", "key3"=>"N/A"}>

这种方法的最大优点是它的显式性 - 我们确实创建了“真实”实例方法。

这种方法的最大缺点是它只为初始键集创建 setter。如果稍后添加密钥,则不会有它们的 setter。如果稍后删除密钥,则仍会定义 setter,并在调用时以静默方式重新添加密钥(这可能不是您所期望的)。

  1. 解决问题的另一种方法是使用:method_missing
class MyClass
  def initialize
    @hash = {
        "key1" => "N/A",
        "key2" => "N/A",
        "key3" => "N/A",
    }
  end

  def method_missing(name, *args)
    if name.end_with?("=") && @hash.key?(name[0..-2])
      @hash[name[0..-2]] = args.first
    else
      super
    end
  end

  def respond_to_missing?(name)
    (name.end_with?("=") && @hash.key?(name[0..-2])) || super
  end
end

c = MyClass.new #=> #<MyClass:0x000000012e3ba090 @hash={"key1"=>"N/A", "key2"=>"N/A", "key3"=>"N/A"}>
c.key1 = "foo"
c #=> #<MyClass:0x000000012e3ba090 @hash={"key1"=>"foo", "key2"=>"N/A", "key3"=>"N/A"}>

它适用于实际的数据结构,在委派调用之前检查密钥是否存在。尝试设置不存在的密钥不会引发方法错误。

这里要记住的是,如果你曾经创建一个实例方法,其中的键是 ,这个 setter 魔术将不适用于这个键(method_missing永远不会被调用这个键)。 与第一种方法相比,第二种方法的另一个缺点是,我们实际上并没有创建二传手本身。因此,检查实例的方法不会暴露它们。key=key@hash

评论

2赞 Holger Just 1/4/2023
使用第一个选项,您将在类上创建 setter 方法。因此,每个新实例将 (1) (重新)再次定义类上的方法,以及 (2) 如果您更改代码并使用具有不同键的 a,则将为所有实例定义任何实例的方法。为了解决这个问题,您可能希望改用在单个实例的单例类中定义方法。@hashself.define_singleton_method
0赞 Konstantin Strukov 1/4/2023
这不会在上面的代码中发生,因为正如你所看到的,哈希在初始值设定项中是硬编码的(我没有改变它),所以实际上所有实例都是使用相同的键集创建的,因此 setter 被重新创建相同。但是,如果初始值设定项将参数化为哈希值,它将与您所说的完全一样,这是一个很好的观点。感谢您指出这一点。
0赞 Jörg W Mittag 1/5/2023
编辑前的代码还会为除第一个实例之外的每个实例生成大量方法重新定义警告。更准确地说,在应用程序的生存期内,它将生成 2 * (#instances - 1) * @hash.keys.size 警告消息(因为每个方法重新定义都会生成两个警告,一个告诉您覆盖的方法,另一个告诉您原始方法)。
1赞 Jörg W Mittag 1/4/2023 #2

你似乎对 Ruby 的对象模型有一点误解,这可能是阻碍你自己找到解决方案的原因。

我正在编写一个具有哈希数据成员的类。

Ruby 没有“数据成员”。它不是“数据成员”,而是一个实例变量。实例变量不是类的“成员”,而是实例的“成员”,因此它被称为实例变量。@hash

哈希被初始化为在构造函数中具有一组具有默认值的键。

Ruby 没有“构造函数”。在 Ruby 中,实例化一个新对象的工作方式如下:

Class 类有一个名为 Class#allocate 的实例方法。顾名思义,此方法为新对象分配存储空间。所以,就你而言,当我们写类似的东西时

new_object = MyClass.allocate

这将为 的新实例分配存储空间,实例化该存储空间中的新实例,返回新实例化的对象并将其绑定到局部变量。MyClassMyClassnew_object

这个新实例化的对象是完全空的,它不包含任何数据(除了标准对象元数据,例如对其类的引用),这意味着它没有实例变量。

如果要使用一组预定义的实例变量初始化对象,则可以向对象发送一条消息,告诉它初始化自身。这通常通过初始值设定项方法完成。按照惯例,此方法称为 。initialize

(注意:这不仅仅是一个约定,这里也有一点点语言魔力,因为它是默认使用可访问性创建的,不像其他方法默认使用可访问性创建。initializeprivatepublic

因此,在分配了我们的对象之后,我们现在将初始化它:Class#allocate

new_object.initialize

或者更确切地说,由于(如上所述)是默认的,我们需要使用 BasicObject#__send__ 来规避访问限制:initializeprivate

new_object.__send__(:initialize)

注意:如果您熟悉 Objective-C,您可能会认识到这一点:

MyClass newObject = [[MyClass alloc] init];

记住总是初始化一个新实例化的对象有点麻烦,所以有一个名为 Class#new 的帮助程序工厂方法,它看起来或多或少是这样的:

class Class
  def new(...)
    obj = allocate
    obj.__send__(:initialize, ...)
    obj
  end
end

BasicObject 中有一个默认实现,它没有参数,也不做任何事情:

class BasicObject
  def initialize; end
end

这保证了永远不会因 NoMethodError 异常而失败。Class#new

这里要意识到的重要一点是,、 和 是与其他任何方法一样,它们可以被重新实现(除了需要做一些 Ruby 本身无法表达的魔法),它们可以被覆盖,它们可以被猴子修补,它们可以被拦截,等等。allocateinitializenewallocate

它们不是“构造函数”,它们有点像方法,但不是真正的方法,对继承有额外的限制,对它们被允许做什么有额外的限制,等等。它们只是方法。

然后,如何生成与哈希中所有键相对应的 setter 方法?

[...]

我想象使用方法,然后使用..each:define_method

对现有代码的最简单和最小的更改如下所示:

class MyClass
  def initialize
    @hash = { key1: 'N/A', key2: 'N/A', key3: 'N/A' }

    @hash.each_key do |key|
      self.class.define_method(:"#{key}=") do |value|
        @hash[key] = value
      end
    end
  end
end

但是,这存在一个问题:如上所述,是每次创建 的新实例时都会运行的实例方法。这意味着每次用户创建新对象(第一次除外)时,他们都会对过多的方法重新定义警告感到恼火:MyClass#initializeMyClass::newMyClass

MyClass.new

MyClass.new
# my_class.rb:6: warning: method redefined; discarding old key1=
# my_class.rb:6: warning: previous definition of key1= was here
# my_class.rb:6: warning: method redefined; discarding old key2=
# my_class.rb:6: warning: previous definition of key2= was here
# my_class.rb:6: warning: method redefined; discarding old key3=
# my_class.rb:6: warning: previous definition of key3= was here

MyClass.new
# my_class.rb:6: warning: method redefined; discarding old key1=
# my_class.rb:6: warning: previous definition of key1= was here
# my_class.rb:6: warning: method redefined; discarding old key2=
# my_class.rb:6: warning: previous definition of key2= was here
# my_class.rb:6: warning: method redefined; discarding old key3=
# my_class.rb:6: warning: previous definition of key3= was here

你可以想象,在一个可能创建数十个、数百个甚至数百万个对象的应用程序中,这可能会变得非常烦人。

您可以通过使用 Object# 在实例化对象的单例类上定义编写器方法来解决这个问题define_singleton_method如下所示:

class MyClass
  def initialize
    @hash = { key1: 'N/A', key2: 'N/A', key3: 'N/A' }

    @hash.each_key do |key|
      define_singleton_method(:"#{key}=") do |value|
        @hash[key] = value
      end
    end
  end
end

但这不是一个优雅的解决方案:单例方法的要点是每个对象都可以有自己独特的方法集。但是,如果您为所有对象定义相同的方法,那么有什么意义呢?只需为所有对象定义一次类中的方法。

处理此问题的方法是将方法的创建移出初始值设定项方法,并且仅在创建类本身时运行一次,如下所示:

class MyClass
  DEFAULT_HASH = { key1: 'N/A', key2: 'N/A', key3: 'N/A' }.freeze
  private_constant :DEFAULT_HASH

  DEFAULT_HASH.each_key do |key|
    define_method(:"#{key}=") do |value|
      @hash[key] = value
    end
  end

  def initialize
    @hash = DEFAULT_HASH.dup
  end
end

在创建默认哈希时存在一些重复,我们可以像这样重构:

class MyClass
  DEFAULT_HASH_KEYS = %i[key1 key2 key3].freeze
  DEFAULT_HASH_VALUE = 'N/A'
  DEFAULT_HASH = DEFAULT_HASH_KEYS.zip([DEFAULT_HASH_VALUE].cycle).to_h.freeze
  private_constant :DEFAULT_HASH_KEYS, :DEFAULT_HASH_VALUE, :DEFAULT_HASH

  DEFAULT_HASH_KEYS.each do |key|
    define_method(:"#{key}=") do |value|
      @hash[key] = value
    end
  end

  def initialize
    @hash = DEFAULT_HASH.dup
  end
end

但是,所有这些功能都已以 Struct 类的形式存在:

MyClass =
  Struct.new(:key1, :key2, :key3) do
    def initialize
      super(*members.map { 'N/A' })
    end
  end

这就是获得所需行为所需要做的全部工作。事实上,这给你的不仅仅是想要的行为。它还为您提供了合理的相等性实现 (Struct#==),而无需您自己定义它,它为您提供了以 (Struct#deconstructStruct#deconstruct_keys) 形式进行模式匹配的解构实现,而无需您自己定义它,等等。除了编写器之外,您还将定义读者,即 、 和 、 和 。MyClass#key1MyClass#key2MyClass#key3MyClass#key1=MyClass#key2=MyClass#key3=

注意:需要了解的一件重要事情是 Struct::new 不会返回 的实例,而是返回 的子类,即 的实例。这是不寻常的:通常,将消息发送到一个类将返回该类的实例,例如 将返回 an ,将返回 a ,将返回 ,依此类推。但将返回 a ,它是 的子类StructStructStructClassnewArray.newArrayHash.newHashMyClass.newMyClassStruct.newClassStruct

与 相关的是,Ruby 标准库中还有 ostruct 库,但它更类似于 a 而不是 .StructHashStruct

还有 Data 类,它类似于不可变值对象,但对于不可变值对象。Struct

评论

0赞 steenslag 1/5/2023
为结构欢呼。它们还带有方法和方法(当 d 时)to_hto_jsonrequire
0赞 PKP 1/5/2023
谢谢!这很有帮助。我决定使用结构:)