13

在 Ruby 中,假设我有一个类Foo可以让我对我的大量 Foos 集合进行分类。所有的 Foo 都是绿色和球形的,这是一个基本的自然规律,所以我定义了类方法如下:

class Foo
  def self.colour
    "green"
  end

  def self.is_spherical?
    true
  end
end

这让我做

Foo.colour # "green"

但不是

my_foo = Foo.new
my_foo.colour # Error!

尽管它my_foo显然是绿色的。

colour显然,我可以定义一个调用的实例方法self.class.colour,但如果我有许多这样的基本特征,那将变得笨拙。

我也可以通过定义method_missing尝试任何缺少的方法的类来做到这一点,但我不清楚这是我应该做的事情还是丑陋的黑客,或者如何安全地做到这一点(特别是因为我实际上在 ActiveRecord在 Rails 中,我知道它用 method_missing 做了一些聪明有趣的东西)。

你会推荐什么?

4

8 回答 8

24

Ruby 附带的 Forwardable 模块可以很好地做到这一点:

#!/usr/bin/ruby1.8

require 'forwardable'

class Foo

  extend Forwardable

  def self.color
    "green"
  end

  def_delegator self, :color

  def self.is_spherical?
    true
  end

  def_delegator self, :is_spherical?

end

p Foo.color                # "green"
p Foo.is_spherical?        # true
p Foo.new.color            # "green"
p Foo.new.is_spherical?    # true
于 2010-07-09T17:31:23.580 回答
21

如果它是普通的 Ruby,那么使用Forwardable是正确的答案

如果是 Rails,我会使用delegate,例如

class Foo
  delegate :colour, to: :class

  def self.colour
    "green"
  end
end

irb(main):012:0> my_foo = Foo.new
=> #<Foo:0x007f9913110d60>
irb(main):013:0> my_foo.colour
=> "green"
于 2014-05-21T22:27:29.290 回答
4

你可以使用一个模块:

module FooProperties
  def colour ; "green" ; end
  def is_spherical? ; true ; end
end

class Foo
  extend FooProperties
  include FooProperties
end

有点难看,但比使用method_missing. 我会尝试在其他答案中添加其他选项...

于 2010-01-28T16:47:59.390 回答
4

从设计的角度来看,我认为,即使所有Foos颜色和球形的答案都是一样的?是实例的属性,Foo因此应该定义为实例方法而不是类方法。

然而,我可以看到一些你想要这种行为的情况,例如当你Bars在你的系统中所有这些都是蓝色的并且你在你的代码中的某个地方传递了一个类并且想知道在你调用之前实例将是什么颜色new在课堂上。

此外,您是正确的,ActiveRecord 确实广泛使用method_missing例如动态查找器,因此如果您沿着这条路线走,您需要确保您的 method_missing 调用了超类中的那个,如果它确定方法名称不是它可以的自己处理。

于 2010-01-28T16:55:59.830 回答
3

我认为最好的方法是使用Dwemthy 的数组方法

我要查找并填写详细信息,但这是骨架

编辑:耶!在职的!

class Object
  # class where singleton methods for an object are stored
  def metaclass
    class<<self;self;end
  end
  def metaclass_eval &block
    metaclass.instance_eval &block
  end
end
module Defaults
  def self.included(klass, defaults = [])
    klass.metaclass_eval do
      define_method(:add_default) do |attr_name|
        # first, define getters and setters for the instances
        # i.e <class>.new.<attr_name> and <class>.new.<attr_name>=
        attr_accessor attr_name

        # open the class's class
        metaclass_eval do
          # now define our getter and setters for the class
          # i.e. <class>.<attr_name> and <class>.<attr_name>=
          attr_accessor attr_name
        end

        # add to our list of defaults
        defaults << attr_name
      end
      define_method(:inherited) do |subclass|
        # make sure any defaults added to the child are stored with the child
        # not with the parent
        Defaults.included( subclass, defaults.dup )
        defaults.each do |attr_name|
          # copy the parent's current default values
          subclass.instance_variable_set "@#{attr_name}", self.send(attr_name)
        end
      end
    end
    klass.class_eval do
      # define an initialize method that grabs the defaults from the class to 
      # set up the initial values for those attributes
      define_method(:initialize) do
        defaults.each do |attr_name|
          instance_variable_set "@#{attr_name}", self.class.send(attr_name)
        end
      end
    end
  end
end
class Foo
  include Defaults

  add_default :color
  # you can use the setter
  # (without `self.` it would think `color` was a local variable, 
  # not an instance method)
  self.color = "green"

  add_default :is_spherical
  # or the class instance variable directly
  @is_spherical = true
end

Foo.color #=> "green"
foo1 = Foo.new

Foo.color = "blue"
Foo.color #=> "blue"
foo2 = Foo.new

foo1.color #=> "green"
foo2.color #=> "blue"

class Bar < Foo
  add_defaults :texture
  @texture = "rough"

  # be sure to call the original initialize when overwriting it
  alias :load_defaults :initialize
  def initialize
    load_defaults
    @color = += " (default value)"
  end
end

Bar.color #=> "blue"
Bar.texture #=> "rough"
Bar.new.color #=> "blue (default value)"

Bar.color = "red"
Bar.color #=> "red"
Foo.color #=> "blue"
于 2010-01-28T16:59:49.333 回答
2

你也可以这样做:

def self.color your_args; your_expression end

define_method :color, &method(:color)
于 2010-12-09T10:23:45.370 回答
1

这听起来有点像逃避现实,但实际上很少需要这样做,因为您可以像调用 Foo.color 一样轻松。例外情况是,如果您有许多定义了颜色方法的类。@var 可能是几个类之一,无论如何您都希望显示颜色。

在这种情况下,我会问自己在哪里使用该方法更多 - 在类上还是在模型上?它几乎总是一个或另一个,将它作为一个实例方法并没有错,即使它在所有实例中都应该是相同的。

在极少数情况下,您希望两者都可以“调用”该方法,您可以执行 @var.class.color (不创建特殊方法)或创建一个特殊方法,如下所示:

def color self.class.color end

我肯定会避免使用包罗万象的 (method_missing) 解决方案,因为它让您无需真正考虑每种方法的用法,以及它是否属于类或实例级别。

于 2010-01-28T16:51:11.490 回答
1

您可以定义一个直通设施:

module Passthrough
  def passthrough(*methods)
    methods.each do |method|
      ## make sure the argument is the right type.
      raise ArgumentError if ! method.is_a?(Symbol)
      method_str = method.to_s
      self.class_eval("def #{method_str}(*args) ; self.class.#{method_str}(*args) ; end")
    end
  end
end

class Foo
  extend Passthrough

  def self::colour ; "green" ; end
  def self::is_spherical? ; true ; end
  passthrough :colour, :is_spherical?
end

f = Foo.new
puts(f.colour)
puts(Foo.colour)

我通常不喜欢使用eval,但在这里应该很安全。

于 2010-01-28T17:01:56.240 回答