<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rusts on aarkegz 技术屋</title><link>https://aarkegz.com/rust/</link><description>Recent content in Rusts on aarkegz 技术屋</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Thu, 11 Dec 2025 09:30:52 +0000</lastBuildDate><atom:link href="https://aarkegz.com/rust/index.xml" rel="self" type="application/rss+xml"/><item><title>如何自定义一种数据交换格式（一）：设计思路与表示语法</title><link>https://aarkegz.com/rust/how-to-create-a-data-exchange-format-in-rust/a/</link><pubDate>Thu, 11 Dec 2025 09:30:52 +0000</pubDate><guid>https://aarkegz.com/rust/how-to-create-a-data-exchange-format-in-rust/a/</guid><description>&lt;blockquote>
&lt;p>series_member:true,parts::design;;&lt;/p>&lt;/blockquote>
&lt;h2 id="引言">引言
&lt;/h2>&lt;p>现实世界中有千千万万的数据格式，数据交换格式（Data Exchange Format），如 JSON、YAML、TOML 等，是其中非常常见又重要的一类。它们在不同系统、不同编程语言之间传递数据，或者作为配置文件被程序读取和解析。在使用上，应用程序通常不会直接自行解析或生成这些格式的数据，而会使用成品序列化/反序列化库（如 Rust 中的 &lt;a class="link" href="https://serde.rs/" target="_blank" rel="noopener"
>serde&lt;/a> 等）将其转换为程序内部的数据结构，或者将数据结构转换为数据交换格式，以方便处理。&lt;/p>
&lt;p>对于大部分的使用场景，使用通用的数据交换格式，和现成的序列化/反序列化库已经足以满足需求了，像 Go 中的 &lt;code>json.Marshal/Unmarshal&lt;/code>、Rust 中的 &lt;code>serde_json::to_string/from_str&lt;/code> 已经非常方便好用，无需程序员关心任何底层细节。不过，总还是有一些程序员，可能是因为足够空闲、足够好奇，或是确实有需求，想要深挖背后的原理，甚至自己动手设计一种数据交换格式，并实现相关的序列化/反序列化库。本文的作者正是其中之一，他出于好奇，设计了一种新的数据交换格式，并仿照 &lt;code>serde&lt;/code> 的设计思路，实现了相应的序列化/反序列化库；并且他还萌生了将这一过程记录下来的想法，作为对数据交换格式、序列化/反序列化相关知识的总结和分享。&lt;/p>
&lt;p>本文为系列文章的第一篇，主要介绍一种新的数据交换格式的设计思路，包括数据类型设计、表示语法设计等内容。&lt;/p>
&lt;h2 id="明确设计思路">明确设计思路
&lt;/h2>&lt;p>首先我们自然是要设计一种新的数据交换格式。设计格式不是闭门造车或异想天开，而是要有具体的应用场景和设计思路，至少要回答清楚以下几个问题：&lt;/p>
&lt;ul>
&lt;li>需要表示&lt;strong>什么数据&lt;/strong>？&lt;/li>
&lt;li>&lt;strong>如何表示&lt;/strong>这些数据？&lt;/li>
&lt;li>面向哪些&lt;strong>使用场景&lt;/strong>？&lt;/li>
&lt;li>还有哪些&lt;strong>特殊偏好&lt;/strong>？&lt;/li>
&lt;/ul>
&lt;p>例如 JSON、YAML、TOML 都是面向通用数据类型的文本格式，但偏向不同：JSON 偏向简洁和易于机器处理，YAML 偏向可读性和人类友好，TOML 则偏向配置文件的易用性。Bson 是面向通用数据类型的二进制格式，偏向高效存储和传输。CSV 是面向表格数据的文本格式，偏向简单可读和易于处理。不同的设计思路所诞生的格式千差万别。&lt;/p>
&lt;p>作为例子，本文希望设计一种&lt;strong>文本表示&lt;/strong>的，面向&lt;strong>通用数据类型&lt;/strong>的数据交换格式，主要面向&lt;strong>短小简单的单行配置与数据&lt;/strong>，需要&lt;strong>尽量减少文本长度&lt;/strong>，并且&lt;strong>方便人手编写&lt;/strong>。同时，为了突出其独特之处（毕竟已经有太多现成的格式了，没有一点独特之处谁会在意呢？），我们把&lt;strong>核心设计目标定为必须使用最少的语法字符&lt;/strong>来表示数据结构。&lt;/p>
&lt;p>显然，JSON是一个不错的参考对象，它也是面向通用数据类型的文本格式，并且设计得相当简洁。本文的作者决定以 JSON 为参考对象，借鉴其数据类型设计和部分语法，同时在此基础上进行简化和修改，以满足本文的设计目标。&lt;/p>
&lt;p>不算字符串使用的引号和字符串内表示转义的反斜杠，JSON 使用了 6 个语法字符（&lt;code>{}[],:&lt;/code>），这个数量显然太多了；既然我们的目标是越少越好，就自然要对 JSON 的语法进行大胆的修改和简化。当然，使用两个字符的组合表达另外的含义（如使用 &lt;code>[:&lt;/code> 表示 &lt;code>{&lt;/code>）这种「作弊」的方式是不行的，我们只能使用单个字符来表示不同的语法含义。&lt;/p>
&lt;h2 id="挑选数据类型">挑选数据类型
&lt;/h2>&lt;p>既然决定了要以 JSON 为参考对象，那么我们可以直接借鉴 JSON 的数据类型设计。JSON 支持以下几种数据类型：空值（Null）、布尔值（Boolean）、数字（Number）、字符串（String）、数组（Array）和对象（Object）。这些数据类型已经足够通用，能够表达绝大多数的数据结构（事实上也被大部分数据交换格式所采用），因此我们直接「拿来主义」，采用相同的数据类型设计。&lt;/p>
&lt;p>不过既然是在 Rust 语言中实现，为了配合对 Rust 语言和开发者的刻板印象，本文的作者慎重决定，对数据类型开展「RIIR」行动——给部分类型足够「锈化」的名称，将 &lt;code>Array&lt;/code> 改为 &lt;code>Vector&lt;/code>，将 &lt;code>Object&lt;/code> 改为 &lt;code>Map&lt;/code>，然后保持其他类型名称不变，并宣称它们已经足够「Rusty」了。&lt;/p>
&lt;h2 id="决定表示语法">决定表示语法
&lt;/h2>&lt;h3 id="空值与布尔值">空值与布尔值
&lt;/h3>&lt;p>在确定了数据类型之后，下一步就是决定如何将每种数据类型表示为文本。同样地，我们可以在 JSON 的数据表示方法上进行简化和修改。最简单的是空值和布尔值，我们直接使用 &lt;code>null&lt;/code>、&lt;code>true&lt;/code> 和 &lt;code>false&lt;/code> 即可；为了保持解析简单，也为了避免伤害&lt;a class="link" href="https://hitchdev.com/strictyaml/why/implicit-typing-removed/" target="_blank" rel="noopener"
>挪威的朋友们&lt;/a>，我们严守 JSON 标准，空值和布尔值的其他变体（如 &lt;code>nil&lt;/code>、&lt;code>yes&lt;/code>、&lt;code>no&lt;/code> 等）都不予考虑，首字母/全字母大写的形式也不支持。&lt;/p>
&lt;h3 id="数字">数字
&lt;/h3>&lt;p>数字的表示也很直观，包括整数（&lt;code>123&lt;/code>）、小数（&lt;code>3.14&lt;/code>）和科学计数法（&lt;code>1.23e4&lt;/code>），以及可选的正负号（&lt;code>-42&lt;/code>、&lt;code>+3.14&lt;/code>），这些也都是 JSON 所支持的。十六进制数（&lt;code>0x2A&lt;/code>）以及特殊值 NaN/Inf，虽然 JSON 并不支持，但也并不罕见，因此也值得我们支持。至于其他表示形式，如八进制和二进制数，以及省略小数点前后0的小数（&lt;code>.5&lt;/code>、&lt;code>2.&lt;/code>），出于保持简洁和清晰的原因，我们仍然不予支持。同样为了保持解析简单以及拼写一致性，NaN 和 Inf 都只支持小写形式（&lt;code>nan&lt;/code>、&lt;code>inf&lt;/code>、&lt;code>+inf&lt;/code>、&lt;code>-inf&lt;/code>）。&lt;/p>
&lt;h3 id="字符串">字符串
&lt;/h3>&lt;p>字符串是一个值得改进的地方，JSON 双引号确实清晰，但也确实太占空间了。无引号的键值在各类配置文件，以及 YAML 和 TOML 等格式中都很常见，因此，我们决定允许无引号字符串的使用，但是为了避免歧义，无引号字符串的内容不能是其他类型的字面量（&lt;code>null&lt;/code>、&lt;code>true&lt;/code>、&lt;code>false&lt;/code>、&lt;code>nan&lt;/code>、&lt;code>inf&lt;/code>），不能以数字或正负号开头（以避免解析时与数字字面量冲突），不能包含空白字符（因为空白字符对提升 Vector 和 Map 的可读性非常重要），也不能包含表示 Vector 和 Map 结构的字符（见下文）。如果需要表示这些内容，则必须使用双引号括起来的字符串。&lt;/p>
&lt;p>对了，字符串里还有一个非常重要的点：转义字符。首先我们肯定要支持 JSON 标准的转义字符 &lt;code>&amp;quot;\/bfnrt&lt;/code> 以及 Unicode 转义 &lt;code>\uXXXX&lt;/code>；此外，考虑到可能有使用者需要存储 BLOB 数据，十六进制字节转义 &lt;code>\xXX&lt;/code> 是值得支持的；最后，为了更好地支持 emoji 等等非 BMP 字符，支持超过 4 位的 Unicode 转义 &lt;code>\u{XXXXXX}&lt;/code> 也是必要的。&lt;/p>
&lt;h3 id="map-与-vector">Map 与 Vector
&lt;/h3>&lt;p>Map 和 Vector 的表示是设计的重点。JSON 使用 &lt;code>{}&lt;/code> 和 &lt;code>[]&lt;/code> 分别表示 Object 和 Array，这种表示方法清晰且易于解析，但一是占用了 4 个字符，这和我们减少语法字符的目标简直是背道而驰，必须要进行改进。首先我们不妨大胆地直接去掉所有括号，只使用逗号 &lt;code>,&lt;/code> 作为元素分隔符，冒号 &lt;code>:&lt;/code> 作为键值分隔符；解析时通过逗号的存在来判断是否为 Map 或 Vector，通过冒号的存在来判断是否为 Map，例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span style="display:flex;">&lt;span>a:b, c:d, e:f &amp;lt;= Map
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>a, b, c, d, e &amp;lt;= Vector
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>看起来不错！这样只需要两个语法字符（&lt;code>:,&lt;/code>）就够了。不过仔细思考不难发现，这样的表示法存在歧义： &lt;code>a:b:c,d:e&lt;/code> 对应的到底是 &lt;code>{a:{b:c},d:e}&lt;/code> 还是 &lt;code>{a:{b:c,d:e}}&lt;/code>？&lt;code>a:b,c&lt;/code> 到底是 &lt;code>[{a:b},c]&lt;/code> 还是 &lt;code>{a:[b,c]}&lt;/code>？原因在于，直接去掉括号会不可避免地丢失 Map 和 Vector 的结束信息，既然如此，补充一个符号表示 Map 和 Vector 的结束是否可以呢？例如使用分号 &lt;code>;&lt;/code> 作为结束符号：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span style="display:flex;">&lt;span>a:b:c;,d:e; = {a:{b:c},d:e}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>a:b:c,d:e;; = {a:{b:c,d:e}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>a:b;,c; = [{a:b},c]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>a:b,c;; = {a:[b,c]}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>确实清晰了不少，虽然 &lt;code>;,&lt;/code> 看起来有点别扭，但歧义的问题确实解决了，好在在现实生活中嵌套的情况并不多见，因此这样的写法尚属可接受。不幸的是，这里还存在一个很隐蔽的问题：&lt;code>a:b;;&lt;/code> 是 &lt;code>{a:[b]}&lt;/code> 还是 &lt;code>[{a:b}]&lt;/code>？丢掉 Map 和 Vector 的开始信息还是导致了问题。不过幸运的是，本文的作者还是找到了一个不增加新字符的方案：复用 &lt;code>:&lt;/code> 作为 Vector 的开始符号，Map 仍然不使用开始符号，这样就解决了上述的歧义：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span style="display:flex;">&lt;span>a::b;; = {a:[b]}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>:a:b;; = [{a:b}]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>虽然上面的写法相比最初的设计复杂了不少，但至少解决了歧义问题，且只使用了 3 个语法字符（&lt;code>:,;&lt;/code>），就支持了任意嵌套的 Map 和 Vector 结构，已经相当不错了。下面是一些示例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span style="display:flex;">&lt;span>bind:&amp;#34;0.0.0.0&amp;#34;, ports::80,443;, log_level:info, max_connections:1000;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>meta:format:twic, version:1.0.0;, payload::&amp;#34;Hello, World!&amp;#34;,&amp;#34;Data No. 2&amp;#34;;;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可读性的确有些下降，但尚在可接受的范围内，双冒号也让人想起了 TOML 中双方括号 &lt;code>[[table]]&lt;/code> 的写法，也不算太突兀。&lt;/p>
&lt;blockquote>
&lt;p>不得不说，这样的设计确实为了减少语法字符付出了可读性的代价，不过既然设计目标已定，并且最后的结果也还算可以接受，那就姑且一试吧。&lt;/p>&lt;/blockquote>
&lt;h2 id="定义形式语法">定义形式语法
&lt;/h2>&lt;p>最后，我们可以得到一个「不太形式的形式语法」定义，如下所示：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span style="display:flex;">&lt;span>value = null | boolean | number | string | vector | map
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>null = &amp;#34;null&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>boolean = &amp;#34;true&amp;#34; | &amp;#34;false&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>number = decimal | hex | special
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>decimal = [ &amp;#34;+&amp;#34; | &amp;#34;-&amp;#34; ] int [ frac ] [ exp ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>hex = [ &amp;#34;+&amp;#34; | &amp;#34;-&amp;#34; ] &amp;#34;0x&amp;#34; hex_digit { hex_digit }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>special = &amp;#34;nan&amp;#34; | [ &amp;#34;+&amp;#34; | &amp;#34;-&amp;#34; ] &amp;#34;inf&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>int = digit { digit }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>frac = &amp;#34;.&amp;#34; digit { digit }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>exp = ( &amp;#34;e&amp;#34; | &amp;#34;E&amp;#34; ) [ &amp;#34;+&amp;#34; | &amp;#34;-&amp;#34; ] digit { digit }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>hex_digit = digit | &amp;#34;a&amp;#34; | &amp;#34;b&amp;#34; | &amp;#34;c&amp;#34; | &amp;#34;d&amp;#34; | &amp;#34;e&amp;#34; | &amp;#34;f&amp;#34; | &amp;#34;A&amp;#34; | &amp;#34;B&amp;#34; | &amp;#34;C&amp;#34; | &amp;#34;D&amp;#34; | &amp;#34;E&amp;#34; | &amp;#34;F&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>digit = &amp;#34;0&amp;#34; | &amp;#34;1&amp;#34; | &amp;#34;2&amp;#34; | &amp;#34;3&amp;#34; | &amp;#34;4&amp;#34; | &amp;#34;5&amp;#34; | &amp;#34;6&amp;#34; | &amp;#34;7&amp;#34; | &amp;#34;8&amp;#34; | &amp;#34;9&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>string = unquoted_string | quoted_string
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unquoted_string = (? any identifier-like string that does not conflict with other types ?)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>quoted_string = (? double-quoted string with escape sequences ?)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>vector = &amp;#34;:&amp;#34; [ value { &amp;#34;,&amp;#34; value } ] &amp;#34;;&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>map = [ key_value { &amp;#34;,&amp;#34; key_value } ] &amp;#34;;&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>key_value = string &amp;#34;:&amp;#34; value
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不太形式化的地方主要在于，其一，未对空白字符进行定义和说明，实际上空白字符可以出现在任意两个 &lt;code>value&lt;/code> 之间，以及在 Map 和 Vector 的组成部分之间，这是非常直观的；其二，未对无引号字符串和双引号字符串的具体内容进行形式化定义，实际上无引号字符串的定义比较复杂，而双引号字符串则需要定义转义字符等内容，这里就不赘述了。&lt;/p>
&lt;h2 id="最重要的事情起一个好名字">最重要的事情：起一个好名字
&lt;/h2>&lt;p>OK，现在终于来到了最重要也是最困难的环节：给这种新格式起一个好名字。名字要简洁易记，最好能直接拼读出来，还要能反映出这种格式的特点和用途。经过一番斟酌，本文的作者最终决定将这种格式命名为 &lt;strong>twic&lt;/strong>：&lt;strong>Tiny Writable Inline Config&lt;/strong>，意为「微型易写单行配置」，同时可以读若「tweak」，意为「微调」，寓意这种格式适合用来进行简单的配置和数据交换。&lt;/p>
&lt;blockquote>
&lt;p>本文的作者在这里认真地推荐所有有着起名困难症的读者使用 AI 解决这个问题，详细描述你的格式设计思路和特点，然后让 AI 帮你生成一些名字供你选择。&lt;/p>&lt;/blockquote>
&lt;h2 id="结语">结语
&lt;/h2>&lt;p>至此，我们完成了一种新的数据交换格式 twic 的设计工作。虽然这种格式在可读性上有所妥协，但它确实实现了使用极少的语法字符来表示通用数据类型的目标，适合用于短小简单的单行配置与数据。在&lt;a class="link" href="../b/" >下一篇文章&lt;/a>中，我们将继续深入，介绍如何在 Rust 语言中实现 twic 的数据结构定义，以及基础的序列化和反序列化功能。&lt;/p></description></item></channel></rss>