這是一個研究筆記,主要是為了向同好請教。除了這個開頭以外,沒有多餘的廢話,也就免了其他的客套。請大家不要抱怨可讀性不好。
1. 在一個名字或者字串前面加上冒號,得到一個symbol對象。還可以通過String#to_sym、Fixnum#to_sym和String#intern得到。
2. 一般用symbol做hash的key,號稱是為了節省記憶體,提高執行效率。
3. 為什麼可以節省記憶體?Ruby中的String是可變對象,這一點跟Java、C#、Python都不一樣。注意跟某些C++標準庫中的COW的basic_string<T>也不一樣。Ruby中每一個String都可以就地改變。可能是因為這個原因,Ruby中兩個內容相同的字串文本量實際上是兩個不同的對象。
a = "hello"
b = "hello"
雖然倆字串內容都一樣,但是你比一下a和b,就知道a.object_id != b.object_id,它們指向的不是同一個對象。結果反而很像未經string pooling最佳化的C語言的行為。到底immutable好還是mutable好,或者還是貌似聰明的COW好,見仁見智了。不過Ruby的設計在把字串用作hash key的時候毛病就大了。比如你寫:
h["ruby"].name = "Ruby"
h["ruby"].author = "matz"
h["ruby"].birth_year = 1995
的時候,"ruby"這個字串動態產生了三次,佔用三倍記憶體。這就嚴重地浪費了記憶體。而用:ruby做為key,因為在整個運行過程中,Ruby runtime保證名為:ruby的symbol對象只有一個,所以就不用產生三個,節省記憶體。
4. 為什麼可以提高執行效率?顯然的原因是免得多次動態產生'ruby'字串了。還不單如此,Hash的key值應該是常量,所以Ruby的Hash對於作為key的String對象都要施加保護,所謂保護,也就是把String凍結了,免得你之後還改變其值。保護當然是有代價的,symbol無需保護,當然是能提高效率的。附帶說明,其他mutable的對象也可以作為hash的key,這是Ruby設計得比較奇怪的地方。在irb裡運行以下代碼,你會發現Ruby的Hash丟值。
h = Hash.new
L = [1, 2]
h[L] = "A big object!"
L << 3 # 居然能改!
h[L] # ==> nil,找不到了,似乎正常
# 可是
h[[1, 2]] # ==> nil,居然還是找不到
# 看看keys
h.keys # ==> {[1, 2, 3]} 似乎還在裡面
h[[1, 2, 3]] # ==> nil
# 可是
h # ==> {[1, 2, 3]=>'A big object'},明明在這裡,就是找不到
h.rehash # ==> 這樣就會一切恢複正常。
這一點上Python的設計要比較容易理解,list根本就是unhashable的,不能用來做hash的key。
回過頭來在說提高效率的事。Symbol效率提高還有第三個原因,那是因為symbol本質上不比一個整數多出多少東西,用Symbol#to_i可以得到一個在整個程式中唯一的整數。Hash完全可以利用這個整數來產生hash值,那豈不是比根據字串內容去算hash值快得多?這還是小意思,既然這個整數是唯一的,那麼產生一個唯一的hash值也就是小菜一碟,要是能保證hash值唯一,那還是什麼hash表,根本就變成數組了。Hash表還可能會衝突,數組根本不會衝突,百分之百保證O(1),當然快。我沒看Ruby源碼,不知道是不是這麼處理的。
5. 為什麼Ruby runtime可以保證每一個symbol唯一?因為Ruby把symbol存放在運行時維護的一個符號表裡了,而這個符號表實際上是一個atom資料結構,其中儲存著當前所有的程式級的name,確保不出現內容相同的多個對象。幾乎每一個語言和系統都會有這樣一個符號表,只不過象C/C++那樣的語言,這個符號表只是在編譯時間存在,運行時就沒了。而Python、Ruby則在運行時也保留這張表備用。有這樣一個現成的資料結構幹嘛不用?
6. 但是這個表中存放的並不光是我們自己主動產生的symbols,還有Ruby解譯器對當前程式進行詞法分析、文法分析後存在其中的、當前程式的所有名字。這可是Ruby引擎用的東西啊,我們只要加上一個冒號,就讓自己的對象跟Ruby引擎內部使用的對象成鄰居了。所以String#intern這個方法叫做intern(內部化)。
.NET Framework中String類也有一個Intern方法,意思是一樣一樣一樣的,在李建忠的經典譯本裡翻譯為“駐留”。
7. 可以用Symbol#all_symbols查看當前定義的全部symbol。可以體驗一下自己往符號表中塞一個對象的感覺,想想你寫的程式跟Ruby引擎能幹一樣的事情,應該還是挺爽的。
8. Python中用不著這個,因為字串是immutable的。放下有用沒用不說,有沒有辦法在Python中intern呢?我還沒找到辦法。有沒有Python牛知道?
補充一下:查到了,Python中做這個事情的函數叫做 intern()。
9. 我覺得Ruby的這個設計是從Perl的glob中簡化而來的。Perl中可以用*a得到對應於符號a的glob,那是一個八爪魚一樣的怪物。Ruby也可以很容易的得到symbol table中的對象,不過沒有把symbol設計成八爪魚。
10. 還有一些小問題沒搞清楚,比如:name跟@name是什麼關係。attr_reader :name,實際上是給attr_reader方法傳了一個symbol作為參數,前者要通過這個symbol找到@name變數,是不是'@' + :name.id2name這麼簡單?大概可以去看看source了。