Имитируйте другой класс Ruby, чтобы объект прошел проверку типа === - PullRequest
10 голосов
/ 07 августа 2011

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

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

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

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

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

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

Ответы [ 2 ]

15 голосов
/ 07 августа 2011

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

Метод Module#=== фактически является псевдонимом из rb_obj_is_kind_of C 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, по-видимому, вставляются в цепочку наследования, но, поскольку один модуль может быть включен в несколько других, в цепочку вставляется не реальный модуль, а прокси-объект, у которого есть таблицы констант и методов, указывающие на исходный модуль.

Например, давайте посмотрим на предков объекта:

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.
3 голосов
/ 07 августа 2011

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

Fixnum === page  #=> false

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

Fixnum === page  #=> true

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...