提问人:PKP 提问时间:1/4/2023 最后编辑:PKP 更新时间:1/5/2023 访问量:118
如何动态定义哈希中所有键的 setter 方法
How to define setter methods for all keys in a hash dynamically
问:
我正在编写一个具有哈希数据成员的类。哈希被初始化为在构造函数中具有一组具有默认值的键。
然后,如何生成与哈希中所有键相对应的 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
答:
有几种方法可以解决问题,但利弊各异。
- 您可以按照您的建议使用,(几乎)(此解决方案有一个严重的缺点,我稍后会回到它):
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,并在调用时以静默方式重新添加密钥(这可能不是您所期望的)。
- 解决问题的另一种方法是使用:
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
评论
@hash
self.define_singleton_method
你似乎对 Ruby 的对象模型有一点误解,这可能是阻碍你自己找到解决方案的原因。
我正在编写一个具有哈希数据成员的类。
Ruby 没有“数据成员”。它不是“数据成员”,而是一个实例变量。实例变量不是类的“成员”,而是实例的“成员”,因此它被称为实例变量。@hash
哈希被初始化为在构造函数中具有一组具有默认值的键。
Ruby 没有“构造函数”。在 Ruby 中,实例化一个新对象的工作方式如下:
Class 类有一个名为
Class#allocate
的实例方法。顾名思义,此方法为新对象分配存储空间。所以,就你而言,当我们写类似的东西时
new_object = MyClass.allocate
这将为 的新实例分配存储空间,实例化该存储空间中的新实例,返回新实例化的对象并将其绑定到局部变量。MyClass
MyClass
new_object
这个新实例化的对象是完全空的,它不包含任何数据(除了标准对象元数据,例如对其类的引用),这意味着它没有实例变量。
如果要使用一组预定义的实例变量初始化对象,则可以向对象发送一条消息,告诉它初始化自身。这通常通过初始值设定项方法完成。按照惯例,此方法称为 。initialize
(注意:这不仅仅是一个约定,这里也有一点点语言魔力,因为它是默认使用可访问性创建的,不像其他方法默认使用可访问性创建。initialize
private
public
因此,在分配了我们的对象之后,我们现在将初始化它:Class#allocate
new_object.initialize
或者更确切地说,由于(如上所述)是默认的,我们需要使用 BasicObject#__send__
来规避访问限制:initialize
private
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 本身无法表达的魔法),它们可以被覆盖,它们可以被猴子修补,它们可以被拦截,等等。allocate
initialize
new
allocate
它们不是“构造函数”,它们有点像方法,但不是真正的方法,对继承有额外的限制,对它们被允许做什么有额外的限制,等等。它们只是方法。
然后,如何生成与哈希中所有键相对应的 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#initialize
MyClass::new
MyClass
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#deconstruct
和 Struct#deconstruct_keys
) 形式进行模式匹配的解构实现,而无需您自己定义它,等等。除了编写器之外,您还将定义读者,即 、 和 、 和 。
MyClass#key1
MyClass#key2
MyClass#key3
MyClass#key1=
MyClass#key2=
MyClass#key3=
注意:需要了解的一件重要事情是 Struct::new
不会返回 的实例,而是返回 的子类,即 的实例。这是不寻常的:通常,将消息发送到一个类将返回该类的实例,例如 将返回 an ,将返回 a ,将返回 ,依此类推。但将返回 a ,它是 的子类。Struct
Struct
Struct
Class
new
Array.new
Array
Hash.new
Hash
MyClass.new
MyClass
Struct.new
Class
Struct
与 相关的是,Ruby 标准库中还有 ostruct
库,但它更类似于 a 而不是 .Struct
Hash
Struct
还有 Data
类,它类似于不可变值对象,但对于不可变值对象。Struct
评论
to_h
to_json
require
评论
MyClass = Struct.new(:key1, :key2, :key3)
c = MyClass.new; c.key1 = "value1"
c.to_h
require 'json'
c.to_json