• 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

  • 相关阅读:
    sql 查询所有数据库、表名、表字段总结
    C# 随机数图片
    修改SQL数据库中表字段类型时,报“一个或多个对象访问此列”错误的解决方法
    ASP.NET 高级编程基础第八篇—Request对象和虚拟路径 转
    HTTP 状态码
    SELECT INTO 和 INSERT INTO SELECT 两种表复制语句
    关于VS2010中无法进级EntityFramework的解决办法
    sql 存储过程 执行中 遇到的 问题 小结
    引用不到using System.Data.Entity.Database;(MVC3)
    如何通过ildasm/ilasm修改assem“.NET研究”bly的IL代码 狼人:
  • 原文地址:https://www.cnblogs.com/coding-my-life/p/13418422.html
Copyright © 2020-2023  润新知