Мимируйте еще один класс Ruby, чтобы объект прошел проверку типа ===

Я хочу создать объект, который действует как определенный класс, например Fixnum, но не является экземпляром этого класса и его подклассов.

Для этого существуют различные варианты использования. В случае Fixnum я хочу определить более конкретный целочисленный тип, который по существу является Fixnum, но также реализует некоторую дополнительную логику. Я не могу подклассифицировать Fixnum, потому что немедленные типы, такие как Fixnum и Symbol, не могут быть подклассы.

Другой пример использования - насмешка в автоматизированных тестах: иногда вы хотите создать объект, который действует как определенный класс (обычно это экземпляр модели), но по техническим причинам не является экземпляром этого точного класса.

Здесь, как создать определенный целочисленный тип, который делегирует все методы встроенной памяти fixnum:

require 'delegate'
require 'forwardable'

# integer representing a page number
class PageNumber < DelegateClass(Integer)
  extend Forwardable

  def initialize(value, name)
    @name = name
    super(value)
  end

  def inspect
    "#{@name} #{to_i}"
  end

  alias_method :to_i, :__getobj__
  def_delegators :to_i, :instance_of?, :kind_of?, :is_a?
end

Этот объект может пройти is_a? и аналогичные проверки:

page = PageNumber.new(1, "page")
page.is_a? Fixnum  #=> true

Но я ничего не могу сделать, чтобы пройти проверку типа Module#===:

# my problem:
Fixnum === page    #=> false

Тот факт, что мой объект не прошел эту проверку, очень неудачен, так как метод === используется внутри case:

case page
when Fixnum
  # it will never get here
when String
  # ...
else
  # ...
end

Мой вопрос в том, как я могу создать макет типа, который проходит проверку ===, не увеличивая методы === на встроенных классах?

Ответ 1

Это хакерское решение, о котором я предупреждал в своем вопросе:

Fixnum === page  #=> false

Numeric.extend Module.new {
  def ===(obj)
    obj.instance_of?(PageNumber) or super
  end
}

Fixnum === page  #=> true

Он решает проблему, но поднимает вопрос, безопасно ли это делать? Я не могу думать о каких-либо недостатках этого метода, но, поскольку мы возимся с очень важным методом, это может быть не то, что мы хотели бы сделать.

Ответ 2

Если мы говорим о МРТ 1 ответ прост: вы не можете.

Метод Module#=== - это псевдоним rb_obj_is_kind_of API-метод API. Реализация последней настолько коротка, что я буду вставлять ее здесь:

VALUE
rb_obj_is_kind_of(VALUE obj, VALUE c)
{
    VALUE cl = CLASS_OF(obj);

    /* Type checking of `c' omitted */

    while (cl) {
    if (cl == c || RCLASS_M_TBL(cl) == RCLASS_M_TBL(c))
        return Qtrue;
    cl = RCLASS_SUPER(cl);
    }
    return Qfalse;
}

Как вы можете видеть, этот метод обходит предков проверяемого объекта и сравнивает их двумя способами: во-первых, он проверяет, является ли предок тем же самым, что и переданный модуль, а затем проверяет, есть та же таблица методов.

Последняя проверка требуется, потому что включенные модули в Ruby, по-видимому, вставлены в цепочку наследования, но поскольку один модуль может быть включен в несколько других, это не настоящий модуль, который вставлен в цепочку, а прокси-объект, который имеет свои таблицы констант и методов, указывающие на исходный модуль.

Например, рассмотрим предков Object:

ruby-1.9.2-p136 :001 > Object.ancestors
 => [Object, Kernel, BasicObject] 
ruby-1.9.2-p136 :002 > Object.ancestors.map { |mod| Object.new.is_a? mod }
 => [true, true, true] 

Здесь Object и BasicObject будут успешно сопоставлены первой проверкой, а Kernel - вторым.

Даже если вы попытаетесь сделать (с расширением C) прокси-объект, который попытается обмануть метод rb_obj_is_kind_of, он должен будет иметь ту же таблицу методов, что и реальный Fixnum, что эффективно включают все методы Fixnum.


1 Я исследовал внутренности для Ruby 1.9, но они ведут себя точно так же в 1.8.