如何在Ruby中“伪造”C#​​样式属性?

时间:2022-10-30 08:08:38

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.
  • 我避免使用alias_method,因为模块包含允许AOP样式的扩展(在这种情况下,对于method_added)而不需要别名。

  • I chose to use class_eval rather than define_method to define the new method in order to be able to support methods that take blocks. This also necessitated the use of alias_method.
  • 我选择使用class_eval而不是define_method来定义新方法,以便能够支持带有块的方法。这也需要使用alias_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).
  • 有一些方法可以获得实际参数名称,这些名称更接近原始输出,但它们并不真正适合这里的响应。您可以查看merb-action-args,我们在其中编写了一些需要获取实际参数名称的代码。它适用于JRuby,Ruby 1.8.x,Ruby 1.9.1(带有gem)和Ruby 1.9 trunk(本机)。

  • The basic technique here is to store a class instance variable when profile or comment is called, which is then applied when a method is added. As in the previous solution, the method_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.
  • 这里的基本技术是在调用profile或comment时存储类实例变量,然后在添加方法时应用该变量。与前面的解决方案一样,method_added挂钩用于跟踪添加新方法的时间,但不是每次都删除挂钩,而是钩子检查实例变量。应用AOP后将删除实例变量,因此它仅应用一次。如果多次使用同样的技术,可以进一步抽象。

  • 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.
  • 一般来说,我试图尽可能贴近你的“规范”,这就是我包含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.
  • 我避免使用alias_method,因为模块包含允许AOP样式的扩展(在这种情况下,对于method_added)而不需要别名。

  • I chose to use class_eval rather than define_method to define the new method in order to be able to support methods that take blocks. This also necessitated the use of alias_method.
  • 我选择使用class_eval而不是define_method来定义新方法,以便能够支持带有块的方法。这也需要使用alias_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).
  • 有一些方法可以获得实际参数名称,这些名称更接近原始输出,但它们并不真正适合这里的响应。您可以查看merb-action-args,我们在其中编写了一些需要获取实际参数名称的代码。它适用于JRuby,Ruby 1.8.x,Ruby 1.9.1(带有gem)和Ruby 1.9 trunk(本机)。

  • The basic technique here is to store a class instance variable when profile or comment is called, which is then applied when a method is added. As in the previous solution, the method_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.
  • 这里的基本技术是在调用profile或comment时存储类实例变量,然后在添加方法时应用该变量。与前面的解决方案一样,method_added挂钩用于跟踪添加新方法的时间,但不是每次都删除挂钩,而是钩子检查实例变量。应用AOP后将删除实例变量,因此它仅应用一次。如果多次使用同样的技术,可以进一步抽象。

  • 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.
  • 一般来说,我试图尽可能贴近你的“规范”,这就是我包含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. :-)

我将把这个元元编程练习再次留下。 :-)