EDIT: I slightly changed the spec, to better match what I imagined this to do.
编辑:我稍微改变了规格,以更好地匹配我想象的那样做。
Well, I don't really want to fake C# attributes, I want to one-up-them and support AOP as well.
好吧,我真的不想伪造C#属性,我想要一次性并支持AOP。
Given the program:
鉴于该计划:
class Object
def Object.profile
# magic code here
end
end
class Foo
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
Foo.new.bar("test")
Foo.new.barbar("test")
puts Foo.get_comment(:snafu)
Desired output:
Foo.bar was called with param: b = "test" test Foo.bar call finished, duration was 1ms test This really should be fixed
Is there any way to achieve this?
有没有办法实现这个目标?
2 个解决方案
#1
I have a somewhat different approach:
我有一个不同的方法:
class Object
def self.profile(method_name)
return_value = nil
time = Benchmark.measure do
return_value = yield
end
puts "#{method_name} finished in #{time.real}"
return_value
end
end
require "benchmark"
module Profiler
def method_added(name)
profile_method(name) if @method_profiled
super
end
def profile_method(method_name)
@method_profiled = nil
alias_method "unprofiled_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
name = "\#{self.class}##{method_name}"
msg = "\#{name} was called with \#{args.inspect}"
msg << " and a block" if block_given?
puts msg
Object.profile(name) { unprofiled_#{method_name}(*args, &blk) }
end
ruby_eval
end
def profile
@method_profiled = true
end
end
module Comment
def method_added(name)
comment_method(name) if @method_commented
super
end
def comment_method(method_name)
comment = @method_commented
@method_commented = nil
alias_method "uncommented_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
puts #{comment.inspect}
uncommented_#{method_name}(*args, &blk)
end
ruby_eval
end
def comment(text)
@method_commented = text
end
end
class Foo
extend Profiler
extend Comment
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
A few points about this solution:
关于这个解决方案的几点:
- I provided the additional methods via modules which could be extended into new classes as needed. This avoids polluting the global namespace for all modules.
- I avoided using alias_method, since module includes allow AOP-style extensions (in this case, for
method_added
) without the need for aliasing. - I chose to use
class_eval
rather thandefine_method
to define the new method in order to be able to support methods that take blocks. This also necessitated the use ofalias_method
. - Because I chose to support blocks, I also added a bit of text to the output in case the method takes a block.
- There are ways to get the actual parameter names, which would be closer to your original output, but they don't really fit in a response here. You can check out merb-action-args, where we wrote some code that required getting the actual parameter names. It works in JRuby, Ruby 1.8.x, Ruby 1.9.1 (with a gem), and Ruby 1.9 trunk (natively).
- The basic technique here is to store a class instance variable when
profile
orcomment
is called, which is then applied when a method is added. As in the previous solution, themethod_added
hook is used to track when the new method is added, but instead of removing the hook each time, the hook checks for an instance variable. The instance variable is removed after the AOP is applied, so it only applies once. If this same technique was used multiple time, it could be further abstracted. - In general, I tried to stick as close to your "spec" as possible, which is why I included the Object.profile snippet instead of implementing it inline.
我通过模块提供了其他方法,可以根据需要扩展到新类。这可以避免污染所有模块的全局命名空间。
我避免使用alias_method,因为模块包含允许AOP样式的扩展(在这种情况下,对于method_added)而不需要别名。
我选择使用class_eval而不是define_method来定义新方法,以便能够支持带有块的方法。这也需要使用alias_method。
因为我选择支持块,所以在方法占用块的情况下,我还在输出中添加了一些文本。
有一些方法可以获得实际参数名称,这些名称更接近原始输出,但它们并不真正适合这里的响应。您可以查看merb-action-args,我们在其中编写了一些需要获取实际参数名称的代码。它适用于JRuby,Ruby 1.8.x,Ruby 1.9.1(带有gem)和Ruby 1.9 trunk(本机)。
这里的基本技术是在调用profile或comment时存储类实例变量,然后在添加方法时应用该变量。与前面的解决方案一样,method_added挂钩用于跟踪添加新方法的时间,但不是每次都删除挂钩,而是钩子检查实例变量。应用AOP后将删除实例变量,因此它仅应用一次。如果多次使用同样的技术,可以进一步抽象。
一般来说,我试图尽可能贴近你的“规范”,这就是我包含Object.profile片段而不是内联实现它的原因。
#2
Great question. This is my quick attempt at an implementation (I did not try to optimise the code). I took the liberty of adding the profile
method to the Module
class. In this way it will be available in every class and module definition. It would be even better to extract it into a module and mix it into the class Module
whenever you need it.
好问题。这是我对实现的快速尝试(我没有尝试优化代码)。我冒昧地将配置文件方法添加到Module类中。通过这种方式,它将在每个类和模块定义中可用。将它提取到模块中并在需要时将其混合到类Module中会更好。
I also didn't know if the point was to make the profile
method behave like Ruby's public
/protected
/private
keywords, but I implemented it like that anyway. All methods defined after calling profile
are profiled, until noprofile
is called.
我也不知道是不是要让profile方法的行为像Ruby的public / protected / private关键字一样,但无论如何我都是这样实现的。在调用配置文件之后定义的所有方法都被分析,直到调用noprofile。
class Module
def profile
require "benchmark"
@profiled_methods ||= []
class << self
# Save any original method_added callback.
alias_method :__unprofiling_method_added, :method_added
# Create new callback.
def method_added(method)
# Possible infinite loop if we do not check if we already replaced this method.
unless @profiled_methods.include?(method)
@profiled_methods << method
unbound_method = instance_method(method)
define_method(method) do |*args|
puts "#{self.class}##{method} was called with params #{args.join(", ")}"
bench = Benchmark.measure do
unbound_method.bind(self).call(*args)
end
puts "#{self.class}##{method} finished in %.5fs" % bench.real
end
# Call the original callback too.
__unprofiling_method_added(method)
end
end
end
end
def noprofile # What's the opposite of profile?
class << self
# Remove profiling callback and restore previous one.
alias_method :method_added, :__unprofiling_method_added
end
end
end
You can now use it as follows:
您现在可以按如下方式使用它:
class Foo
def self.method_added(method) # This still works.
puts "Method '#{method}' has been added to '#{self}'."
end
profile
def foo(arg1, arg2, arg3 = nil)
puts "> body of foo"
sleep 1
end
def bar(arg)
puts "> body of bar"
end
noprofile
def baz(arg)
puts "> body of baz"
end
end
Call the methods as you would normally:
像往常一样调用方法:
foo = Foo.new
foo.foo(1, 2, 3)
foo.bar(2)
foo.baz(3)
And get benchmarked output (and the result of the original method_added
callback just to show that it still works):
并获得基准测试输出(原始method_added回调的结果只是为了表明它仍然有效):
Method 'foo' has been added to 'Foo'.
Method 'bar' has been added to 'Foo'.
Method 'baz' has been added to 'Foo'.
Foo#foo was called with params 1, 2, 3
> body of foo
Foo#foo finished in 1.00018s
Foo#bar was called with params 2
> body of bar
Foo#bar finished in 0.00016s
> body of baz
One thing to note is that it is impossible to dynamically get the name of the arguments with Ruby meta-programming. You'd have to parse the original Ruby file, which is certainly possible but a little more complex. See the parse_tree and ruby_parser gems for details.
需要注意的一点是,使用Ruby元编程动态获取参数名称是不可能的。你必须解析原始的Ruby文件,这肯定是可能的,但有点复杂。有关详细信息,请参阅parse_tree和ruby_parser gems。
A fun improvement would be to be able to define this kind of behaviour with a class method in the Module
class. It would be cool to be able to do something like:
一个有趣的改进是能够使用Module类中的类方法定义这种行为。能够做类似的事情会很酷:
class Module
method_wrapper :profile do |*arguments|
# Do something before calling method.
yield *arguments # Call original method.
# Do something afterwards.
end
end
I'll leave this meta-meta-programming exercise for another time. :-)
我将把这个元元编程练习再次留下。 :-)
#1
I have a somewhat different approach:
我有一个不同的方法:
class Object
def self.profile(method_name)
return_value = nil
time = Benchmark.measure do
return_value = yield
end
puts "#{method_name} finished in #{time.real}"
return_value
end
end
require "benchmark"
module Profiler
def method_added(name)
profile_method(name) if @method_profiled
super
end
def profile_method(method_name)
@method_profiled = nil
alias_method "unprofiled_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
name = "\#{self.class}##{method_name}"
msg = "\#{name} was called with \#{args.inspect}"
msg << " and a block" if block_given?
puts msg
Object.profile(name) { unprofiled_#{method_name}(*args, &blk) }
end
ruby_eval
end
def profile
@method_profiled = true
end
end
module Comment
def method_added(name)
comment_method(name) if @method_commented
super
end
def comment_method(method_name)
comment = @method_commented
@method_commented = nil
alias_method "uncommented_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
puts #{comment.inspect}
uncommented_#{method_name}(*args, &blk)
end
ruby_eval
end
def comment(text)
@method_commented = text
end
end
class Foo
extend Profiler
extend Comment
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
A few points about this solution:
关于这个解决方案的几点:
- I provided the additional methods via modules which could be extended into new classes as needed. This avoids polluting the global namespace for all modules.
- I avoided using alias_method, since module includes allow AOP-style extensions (in this case, for
method_added
) without the need for aliasing. - I chose to use
class_eval
rather thandefine_method
to define the new method in order to be able to support methods that take blocks. This also necessitated the use ofalias_method
. - Because I chose to support blocks, I also added a bit of text to the output in case the method takes a block.
- There are ways to get the actual parameter names, which would be closer to your original output, but they don't really fit in a response here. You can check out merb-action-args, where we wrote some code that required getting the actual parameter names. It works in JRuby, Ruby 1.8.x, Ruby 1.9.1 (with a gem), and Ruby 1.9 trunk (natively).
- The basic technique here is to store a class instance variable when
profile
orcomment
is called, which is then applied when a method is added. As in the previous solution, themethod_added
hook is used to track when the new method is added, but instead of removing the hook each time, the hook checks for an instance variable. The instance variable is removed after the AOP is applied, so it only applies once. If this same technique was used multiple time, it could be further abstracted. - In general, I tried to stick as close to your "spec" as possible, which is why I included the Object.profile snippet instead of implementing it inline.
我通过模块提供了其他方法,可以根据需要扩展到新类。这可以避免污染所有模块的全局命名空间。
我避免使用alias_method,因为模块包含允许AOP样式的扩展(在这种情况下,对于method_added)而不需要别名。
我选择使用class_eval而不是define_method来定义新方法,以便能够支持带有块的方法。这也需要使用alias_method。
因为我选择支持块,所以在方法占用块的情况下,我还在输出中添加了一些文本。
有一些方法可以获得实际参数名称,这些名称更接近原始输出,但它们并不真正适合这里的响应。您可以查看merb-action-args,我们在其中编写了一些需要获取实际参数名称的代码。它适用于JRuby,Ruby 1.8.x,Ruby 1.9.1(带有gem)和Ruby 1.9 trunk(本机)。
这里的基本技术是在调用profile或comment时存储类实例变量,然后在添加方法时应用该变量。与前面的解决方案一样,method_added挂钩用于跟踪添加新方法的时间,但不是每次都删除挂钩,而是钩子检查实例变量。应用AOP后将删除实例变量,因此它仅应用一次。如果多次使用同样的技术,可以进一步抽象。
一般来说,我试图尽可能贴近你的“规范”,这就是我包含Object.profile片段而不是内联实现它的原因。
#2
Great question. This is my quick attempt at an implementation (I did not try to optimise the code). I took the liberty of adding the profile
method to the Module
class. In this way it will be available in every class and module definition. It would be even better to extract it into a module and mix it into the class Module
whenever you need it.
好问题。这是我对实现的快速尝试(我没有尝试优化代码)。我冒昧地将配置文件方法添加到Module类中。通过这种方式,它将在每个类和模块定义中可用。将它提取到模块中并在需要时将其混合到类Module中会更好。
I also didn't know if the point was to make the profile
method behave like Ruby's public
/protected
/private
keywords, but I implemented it like that anyway. All methods defined after calling profile
are profiled, until noprofile
is called.
我也不知道是不是要让profile方法的行为像Ruby的public / protected / private关键字一样,但无论如何我都是这样实现的。在调用配置文件之后定义的所有方法都被分析,直到调用noprofile。
class Module
def profile
require "benchmark"
@profiled_methods ||= []
class << self
# Save any original method_added callback.
alias_method :__unprofiling_method_added, :method_added
# Create new callback.
def method_added(method)
# Possible infinite loop if we do not check if we already replaced this method.
unless @profiled_methods.include?(method)
@profiled_methods << method
unbound_method = instance_method(method)
define_method(method) do |*args|
puts "#{self.class}##{method} was called with params #{args.join(", ")}"
bench = Benchmark.measure do
unbound_method.bind(self).call(*args)
end
puts "#{self.class}##{method} finished in %.5fs" % bench.real
end
# Call the original callback too.
__unprofiling_method_added(method)
end
end
end
end
def noprofile # What's the opposite of profile?
class << self
# Remove profiling callback and restore previous one.
alias_method :method_added, :__unprofiling_method_added
end
end
end
You can now use it as follows:
您现在可以按如下方式使用它:
class Foo
def self.method_added(method) # This still works.
puts "Method '#{method}' has been added to '#{self}'."
end
profile
def foo(arg1, arg2, arg3 = nil)
puts "> body of foo"
sleep 1
end
def bar(arg)
puts "> body of bar"
end
noprofile
def baz(arg)
puts "> body of baz"
end
end
Call the methods as you would normally:
像往常一样调用方法:
foo = Foo.new
foo.foo(1, 2, 3)
foo.bar(2)
foo.baz(3)
And get benchmarked output (and the result of the original method_added
callback just to show that it still works):
并获得基准测试输出(原始method_added回调的结果只是为了表明它仍然有效):
Method 'foo' has been added to 'Foo'.
Method 'bar' has been added to 'Foo'.
Method 'baz' has been added to 'Foo'.
Foo#foo was called with params 1, 2, 3
> body of foo
Foo#foo finished in 1.00018s
Foo#bar was called with params 2
> body of bar
Foo#bar finished in 0.00016s
> body of baz
One thing to note is that it is impossible to dynamically get the name of the arguments with Ruby meta-programming. You'd have to parse the original Ruby file, which is certainly possible but a little more complex. See the parse_tree and ruby_parser gems for details.
需要注意的一点是,使用Ruby元编程动态获取参数名称是不可能的。你必须解析原始的Ruby文件,这肯定是可能的,但有点复杂。有关详细信息,请参阅parse_tree和ruby_parser gems。
A fun improvement would be to be able to define this kind of behaviour with a class method in the Module
class. It would be cool to be able to do something like:
一个有趣的改进是能够使用Module类中的类方法定义这种行为。能够做类似的事情会很酷:
class Module
method_wrapper :profile do |*arguments|
# Do something before calling method.
yield *arguments # Call original method.
# Do something afterwards.
end
end
I'll leave this meta-meta-programming exercise for another time. :-)
我将把这个元元编程练习再次留下。 :-)