Ruby on rails的常量自动加载

Ruby中常量的运行机制:

1.嵌套

类和模块可以嵌套,来组成命名空间
命名空间在ruby中就是一个沙盒,在其中定义的常量名和方法名可以不用担心名称与其他常量名和方法名冲突

module XML
    class SAXParser
        # (1)
        puts Module.nesting 
    end
end

可以在代码的任意位置调用Module.nesting来审查当前的嵌套结构,比如(1)处的嵌套就是[XML::SAXParser, XML]

借着上述的类XML::SAXParser来解释几个关键概念:

  • 该类的名字叫做XML::SAXParser
  • 该类内部的嵌套为 [XML::SAXParser, XML]
  • 该类内部的命名空间是 XML::SAXParser
  • 该类的嵌套是由类XML::SAXParser和模块XML组成,外层是一个模块,第二层一个类
这里还涉及一个官方特意点出的概念:

组成嵌套的是类和模块“对象”,而不是访问它们的常量,与它们的名称也没有关系。

解析:
  1. 组成嵌套的是类和模块“对象”,首先这里的类和模块对象指的是它们都是ClassModule的对象,在ruby中我们常用的类它们本身也是个对象,只不过它们是Class的对象,而模块是Module的对象,从上面的Module.nesting结果也可以看到,嵌套中有个2个元素,一个类XML::SAXParser和一个模块XML
  2. “而不是访问它们的常量”,在上面的例子中我们是通过XML::SAXParser常量即可访问到我们定义的类,但事实上同样的常量名,是可以有完全不同的嵌套结构的,可以见下面的示例
  3. “与它们的名称也没有关系”,的确,在上面定义的类中,该类的名称是"XML::SAXParser",但是对于另一种定义该类的方式,名称一样的情况下,嵌套也可以截然不同,见下面的示例
class XML::SAXParser
# (2)
    puts Module.nesting
end
puts XML::SAXParser.name

上面这个例子中,访问的常量也是XML::SAXParser,常量的名称也是"XML::SAXParser",所以说光看到一个常量的名称或者访问它的方式,是无法确定它的嵌套的。

为什么要弄清嵌套的含义呢?

因为嵌套影响着其内部的常量的查找的方式,这样当在一个嵌套内使用一个未定义的常量的时候我们能够知道这个常量是如何查找的

嵌套是解释器维护的一个内部堆栈,根据以下规则修改:

  • 执行 class 关键字后面的定义体时,类对象入栈;执行完毕后出栈。

  • 执行 module 关键字后面的定义体时,模块对象入栈;执行完毕后出栈。

  • 执行 class << object 打开的单例类时,类对象入栈;执行完毕后出栈。

  • 调用 instance_eval 时如果传入字符串参数,接收者的单例类入栈求值的代码所在的嵌套层次。调用 class_eval 或 module_eval 时如果传入字符串参数,接收者入栈求值的代码所在的嵌套层次.

  • 顶层代码中由 Kernel#load 解释嵌套是空的,除非调用 load 时把第二个参数设为真值;如果是这样,Ruby 会创建一个匿名模块,将其入栈。

这个入栈规则我们仍旧以上面的XML::Parser来做解释:

module XML
    class  SAXParser
    end
end
  1. 先碰到module关键字,将XML模块对象入栈


    image.png
  2. 解释到第二行,发现还没执行完,继续执行,发现class关键字,将类对象XML::Parser入栈


    image.png
定义类和模块是为常量赋值

前面说到通过class或者module定义类或者模块的时候其实都是在新建类或者模块对象

module Admin
end

等效于

Admin = Module.new
类和模块都有维护一个常量表

通常我们所说的String类,是不准确的,实际上它是Object常量(至于为什么是Object,这是由于ruby中的常量解析算法决定的)存储的类对象中所维护一个常量表中的一个常量指向的一个类对象,该类对象名叫"String"


image.png

2.解析算法

相对常量解析算法

举一个例子:

module A
    class B
        class C
            p Module.nesting # => [A::B::C, A::B, A]
            User
        end
    end
end

使用cref表示嵌套中第一个元素,比如前面的XML::SAXParser,如果没有嵌套则表示Object
解析算法如下:

  1. 如果嵌套不为空,在嵌套中按元素顺序查找常量。元素的祖先忽略不计。

按照嵌套所示顺序,从A::B::C::User开始查找,一直到 A::User

  1. 如果未找到,算法向上,进入 cref 的祖先链。

如果查找到A::User都不存在的话,进入cref的祖先链


image.png

祖先链也就是cref的父类
这里还需要注意一点,所有的类在没有显示地继承于哪个类的情况下,都是继承自Object这个类,同时我们在顶层自定义的类或者模块也是属于Object中的,在查找顶层常量的时候大多到这一步就结束了,举个例子

class User
end
module Admin
    class Man
        puts User
    end
end   

上面这段代码,User能够正确找到它是个顶层常量,因为Admin::Man的父类是Object

  1. 如果未找到,而且 cref 是个模块,在 Object 中查找常量。
    这个比较好理解,父级命名空间是个模块的话,最后也是会到顶层的Object中查找

  2. 如果未找到,在 cref 上调用 const_missing 方法。这个方法的默认行为是抛出 NameError 异常,不过可以覆盖。

注意,结合2,3这两点我们会疑惑,好像父级命名空间不论是类还是模块最终都会去Object中也就是顶层中去查找,事实上如果父级命名空间是类并且继承自BasicObject(BasicObject是Object的父类)的话,那么在该命名空间下就会连顶层的常量都找不到了

class User
end
module Admin
    class Man < BasicObject
        # 同时测试下第4点提到的const_missing方法,应该是定义在cref中,也就是这里的Admin::Man
        def self.const_missing(*args)
        puts "Ah oh! 找不到了#{args}"
        end
        puts User
    end
end

但是把上面的BasicObject换成Object或者去掉 < BasicObject就不会有这样的问题

限定常量的解析算法

所谓限定常量指的就是这种形式的常量:

Billing::Invoice

这种形式限定了Invoice常量最起码都是Billing这个命名空间之下的,这个时候先将左边的命名空间作为相对常量查找,找到之后再在该常量下或者该常量的祖先类中查找常量

词汇表

父级命名空间

仅仅指代常量的名称中去除最右边的那一部分后剩余的部分

加载机制

Rails中的通过config.cache_classes设置是否需要缓存已加载的常量。如果是true则在程序中使用Kernel#require来加载,否则使用Kernel#load

自动加载可用性

自动加载,就是执行代码时碰到一个暂时未定义的常量(可以是类或者模块或者一个其他的常量)的时候,Rails会按照该常量的嵌套去尝试自动加载,比如:

class BeachHouse < House
end

如果加载这个文件时发现House还未定义,就会去自动加载该类
在Rails的生产环境中,会及早地加载应用中的文件

autoload_paths

在ruby中,如果使用require引入一个相对文件名时,那么ruby将会在LOAD_PATH中列出的目录中寻找文件。 而在Rails中,与LOAD_PATH类似,Rails会在autoloads中寻找对应的文件,默认情况下这些目录包含:

  • 引擎(rails应用和用到Railsengine的gem)中的app目录下所有子目录


    image.png

如图是foreman项目的autoload_paths目录,因为katello、foreman_remote_execution和foreman-tasks都是用到了RailsEngine所以它们的app目录也会加在当前的autoload_paths目录下

  • 应用和引擎中的名为app/*/concerns的二级目录
  • test/mailers/previews目录

此外,这些目录可以使用config.autoload_paths配置。例如,以前这些lib在这一系列目录中,但是现在不在了,可以通过config/application.rb文件添加下述配置,将其纳入其中:

config.autoload_paths << "#{Rails.root}/lib"

自动加载算法

相对引用

以一个例子来讲解:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

在这里,PostsController,ApplicationController,Post三者都是属于相对引用,假设代码执行到这个位置的时候,这个三个常量全都是未定义的情况下,该如何处理,分别对应了3种情况:

  1. class,module后面的关键后的常量:即便是一个未定义的常量也不会触发自动加载,而是由ruby直接定义这个控制器类
  2. 顶层常量:ApplicationController就是一个顶层常量,因为从当前的代码来看,外部没有任何嵌套了。然后rails会开始检查所有autoload_paths中的目录,是否有application_controller.rb文件。
  3. 命名空间:不同于ApplicationController是直接去autoload_paths中寻找对应文件,这是因为它没有嵌套。而Post的就不同了,它的嵌套是PostsController,此时就需要涉及命名空间的算法

那么在上面的代码中,就会按照:

  1. PostsController::Post
  2. Post的顺序来查找

这样的顺序来查找,上面这两步在rails中有些许的不同:
1.1 查找PostsController::Post时,首先会利用underscore方法,将常量名转换为文件名,以此来查找是否有对应的posts_controller/post.rb文件


image.png

1.2 如果没有找到的话,那么会去寻找名叫post的目录,也就是:

app/assets/posts_controller/post
app/controllers/posts_controller/post
app/helpers/posts_controller/post
...
test/mailers/previews/posts_controller/post

因为,在rails中,只要在autoload_paths目录中新建了目录,那么我们即便不显示地去用module来定义这个模块,rails也会自动定义一个模块赋值给该常量


image.png

限定引用

所谓限定引用,前面已经提过就是类似:

Billing::Invoice

这样的形式的调用
如果缺失限定常量,Rails不会在父级命名空间中查找,例如:

#module Man
#    class User
#    end
#end

#class User
#end
module Admin
    # 在Admin中调用User
    User
end

或者

Admin::User

在Admin模块中调用了User常量,假如User不存在的话,那么Rails仅仅知道当前缺的是Admin模块下的一个名叫User的常量(而不会知道说这个User应该是在顶级命名空间或者在Man这个模块下的一个常量)

如果存在一个顶级User常量,那么在前面的例子中ruby将会解析,但是在后者的情况下不会解析(因为已经限定了必须是在Admin模块的命名空间下)。通常来说,Rails的解析算法和Ruby不太一样,Rails会尝试以下方式来解析:

如果父级命名空间中的类或者模块没有缺失的常量,那么Rails就认为其是一个相对常量。否则是限定常量

比如上面的例子中,父级命名空间就是Admin,并且在此之前我们并没有定义过Admin::User常量,那么这个时候,就当作相对引用来处理(也就是先查找/admin/user.rb etc.,在查找/user.rb etc.),否则Admin中也存在User常量,那么就当限定引用来处理,也就是当作Admin::User来处理

image.png

再来举个例子:
假设User是一个已经定义了的顶层常量,并且现在有如下调用:

module Admin
  User
end

如果和ruby的解析算法一样的话,那么很明显,这里的User就会解析为顶层的那个User,但是对于Rails而言,这样就不会触发自动加载了,所以正如官方提到的,Rails会认为其是一个限定常量,会去查找Admin::User这个常量

总结:

ruby的常量解析算法指示了在ruby中碰到一个常量时会如何去解析它,前提是在所有文件常量都已经加载完成的情况下;而Rails还含有自动加载,也就是当常量不存在的时候还会去寻找对应的文件去自动加载,查找顺序还是和ruby中的相对引用和限定引用的查找顺序一样,只是在每一层嵌套中查找时,如果未定义的话会先去尝试自动加载,自动加载仍然没找到后再去找下一层的嵌套

碰到常量未加载如何解决?

  1. 如果打开控制台或者模拟的运行环境(比如rails runner或者直接在项目中打印日志),那么就打印缺失常量所在的命名空间关联的模块/类是否已加载,没有加载的话,检查autoload_paths,再检查这些路径中是否已经有对应相同层级结构的文件或者目录,有个时候常量找不到可能是因为其父级命名空间的嵌套已经被其他地方的同名结构的模块覆盖,这个时候也可以找到那个模块/类,在其中打印日志检查是否生效
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容