在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前端使用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
|
请发表评论