Ruby:我可以完全透明地将一个 class 包裹在另一个 class 中吗?

Ruby: Can I wrap a class in another class completely transparently?

我需要像下面这样创建一个class(真正的问题更复杂)

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)

最后一句不行,说"no implicit convertion of a2 to array"。 a1 怎么知道 a2 不是数组?我为 a2 实现了 is_a?kind_of?,因此如果询问它是否为数组,它会 returns true。我想要的是 a1 认为 a2 是一个数组,然后在 a2 上调用它需要进行合并的任何方法。

我希望任何其他 class 也能发生同样的情况,即,将它包裹在 class 中,但让它像没有包裹一样工作。

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

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

不可能的主要原因有以下三个:

  • Reference Equality:Reference Equality (BasicObject#equal?) 破坏模拟,因此 OO。 Comparing for Reference Equality 可以让你区分模拟对象和被模拟对象,这应该是不可能的。您可以猴子补丁 BasicObject 来删除或替换 equal?,但这可能会破坏左右。
  • 布尔运算符和条件:布尔运算符和条件仅将nilfalse这两个单例对象视为falsy。无法编写自己的 falsy 对象,因此,无法模拟 nilfalse.
  • Classes 而不是作为类型的协议:一些核心库和标准库方法以及一些硬编码的内部机制期望对象是特定 class 才能与它们一起工作,即使根据 OO 原则,使用相同协议的对象应该具有相同的类型,而不管其 Class.

虽然你可以走得很远:

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

但是,在您的特定情况下,您 运行 遇到了它根本不起作用的情况之一:而 to_ary 让您在制作模拟 Array,在您的情况下,您需要进一步跟踪您的 IArray,但当然将其转换为 Array 会同时失去其身份和其他行为。不幸的是,你完蛋了。您无能为力。

在理想的 OO 世界中,两个使用相同协议的对象应该被认为是同一类型,因此 Array#concat 不应该关心它的参数是否是 [= 的实例29=](或者可以转换为一个),而是它的参数是否使用与 Array 相同的协议(或者更准确地说:使用 concat 实际上使用的协议的子集 需要)。

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

但是,这意味着操作不可能同时检查两个对象的表示(该操作要么是第三个对象上的方法,这意味着它不能检查任何一个对象的表示,要么它是两个对象之一的方法,在这种情况下,它可以检查自己对象的表示,但不能检查另一个对象的表示),这意味着不可能在 OO 中编写需要同时访问两个对象表示的操作.

例如连接两个链表是 O(1),如果您可以访问第一个列表的最后一个元素的 next 指针,以及第二个列表的第一个元素的 prev 指针,但在 OO 中,您最多可以访问一个两者中的一个(除非这两个列表明确公开了一个 public 方法来访问这两个指针)。将一个数组连接到另一个数组需要快速访问两个数组的内部表示,因此 Ruby 决定在这里打破 OO 封装并要求两个对象都是 class 它知道内部

的内存布局

这很不幸,而且不完全是面向对象的,但即使是像 Smalltalk 这样的 "hardcore OO" 语言也会为它们的一些核心数据类型做出权衡。 (例如数字、字符串、数组和布尔值。)

在像 YARV、JRuby 和其他实现中,核心库的重要部分是通过对实现内部的特权访问来实现的,还有另一个问题,因为它非常诱人(并且没有办法阻止这种情况)核心方法绕过 Ruby 语义,以便更方便地实现。一个完全不相关的例子:在 C 中的 YARV 中实现 Enumerable#inject 的各种复合体 "overloads",或者在 Java 中的 JRuby 中实现很容易:在 YARV 中,C 函数具有特权访问解释器内部结构,因此可以检查以某些人试图重新实现 Ruby 中的方法所传递的参数,而在 JRuby 中,有一些胶水魔法可以让你实现这些重载方法作为实际 Java 重载方法,以提供更多便利。

同样,由于所有核心方法都有权访问对象在实现的 GC 内存中的内部表示,因此它们通常会通过直接检查对象在内存中的表示而不是去检查对象的 class通过 classis_a?kind_of?instance_of?.

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

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]

还有哪些其他方法应该添加到这个 class 中,它会在哪里中断?谢谢...