下班前龍哥說在 Mailing List 看到了一段 Code 很有趣。
1a = Class.new
2
3p a #=> #<Class:0x0000558d34f68b48>
4p a.name #=> nil
5
6B = a
7p a.name #=> 'B'
8
9C = a
10p C.name #=> 'B'
裡面 C = a
到底發生了什麼事情,是很值得討論的,因為有了線索是 rb_const_set
可以找到原因,所以就利用下班時間來讀看看這段。
關於前面的用法可以參考之前寫過的自由的 Ruby 類別來了解原因。
我大致上翻了一下 Ruby 的原始碼,這段程式主要是定義在 variable.c
這個檔案,在 Ruby 裡面我們可以簡單把 Constant(常數)理解為一種特殊的變數,跟一些語言在使用了 const
關鍵字後無法更改的概念上是不太一樣。
常數如何被賦值
因為 rb_const_set
接受了一些參數我們不好理解,所以先看看是由誰來呼叫他。
1void
2rb_define_const(VALUE klass, const char *name, VALUE val)
3{
4 ID id = rb_intern(name);
5
6 if (!rb_is_const_id(id)) {
7 rb_warn("rb_define_const: invalid name `%s' for constant", name);
8 }
9 rb_const_set(klass, id, val);
10}
11
12void
13rb_define_global_const(const char *name, VALUE val)
14{
15 rb_define_const(rb_cObject, name, val);
16}
在 Ruby 裡面我們要定義一個常數 A = x
是透過 rb_define_const
來實現的,如果是 Global 的話,就會直接定義在 Object
下面,而我們提供給 rb_const_set
的三個參數裡面 ID
這個數值可以先來看一下 rb_intern
的用法。
現在我們在 Ruby 裡面會經常使用 :name
這樣的寫法,表示他是一種 Symbol 而在 Ruby 裡面的實作,都會透過 rb_intern
這個方法來從 char*
轉換過去,基本上我們可以理解 Ruby 所有物件、常數的命名,都會被統一記錄起來,方便之後重複使用。
不過這邊有趣的地方其實是他會檢查這個 ID
類型,往下追之後會看到像這樣的檢查
1#define is_const_id(id) (id_type(id)==ID_CONST)
不過關於這段稍微追了下發現又是一個有點長的過程,這邊簡單解釋就是在定義 Symbol 的時候 Ruby 會依照這個 Symbol 的特性去區分出他的類型,像是
$
開頭會標記成 Golbal Variable 這樣的感覺
常數賦值的過程
接下來我們就可以往 rb_const_set
深入來看,因為整體是相對長的,我們就針對需要的部份重點式的閱讀。
1 rb_const_entry_t *ce;
2 struct rb_id_table *tbl = RCLASS_CONST_TBL(klass);
3
4 if (NIL_P(klass)) {
5 rb_raise(rb_eTypeError, "no class/module to define constant %"PRIsVALUE"", QUOTE_ID(id));
6 }
7
8 check_before_mod_set(klass, id, val, "constant");
9 if (!tbl) {
10 // PART1
11 }
12 else {
13 // PART2
14 }
第一階段 Ruby 會先去看看這個 Class 裡面是不是已經初始化過紀錄下面所屬的常數的一個表格,如果沒有的話就初始化一個出來。已經存在的話則是做 Autoload 動作,如果有讀取到對應的常數,那就會跳出錯誤警告,沒有的話就跟前面產生表的行為一樣,把這個常數插入進去。
看起來常數的賦值應該就這樣結束了,不過為了處理一些特殊情況,所以往下會看到一段註解。
1 /*
2 * Resolve and cache class name immediately to resolve ambiguity
3 * and avoid order-dependency on const_tbl
4 */
這就是我們這次要討論的問題來源,要觸發這個處理依照原始碼的實作要滿足某些條件才行。
1if (rb_cObject && (RB_TYPE_P(val, T_MODULE) || RB_TYPE_P(val, T_CLASS))) {
Object
是有被定義的(正常情況下都應該是被定義的)- 賦予的數值必須是 Module 或者 Class
接下來要再滿足另一個條件,就是通過 rb_class_path_cached
的檢查
1if (NIL_P(rb_class_path_cached(val))) {
因為裡面的實作也有點多,所以這邊直接去找了一下文件關於 rb_class_path
的用途,然後再去看 rb_class_path_cached
的這段實作。
1VALUE
2rb_class_path_cached(VALUE klass)
3{
4 st_table *ivtbl;
5 st_data_t n;
6
7 if (!RCLASS_EXT(klass)) return Qnil;
8 if (!(ivtbl = RCLASS_IV_TBL(klass))) return Qnil;
9 if (st_lookup(ivtbl, (st_data_t)classpath, &n)) return (VALUE)n;
10 if (st_lookup(ivtbl, (st_data_t)tmp_classpath, &n)) return (VALUE)n;
11 return Qnil;
12}
大致上我們可以理解成每個有名字的 Class 或 Module 都會被記錄起來,所以這邊要找的條件是「匿名的 Class 或是 Module」都符合條件後,就會做下面的動作。
1 if (klass == rb_cObject) {
2 rb_ivar_set(val, classpath, rb_id2str(id));
3 rb_name_class(val, id);
4 }
5 else {
6 VALUE path;
7 ID pathid;
8 st_data_t n;
9 st_table *ivtbl = RCLASS_IV_TBL(klass);
10 if (ivtbl &&
11 (st_lookup(ivtbl, (st_data_t)(pathid = classpath), &n) ||
12 st_lookup(ivtbl, (st_data_t)(pathid = tmp_classpath), &n))) {
13 path = rb_str_dup((VALUE)n);
14 rb_str_append(rb_str_cat2(path, "::"), rb_id2str(id));
15 OBJ_FREEZE(path);
16 rb_ivar_set(val, pathid, path);
17 rb_name_class(val, id);
18 }
19 }
假設這個常數是定義在 Object
(全域)的情況,那麼就直接對他做兩件事情:
- 設定
classpath
(就是前面的暫存檢查) - 對這個匿名的 Class 或 Module 設定名字為當下的常數
如果是定義在某個 Class 或 Module 下面的話,因為 classpath
就不會是剛好的,所以要先算過(產生) classpath
然後再做一樣的事情。
總結
找完之後我們就可以解釋為什麼文章一開始的 C = a
再去問 C.name
會得到 B
這個結果了,主要是因為已經被命名過的 Class 會在記憶體中製作一個類似捷徑的東西,讓下次去呼叫這個 Class 或 Module 可以更快。
而給這個 Class 或 Module 物件命名的時機點,就在於它被記錄到捷徑的時機,所以即使再次賦予給其他常數,也不會改變他的名字。
這樣我們可以延伸出來的問題是
C = a
的情況下,因為classpath
是 Cache 在B
上面,這時候使用C
是不是會比B
更慢呢?而匿名的 Class 和 Module 會不會對效能有所影響。