Ruby:我可以完全透明地在另一个类中包装一个类吗?

时间:2016-10-19 14:06:57

标签: ruby jruby

我需要创建一个类如下(真正的问题更复杂)

class IArray
  attr_reader :array

  # array is a Ruby Array
  def initialize(array)
    @array = array
  end

  def method_missing(...)
    # forwards every call to @array
  end
end

现在在我的代码中我想做

a1 = [1, 2, 3]
a2 = IArray.new([4, 5])
a1.concat(a2)

最后一个语句不起作用,说“没有将a2隐式转换为数组”。 a1如何知道a2不是数组?我已为is_a?实施kind_of?a2,以便在询问是否为数组时返回tru e。我想要的是a1认为a2是一个数组,然后调用a2上需要进行合并的任何方法。

我希望任何其他类都能发生同样的情况,即只需将它包装在一个类中,但让它像没有被包装一样工作。

3 个答案:

答案 0 :(得分:2)

不,不幸的是,在Ruby中,一个对象无法完全模拟另一个对象。这是一个不幸的限制,因为一个模拟另一个的对象是OO的基石之一,而Ruby是一种OO语言,所以这应该是可行的。

有三个主要原因导致不可能:

  • 参考平等:参考平等(BasicObject#equal?)打破模拟,从而打破OO。通过比较Reference Equality,您可以将模拟对象与模拟对象区分开来,这是不可能的。您可以使用BasicObject修补程序来删除或替换equal?,但这可能会使事情发生左右分裂。
  • 布尔运算符和条件:布尔运算符和条件只考虑两个单例对象nilfalse falsy 。无法编写自己的 falsy 对象,因此无法模拟nilfalse
  • 类而不是作为类型的协议:一些核心库和标准库方法以及一些硬编码内部组件希望对象是特定类的实例,以便能够使用它们,即使根据对于OO原则,说出相同协议的对象应该具有相同的类型,而不管其类别。

你可以走得很远,但是:

  • 核心库和标准库中很少有方法(实际上,没有方法,我相信)检查参考平等。
  • 您很少需要模拟nilfalse
  • 几乎总是当一个特定的子程序需要一个特定的类时,它将提供一个间接级别作为逃生舱口并调用一些转换方法(例如一元前缀&符号&操作员调用to_proc,一元前缀星号*" splat"运算符调用to_aprint调用to_sArray#[]调用to_int,等等上)。另外,至少在Numeric s,有一个定义的强制协议。

但是,在您的特定情况下,您遇到的情况之一就是它无法正常工作:而to_ary可让您在制作模拟Array的内容方面有很长的路要走在您的情况下,您需要进一步跟踪您的IArray,但当然将其转换为Array会丢失其身份及其附加行为。不幸的是,你搞砸了。你无能为力。

在理想的OO世界中,两个使用相同协议的对象应该被认为是相同的类型,ergo Array#concat不应该关心它的参数是否是{{的实例1}}(或者可以转换为1),而是它的参数是否与Array说明相同的协议(或更确切地说:说Array 实际的协议的子集要求)。

我只能推测为什么Ruby在这种情况下不遵循OO范例:性能。在OO中,一个对象永远不会知道另一个对象的表示,即使这两个对象属于同一类型(或同一类)。这是基于抽象数据类型的面向对象数据抽象和数据抽象之间的根本区别:ADT实例可以检查相同类型的其他实例的表示,对象可以 任何其他对象的表示形式,即使它具有相同的类型(或类)。

然而,这意味着操作不可能同时检查两个对象的表示(该操作是第三个对象的方法,这意味着它不能检查任何一个对象的表示,或者它是两个对象之一的方法,在这种情况下,它可以检查自己的对象的表示而不是其他对象,这意味着在OO中编写需要访问的对象是不可能的。一次表示两个对象。

E.g。连接两个链接列表是O(1),如果您有权访问第一个列表的最后一个元素的concat指针,那么第二个列表的第一个元素是{{1} {1}}指针,但在OO中,您最多可以访问其中一个(除非这两个列表显式公开了一个允许访问这两个指针的公共方法)。将数组连接到另一个数组需要快速访问两个数组的内部表示,因此Ruby决定在这里破坏OO封装,并且要求两个对象都是一个知道内部内存布局的类。

这是不幸的,而不是纯粹的OO,但这是一个权衡,甚至"硬核OO"像Smalltalk这样的语言可以用于某些核心数据类型。 (例如数字,字符串,数组和布尔值。)

在YARV,JRuby等实现中,核心库的重要部分通过对实现内部的特权访问来实现,还有另一个问题,因为它非常诱人(并且没有为了更方便的实现,核心方法绕过Ruby语义的方法可以防止这种情况发生。一个完全不相关的例子:实现各种复杂的"重载" C中的YARV中的next或Java中的JRuby中的prev很容易:在YARV中,C函数具有对解释器内部的特权访问,因此可以检查以某人试图重新实现的方式传递的参数。 Ruby中的方法不能,在JRuby中,有一些胶水魔法允许你将这些重载方法实现为实际的Java重载方法,以获得更多便利。

同样,由于所有核心方法都具有对实现的GC内存中对象的内部表示的特权访问,因此它们通常会通过直接检查其内存中的表示来检查对象的类,而不是通过{ {1}},Enumerable#injectclassis_a?

答案 1 :(得分:1)

看看SimpleDelegator。我认为这将满足您的需求。

答案 2 :(得分:0)

这是我的(可能是问题的不完整解决方案)。为了使其工作,所有ruby对象必须在RBProxyObject中“打包”,如下所示:

class RBProxyObject

  attr_reader :ruby_obj

  def initialize(ruby_obj)
    @ruby_obj = ruby_obj
  end

  def is_a?(klass)
    @ruby_obj.is_a?(klass)
  end

  def kind_of?(klass)
    @ruby_obj.is_a?(klass)
  end

  def method_missing(symbol, *args, &blk)
    begin
      @ruby_obj.send(symbol, *args, &blk)
    rescue TypeError
      args[0].native(symbol, @ruby_obj, &blk)
    end
  end

  def native(*args)
    method = args.shift
    other = args.shift
    other.send(method, @ruby_obj, *args)
  end

end

现在应该如何使用它:

a1 = [1, 2, 3]
a2 = [4, 5]

p1 = RBProxyObject.new(a1)
p2 = RBProxyObject.new(a2)


> p p1[0]
  1
> p p2[1]
  5

> p p1.is_a? Array
  true

> p p1.concat(p2)
  [1, 2, 3, 4, 5]

应该在这个类中添加哪些其他方法以及它可以在哪里破解?感谢...