第三章 IFC 解析器的實現
3.1 雜凑
在詞法階段,我們獲得了 Token,其由兩部分組成:Token 類型和 Token 值,
比如一個 IFC 類別名的 Token,其類型是 EntityName,(就上例而言)其值是
11
IFCPERSON,但很明顯,這個值是一個字元型(String),所以我們並不能直接使 用 new 操作符構建類,所以通常的一個簡單做法是:
// Initialize factory
Map<String, Class> factory = new HashMap<>();
factory.put("IFCPERSON", IfcPerson.class);
// During parsing
IfcPerson person = factory.get("IFCPERSON").newInstance(…);
這是軟體設計模式中常見的工廠模式(Factory Pattern),我們使用一個 Key-Value 容器作為工廠,當我們獲得一個類型為 EntityName 的 Token 後,我們 用它的值在工廠裏尋找對應的類型類(Class),類型類是 Java 反射(Reflection)
裏的基礎,其擁有一個特別的方法 newInstance 來構造對應的類別。這樣我們就通 過一個字串構建出了對應的類別。
考慮本文在第三章所著的 BNF,其中的<ifc-entity>即代表著類型為 EntityName 的 Token,其定義為:
<ifc-entity> ::= [A-Z] [A-Z0-9]*
這裏我們假設了 IFC 的類別名是由大寫字母和數字組成的,所以對於工廠容 器我們使用了 String 作為它的 Key。但實際上 IFC 的類別名是有限的,所以
<ifc-entity>其實也可以寫成如下形式:
<ifc-entity> ::= 'IFCPERSON' | 'IFCDIRECTION' | …
12
有限的組合意味著我們並不一定需要依賴於 HashMap 這類容器。如果可以把 IFC 裏 849 個類別名映射成正整數的話,我們就可以將其儲存於數組之中。數組的 訪問可以認為是恒定 O(1)的,這樣解析器的性能相較於 HashMap 的版本會有非常 大的提升。而為了將 String 映射為 Integer,我們需要找到一個合適的雜凑函數,
且符合以下兩個條件:
1)產生的雜凑沒有碰撞
2)最小雜凑值和最大雜凑值的差值應儘量小,848 是最優解 例如 Java 中 String 的雜凑函數偽代碼如下:
set hash = 0
for each character in string
____hash = hash * 31 + ascii(character) end for
在回圈內部,每一輪雜凑值都會乘上一個質數 31,所以最終的雜凑值也會隨 著字串的長度成幾何級數增加(並溢出)。這樣保證了當我們雜凑字串時,針對 兩個只有微小差別的字串(長度差 1 或者只有一個字母不同等情況),最後得到 的雜凑值差別會非常大,這確保了雜凑的離散性。ascii 函數是獲得字元
(character)ASCII 數值的函數。
但是本研究設計的雜凑需要盡可能地減少離散性,使最終的雜凑分佈在一個 很小的範圍內,以下是實現代碼:
set hash = 0
13
for each character in string
____hash = hash + VALUE(character) * 13 ____hash = hash xor 0x16ADA
end for
hash = hash / 2
其中係數降為了 13,且移到了與字元值相乘的地方,這樣雜凑的增長速度就 會降低,但碰撞的幾率也大幅增加,於是在每次計算後,雜凑值會再跟 0x16ADA
(10110101011011010)進行一次異或(xor)計算,這次操作相當於打亂雜凑值的
#4692875= IFCPROPERTYSET('1w46N3aqXDgflkE91KriAK',#41,…);
#4692877= IFCRELDEFINESBYPROPERTIES('1Hwq42Jnf1e8MaXnNoMs0a',…);
#4692881= IFCAXIS2PLACEMENT3D(#6,$,$);
#4794946= IFCRELAGGREGATES('3o9c2jHPz0UgvyZpd1xn_J',#41,…);
#4692883= IFCCARTESIANPOINT((-5991.,-3463.,0.));