概述
控制流只有在驱动数据时才有用,因此本章将 Zig 的核心集合类型——数组(array)、切片(slice)和哨兵终止字符串——基于实际用法进行阐述,同时保持值语义的显式性。参见 #Arrays 和 #Slices 作为参考。
我们还会让指针、可选类型和对齐友好的类型转换变得日常化,展示如何安全地重新解释内存,同时保留边界检查和可变性的清晰性。参见 #Pointers 和 #alignCast 获取详细信息。
Zig 的类型系统分类
在深入研究具体的集合类型之前,了解数组、切片和指针在 Zig 类型系统中的位置会很有帮助。Zig 中的每种类型都属于一个类别,每个类别都提供特定的操作:
本章的关键见解:
- 数组 是编译时长度已知的聚合类型——它们的大小为
element_size * length - 切片 是指针类型,存储指针和运行时长度——始终为 2 × 指针大小
- 指针 有多种形式(单项
*T、多项[*]T、切片[]T),具有不同的安全保证 - 所有类型都公开其大小和对齐,这会影响结构体布局和内存分配
这种类型感知的设计使编译器能够对切片强制执行边界检查,同时在你显式选择退出安全性时,允许对多项指针进行指针算术运算。
学习目标
- 区分数组的值语义与切片的视图,包括用于安全回退的零长度习语。
- 导航指针形式(
*T、[*]T、?*T)并解包可选类型,而不牺牲安全仪器(参见 #Optionals)。 - 在与其他 API 互操作时应用哨兵终止字符串和对齐感知转换(
@alignCast、@bitCast、@intCast)(参见 #Sentinel-Terminated-Pointers 和 #Explicit-Casts)。
在内存中构建集合
数组拥有存储,而切片借用存储,因此编译器围绕长度、可变性和生命周期强制执行不同的保证;掌握它们的相互作用可以使迭代可预测,并将大多数边界检查移至调试构建。
数组作为拥有的存储
数组在其类型中携带长度,按值复制,并为你提供一个可变的基线,从中可以划分只读和读写切片。
const std = @import("std");
/// Prints information about a slice including its label, length, and first element.
/// 打印有关切片的信息,包括其标签、长度和第一个元素。
/// If the slice is empty, displays -1 as the head value.
/// 如果切片为空,则显示-1作为头值。
fn describe(label: []const u8, data: []const i32) void {
// Get first element or -1 if slice is empty
// 获取第一个元素,如果切片为空则为-1
const head = if (data.len > 0) data[0] else -1;
std.debug.print("{s}: len={} head={d}\n", .{ label, data.len, head });
}
/// Demonstrates array and slice fundamentals in Zig, including:
/// 演示Zig中的数组和切片基础,包括:
/// - Array declaration and initialization
/// - 数组声明和初始化
/// - Creating slices from arrays with different mutability
/// - 从具有不同可变性的数组创建切片
/// - Modifying arrays through direct indexing and slices
/// - 通过直接索引和切片修改数组
/// - Array copying behavior (value semantics)
/// - 数组复制行为(值语义)
/// - Creating empty and zero-length slices
/// - 创建空切片和零长度切片
pub fn main() !void {
// Declare mutable array with inferred size
// 声明具有推断大小的可变数组
var values = [_]i32{ 3, 5, 8, 13 };
// Declare const array with explicit size using anonymous struct syntax
// 使用匿名结构语法声明具有显式大小的const数组
const owned: [4]i32 = .{ 1, 2, 3, 4 };
// Create a mutable slice covering the entire array
// 创建覆盖整个数组的可变切片
var mutable_slice: []i32 = values[0..];
// Create an immutable slice of the first two elements
// 创建前两个元素的不可变切片
const prefix: []const i32 = values[0..2];
// Create a zero-length slice (empty but valid)
// 创建零长度切片(空但有效)
const empty = values[0..0];
// Modify array directly by index
// 通过索引直接修改数组
values[1] = 99;
// Modify array through mutable slice
// 通过可变切片修改数组
mutable_slice[0] = -3;
std.debug.print("array len={} allows mutation\n", .{values.len});
describe("mutable_slice", mutable_slice);
describe("prefix", prefix);
// Demonstrate that slice modification affects the underlying array
// 演示切片修改会影响底层数组
std.debug.print("values[0] after slice write = {d}\n", .{values[0]});
std.debug.print("empty slice len={} is zero-length\n", .{empty.len});
// Arrays are copied by value in Zig
// 在Zig中,数组按值复制
var copy = owned;
copy[0] = -1;
// Show that modifying the copy doesn't affect the original
// 显示修改副本不会影响原始数组
std.debug.print("copy[0]={d} owned[0]={d}\n", .{ copy[0], owned[0] });
// Create a slice from an empty array literal using address-of operator
// 使用地址运算符从空数组字面量创建切片
const zero: []const i32 = &[_]i32{};
std.debug.print("zero slice len={} from literal\n", .{zero.len});
}
$ zig run arrays_and_slices.zigarray len=4 allows mutation
mutable_slice: len=4 head=-3
prefix: len=2 head=-3
values[0] after slice write = -3
empty slice len=0 is zero-length
copy[0]=-1 owned[0]=1
zero slice len=0 from literal可变切片和原始数组共享存储,而 []const 前缀阻止写入——这是一个有意为之的边界,强制只读消费者保持诚实。
内存布局:数组 vs 切片
理解数组和切片在内存中的布局方式,可以阐明为什么"数组拥有存储而切片借用存储",以及为什么数组到切片的强制转换是一个廉价操作:
为什么这很重要:
- 数组具有 值语义:赋值数组会复制所有元素
- 切片具有 引用语义:赋值切片只复制指针和长度
- 数组到切片的强制转换(
&array)很廉价——它不复制数据,只创建一个描述符 - 切片是"胖指针":它们携带运行时长度信息,支持边界检查
这就是为什么函数通常接受切片作为参数——它们可以处理数组、切片以及两者的部分,而无需复制底层数据。
实践中的字符串和哨兵
哨兵终止数组桥接到 C API 而不放弃切片的安全性;你可以使用 std.mem.span 重新解释字节流,并在保留哨兵约定时仍然可以改变底层缓冲区。
const std = @import("std");
/// Demonstrates sentinel-terminated strings and arrays in Zig, including:
/// 演示Zig中的哨兵终止字符串和数组,包括:
/// - Zero-terminated string literals ([:0]const u8)
/// - 零终止字符串字面量([:0]const u8)
/// - Many-item sentinel pointers ([*:0]const u8)
/// - 多项哨兵指针([*:0]const u8)
/// - Sentinel-terminated arrays ([N:0]T)
/// - 哨兵终止数组([N:0]T)
/// - Converting between sentinel slices and regular slices
/// - 在哨兵切片和常规切片之间转换
/// - Mutation through sentinel pointers
/// - 通过哨兵指针进行修改
pub fn main() !void {
// String literals in Zig are sentinel-terminated by default with a zero byte
// Zig中的字符串字面量默认以零字节哨兵终止
// [:0]const u8 denotes a slice with a sentinel value of 0 at the end
// [:0]const u8表示末尾有哨兵值0的切片
const literal: [:0]const u8 = "data fundamentals";
// Convert the sentinel slice to a many-item sentinel pointer
// 将哨兵切片转换为多项哨兵指针
// [*:0]const u8 is compatible with C-style null-terminated strings
// [*:0]const u8与C风格的空终止字符串兼容
const c_ptr: [*:0]const u8 = literal;
// std.mem.span converts a sentinel-terminated pointer back to a slice
// std.mem.span将哨兵终止指针转换回切片
// It scans until it finds the sentinel value (0) to determine the length
// 它扫描直到找到哨兵值(0)以确定长度
const bytes = std.mem.span(c_ptr);
std.debug.print("literal len={} contents=\"{s}\"\n", .{ bytes.len, bytes });
// Declare a sentinel-terminated array with explicit size and sentinel value
// 声明具有显式大小和哨兵值的哨兵终止数组
// [6:0]u8 means an array of 6 elements plus a sentinel 0 byte at position 6
// [6:0]u8表示一个6元素的数组加上位置6的哨兵0字节
var label: [6:0]u8 = .{ 'l', 'a', 'b', 'e', 'l', 0 };
// Create a mutable sentinel slice from the array
// 从数组创建可变哨兵切片
// The [0.. :0] syntax creates a slice from index 0 to the end, with sentinel 0
// [0.. :0]语法创建从索引0到末尾的切片,带哨兵0
var sentinel_view: [:0]u8 = label[0.. :0];
// Modify the first element through the sentinel slice
// 通过哨兵切片修改第一个元素
sentinel_view[0] = 'L';
// Create a regular (non-sentinel) slice from the first 4 elements
// 从前4个元素创建常规(非哨兵)切片
// This drops the sentinel guarantees but provides a bounded slice
// 这会取消哨兵保证但提供有界切片
const trimmed: []const u8 = sentinel_view[0..4];
std.debug.print("trimmed slice len={} -> {s}\n", .{ trimmed.len, trimmed });
// Convert the sentinel slice to a many-item sentinel pointer
// 将哨兵切片转换为多项哨兵指针
// This allows unchecked indexing while preserving sentinel information
// 这允许unchecked索引,同时保留哨兵信息
const tail: [*:0]u8 = sentinel_view;
// Modify element at index 4 through the many-item sentinel pointer
// 通过多项哨兵指针修改索引4处的元素
// No bounds checking occurs, but the sentinel guarantees remain valid
// 不会发生边界检查,但哨兵保证仍然有效
tail[4] = 'X';
// Demonstrate that mutations through the pointer affected the original array
// 演示通过指针的修改影响了原始数组
// std.mem.span uses the sentinel to reconstruct the full slice
// std.mem.span使用哨兵重构完整切片
std.debug.print("full label after mutation: {s}\n", .{std.mem.span(tail)});
}
$ zig run sentinel_strings.zigliteral len=17 contents="data fundamentals"
trimmed slice len=4 -> Labe
full label after mutation: LabeX哨兵切片保持尾部的零完整,因此即使在本地修改之后,为 FFI 取一个 [*:0]u8 仍然是合理的,而普通切片则在 Zig 内提供符合人体工程学的迭代(参见 #Type-Coercion)。
std.mem.span 将哨兵指针转换为普通切片而不克隆数据,非常适合在返回到指针 API 之前临时需要边界检查或切片辅助函数的情况。
不可变和可变视图
当调用者仅检查数据时,优先使用 []const T——Zig 会乐意将可变切片强制转换为 const 视图,为你提供 API 清晰度,并防止意外写入首先编译。
指针模式和转换工作流
当你共享存储、与外部布局互操作或超出切片边界时,指针就会出现;通过依靠可选包装器和显式转换,你可以保持意图清晰,并允许在假设被打破时触发安全检查。
指针形状参考
Zig 提供多种指针类型,每种类型都有不同的安全保证和用例。理解何时使用每种形状对于编写安全、高效的代码至关重要:
比较表:
| 形状 | 示例 | 长度已知? | 边界检查? | 常见用途 |
|---|---|---|---|---|
*T | *i32 | 单个元素 | 是(隐式) | 对单项的引用 |
[*]T | [*]i32 | 未知 | 否 | C 数组、指针算术 |
[]T | []i32 | 运行时(在切片中) | 是 | Zig 主要集合类型 |
?*T | ?*i32 | 单个(如果非空) | 是 + null 检查 | 可选引用 |
[*:0]T | [*:0]u8 | 直到哨兵 | 哨兵必须存在 | C 字符串(char*) |
[:0]T | [:0]u8 | 运行时 + 哨兵 | 是 + 哨兵保证 | 用于 C API 的 Zig 字符串 |
指南:
- 默认使用切片(
[]T)用于所有 Zig 代码——它们提供安全性和便利性 - 使用单项指针(
*T)当你需要修改单个值或按引用传递时 - 避免多项指针(
[*]T),除非与 C 接口或在性能关键的内部循环中 - 使用可选指针(
?*T)当 null 是一个有意义的状态时,而不是用于错误处理 - 使用哨兵类型(
[*:0]T、[:0]T)在 C 边界,内部转换为切片
用于共享可变性的可选指针
可选单项指针暴露可变性而不猜测生命周期——仅在存在时捕获它们,通过解引用进行修改,并在指针不存在时优雅地回退。
const std = @import("std");
/// A simple structure representing a sensor device with a numeric reading.
/// 一个简单的结构,表示具有数值的传感器设备。
const Sensor = struct {
reading: i32,
};
/// Prints a sensor's reading value to debug output.
/// 将传感器的读数值打印到调试输出。
/// Takes a single pointer to a Sensor and displays its current reading.
/// 接受指向传感器的单个指针并显示其当前读数。
fn report(label: []const u8, ptr: *Sensor) void {
std.debug.print("{s} -> reading {d}\n", .{ label, ptr.reading });
}
/// Demonstrates pointer fundamentals, optional pointers, and many-item pointers in Zig.
/// 演示Zig中的指针基础、可选指针和多项指针。
/// This example covers:
/// - Single-item pointers (*T) and pointer dereferencing
/// - 单项指针(*T)和指针解引用
/// - Pointer aliasing and mutation through aliases
/// - 指针别名和通过别名进行修改
/// - Optional pointers (?*T) for representing nullable references
/// - 代表可空引用的可选指针(?*T)
/// - Unwrapping optional pointers with if statements
/// - 使用if语句解包可选指针
/// - Many-item pointers ([*]T) for unchecked multi-element access
/// - 用于unchecked多元素访问的多项指针([*]T)
/// - Converting slices to many-item pointers via .ptr property
/// - 通过.ptr属性将切片转换为多项指针
pub fn main() !void {
// Create a sensor instance on the stack
// 在栈上创建传感器实例
var sensor = Sensor{ .reading = 41 };
// Create a single-item pointer alias to the sensor
// 创建指向传感器的单项指针别名
// The & operator takes the address of sensor
// &运算符获取sensor的地址
var alias: *Sensor = &sensor;
// Modify the sensor through the pointer alias
// 通过指针别名修改传感器
// Zig automatically dereferences pointer fields
// Zig自动解引用指针字段
alias.reading += 1;
report("alias", alias);
// Declare an optional pointer initialized to null
// 声明初始化为null的可选指针
// ?*T represents a pointer that may or may not hold a valid address
// ?*T表示可能持有或可能不持有有效地址的指针
var maybe_alias: ?*Sensor = null;
// Attempt to unwrap the optional pointer
// 尝试解包可选指针
// This branch will not execute because maybe_alias is null
// 此分支不会执行,因为maybe_alias为null
if (maybe_alias) |pointer| {
std.debug.print("unexpected pointer: {d}\n", .{pointer.reading});
} else {
std.debug.print("optional pointer empty\n", .{});
}
// Assign a valid address to the optional pointer
// 将有效地址分配给可选指针
maybe_alias = &sensor;
// Unwrap and use the optional pointer
// 解包并使用可选指针
// The |pointer| capture syntax extracts the non-null value
// |pointer|捕获语法提取非null值
if (maybe_alias) |pointer| {
pointer.reading += 10;
std.debug.print("optional pointer mutated to {d}\n", .{sensor.reading});
}
// Create an array and a slice view of it
// 创建数组及其切片视图
var samples = [_]i32{ 5, 7, 9, 11 };
const view: []i32 = samples[0..];
// Extract a many-item pointer from the slice
// 从切片中提取多项指针
// Many-item pointers ([*]T) allow unchecked indexing without length tracking
// 多项指针([*]T)允许在无需长度跟踪的情况下进行unchecked索引
const many: [*]i32 = view.ptr;
// Modify the underlying array through the many-item pointer
// 通过多项指针修改底层数组
// No bounds checking is performed at this point
// 此时不执行边界检查
many[2] = 42;
std.debug.print("slice view len={}\n", .{view.len});
// Verify that the modification through many-item pointer affected the original array
// 验证通过多项指针的修改影响了原始数组
std.debug.print("samples[2] via many pointer = {d}\n", .{samples[2]});
}
$ zig run pointers_and_optionals.zigalias -> reading 42
optional pointer empty
optional pointer mutated to 52
slice view len=4
samples[2] via many pointer = 42?*Sensor 门槛将修改保持在模式匹配后面,而多项指针([*]i32)通过放弃边界检查来记录别名风险——这是一种故意的权衡,仅保留给紧密循环和 FFI。
对齐和重新解释数据
当你必须重新解释原始字节时,使用转换内建函数来提升对齐、更改指针元素类型,并保持整数/浮点转换的显式性,以便调试构建可以捕获未定义的假设(参见 #bitCast)。
const std = @import("std");
/// Demonstrates memory alignment concepts and various type casting operations in Zig.
/// 演示Zig中的内存对齐概念和各种类型转换操作。
/// This example covers:
/// - Memory alignment guarantees with align() attribute
/// - 使用align()属性的内存对齐保证
/// - Pointer casting with alignment adjustments using @alignCast
/// - 使用@alignCast的指针转换和对齐调整
/// - Type punning with @ptrCast for reinterpreting memory
/// - 使用@ptrCast进行类型转换以重新解释内存
/// - Bitwise reinterpretation with @bitCast
/// - 使用@bitCast进行按位重新解释
/// - Truncating integers with @truncate
/// - 使用@truncate截断整数
/// - Widening integers with @intCast
/// - 使用@intCast扩展整数
/// - Floating-point precision conversion with @floatCast
/// - 使用@floatCast进行浮点精度转换
pub fn main() !void {
// Create a byte array aligned to u64 boundary, initialized with little-endian bytes
// 创建对齐到u64边界的字节数组,用小端字节初始化
// representing 0x11223344 in the first 4 bytes
// 在前4个字节中表示0x11223344
var raw align(@alignOf(u64)) = [_]u8{ 0x44, 0x33, 0x22, 0x11, 0, 0, 0, 0 };
// Get a pointer to the first byte with explicit u64 alignment
// 获取具有显式u64对齐的第一个字节的指针
const base: *align(@alignOf(u64)) u8 = &raw[0];
// Adjust alignment constraint from u64 to u32 using @alignCast
// 使用@alignCast将对齐约束从u64调整为u32
// This is safe because u64 alignment (8 bytes) satisfies u32 alignment (4 bytes)
// 这是安全的,因为u64对齐(8字节)满足u32对齐(4字节)
const aligned_bytes = @as(*align(@alignOf(u32)) const u8, @alignCast(base));
// Reinterpret the byte pointer as a u32 pointer to read 4 bytes as a single integer
// 将字节指针重新解释为u32指针,将4字节作为单个整数读取
const word_ptr = @as(*const u32, @ptrCast(aligned_bytes));
// Dereference to get the 32-bit value (little-endian: 0x11223344)
// 解引用以获取32位值(小端:0x11223344)
const number = word_ptr.*;
std.debug.print("32-bit value = 0x{X:0>8}\n", .{number});
// Alternative approach: directly reinterpret the first 4 bytes using @bitCast
// 替代方法:使用@bitCast直接重新解释前4个字节
// This creates a copy and doesn't require pointer manipulation
// 这会创建一个副本,不需要指针操作
const from_bytes = @as(u32, @bitCast(raw[0..4].*));
std.debug.print("bitcast copy = 0x{X:0>8}\n", .{from_bytes});
// Demonstrate @truncate: extract the least significant 8 bits (0x44)
// 演示@truncate:提取最低有效8位(0x44)
const small: u8 = @as(u8, @truncate(number));
// Demonstrate @intCast: widen unsigned u32 to signed i64 without data loss
// 演示@intCast:将无符号u32扩展为有符号i64而不丢失数据
const widened: i64 = @as(i64, @intCast(number));
std.debug.print("truncate -> 0x{X:0>2}, widen -> {d}\n", .{ small, widened });
// Demonstrate @floatCast: reduce f64 precision to f32
// 演示@floatCast:将f64精度降低到f32
// May result in precision loss for values that cannot be exactly represented in f32
// 对于无法在f32中精确表示的值,可能会导致精度损失
const ratio64: f64 = 1.875;
const ratio32: f32 = @as(f32, @floatCast(ratio64));
std.debug.print("floatCast ratio -> {}\n", .{ratio32});
}
$ zig run alignment_and_casts.zig32-bit value = 0x11223344
bitcast copy = 0x11223344
truncate -> 0x44, widen -> 287454020
floatCast ratio -> 1.875通过链式使用 @alignCast、@ptrCast 和 @bitCast,你可以显式地断言布局关系,而随后的 @truncate/@intCast 转换在跨 API 缩窄或扩宽时保持整数宽度的诚实。
注意事项与陷阱
- 哨兵终止指针非常适合 C 桥接,但在 Zig 内部优先使用切片,以便边界检查保持可用,API 公开长度。
- 使用
@alignCast升级指针对齐在调试模式下如果地址未对齐仍会陷入——在提升之前证明前提条件。 - 多项指针(
[*]T)放弃边界检查;谨慎使用它们,并记录安全切片本应强制执行的不变式。
练习
- 扩展
arrays_and_slices.zig,从运行时数组创建零长度可变切片,然后通过std.ArrayList追加以观察切片视图如何保持有效。 - 修改
sentinel_strings.zig以接受用户提供的[:0]u8,并通过返回错误联合来防范缺少哨兵的输入。 - 增强
alignment_and_casts.zig,增加一个分支在截断之前拒绝低字节为零的值,展示@intCast如何依赖调用者提供的范围保证。