1. 程式人生 > 實用技巧 >lua使用自定義型別作key

lua使用自定義型別作key

  前端使用typescript,後端使用C++和lua,在討論後端下發的int64型別值如何處理時,我建議前端使用long.js,但前端說他需要用這個作key,而js沒法用自定義型別作key。我回了一句“js居然沒法用自定義型別作key,這麼弱”,但是說完這句話,我就愣住了,貌似那裡不對。

  我認為任何一門邏輯完備的語言(基礎的資料結構和流程控制等等),都能實現這麼一個邏輯,js顯然也能實現。說“無法”無非就是實現代價過大,比如需要引入其他庫、修改更多的程式碼,執行效率更低等等原因。但我之所以會愣住,是因為當時腦中想的是“C++和lua都支援,而js這個應該這麼廣的語言居然不支援“,然而轉念一想,lua支援任意資料型別作key,但和用自定義型別作key完全不是一碼事。

  首先,來看下C++中的自定義型別為key

#include <unordered_map>
#include <iostream>

class MyInt64
{
public:
    MyInt64(int hi, int lo)
    {
        this->hi = hi;
        this->lo = lo;
    }

    ~MyInt64()
    {
    }

    int hi;
    int lo;
};

class MyInt64Hash
{
public:
    std::size_t 
operator()(const MyInt64 &i) const { int64_t hash = i.hi; return (hash << 32) + i.lo; } }; struct MyInt64Equal { bool operator () (const MyInt64 &lhs, const MyInt64 &rhs) const { return lhs.hi == rhs.hi && lhs.lo == rhs.lo; } };
int main() { std::unordered_map<MyInt64, int, MyInt64Hash, MyInt64Equal> map; MyInt64 i1(1, 1); MyInt64 i2(1, 1); map.emplace(i1, 999); auto found = map.find(i2); if (found != map.end()) { std::cout << found->second << std::endl; // 輸出 999 } return 0; }

在上面的程式碼中i1和i2是不同的兩個變數(它們的地址不一樣),但是他們的值是一樣的,所以對map進行操作時,他們操作的是同一個值。而在lua中,固然可以用任意型別的變數作為key,比如

local i1 = {1, 1}
local i2 = {1, 1}

local map = {}

map[i1] = 999

print(map[i2]) -- 輸出nil

顯然,這不預期的結果啊。在前後端互動的過程中,可以保證數值是一致的,但哪能保證他們的地址是一致的呢(如果能知道地址,就直接拿到變量了,還要這個map幹啥)。我使用lua將近5年了,還是第一次遇到這個問題,我的第一反應是這個在元表中有一個__hash之類的吧,然而元表中有__eq,卻沒有__hash,那這意味著,官方是沒有直接實現這一個功能的。

假如讓我實現這個功能,我的第一反應是使用元表的__index和__newindex來劫持這個map的讀寫,但這樣一來,就需要把真正的資料存在另一個table中,然後為了實現遍歷,還需要重新實現__pair,那事情就變得複雜多了。這個功能雖然小眾,但如果支援的話,還是十分方便的,是什麼原因C++支援這個自定義hash,而lua不支援呢,這我倒來了興趣。

網上查了一下,原來這個事情在10多年前早有定論:http://lua-users.org/lists/lua-l/2009-02/msg00000.html,總結一下,大概有以下幾點原因

1. lua使用非基礎型別(table、userdata等等)為key時,直接使用記憶體地址,這相當於沒有hash函式,效率高

2. 實現__hash這個函式很容易,但是什麼時候呼叫很難確定。最好的方案是如果存在__hash,則呼叫__hash,否則按原方案使用記憶體地址作為hash。但是這個方案會導致table的效率大幅下降。因為即使沒有__hash,原來直接取記憶體地址這一個步驟,變成了判斷是否存在元表、是否存在__hash、取記憶體地址三個步驟,而table是lua唯一的資料結構,包括標準庫在內都是使用table,這顯然會導致整個語言的效率大幅下降

3. 在第2點的基礎上,即使不考慮效率問題,自定義key的mutable(可變)讓整個table的維護變成無比複雜。例如

local i1 = {1, 1}

local map = {}

map[i1] = 999
i1[1] = nil -- 這時候map需要做什麼變化???

當key中的數值被修改時,那__hash得到的值必定會變化,如果重新調整map中的元素hash,顯然很低效,也不現實(需要跟蹤i1所有值的變化,包括它的子table的子table的子table...),如果不調整,那整個hash就對不上了。C++不存在這個問題是因為C++會把key複製一份變成const,但是在lua中複製這一份table完全不現實,因為這個table可能無比巨大

4. 上面說了那麼多__hash缺點,假如我不實現__hash,以現在的程式碼,我就能更優雅地解決問題,又何必去實現__hash

local i1 = {1, 1}
local i2 = {1, 1}

local map = {}

-- 假設lua不支援int64,而我們需要用int64作key,那使用使用 高位_低位 這個字串作key
setmetatable(map, {
    __index = function(t, k)
        return rawget(t, k[1] .. "_" .. k[2])
    end,
    __newindex = function(t, k, v)
        return rawset(t, k[1] .. "_" .. k[2], v)
    end
})

map[i1] = 999

print(map[i2]) -- 輸出999

當然我這個例子是針對自己的業務邏輯寫的,並不是很通用,需要更通用的方式可以考慮自己封裝,參考:http://lua-users.org/wiki/ComparisonByValue