Ruby 3.5 新特性:读取时的命名空间 (Namespace on read)
项目
通用
个人资料
Ruby
自定义查询
操作 复制链接 喜欢3
功能 #21311
打开
读取时的命名空间 (修订版)
由 tagomoris (Satoshi Tagomori) 添加于 6 天前。更新于 大约 3 小时前。 状态: 打开 负责人: tagomoris (Satoshi Tagomori) 目标版本: 3.5 [ruby-core:121837] 描述 本功能取代了 #19744
概念¶
本提案提出一个新特性,用于在 Ruby 中定义虚拟的顶层命名空间。这些命名空间可以独立于其他命名空间 require/load
库(.rb
文件或原生扩展)。required/loaded
库的依赖也会在命名空间中 required/loaded
。
此功能在初始阶段将默认禁用,并通过环境变量 RUBY_NAMESPACE=1
作为实验性功能启用。(将来可能会默认启用。)
"读取时" 方法¶
这里的 "写入时" 方法是指在加载端定义命名空间的设计。例如,Java 包是在 .java
文件中定义的,并且需要将命名空间彼此分离。它可以很容易地实现,但它需要所有的库都使用包声明进行更新。(在我看来,这在 Ruby 生态系统中几乎是不可能的。)
"读取时" 方法是创建命名空间,然后在其中 require/load
应用程序和库。程序员可以在 "读取" 时控制命名空间的分隔。因此,我们可以逐步引入命名空间的分隔。
动机¶
"读取时的命名空间" 可以解决以下 2 个问题,并且可以为解决另一个问题开辟道路:
- 避免库之间的名称冲突
- 应用程序可以安全地
require
两个使用相同模块名称的不同库。
- 应用程序可以安全地
- 避免意外的全局共享模块/对象
- 应用程序可以创建独立的/非共享的模块实例。
- 可以
require
多个版本的 gem- 如果
rubygems/bundler
将支持读取时的命名空间,应用程序开发者将减少 gem 依赖之间的版本冲突。(需要来自 RubyGems/Bundler 和/或其他打包系统的支持)
- 如果
有关动机的详细信息,请参阅 [功能 #19744]。
如何使用命名空间¶
# app1.rb
PORT = 2048
class App
def self.port = ::PORT
def val = PORT.to_s
end
p App.port # 2048
# app2.rb
class Number
def double = self * 2
end
PORT = 2048.double
class App
def self.port = ::PORT
def val = PORT.double.to_s
end
p App.port # 4096
# main.rb - 执行命令 `ruby main.rb`
ns1 = Namespace.new
ns1.require('./app1') # 2048
ns2 = Namespace.new
ns2.require('./app2') # 4096
PORT = 8080
class App
def self.port = ::PORT
def val = PORT.to_s
end
p App.port # 8080
p App.new.val # "8080"
p ns1::App.port # 2048
p ns1::App.new.val # "2048"
p ns2::App.port # 4096
p ns2::App.new.val # "8192"
1.double # NoMethodError
命名空间规范¶
命名空间的类型¶
有两种命名空间类型,"根 (root)" 命名空间和 "用户 (user)" 命名空间。"根" 命名空间唯一存在于 Ruby 进程中,而 Ruby 程序员可以根据需要创建任意数量的 "用户" 命名空间。
根命名空间¶
根命名空间是一个唯一的命名空间,在 Ruby 进程启动时定义。它只包含内置的类/模块/常量,这些类/模块/常量无需任何 require
调用即可使用,包括 RubyGems 本身(当未指定 --disable-gems
时)。
在此,"内置" 类/模块是指用户脚本开始评估时无需任何 require/load
调用即可访问的类/模块。
用户命名空间¶
用户命名空间是运行用户 Ruby 脚本的命名空间。"main" 命名空间是运行由 ruby
命令行参数指定的用户 .rb
脚本的命名空间。其他用户命名空间("可选 (optional)" 命名空间)可以通过 Namespace.new
调用创建。
在用户命名空间(包括 main 命名空间和可选命名空间)中,内置类/模块的定义从根命名空间复制,而其他新的类/模块在命名空间中定义,与其他(根/用户)命名空间分开。新定义的类/模块是 main 命名空间中的顶层类/模块,例如 App
,但在可选命名空间中,类/模块在命名空间(Module 的子类)下定义,例如 ns::App
。
在该命名空间 ns
中,ns::App
可以作为 App
(或 ::App
)访问。无法从不同命名空间 ns
的代码访问 main 命名空间中的 App
。
常量、类变量和全局变量¶
常量、内置类的类变量和全局变量也按命名空间分隔。在命名空间中设置的类/全局变量的值在其他命名空间中不可见。
方法和 Proc¶
在命名空间中定义的方法以定义的命名空间运行,即使从其他命名空间调用也是如此。在命名空间中创建的 Proc 也以定义的命名空间运行。
动态链接库¶
动态链接库(通常是 .so
文件)以及 .rb
文件也加载到命名空间中。
开放类(对内置类的更改)¶
在用户命名空间中,可以修改内置类定义。但是,这些操作被处理为从根命名空间复制类定义时的写时复制 (copy-on-write),并且更改后的定义仅在(用户)命名空间中可见。
无法从其他命名空间修改根命名空间中的定义。在根命名空间中定义的方法仅使用根命名空间定义运行。
启用命名空间¶
启动 Ruby 进程时,指定 RUBY_NAMESPACE=1
环境变量。此处 1
是唯一有效的值。
只能在 Ruby 进程启动时启用命名空间功能。启动 Ruby 脚本后设置 RUBY_NAMESPACE=1
不起作用。
Pull-request¶
https://github.com/ruby/ruby/pull/13226 相关问题 1 (0 个未解决 — 1 个已关闭) 与 Ruby 相关 - 功能 #19744: 读取时的命名空间| 已关闭| 操作 ---|---|--- 与以下相关:是以下内容的副本:有以下副本:被以下内容阻止:位于以下内容之前:跟随:复制到:从以下内容复制:问题 # 延迟:天 取消
喜欢1操作 复制链接 #1 [ruby-core:121839]
由 baweaver (Brandon Weaver) 更新于 6 天前
作为概念验证,这是一个非常有价值的想法,并且将给用户一个实验的机会。
我想知道这在长期的人体工程学方面如何,以及是否可能在 Ruby 4 中引入一个新的 namespace
关键字来包装,该关键字比 module
更强大:
namespace NamespaceOne
require "./app1"
end
namespace NamespaceTwo
require "./app2"
end
p NamespaceOne::App.port # 2048
p NamespaceOne::App.val # "2048"
p NamespaceTwo::App.port # 4096
p NamespaceTwo::App.val # "8192"
在 namespace
内部运行的 require
可以提供与上述相同的功能,但也可以为定义其他代码提供一个隔离的环境:
namespace Payrolls
class Calculator; end
private class RunTaxes; end
end
Payrolls::Calculator # 可以访问
Payrolls:RunTaxes # 引发违规错误
namespace Payments
class RecordTransaction; end
end
对于 Ruby 3.x,我同意所提出的语法适合实验,但会要求我们考虑在 Ruby 4.x 中使用 namespace
关键字使它成为一个顶层概念,以完全隔离包装的状态。
喜欢0操作
复制链接
#2 [ruby-core:121840]
由 fxn (Xavier Noria) 更新于 6 天前 · 已编辑
几个快速问题:
假设一个正常的执行上下文,文件中顶层的嵌套是空的。如果该文件在命名空间下加载,它也会是空的吗?
描述中提到了类和模块,这有点直观。它们是相关的,因为它们是常量的容器。但是,我们知道,常量可以存储类和模块对象之外的任何东西。特别是,来自根命名空间的常量,递归地,可以存储任何类型的对象,这些对象在内部可以引用任何其他对象。这里有一个指针图。
那么,当创建一个命名空间时,我们是否必须认为整个对象树都被深度克隆了?(可能使用 CoW,但在概念上?)例如,让我们想象 C::X
是根命名空间中的一个字符串,我们创建 ns
。ns::C::X.clear
会清除两个命名空间中的字符串吗?
我想全局变量会保持全局状态?
喜欢1操作
复制链接
#3 [ruby-core:121841]
由 tagomoris (Satoshi Tagomori) 更新于 6 天前
@baweaver 我对添加 namespace
关键字没有强烈的意见,但是通过在 Namespace.new
上添加一个块参数可以提供类似的 UX,而无需更改语法。
NamespaceOne = Namespace.new do
require "./app1"
end
p NamespaceOne::App.port #=> 2048
这看起来不那么聪明,但可能不是最糟糕的。拥有 Kernel#namespace
可能是另一种选择。
NamespaceOne = namespace do
require "./app1"
end
喜欢0操作 复制链接 #4 [ruby-core:121842]
由 tagomoris (Satoshi Tagomori) 更新于 6 天前
fxn (Xavier Noria) 在 #note-2 中写道:
几个快速问题: 假设一个正常的执行上下文,文件中顶层的嵌套是空的。如果该文件在命名空间下加载,它也会是空的吗?
是的。那时,self
将是一个从可选命名空间中的 main
克隆(不同)的对象。
那么,当创建一个命名空间时,我们是否必须认为整个对象树都被深度克隆了?(可能使用 CoW,但在概念上?) 从概念上讲,是的。定义被深度克隆。但是对象(存储在常量等中)不会被克隆(参见下文)。 例如,让我们想象
C::X
是根命名空间中的一个字符串,我们创建ns
。ns::C::X.clear
会清除两个命名空间中的字符串吗?
是的。(我希望内置的类/模块没有这样的可变对象,但它们应该有 :-( )
我想全局变量会保持全局状态?
全局变量也按命名空间分隔。想象一下 $LOAD_PATH
和 $LOADED_FEATURES
,它们具有不同的加载路径集和实际加载的文件路径,这些路径在每个命名空间中应该是不同的。为库或其他应用程序提供对全局变量意外更改的保护是命名空间概念的一部分。
喜欢0操作
复制链接
#5
由 tagomoris (Satoshi Tagomori) 更新于 6 天前
- 描述 已更新 (差异)
喜欢0操作 复制链接 #6 [ruby-core:121845]
由 fxn (Xavier Noria) 更新于 6 天前 · 已编辑
感谢 @tagomoris (Satoshi Tagomori)。
从概念上讲,是的。定义被深度克隆。但是对象(存储在常量等中)不会被克隆(参见下文)。
让我更好地理解这一点。
在 Ruby 中,对象存储在常量中。从概念上讲,存储字符串对象的常量 X
和存储类对象的常量 C
在根本上没有区别。您的意思是命名空间创建会遍历常量树,仅克隆作为类和模块对象的值,并保留其余的对象引用,这些引用在命名空间之间共享?
即使在类和模块的情况下,它们的 ivar 中的对象会发生什么?
我不知道关于内置的内容,但在用户定义的类/模块的情况下,我不认为我们可以假设它们不会改变它们的状态。当创建命名空间时,我们可以在根命名空间中有 2500 个它们。 喜欢0操作 复制链接 #7 [ruby-core:121846]
由 tagomoris (Satoshi Tagomori) 更新于 6 天前
fxn (Xavier Noria) 在 #note-6 中写道:
在 Ruby 中,对象存储在常量中。从概念上讲,存储字符串对象的常量
X
和存储类对象的常量C
在根本上没有区别。您的意思是命名空间创建会遍历常量树,仅克隆作为类和模块对象的值,并保留其余的对象引用,这些引用在命名空间之间共享?
例如,String
是一个内置的类和 Class
对象值,存储为 ::String
常量。在命名空间 ns1
中,我们可以更改 String
的定义(例如,添加一个常量 String::X = "x"
)。但是即使在这种情况下,String
的值也是相同的。::String == ns1::String
返回 true。
这意味着,值(CRuby 世界中的 VALUE
)是相同的,并且在创建命名空间时不复制,但是支持的类定义(结构 rb_classext_t
)是不同的,并且这些是 CoW 目标。
即使在类和模块的情况下,它们的 ivar 中的对象会发生什么?
类 ivar(类的实例变量表)被复制,但 ivar 值不复制。这类似于类的常量(常量表)。
我不知道关于内置的内容,但在用户定义的类/模块的情况下,我不认为我们可以假设它们不会改变它们的状态。当创建命名空间时,我们可以在根命名空间中有 2500 个它们。
在命名空间上下文中,"内置类/模块" 是在任何用户脚本评估之前定义的类和模块。(我将很快更新票证描述。)它们的总数是,类 685,模块 40(和内部 iclass 51)。任何用户定义的类/模块都不会在根命名空间中定义。 喜欢0操作 复制链接 #8
由 tagomoris (Satoshi Tagomori) 更新于 6 天前
- 描述 已更新 (差异)
喜欢0操作 复制链接 #9 [ruby-core:121848]
由 fxn (Xavier Noria) 更新于 6 天前
任何用户定义的类/模块都不会在根命名空间中定义。 啊,这是关键。 那么,在这个脚本中会发生什么?
# main.rb
App = Class.new
ns1 = Namespace.new
ns1.require("./app1") # 定义/重新打开 App
App
和 ns1::App
是否具有相同的对象 ID?
或者,该特性是否假设如果你想隔离事物,那必须是创建任何常量、全局变量等之前的第一件事?
喜欢0操作
复制链接
#10 [ruby-core:121849]
由 byroot (Jean Boussier) 更新于 6 天前
在 Namespace.new 上添加块参数可以提供类似的 UX,而无需更改语法。 但这无法正确处理常量定义。类似于人们今天被
Struct.new do
欺骗的方式。
Foo = Struct.new(:bar) do
BAZ = 1 # 这是 Object::BAZ
end
这就是我提交 [功能 #20993] 的原因,它允许你执行以下操作:
module MyNamespace = Namespace.new
BAZ = 1 # 这是 MyNamespace::BAZ
end
喜欢0操作 复制链接 #11 [ruby-core:121851]
由 Eregon (Benoit Daloze) 更新于 6 天前
@fxn main 命名空间和用户命名空间是独立的,但 main 命名空间可以通过 ns::SomeConstant
引用用户命名空间。因此,此处的 App
从 main
在 ns1
中是无法访问的,实际上在 main 命名空间中定义的所有常量在用户命名空间中都是无法访问的,请参阅 https://bugs.ruby-lang.org/issues/21311#User-namespace 的末尾。
喜欢0操作
复制链接
#12 [ruby-core:121852]
由 Eregon (Benoit Daloze) 更新于 6 天前 · 已编辑
我认为这通过在每个命名空间(包括 main 命名空间)中拥有所有内置类/模块的 CoW 副本来解决 https://bugs.ruby-lang.org/issues/19744#note-74,很好。从快速阅读来看,它对我来说听起来是正确的。在实践中,语义可能有些令人惊讶:
- 例如,
String#start_with?
在所有命名空间中都可用,但String#to_time
仅在加载了activesupport
的命名空间中可用(如果你知道哪些方法是核心方法,这很清楚,但正如我们从民意调查中看到的那样,这并不总是很清楚)。 - 核心类和模块是写时复制和共享引用,但是用户定义的类和模块是完全独立的(除非 main 命名空间可以通过
ns::Foo
显式地引用来自另一个命名空间的任何内容,甚至可以存储它们),这是一种双重情况,并且有点不一致。我认为这在语义上是必要的,因为来自另一个命名空间的String
仍然应该是obj.is_a?(String)
。 - 任何用户定义的类实例在另一个命名空间中都不会是
is_a?
,并且这对于 stdlib/默认 gem/捆绑的 gem 来说可能特别令人困惑,例如,在ns1
中创建的Date
或Pathname
在main
中不会是is_a?(Date)
,例如ns1::TODAY.is_a?(Date) # => false
或ns1::Date.today # => false
。此外Pathname('/') == ns1::Pathname('/') # => false
。(所有这些示例都在main
命名空间中运行)
对于最后一点,我怀疑可能需要一种以某种方式将对象从一个命名空间转换到另一个命名空间的方法,这听起来很难。除非它们真的不需要命名空间之间的任何通信,否则不同的进程(或进程中的多个解释器)可能是更好的权衡(特别是可以并行运行和更强的隔离)。 喜欢3操作 复制链接 #13 [ruby-core:121853]
由 byroot (Jean Boussier) 更新于 6 天前
虽然我认为命名空间是对 Ruby 的一个很好的补充,但我并不相信这个命名空间的特定实现是 Ruby 所需要的。 首先,我不相信动机:
避免库之间的名称冲突:应用程序可以安全地
require
两个使用相同模块名称的不同库。 这是一个经常发生的问题吗?我相信 Ruby 有一个非常成熟的约定,即库公开一个模块,其名称与其 gem 名称相对应。 在我看来,实际的顶层模块名称冲突非常罕见。 避免意外的全局共享模块/对象 同样,从我的经验来看,这种情况非常罕见,通常被认为是 bug,并迅速修复。 我们是否有具体案例表明这是一个持续存在的问题? 可以require
多个版本的 gem 我记得过去对此进行过讨论。就个人而言,这是一个我强烈反对的功能,因为它极难推理。 如果你有一个库 A 使用 gem G 的版本 1,和一个库 B 使用 gem G 的版本 2,并且最终将一个 G-v2 对象传递给 A,你可能会陷入一个痛苦的世界。 我知道这个功能对于 bundler 特别有用,可以让他们在内部使用 gem 而不会与应用程序冲突(他们目前通过 vendor 来解决这个问题),但除此之外,我不相信这是一个理想的功能。 我明白你可能会遇到一种棘手的情况,即两个依赖项本质上是不兼容的,因为它们需要另一个依赖项的冲突版本,就像几年前 Faraday 2 过渡时发生的那样,但我并不相信以这种方式解决问题是一个净收益。 命名空间 monkey patch 这个不在你的票证中,但从之前的公开演讲中我了解到它是其中之一? 同样,我想问一下 monkey patch 到底有多大的问题。诚然,15 年前,许多流行的 gem 会不负责任地 monkey patch 核心类,但我相信这些日子早已过去。除了 ActiveSupport(它作为框架通过)之外,很少有 gem 附带 monkey patch。 一个值得注意的例外是 "协议" 类型的方法,例如to_json
、to_yaml
、to_msgpack
等。 此外,我在等待修复程序