Spec-Zone.ru › Ruby 3.4

Уточнения

Благодаря открытым классам Ruby можно переопределять существующие классы или добавлять в них функциональность. Это называется «monkey patch». К сожалению, область действия таких изменений глобальна. Все пользователи класса, изменённого с помощью monkey patch, видят одни и те же изменения. Это может привести к непреднамеренным побочным эффектам или нарушить работу программ.

Уточнения предназначены для уменьшения влияния monkey patch на других пользователей изменённого класса. Уточнения позволяют локально расширить класс. Уточнения могут изменять как классы, так и модули.

Пример простого уточнения:

class C
  def foo
    puts "C#foo"
  end
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

Сначала определяется класс C. Затем с помощью Module#refine создаётся уточнение для C.

Module#refine создаёт анонимный модуль, содержащий изменения или уточнения класса (C в примере). self в блоке refine — это этот анонимный модуль, аналогично Module#module_eval.

Активируйте уточнение с помощью using:

using M

c = C.new

c.foo # prints "C#foo in M"

Область действия

Уточнения можно активировать на верхнем уровне, а также внутри классов и модулей. Активировать уточнения в области видимости метода нельзя. Уточнения остаются активными до конца определения текущего класса или модуля либо до конца текущего файла, если они используются на верхнем уровне.

Уточнения можно активировать в строке, переданной в Kernel#eval. Уточнения остаются активными до конца строки eval.

Область действия уточнений определяется лексически. Уточнения активны только в области видимости после вызова using. Уточнение не будет активировано для кода, расположенного до оператора using.

При выходе за пределы области видимости уточнение деактивируется. Это означает, что уточнение деактивируется, если вы подключаете или загружаете файл либо вызываете метод, определённый вне текущей области видимости:

class C
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

def call_foo(x)
  x.foo
end

using M

x = C.new
x.foo       # prints "C#foo in M"
call_foo(x) #=> raises NoMethodError

Если метод определён в области видимости, где активно уточнение, уточнение будет активно при вызове этого метода. В этом примере используются несколько файлов:

c.rb:

class C
end

m.rb:

require "c"

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

m_user.rb:

require "m"

using M

class MUser
  def call_foo(x)
    x.foo
  end
end

main.rb:

require "m_user"

x = C.new
m_user = MUser.new
m_user.call_foo(x) # prints "C#foo in M"
x.foo              #=> raises NoMethodError

Поскольку уточнение M активно в m_user.rb, где определён MUser#call_foo, оно также активно, когда main.rb вызывает call_foo.

Поскольку using является методом, уточнения активны только после его вызова. Вот примеры, показывающие, когда уточнение M активно, а когда нет.

В файле:

# not activated here
using M
# activated here
class Foo
  # activated here
  def foo
    # activated here
  end
  # activated here
end
# activated here

В классе:

# not activated here
class Foo
  # not activated here
  def foo
    # not activated here
  end
  using M
  # activated here
  def bar
    # activated here
  end
  # activated here
end
# not activated here

Обратите внимание, что уточнения в M не активируются автоматически, если класс Foo впоследствии открывается повторно.

В eval:

# not activated here
eval <<EOF
  # not activated here
  using M
  # activated here
EOF
# not activated here

Без вычисления:

# not activated here
if false
  using M
end
# not activated here

Если в одном и том же модуле определено несколько уточнений в нескольких блоках refine, при вызове уточнённого метода (любого из методов to_json в примере ниже) активны все уточнения из этого модуля:

module ToJSON
  refine Integer do
    def to_json
      to_s
    end
  end

  refine Array do
    def to_json
      "[" + map { |i| i.to_json }.join(",") + "]"
    end
  end

  refine Hash do
    def to_json
      "{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
    end
  end
end

using ToJSON

p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]"

Поиск методов

При поиске метода для экземпляра класса C Ruby проверяет:

  • Если для C активны уточнения, они проверяются в обратном порядке активации:

    • Модули, добавленные перед уточнением для C

    • Уточнение для C

    • Модули, подключённые к уточнению для C

  • Модули, добавленные перед C

  • C

  • Модули, подключённые к C

Если метод не найден ни на одном из этапов, поиск повторяется для суперкласса C.

Обратите внимание, что методы подкласса имеют приоритет над уточнениями суперкласса. Например, если метод / определён в уточнении для Numeric, 1 / 2 вызывает исходный Integer#/, поскольку Integer является подклассом Numeric и проверяется раньше уточнений суперкласса Numeric. Поскольку метод / также присутствует в дочернем классе Integer, поиск метода не переходит к суперклассу.

Однако если метод foo определён для Numeric в уточнении, 1.foo вызывает этот метод, поскольку foo отсутствует в Integer.

super

При вызове super поиск метода проверяет:

  • Подключённые модули текущего класса. Обратите внимание, что текущим классом может быть уточнение.

  • Если текущий класс является уточнением, поиск метода продолжается так, как описано выше в разделе «Поиск методов».

  • Если у текущего класса есть непосредственный суперкласс, поиск метода продолжается так, как описано выше в разделе «Поиск методов», но для суперкласса.

Обратите внимание, что super в методе уточнения вызывает метод уточняемого класса, даже если в том же контексте активировано другое уточнение. Это справедливо только для super в методе уточнения и не относится к super в методе модуля, подключённого к уточнению.

Интроспекция методов

При использовании методов интроспекции, таких как Kernel#method или Kernel#methods, уточнения не учитываются.

В будущем это поведение может измениться.

Refinement — наследование через Module#include

Если модуль X подключён к модулю Y, Y наследует уточнения X.

Например, в следующем коде C наследует уточнения A и B:

module A
  refine X do ... end
  refine Y do ... end
end
module B
  refine Z do ... end
end
module C
  include A
  include B
end

using C
# Refinements in A and B are activated here.

Уточнения в потомках имеют более высокий приоритет, чем уточнения предков.

Дополнительная литература

Текущую спецификацию реализации уточнений см. на странице github.com/ruby/ruby/wiki/Refinements-Spec. Спецификация также содержит дополнительные сведения.

Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API