Zig's Lovely Syntax 这篇文章探讨了编程语言 Zig 的语法特点,并将其与 Rust、C 等其他语言进行了比较,指出 Zig 在语法设计上的优点和一些独特之处。作者认为,尽管语法是语言最不有趣的细节,但 Zig 在许多方面做到了“恰到好处”。
文章的主要观点和细节总结如下:
-
整数字面量 (Integer Literals):
- Zig 不使用后缀来指定整数类型(如
92u8),而是将所有整数字面量视为 comptime_int 类型。
- 这些字面量在编译时已知,并在赋值或类型转换时隐式强制转换为特定类型(例如
const x: i32 = 92;)。
- 这并非类型推断,而是隐式编译时强制转换,通常意味着
var x = 92; 需要显式类型。
-
字符串字面量 (String Literals):
- Zig 提供了独特的原始多行字符串语法,以
\ 开头,每行以 \ 引导。
- 这种语法避免了转义
\ 的问题,能很好地处理缩进,并且每个行都是一个单独的词法标记,使得换行符始终是空白符。
- 作者认为这是 Zig 相对于 Rust 的一个巨大改进,Rust 的
r##""## 语法存在缩进、嵌套和未闭合字面量等问题。
-
记录字面量 (Record Literals):
- Zig 采用类似 C 的记录字面量语法,例如
const p: Point = .{ .x = 1, .y = 2 };。
.{ 看起来有点奇怪,但 .x = 1 与赋值语法 obj.x = 1 一致,这使得通过搜索 .x = 来查找字段写入操作非常有用,有助于代码理解。
-
前缀类型 (Prefix Types):
- 与 C 语言复杂的类型声明规则不同,Zig 的所有类型都是前缀形式,例如
u32、[3]u32、?[3]u32、*const ?[3]u32。
- 指针类型是前缀,但指针解引用是后缀 (
ptr.* = 92;),阅读起来更自然。
-
标识符 (Identifiers):
- Zig 支持“原始”标识符语法
@ "a name with a space",用于避免关键字冲突或导出非标准名称的符号。它重用了 Zig 的内置函数 (@TypeOf) 和字符串语法。
-
函数 (Functions):
- Zig 像 Rust 一样使用
fn foo 语法声明函数,将 fn 关键字和函数名放在一起,便于搜索。
- Zig 函数声明省略了 Rust 的
-> 箭头,例如 fn add(x: i32, i32) i32。作者认为这减少了视觉噪音,因为 Zig 没有带有推断返回类型的 Lambda 表达式,返回类型总是强制性的。
- 作者更喜欢
void 而不是 () 作为类型名称。
-
局部变量 (Locals):
- Zig 使用
const 和 var 绑定值,与 Rust 的 const(在 Zig 中相当于 comptime)略有不同。
- 作者认为
const 比 var 更长,不够简洁,Kotlin 的 val、var、fun 更优。
- Zig 像 Rust 一样使用
'name' (':' Type)? 的类型声明语法,优于 Type 'name',因为可选后缀更容易解析。
-
连接即控制流 (Conjunction Is Control Flow):
- Zig 不使用
&& 和 ||,而是使用 and 和 or 关键字。
- 作者认为这更容易阅读和输入,更深层的原因是这些布尔运算符是短路的,属于控制流,使用关键字强调了这一点,避免了误解。位操作仍使用
& 和 |。
-
显式返回 (Explicit return):
- Zig 像 Rust 一样有语句和表达式,但更偏向语句,要求显式
return。
- 没有 Lambda 使得
return 的作用域始终清晰。
- 块表达式的值是
void,不以表达式结尾,这消除了 Rust 中关于分号的“心智负担”。
- 如果需要块返回一个值,Zig 支持通过标签块
break :blk value 来实现。
-
If 语句 (If):
- Rust 强制
if 语句使用大括号,以避免“悬空 else”问题,但这使得单行 if 显得笨重。
- Zig 采用传统做法,要求括号,大括号可选,允许三元运算符风格的
if (a) b else c。
- Zig 编译器的一部分是强制性的、不可配置的格式化器,可以捕获可能掩盖错误的格式化问题(例如
1 -2),这弥补了语法上的潜在风险。
-
循环 (Loops):
- Zig 像 Python 一样允许循环带有
else 子句。
- 循环是表达式,使得命令式搜索非常简洁,例如用于编译时查找类型。
- Zig 没有像 Rust 或 Go 那样专门的无限循环语法,而是通过
while (true) 或 for (0..safety_bound) 来表达,这与 Zig 的编译时语义紧密相关。
for、while、if、switch 和 catch 都使用 Ruby/Rust 风格的捕获值命名语法 |element|。
-
名称的清晰度 (Clarity of Names):
- Zig 禁止变量遮蔽 (shadowing)。
- 没有“Prelude”,所有标准库内容都需要显式
@import("std")。
- 没有全局导入,需要显式导入特定项,例如
const ArrayList = std.ArrayList;。
- Zig 没有继承、混入、ADL、扩展函数、隐式 trait 等,因此
x.foo() 保证 foo 是 x 类型上声明的方法。
- 曾允许方法和字段同名,但后来被移除。
- Zig 没有命名空间,同一作用域中只能有一种
foo,这极大地简化了名称解析,作者惊叹于这种方法带来的便利。
-
一切皆表达式 (Everything Is an Expression):
- 这是 Zig 语法最显著的(通过其缺失)特点,与 Zig 最深刻的语义密切相关。值、类型和模式都使用相同的表面语法表示,然后在语义分析阶段进行分类。
- 这与许多语言不同,后者通常为值、类型和模式提供不同的语法家族。
- 这种统一性减少了语言的“繁忙感”,例如在类型位置使用
if 表达式是可能的。
-
泛型 (Generics):
- 泛型类型实例看起来像函数调用:
ArrayList(u32)。
- Zig 泛型参数从不推断,如果函数接受 3 个编译时参数和 2 个运行时参数,则总是以 5 个参数语法调用。
- 通过编译时闭包 (comptime closures) 解决频繁指定类型的问题,一旦类型在变量中被捕获,后续使用就不需要再次指定。作者认为这使得 Zig 中的类型注释负担很低。
-
声明字面量和结果位置语义 (Declaration Literals and Result Location Semantics):
- Zig 没有 Hindley-Milner 类型推断,但依赖于“结果位置语义”进行类型传播。
- 当
if 表达式的两个分支产生不同 comptime_int 值时,需要在赋值时显式指定类型或向下推导强制转换。
- Zig 的编译器更像一个解释器,它在评估表达式时向下传递结果位置和类型。
- 这解释了枚举的简洁
.variant 语法和记录字面量的开头点 .{}(@ResultType().whatever 的简写)。
.{} 语法一开始可能让人感到奇怪,但提供了强大的 API,例如免费获得了命名参数和默认参数。
-
内置函数 (Built-ins):
- Zig 使用
@ 前缀表示内置函数,例如 @divExact、@bitCast、@as、@import。
- 这提供了一个独立的语法命名空间来处理编译器支持的原始操作,避免了操作符重载等问题。
@as(i32, 92) 中的类型在前,是结果类型语义的应用。
@import("./foo.zig") 是作者最喜欢的内置函数,它清晰地指明了文件来源,并且是一个“反向语法糖”,尽管看起来像函数调用,但参数必须是语法上的字符串字面量。
总结性思考:
作者认为 Zig 的这些“愚蠢的语法决定”加起来使得语言读起来非常愉快。他强调,语言功能越少,所需的语法就越少,而更少的语法通常是好事。语言功能之间并非正交,会相互影响。即使功能集固定,选择一个好的具体语法(解析无歧义、易于搜索、易读易写)仍然需要大量工作。虽然可以借鉴其他语言的成熟方案,但也应勇于创新。
唯一不喜欢的地方:
作者在撰写文章时发现了一个不喜欢的语法形式:带有增量操作的 while 循环 while (i < 10) : (i+=1) { ... }。他认为这类似于 C 语言的 for 循环(缺少声明部分),控制流跳跃,并且 : 是 Zig 中唯一一个用符号而非关键字表达控制流的例子,感觉不一致。由于现在有了 for(0..10) |i| 形式,他认为这种 while 循环变得多余。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、Telegram 群组
Zig's Lovely Syntax 这篇文章探讨了编程语言 Zig 的语法特点,并将其与 Rust、C 等其他语言进行了比较,指出 Zig 在语法设计上的优点和一些独特之处。作者认为,尽管语法是语言最不有趣的细节,但 Zig 在许多方面做到了“恰到好处”。
文章的主要观点和细节总结如下:
整数字面量 (Integer Literals):
92u8),而是将所有整数字面量视为comptime_int类型。const x: i32 = 92;)。var x = 92;需要显式类型。字符串字面量 (String Literals):
\开头,每行以\引导。\的问题,能很好地处理缩进,并且每个行都是一个单独的词法标记,使得换行符始终是空白符。r##""##语法存在缩进、嵌套和未闭合字面量等问题。记录字面量 (Record Literals):
const p: Point = .{ .x = 1, .y = 2 };。.{看起来有点奇怪,但.x = 1与赋值语法obj.x = 1一致,这使得通过搜索.x =来查找字段写入操作非常有用,有助于代码理解。前缀类型 (Prefix Types):
u32、[3]u32、?[3]u32、*const ?[3]u32。ptr.* = 92;),阅读起来更自然。标识符 (Identifiers):
@ "a name with a space",用于避免关键字冲突或导出非标准名称的符号。它重用了 Zig 的内置函数 (@TypeOf) 和字符串语法。函数 (Functions):
fn foo语法声明函数,将fn关键字和函数名放在一起,便于搜索。->箭头,例如fn add(x: i32, i32) i32。作者认为这减少了视觉噪音,因为 Zig 没有带有推断返回类型的 Lambda 表达式,返回类型总是强制性的。void而不是()作为类型名称。局部变量 (Locals):
const和var绑定值,与 Rust 的const(在 Zig 中相当于comptime)略有不同。const比var更长,不够简洁,Kotlin 的val、var、fun更优。'name' (':' Type)?的类型声明语法,优于Type 'name',因为可选后缀更容易解析。连接即控制流 (Conjunction Is Control Flow):
&&和||,而是使用and和or关键字。&和|。显式返回 (Explicit return):
return。return的作用域始终清晰。void,不以表达式结尾,这消除了 Rust 中关于分号的“心智负担”。break :blk value来实现。If 语句 (If):
if语句使用大括号,以避免“悬空 else”问题,但这使得单行if显得笨重。if (a) b else c。1 -2),这弥补了语法上的潜在风险。循环 (Loops):
else子句。while (true)或for (0..safety_bound)来表达,这与 Zig 的编译时语义紧密相关。for、while、if、switch和catch都使用 Ruby/Rust 风格的捕获值命名语法|element|。名称的清晰度 (Clarity of Names):
@import("std")。const ArrayList = std.ArrayList;。x.foo()保证foo是x类型上声明的方法。foo,这极大地简化了名称解析,作者惊叹于这种方法带来的便利。一切皆表达式 (Everything Is an Expression):
if表达式是可能的。泛型 (Generics):
ArrayList(u32)。声明字面量和结果位置语义 (Declaration Literals and Result Location Semantics):
if表达式的两个分支产生不同comptime_int值时,需要在赋值时显式指定类型或向下推导强制转换。.variant语法和记录字面量的开头点.{}(@ResultType().whatever的简写)。.{}语法一开始可能让人感到奇怪,但提供了强大的 API,例如免费获得了命名参数和默认参数。内置函数 (Built-ins):
@前缀表示内置函数,例如@divExact、@bitCast、@as、@import。@as(i32, 92)中的类型在前,是结果类型语义的应用。@import("./foo.zig")是作者最喜欢的内置函数,它清晰地指明了文件来源,并且是一个“反向语法糖”,尽管看起来像函数调用,但参数必须是语法上的字符串字面量。总结性思考:
作者认为 Zig 的这些“愚蠢的语法决定”加起来使得语言读起来非常愉快。他强调,语言功能越少,所需的语法就越少,而更少的语法通常是好事。语言功能之间并非正交,会相互影响。即使功能集固定,选择一个好的具体语法(解析无歧义、易于搜索、易读易写)仍然需要大量工作。虽然可以借鉴其他语言的成熟方案,但也应勇于创新。
唯一不喜欢的地方:
作者在撰写文章时发现了一个不喜欢的语法形式:带有增量操作的
while循环while (i < 10) : (i+=1) { ... }。他认为这类似于 C 语言的for循环(缺少声明部分),控制流跳跃,并且:是 Zig 中唯一一个用符号而非关键字表达控制流的例子,感觉不一致。由于现在有了for(0..10) |i|形式,他认为这种while循环变得多余。加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: