Ruby 中 Constant 和 Class 的關係

下班前龍哥說在 Mailing List 看到了一段 Code 很有趣。

1
2
3
4
5
6
7
8
9
10
a = Class.new

p a #=> #<Class:0x0000558d34f68b48>
p a.name #=> nil

B = a
p a.name #=> 'B'

C = a
p C.name #=> 'B'

裡面 C = a 到底發生了什麼事情,是很值得討論的,因為有了線索是 rb_const_set 可以找到原因,所以就利用下班時間來讀看看這段。

關於前面的用法可以參考之前寫過的自由的 Ruby 類別來了解原因。

我大致上翻了一下 Ruby 的原始碼,這段程式主要是定義在 variable.c 這個檔案,在 Ruby 裡面我們可以簡單把 Constant(常數)理解為一種特殊的變數,跟一些語言在使用了 const 關鍵字後無法更改的概念上是不太一樣。

常數如何被賦值

因為 rb_const_set 接受了一些參數我們不好理解,所以先看看是由誰來呼叫他。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
rb_define_const(VALUE klass, const char *name, VALUE val)
{
ID id = rb_intern(name);

if (!rb_is_const_id(id)) {
true rb_warn("rb_define_const: invalid name `%s' for constant", name);
}
rb_const_set(klass, id, val);
}

void
rb_define_global_const(const char *name, VALUE val)
{
rb_define_const(rb_cObject, name, val);
}

在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
rb_const_entry_t *ce;
struct rb_id_table *tbl = RCLASS_CONST_TBL(klass);

if (NIL_P(klass)) {
rb_raise(rb_eTypeError, "no class/module to define constant %"PRIsVALUE"", QUOTE_ID(id));
}

check_before_mod_set(klass, id, val, "constant");
if (!tbl) {
// PART1
}
else {
// PART2
}

第一階段 Ruby 會先去看看這個 Class 裡面是不是已經初始化過紀錄下面所屬的常數的一個表格,如果沒有的話就初始化一個出來。已經存在的話則是做 Autoload 動作,如果有讀取到對應的常數,那就會跳出錯誤警告,沒有的話就跟前面產生表的行為一樣,把這個常數插入進去。

看起來常數的賦值應該就這樣結束了,不過為了處理一些特殊情況,所以往下會看到一段註解。

1
2
3
4
/*
* Resolve and cache class name immediately to resolve ambiguity
* and avoid order-dependency on const_tbl
*/

這就是我們這次要討論的問題來源,要觸發這個處理依照原始碼的實作要滿足某些條件才行。

1
if (rb_cObject && (RB_TYPE_P(val, T_MODULE) || RB_TYPE_P(val, T_CLASS))) {
  1. Object 是有被定義的(正常情況下都應該是被定義的)
  2. 賦予的數值必須是 Module 或者 Class

接下來要再滿足另一個條件,就是通過 rb_class_path_cached 的檢查

1
if (NIL_P(rb_class_path_cached(val))) {

因為裡面的實作也有點多,所以這邊直接去找了一下文件關於 rb_class_path 的用途,然後再去看 rb_class_path_cached 的這段實作。

1
2
3
4
5
6
7
8
9
10
11
12
VALUE
rb_class_path_cached(VALUE klass)
{
st_table *ivtbl;
st_data_t n;

if (!RCLASS_EXT(klass)) return Qnil;
if (!(ivtbl = RCLASS_IV_TBL(klass))) return Qnil;
if (st_lookup(ivtbl, (st_data_t)classpath, &n)) return (VALUE)n;
if (st_lookup(ivtbl, (st_data_t)tmp_classpath, &n)) return (VALUE)n;
return Qnil;
}

大致上我們可以理解成每個有名字的 Class 或 Module 都會被記錄起來,所以這邊要找的條件是「匿名的 Class 或是 Module」都符合條件後,就會做下面的動作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (klass == rb_cObject) {
rb_ivar_set(val, classpath, rb_id2str(id));
rb_name_class(val, id);
}
else {
VALUE path;
ID pathid;
st_data_t n;
st_table *ivtbl = RCLASS_IV_TBL(klass);
if (ivtbl &&
(st_lookup(ivtbl, (st_data_t)(pathid = classpath), &n) ||
st_lookup(ivtbl, (st_data_t)(pathid = tmp_classpath), &n))) {
path = rb_str_dup((VALUE)n);
rb_str_append(rb_str_cat2(path, "::"), rb_id2str(id));
OBJ_FREEZE(path);
rb_ivar_set(val, pathid, path);
rb_name_class(val, id);
}
}

假設這個常數是定義在 Object(全域)的情況,那麼就直接對他做兩件事情:

  1. 設定 classpath (就是前面的暫存檢查)
  2. 對這個匿名的 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 會不會對效能有所影響。

Buy me a CoffeeBuy me a Coffee

留言