如何正确使用 Ruby DelegateClass 来包装 YAML::Store?
How do I properly use the Ruby DelegateClass to wrap YAML::Store?
测试环境
我在 Ruby 3.0.2 和 Ruby 2.73 中都试过了,结果相似。对于手头的问题应该无关紧要,因为我也在不同的 shell 和 ruby 管理器下尝试过这个,但这主要是在
下测试的
- fish,版本 3.3.1
- chruby: 0.3.9
- chruby-鱼:0.8.2
- macOS 11.6
描述问题(后续部分中的代码和错误)
我正在尝试使用 Delegator class 中糟糕的(甚至可能没有记录的)DelegateClass 来为 YAML::Store 创建一个允许我任意读写的外观进出 YAML 存储的密钥。但是,我显然不明白如何正确地委托给 YAML::Store 实例,或者以我想要的方式覆盖或扩展功能。
为简单起见,我将示例编写为自执行 Ruby 文件名 example.rb,因此请滚动到末尾以查看对 classes 的实际调用。我希望我的错误是微不足道的,但如果我从根本上误解了如何实际执行 CollaboratorWithData#write 和 ollaboratorWithData#read 到 MultiWriter 的委托,请教育我。
注意:我知道如何通过简单地将 YAML::Store 视为在我的 class 中实例化的对象或什至作为继承自 [= 的单独对象来解决此问题60=](例如 class MultiWriter < YAML::Store
),但我非常想了解如何正确使用 Forwardable、SimpleDelegator 和 Delegate 在一般情况下和这个特定用例中包装对象。
自执行代码文件(需要一些垂直滚动)
#!/usr/bin/env ruby
require 'delegate'
require 'yaml/store'
module ExampleDelegator
attr_accessor :yaml_store, :data
class CollaboratorWithData
def initialize
@yaml_store = MultiWriter.new
@data = {}
end
def some_data
{a: 1, b:2, c: [1, 2, 3]}
end
end
class MultiWriter < DelegateClass(YAML::Store)
attr_reader :store
def initialize file_name="store.yml", thread_safe=true
super
@store = self
end
def write **kwargs
@store.transaction { kwargs.each { |k, v| @store[k] = v } }
end
def read *keys
@store.transaction(read_only=true) { keys.map { |k| @store[k] } }
end
end
end
if __FILE__ == [=12=]
include ExampleDelegator
c = CollaboratorWithData.new
c.data = c.some_data
c.write(c.data)
end
运行 文件
时出错
初始化器错误
Traceback (most recent call last):
5: from ./example.rb:40:in `<main>'
4: from ./example.rb:40:in `new'
3: from ./example.rb:11:in `initialize'
2: from ./example.rb:11:in `new'
1: from ./example.rb:24:in `initialize'
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/delegate.rb:71:in `initialize': wrong number of arguments (given 2, expected 1) (ArgumentError)
请注意,如果您仔细查看 YAML::Store#new 的调用,其中一个可能的签名 确实 有两个参数。我不明白为什么它不允许我在 IRB 中指定线程安全:
foo = YAML::Store.new 'foo.yml', true
#=> #<Psych::Store:0x00007f9f520f52c8 @opt={}, @filename="foo.yml", @abort=false, @ultra_safe=false, @thread_safe=true, @lock=#<Thread::Mutex:0x00007f9f520f5138>>
使用修改后的初始化程序的方法委托出错
即使我去掉了thread_safe参数,在调用委托的#write方法时我仍然从CollaboratorWithData得到NoMethodError,这让我相信有我在初始值设定项之外执行委派的问题。
./example.rb
Traceback (most recent call last):
./example.rb:42:in `<main>': undefined method `write' for #<ExampleDelegator::CollaboratorWithData:0x00007ff86e0e15c8> (NoMethodError)
复习关于委派的一些问题:
- 可转发:
此模块在扩展时允许您指定应委托给指定对象(通常是实例变量)的特定方法示例:
require 'forwardable'
class A
attr_reader :obj
extend Forwardable
def_delegator :@obj, :<<
def initialize(val)
@obj = val
end
end
a = A.new([])
a << 1
#=> [1]
a.obj
#=> [1]
Delegator:继承此 class 允许将方法委托给通过 new
传递的对象。该对象在实例化时使用 __setobj__
方法内部化,调用通过 method_missing
转发;但是这里需要注意的是,它使用了 2 个默认未定义的方法(__setobj__
和 __getobj__
),您需要在继承 class
中定义它们
SimpleDelegator:继承自Delegator
,在大多数方面都具有相同的个性;但是它预定义了前面讨论的 2 种方法。通常从 SimpleDelegator
继承优于直接从 Delegator
class 继承,因为它易于使用。
DelegateClass:与SimpleDelegator
类似,但它创建了一个新的匿名class,它定义了class 作为参数传入,然后您的继承 class 直接从这个匿名 class 继承,这允许方法通过继承而不是使用 method_missing 进行委托。例如
class A < Delegator;end
class B < DelegateClass(String);end
A.ancestors
#=> [A, Delegator, #<Module:0x00007fffcc1cc9c8>, BasicObject]
B.ancestors
#=> [B, #<Class:0x00007fffcc5603c8>, Delegator, #<Module:0x00007fffcc1cc9c8>, BasicObject]
A.instance_methods - Object.instance_methods
#=> [:__getobj__, :__setobj__, :method_missing, :marshal_dump, :marshal_load]
B.instance_methods - Object.instance_methods
#=> Array of all the public the instance methods of String plus the above
当您希望使用特定类型的对象实例化 class 时,这通常更可取,因为模块注入避免了与 method_missing
一起出现的完整继承链遍历。这并不意味着您不能用另一个对象实例化此 class(但是,如果您这样做并且该对象定义了传递给 DelegateClass
的 class 参数中未定义的方法,它将回退到默认 method_missing
行为)
现在输入您的代码
您的第一个错误是对 super
的调用。 Delegator
需要 1 个参数,其中 Object
的一个实例被委托给(YAML::Store
在你的情况下)但是你已经定义了 2 个参数(这两个参数都不代表你希望委托给的对象) 并且当您调用 super
时,这两个参数都会被转发,因此会出现错误。
删除 thread_safe
有效,因为您现在只有一个参数,但在这种情况下您的委托对象实际上是字符串 "store.yml"
例如,以下修改应该有效(类似于 Source Code
中提供的示例
module ExampleDelegator
class MultiWriter < DelegateClass(YAML::Store)
attr_reader :store
def initialize file_name="store.yml", thread_safe=true
@store = YAML::Store.new(file_name,thread_safe)
super(@store)
end
end
end
您的第二个问题是您在 CollaboratorWithData
对象上调用 write
,该对象未定义此方法且未以其他方式委派。
虽然我不是 100% 确定你的意图是什么,因为模块主体中还有一些其他奇怪的东西,比如 attr_accessor
然后被包含在全局 space 中我没有看到 Delegator
的真正原因,因为您可以通过将 yaml_store
实例变量设置为 YAML::Store
的实例(正如您已经提到的)直接使用对象,但是您如果您愿意,可以按如下方式重写代码以使用委托
require 'delegate'
require 'yaml/store'
require 'forwardable'
module ExampleDelegator
class CollaboratorWithData
extend Forwardable
def_delegators :@yaml_store, :read, :write
attr_reader :yaml_store
attr_accessor :data
def initialize(file_name="store.yml", thread_safe=true)
@yaml_store = MultiWriter.new(YAML::Store.new(file_name,thread_safe))
@data = {}
end
def some_data
{a: 1, b:2, c: [1, 2, 3]}
end
end
class MultiWriter < DelegateClass(YAML::Store)
def write **kwargs
transaction { kwargs.each { |k, v| @store[k] = v } }
end
def read *keys
transaction(read_only=true) { keys.map { |k| @store[k] } }
end
end
end
测试环境
我在 Ruby 3.0.2 和 Ruby 2.73 中都试过了,结果相似。对于手头的问题应该无关紧要,因为我也在不同的 shell 和 ruby 管理器下尝试过这个,但这主要是在
下测试的- fish,版本 3.3.1
- chruby: 0.3.9
- chruby-鱼:0.8.2
- macOS 11.6
描述问题(后续部分中的代码和错误)
我正在尝试使用 Delegator class 中糟糕的(甚至可能没有记录的)DelegateClass 来为 YAML::Store 创建一个允许我任意读写的外观进出 YAML 存储的密钥。但是,我显然不明白如何正确地委托给 YAML::Store 实例,或者以我想要的方式覆盖或扩展功能。
为简单起见,我将示例编写为自执行 Ruby 文件名 example.rb,因此请滚动到末尾以查看对 classes 的实际调用。我希望我的错误是微不足道的,但如果我从根本上误解了如何实际执行 CollaboratorWithData#write 和 ollaboratorWithData#read 到 MultiWriter 的委托,请教育我。
注意:我知道如何通过简单地将 YAML::Store 视为在我的 class 中实例化的对象或什至作为继承自 [= 的单独对象来解决此问题60=](例如 class MultiWriter < YAML::Store
),但我非常想了解如何正确使用 Forwardable、SimpleDelegator 和 Delegate 在一般情况下和这个特定用例中包装对象。
自执行代码文件(需要一些垂直滚动)
#!/usr/bin/env ruby
require 'delegate'
require 'yaml/store'
module ExampleDelegator
attr_accessor :yaml_store, :data
class CollaboratorWithData
def initialize
@yaml_store = MultiWriter.new
@data = {}
end
def some_data
{a: 1, b:2, c: [1, 2, 3]}
end
end
class MultiWriter < DelegateClass(YAML::Store)
attr_reader :store
def initialize file_name="store.yml", thread_safe=true
super
@store = self
end
def write **kwargs
@store.transaction { kwargs.each { |k, v| @store[k] = v } }
end
def read *keys
@store.transaction(read_only=true) { keys.map { |k| @store[k] } }
end
end
end
if __FILE__ == [=12=]
include ExampleDelegator
c = CollaboratorWithData.new
c.data = c.some_data
c.write(c.data)
end
运行 文件
时出错初始化器错误
Traceback (most recent call last):
5: from ./example.rb:40:in `<main>'
4: from ./example.rb:40:in `new'
3: from ./example.rb:11:in `initialize'
2: from ./example.rb:11:in `new'
1: from ./example.rb:24:in `initialize'
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/delegate.rb:71:in `initialize': wrong number of arguments (given 2, expected 1) (ArgumentError)
请注意,如果您仔细查看 YAML::Store#new 的调用,其中一个可能的签名 确实 有两个参数。我不明白为什么它不允许我在 IRB 中指定线程安全:
foo = YAML::Store.new 'foo.yml', true
#=> #<Psych::Store:0x00007f9f520f52c8 @opt={}, @filename="foo.yml", @abort=false, @ultra_safe=false, @thread_safe=true, @lock=#<Thread::Mutex:0x00007f9f520f5138>>
使用修改后的初始化程序的方法委托出错
即使我去掉了thread_safe参数,在调用委托的#write方法时我仍然从CollaboratorWithData得到NoMethodError,这让我相信有我在初始值设定项之外执行委派的问题。
./example.rb
Traceback (most recent call last):
./example.rb:42:in `<main>': undefined method `write' for #<ExampleDelegator::CollaboratorWithData:0x00007ff86e0e15c8> (NoMethodError)
复习关于委派的一些问题:
- 可转发: 此模块在扩展时允许您指定应委托给指定对象(通常是实例变量)的特定方法示例:
require 'forwardable'
class A
attr_reader :obj
extend Forwardable
def_delegator :@obj, :<<
def initialize(val)
@obj = val
end
end
a = A.new([])
a << 1
#=> [1]
a.obj
#=> [1]
Delegator:继承此 class 允许将方法委托给通过
中定义它们new
传递的对象。该对象在实例化时使用__setobj__
方法内部化,调用通过method_missing
转发;但是这里需要注意的是,它使用了 2 个默认未定义的方法(__setobj__
和__getobj__
),您需要在继承 classSimpleDelegator:继承自
Delegator
,在大多数方面都具有相同的个性;但是它预定义了前面讨论的 2 种方法。通常从SimpleDelegator
继承优于直接从Delegator
class 继承,因为它易于使用。DelegateClass:与
SimpleDelegator
类似,但它创建了一个新的匿名class,它定义了class 作为参数传入,然后您的继承 class 直接从这个匿名 class 继承,这允许方法通过继承而不是使用 method_missing 进行委托。例如
class A < Delegator;end
class B < DelegateClass(String);end
A.ancestors
#=> [A, Delegator, #<Module:0x00007fffcc1cc9c8>, BasicObject]
B.ancestors
#=> [B, #<Class:0x00007fffcc5603c8>, Delegator, #<Module:0x00007fffcc1cc9c8>, BasicObject]
A.instance_methods - Object.instance_methods
#=> [:__getobj__, :__setobj__, :method_missing, :marshal_dump, :marshal_load]
B.instance_methods - Object.instance_methods
#=> Array of all the public the instance methods of String plus the above
当您希望使用特定类型的对象实例化 class 时,这通常更可取,因为模块注入避免了与 method_missing
一起出现的完整继承链遍历。这并不意味着您不能用另一个对象实例化此 class(但是,如果您这样做并且该对象定义了传递给 DelegateClass
的 class 参数中未定义的方法,它将回退到默认 method_missing
行为)
现在输入您的代码
您的第一个错误是对 super
的调用。 Delegator
需要 1 个参数,其中 Object
的一个实例被委托给(YAML::Store
在你的情况下)但是你已经定义了 2 个参数(这两个参数都不代表你希望委托给的对象) 并且当您调用 super
时,这两个参数都会被转发,因此会出现错误。
删除 thread_safe
有效,因为您现在只有一个参数,但在这种情况下您的委托对象实际上是字符串 "store.yml"
例如,以下修改应该有效(类似于 Source Code
中提供的示例module ExampleDelegator
class MultiWriter < DelegateClass(YAML::Store)
attr_reader :store
def initialize file_name="store.yml", thread_safe=true
@store = YAML::Store.new(file_name,thread_safe)
super(@store)
end
end
end
您的第二个问题是您在 CollaboratorWithData
对象上调用 write
,该对象未定义此方法且未以其他方式委派。
虽然我不是 100% 确定你的意图是什么,因为模块主体中还有一些其他奇怪的东西,比如 attr_accessor
然后被包含在全局 space 中我没有看到 Delegator
的真正原因,因为您可以通过将 yaml_store
实例变量设置为 YAML::Store
的实例(正如您已经提到的)直接使用对象,但是您如果您愿意,可以按如下方式重写代码以使用委托
require 'delegate'
require 'yaml/store'
require 'forwardable'
module ExampleDelegator
class CollaboratorWithData
extend Forwardable
def_delegators :@yaml_store, :read, :write
attr_reader :yaml_store
attr_accessor :data
def initialize(file_name="store.yml", thread_safe=true)
@yaml_store = MultiWriter.new(YAML::Store.new(file_name,thread_safe))
@data = {}
end
def some_data
{a: 1, b:2, c: [1, 2, 3]}
end
end
class MultiWriter < DelegateClass(YAML::Store)
def write **kwargs
transaction { kwargs.each { |k, v| @store[k] = v } }
end
def read *keys
transaction(read_only=true) { keys.map { |k| @store[k] } }
end
end
end