Zig 的哲学

Zig 是一门现代、底层的通用编程语言,被许多程序员视为 C 语言的现代化改进版本。Zig 的核心理念可以用"少即是多"来概括——它不是通过不断增加新特性来变得现代,而是通过移除 C 和 C++ 中令人困扰的特性来实现改进。

正如 Zig 官网所言:

“Focus on debugging your application rather than debugging your programming language knowledge”
(专注于调试你的应用程序,而不是调试你对编程语言的知识)

Zig 强调显式优于隐式

  • 没有预处理器宏,你写的代码就是实际编译的代码
  • 没有隐藏的控制流
  • 没有标准库函数在背后偷偷进行内存分配

这种设计哲学使得 Zig 代码更易读、更易调试,同时在边缘情况下表现出更一致和稳健的行为。

在 Zig 中创建对象

Zig 中最重要的两种类型便是可变(mutable)不可变(immutable)。可变类型允许你修改其值,而不可变类型则不允许。不可变类型通常用于存储常量数据,而可变类型则用于存储变量数据。

1
2
3
4
5
6
const age = 24;
// 下面这行代码是无效的!编译会报错
// age = 25;

var mutable_age: u8 = 24;
mutable_age = 25; // 这是合法的

使用 const 声明的变量是不可变的,而使用 var 声明的变量是可变的。

变量必须初始化

在 Zig 中,所有变量默认都需要有初始化值。如果你暂时不想给变量赋具体值,可以使用 undefined 来初始化:

1
var age: u8 = undefined;

但需要注意,undefined 是一个特殊的值,表示变量尚未被初始化。应当在代码中尽量减少使用 undefined,因为这会降低代码的可读性和安全性。

类型推断

Zig 支持类型推断,编译器可以根据初始值自动推断变量类型:

1
2
const inferred = 24;      // 编译器推断为 comptime_int
const explicit: u8 = 24; // 显式指定为 u8

特别注意:Zig 的严格检查

Zig 编译器有两个非常严格的检查规则:

1. 可变变量必须被修改

如果你创建了一个可变变量(使用 var),但没有对其进行修改,编译时会发生错误:

1
2
3
// 错误:可变变量未被修改
var x: i32 = 10;
_ = x;

正确做法:如果不需要修改,应该使用 const

1
2
const x: i32 = 10;
_ = x;

2. 不允许未使用的变量

如果你创建了一个变量但是没有使用,编译时也会报错:

1
const unused = 15; // 错误:未使用的局部变量

解决方案是使用下划线语法来显式忽略:

1
2
const age = 15;
_ = age; // 告诉编译器:我故意不使用这个变量

这种设计强制程序员保持代码整洁,避免遗留无用的变量。

代码块与作用域

在 Zig 中,**代码块(block)**是用花括号 {} 包围的代码区域。代码块可以包含其他代码块,形成嵌套结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
const std = @import("std");

pub fn main() void {
// 外层代码块
const x: i32 = 10;
{
// 内层代码块
const y: i32 = 20;
std.debug.print("x + y = {d}\n", .{x + y});
}
// y 在这里不可见,编译会报错
// std.debug.print("{d}\n", .{y});
}

代码块作为表达式

Zig 的一个强大特性是代码块可以作为表达式使用,并且可以有返回值:

1
2
3
4
5
6
const result = blk: {
const a = 10;
const b = 20;
break :blk a + b; // 使用 break 返回代码块的值
};
// result 的值为 30

使用 blk: 为代码块添加标签,然后用 break :标签名 返回值 的形式返回结果。这在需要根据条件计算初始值时非常有用:

1
2
3
4
5
6
7
const value = blk: {
if (condition) {
break :blk 100;
} else {
break :blk 200;
}
};

原生数据类型

Zig 提供了丰富的原生数据类型:

整数类型

无符号整数(Unsigned integers):只能表示非负数

  • u8 - 8 位整数(0 到 255)
  • u16 - 16 位整数(0 到 65,535)
  • u32 - 32 位整数(0 到 4,294,967,295)
  • u64 - 64 位整数
  • u128 - 128 位整数

有符号整数(Signed integers):可以表示负数

  • i8 - 8 位整数(-128 到 127)
  • i16 - 16 位整数(-32,768 到 32,767)
  • i32 - 32 位整数
  • i64 - 64 位整数
  • i128 - 128 位整数

浮点数类型

  • f16 - 16 位浮点数
  • f32 - 32 位浮点数(单精度)
  • f64 - 64 位浮点数(双精度)
  • f128 - 128 位浮点数

其他基本类型

  • bool - 布尔类型,truefalse
  • void - 空类型,表示没有值
  • noreturn - 表示函数不会返回(如无限循环或 panic)

C ABI 兼容类型

Zig 提供了与 C 语言 ABI 兼容的类型,方便与 C 代码交互:

  • c_char, c_short, c_ushort
  • c_int, c_uint, c_long, c_ulong
  • c_longlong, c_ulonglong
  • c_float, c_double

指针大小整数

  • isize - 有符号指针大小整数
  • usize - 无符号指针大小整数(常用于数组索引和内存大小)

查阅 Zig 官方文档 获取完整的类型表格。

数组

数组是一种存储相同类型元素的数据结构。Zig 数组的关键特点是:

数组大小必须在编译时确定,一旦创建就不能改变。

这与 C 语言的设计一致。

创建数组

1
2
3
4
5
6
7
8
9
10
11
// 显式指定大小
const ns = [4]u8{48, 24, 12, 6};

// 使用 _ 让编译器自动计算大小
const ls = [_]f64{432.1, 87.2, 900.05};

// 创建重复值的数组
const zeros = [_]u8{0} ** 10; // 10 个 0
const ones = [_]u8{1} ** 5; // 5 个 1

_ = ns; _ = ls; _ = zeros; _ = ones;

访问数组元素

Zig 使用零索引,即第一个元素的索引是 0:

1
2
3
4
5
const std = @import("std");

const ns = [4]u8{48, 24, 12, 6};
std.debug.print("第三个元素: {d}\n", .{ns[2]}); // 输出: 12
std.debug.print("数组长度: {d}\n", .{ns.len}); // 输出: 4

数组切片

Zig 支持类似 Python 的切片语法,可以创建数组的视图(不复制数据):

1
2
3
4
5
6
7
8
9
10
11
12
const ns = [4]u8{48, 24, 12, 6};

// 切片语法 [start..end],包含 start,不包含 end
const sl1 = ns[1..3]; // 包含索引 1 和 2 的元素: {24, 12}

// 从索引 1 到末尾
const sl2 = ns[1..]; // {24, 12, 6}

// 从开始到索引 3(不包含)
const sl3 = ns[0..3]; // {48, 24, 12}

_ = sl1; _ = sl2; _ = sl3;

切片可以看作是一对数据:指向数据的指针 [*]T 和元素数量 usize

数组运算符

Zig 提供了两个强大的数组运算符,只能在编译时确定长度的情况下使用

++ 数组连接

将两个数组拼接成新数组:

1
2
3
4
5
const a = [_]u8{1, 2, 3};
const b = [_]u8{4, 5};
const c = a ++ b; // {1, 2, 3, 4, 5}

std.debug.print("连接结果: {any}\n", .{c});

** 数组重复

将数组重复指定次数:

1
2
3
4
const a = [_]u8{1, 2};
const c = a ** 3; // {1, 2, 1, 2, 1, 2}

std.debug.print("重复结果: {any}\n", .{c});

编译时 vs 运行时

理解 Zig 中**编译时(comptime)运行时(runtime)**的区别非常重要:

特性 数组 切片
大小 编译时确定 运行时确定
类型 [N]T []T
存储位置 栈或全局 指向现有数据

数组大小必须在编译时确定,这意味着你不能在运行时动态创建数组。但你可以使用切片来处理运行时才知道大小的数据:

1
2
3
4
5
6
fn printSlice(data: []const u8) void {
// data 是一个切片,大小在运行时确定
for (data) |byte| {
std.debug.print("{d} ", .{byte});
}
}

Ziglings 练习

接下来继续我们的 Ziglings 之旅,这次从第 2 个练习开始。

Exercise 002: 导入标准库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> zig build
_ _ _
___(_) __ _| (_)_ __ __ _ ___
|_ | |/ _' | | | '_ \ / _' / __|
/ /| | (_| | | | | | | (_| \__ \
/___|_|\__, |_|_|_| |_|\__, |___/
|___/ |___/

"Look out! Broken programs below!"

Compiling: 002_std.zig
error: 1 compilation errors
exercises\002_std.zig:14:5: error: expected type expression, found '='
??? = @import("std");
^

Edit exercises/002_std.zig and run 'zig build' again.

解答:需要正确导入 std 模块:

1
const std = @import("std");

Exercise 003: 数据类型

这个练习深入理解数据类型,包括:

  • 使用 var 声明可变变量
  • 理解 u8u64i8 等类型的取值范围
1
2
3
4
5
6
7
8
9
10
11
12
const std = @import("std");

pub fn main() void {
var n: u8 = 50; // u8 范围: 0-255
n = n + 5; // 55

const pi: f32 = 3.14; // 32位浮点数

const negative: i8 = -10; // i8 范围: -128 到 127

std.debug.print("n={d}, pi={d}, negative={d}\n", .{n, pi, negative});
}

Exercise 004: 数组索引

涉及数组索引和可变变量的修改:

1
2
3
4
5
6
7
8
9
10
const std = @import("std");

pub fn main() void {
var nums = [4]u8{10, 20, 30, 40};

// 修改数组元素
nums[1] = 25;

std.debug.print("第二个元素: {d}\n", .{nums[1]});
}

Exercise 005: 数组运算符

练习使用 ++** 运算符:

1
2
3
4
5
6
7
8
9
10
11
12
const std = @import("std");

pub fn main() void {
const a = [_]u8{1, 2};
const b = [_]u8{3, 4};

const concatenated = a ++ b; // {1, 2, 3, 4}
const repeated = a ** 3; // {1, 2, 1, 2, 1, 2}

std.debug.print("连接: {any}\n", .{concatenated});
std.debug.print("重复: {any}\n", .{repeated});
}

通过本文,我们深入了解了 Zig 中变量声明、数据类型、代码块和数组的核心概念。Zig 的严格编译检查虽然初看起来有些繁琐,但它们帮助我们在编译期就发现潜在问题,从而编写出更健壮、更可靠的代码。下一篇文章我们将继续探索 Zig 的控制流和函数。