给 sproto 增加 unordered map 的支持

花了两天给 sproto 增加了 unordered map 的支持。

问题是这样的:

sproto 支持数组,但很多情况下,业务处理中,我们并不用数组来保存大量的相同类型的结构数据。因为那样不方便检索。

比如你要配置若干地图表、NPC 表等等的信息,固然可以用 sproto 的 array 来保存。但是在运行时,你更希望用定义好的 id 来检索它们。如果 sproto 不支持 unordered map 的话,你就需要在 decode 之后,对 array table 做一次遍历,用一张新表来建立索引。

google protocal buffers 2 也有这个问题,据说第 3 版要增加 map 用来兼容 json ,这个话题最后再说。

为了解决这个问题,最简单的方法是在 sproto 的定义中增加一个特性,允许给 array 定义一个主索引。建议编码器在编解码这个数组的时候,按类型的主索引去处理这张表。

比如

.Person {
    name 0 : string
    id 1 : integer
    email 2 : string

    .PhoneNumber {
        number 0 : string
        type 1 : integer
    }

    phone 3 : *PhoneNumber
}

.AddressBook {
    person 0 : *Person(id)
}

这里的 AddressBook 类型中 person 这个字段,原本是一个 Person 类型的数组列表。如果没有在后面加上 (id) 的话,就建议编码器用 Person 类型中的 id 字段做为主索引。

主索引字段可以是所有的内建类型(integer boolean 或 string ),但不可以是自定义类型或数组。

这个只是一个可选项,并不需要修改 sproto 的 wire type ,所以增加这个特性后,和之前的版本是完全兼容的。

在编解码过程中,利用这个额外信息,可以把数据处理为 unordered map 而不是 list 。目前的 lua binding 实现会利用这个信息。

一旦你设定了主索引,编码的时候就不再用数字迭代,而改用 lua_next 。我在这里的实现上偷了个懒、因为严格意义上,我们还需要检查输入的 unordered map 的 key 是否和对应的 value 中主索引项的值完全相同。不过一旦实现这个检查,不仅代码会复杂的多,还会对之前的数据造成一些不兼容(目前你完全可以依旧把 list 输入编码为 unordered map )。

解码的时候,如果标注了主索引,就会在解码每个数据项时,取出对应的值作为这一项的 key 。


对于 test.lua 中的范例:

local sp = parser.parse [[
.Person {
    name 0 : string
    id 1 : integer
    email 2 : string

    .PhoneNumber {
        number 0 : string
        type 1 : integer
    }

    phone 3 : *PhoneNumber
}

.AddressBook {
    person 0 : *Person(id)
    others 1 : *Person
}
]]

sp = core.newproto(sp)
core.dumpproto(sp)
local st = core.querytype(sp, "AddressBook")

local ab = {
    person = {
        [10000] = {
            name = "Alice",
            id = 10000,
            phone = {
                { number = "123456789" , type = 1 },
                { number = "87654321" , type = 2 },
            }
        },
        [20000] = {
            name = "Bob",
            id = 20000,
            phone = {
                { number = "01234567890" , type = 3 },
            }
        }
    },
    others = {
        {
            name = "Carol",
            id = 30000,
            phone = {
                { number = "9876543210" },
            }
        },
    }
}

collectgarbage "stop"

local code = core.encode(st, ab)
local addr = core.decode(st, code)
print_r(addr)

你将看到这样的输出:

=== 3 types ===
AddressBook
        person (0) *Person(1)
        others (1) *Person
Person
        name (0) string
        id (1) integer
        email (2) string
        phone (3) *Person.PhoneNumber
Person.PhoneNumber
        number (0) string
        type (1) integer
=== 0 protocol ===
+person+10000+phone+1+type [1]
|      |     |     | +number [123456789]
|      |     |     +2+type [2]
|      |     |       +number [87654321]
|      |     +id [10000]
|      |     +name [Alice]
|      +20000+phone+1+type [3]
|            |       +number [01234567890]
|            +id [20000]
|            +name [Bob]
+others+1+phone+1+number [9876543210]
         +id [30000]
         +name [Carol]

注意 person 子结构下有两项数据,它们的 key 分别是 10000 和 20000,对应 person.id = 10000, 和 person.id = 20000 ,而不再是 1 和 2 了。


最后谈谈 google protocal buffers 3 。

根据 最近的 release log ,他们打算支持

message Foo {
  map values = 1;
}

这样的数据结构。

我个人觉得 protobuffer 这样选择主要是它的应用语言相关:主要用于 C++ 的程序方便处理数据。所以 unordered map 的定义语法也和 C++ 类似。估计最终也可以映射到 C++ 的 map 中去。

但如果主要用动态语言做开发的话,我个人很不喜欢引入这种新特性。其实实际使用中,map 的值是一个简单类型的环境真的不多,这种数据结构如我前面所说,也就是为了在业务处理中,有一个方便的索引手段而已。

protobuffer 的设计已有一定的年头,看起来 google 自己也觉得积重难返了。第 3 版已经不考虑向前兼容性,大刀阔斧的修改是件好事;原话是:

We recommend that new Protocol Buffers users use proto3. However, we do not generally recommend that existing users migrate from proto2 from proto3 due to API incompatibility, and we will continue to support proto2 for a long time.

但如果开新项目,何不尝试一下其他呢?我是说,如果你在用 lua 做开发,sproto 会是个更好的选择。

ps. sproto 的 python binding 目前由我的同事开发中。

pps. 这次的修改暂时在 github 的 sproto 项目的 map 分支中,一旦确认基本没有问题,会合并到 master 分支上。

评论