Можно ли дать подмодулю то же имя, что и класс верхнего уровня?
Фон:
Здесь проблема, отведенная до минимального примера:
# bar.rb
class Bar
end
# foo/bar.rb
module Foo::Bar
end
# foo.rb
class Foo
include Foo::Bar
end
# runner.rb
require 'bar'
require 'foo'
➔ ruby runner.rb
./foo.rb:2: warning: toplevel constant Bar referenced by Foo::Bar
./foo.rb:2:in `include': wrong argument type Class (expected Module) (TypeError)
from ./foo.rb:2
from runner.rb:2:in `require'
from runner.rb:2
Ответы
Ответ 1
Отлично; ваш образец кода очень разъясняет. То, что у вас есть, - это круговая зависимость, характерная для сада, скрытая особенностями оператора Ruby scope-resolution.
Когда вы запускаете код Ruby require 'foo'
, ruby находит foo.rb
и выполняет его, а затем находит foo/bar.rb
и выполняет это. Поэтому, когда Ruby встречает ваш класс Foo
и выполняет include Foo::Bar
, он ищет константу с именем Bar
в классе Foo
, потому что это означает Foo::Bar
. Когда он не находит его, он ищет другие охватывающие области для констант с именем Bar
и в конечном итоге находит его на верхнем уровне. Но это Bar
является классом и поэтому не может быть include
d.
Даже если вы могли бы убедить require
выполнить foo/bar.rb
до foo.rb
, это не поможет; module Foo::Bar
означает "найти константу Foo
, а если это класс или модуль, начните определять модуль внутри него под названием Bar
". Foo
еще не будет создан, поэтому запрос все равно не будет выполнен.
Переименование Foo::Bar
в Foo::UserBar
тоже не поможет, так как столкновение имен не в конечном итоге виновато.
Так что вы можете сделать? На высоком уровне вам нужно как-то сломать цикл. Проще всего определить Foo
в двух частях, например:
# bar.rb
class Bar
A = 4
end
# foo.rb
class Foo
# Stuff that doesn't depend on Foo::Bar goes here.
end
# foo/bar.rb
module Foo::Bar
A = 5
end
class Foo # Yep, we re-open class Foo inside foo/bar.rb
include Bar # Note that you don't need Foo:: as we automatically search Foo first.
end
Bar::A # => 4
Foo::Bar::A # => 5
Надеюсь, что это поможет.
Ответ 2
Вот более минимальный пример, демонстрирующий это поведение:
class Bar; end
class Foo
include Foo::Bar
end
Вывод:
warning: toplevel constant Bar referenced by Foo::Bar
TypeError: wrong argument type Class (expected Module)
И вот еще более минимально:
Bar = 0
class Foo; end
Foo::Bar
Вывод:
warning: toplevel constant Bar referenced by Foo::Bar
Объяснение простое, нет ошибки: в Foo
нет Bar
, а Foo::Bar
еще не определено. Для определения Foo::Bar
сначала необходимо определить Foo
. Следующий код работает нормально:
class Bar; end
class Foo
module ::Foo::Bar; end
include Foo::Bar
end
Однако для меня есть что-то неожиданное. Следующие два блока ведут себя по-другому:
Bar = 0
class Foo; end
Foo::Bar
выдает предупреждение:
warning: toplevel constant Bar referenced by Foo::Bar
но
Bar = 0
module Foo; end
Foo::Bar
создает ошибку:
uninitialized constant Foo::Bar (NameError)
Ответ 3
Вот еще один забавный пример:
module SomeName
class Client
end
end
module Integrations::SomeName::Importer
def perform
...
client = ::SomeName::Client.new(...)
...
end
end
Это производит:
block in load_missing_constant': uninitialized constant Integrations::SomeName::Importer::SomeName (NameError)
Ruby (2.3.4) просто переходит к первому вхождению "SomeName", которое он может найти, а не к верхнему уровню.
Чтобы обойти это, нужно либо использовать лучшее вложение модулей/классов (!!), либо использовать Kernel.const_get('SomeName')