Rails 发现一个奇怪的 case:全局变量中的对象 class 竟然不相等

tomanderson · 2021年11月24日 · 最后由 xiaox 回复于 2021年11月26日 · 659 次阅读

rails 中,initializer 里 new 一个 Clazz 对象加入数组(全局变量): clazz = Clazz.new $arr = [clazz]

rails 启动后,再判断$arr 中的 clazz 对象是否为 Clazz: Clazz === $arr.first

得到的结果竟然是 false?!

查看$arr.first.class,确实是 Clazz,当然它也可以正常执行 Clazz 类的任何实例方法。

所以这是为什么呢?请教。

我 new 了一个空白项目,只增加 2 行代码,复现了这个问题。

方法如下:

  1. rails new rails_test --api
  2. rails g model Clazz
  3. 在 initializers 下新增 global_vars.rb,内容为 $clazz = Clazz.new
  4. 在 controllers 新增 test_controller,内容为 render json: Clazz === $clazz
  5. 访问 localhost:3000/test,显示 false(routes.rb 需相应修改)

默认的初始项目,rails 版本是最新的,ruby 版本是 3.0.1,不过 3.0.2 也试过一样。

好像传不了附件,那我就把项目打包放到这个网址了,想复现的可以下载:

https://fanyipdf.com/rails_test.zip

提供一个最小的可重现的项目,让大伙看看具体是怎么回事吧。

=== 的行为是可以覆盖的,看 Clazz 有没有覆盖这个行为。

Rei 回复

查了没有,而且用 instance_of?也是一样的

qichunren 回复

做出来了,rails new 一个空白项目,两行代码重现。明天发上来供大家围观,不会真的是 rails 的 bug 吧。。

可能这个 Clazz 被 remove_const 过?

顺手在 rails 6.1.1 上试了下,完全没复现。一切正常

我测试了你提供的项目,发现的确有这个问题。原因待查。我用 is_a?也是有问题。

qichunren 回复

好像只有在 controller、model 中调用全局变量才会这样,如果在 global_vars 直接判断是正常的。is_a、instance_of、kind_of 我全试过了都一样

tomanderson 回复

我找到原因了。我首先是去 github 上翻找 ISSUE,找到了 2012 年的这个, 原因在于 development 模式下,app 目录下的代码会重新加载。

有两种方法可以说明这个问题:

  1. 在 production 模式,测试结果是预期的: bundle exec rails s -e production
  2. 将 clazz 的文件从 app/models 目录中移动到 lib 目录,然后在 initializer 中手动 require 一下:require File.expand_path("../../../lib/clazz.rb", __FILE__),测试结果是预期的。
qichunren 回复

感谢大佬,这么快就找到原因了😀 能否再请指教一下,应该怎样写这个代码呢?我只是想在 controller/model 使用全局变量而已,如果 dev 和 pro 模式的行为不一样,难道要写两种方法来判断吗?

tomanderson 回复

不推荐使用全局变量,你使用全局变量最终是要实现什么样的需求?不用全局变量也应该可以实现的。

我调试了一下,开发环境下在 initializer 里面打印 Clazz 的 object_id 和在 rails console 里面打印的 object_id 是不同的,不仅如此,每次修改 Clazz 的代码,它的 object_id 都会变化。但是生产模式不会有这个问题,所以我猜想是 Rails 开发模式的 auto reload 造成的。

我查文档证实了这个想法 https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#reloading

为了实现开发过程修改源码后立即反应到已经启动的应用里面,实际上 Rails auto reload 会移除旧的 class 常量,然后用同一个名字新建一个 class。class 已经不同了,所以用 === 检查是 false。详情可以看上面的连接。

结论是,在 auto reload 开启的环境下,不要 cache 可以 reload 的 class/module。

qichunren 回复

我需要在运行时记录一个状态值,运行过程中值会变化,多个不同的方法都需要用到这个值,而且多个 workers 需要能取到。当然不用全局变量肯定也有别的办法,比如 redis,但全局变量是我能想到的最简单的方法了。

Rei 回复

试了下,确实把 config.cache_classes 设为 true 就正常了。但这样的话 dev 模式下就不能改了代码直接生效了,还有什么别的解法吗

tomanderson 回复

全局变量是代码的 bad smell,把需求说出来看有没有其他解法。

tomanderson 回复

把类定义放在 auto load path 以外的地方,例如 `lib'。

全局变量在多进程模式是不会共享的,在一个进程改了另一个进程还是旧的。比较推荐 redis。

“记录一个状态值”,具体是什么值呢?不同的数据有不同的处理方法,有持久化的,动态缓存的,或是只有启动时更改的配置。不同的数据有不同的处理方法。要说具体需求才能给出合适的解答。

建议看看 X-Y Problem https://coolshell.cn/articles/10804.html

用全局变量确实不好,但就这个 load 的多次的情况是有解的:

# ActiveSupport.on_load(:action_controller) do |base|
ActiveSupport.on_load(:active_record) do |base|
  $clazz = Clazz.new

  p Clazz === $clazz
end

(两个 callback 都可以)

不确定你具体需求,如果只需要这个全局变量存活于一个 request 周期内,那 Rails 提供了一个https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html

好问题。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号