Chapter 03Data Fundamentals

数据基础

概述

控制流只有在驱动数据时才有用,因此本章将 Zig 的核心集合类型——数组(array)、切片(slice)和哨兵终止字符串——基于实际用法进行阐述,同时保持值语义的显式性。参见 #Arrays#Slices 作为参考。

我们还会让指针、可选类型和对齐友好的类型转换变得日常化,展示如何安全地重新解释内存,同时保留边界检查和可变性的清晰性。参见 #Pointers#alignCast 获取详细信息。

Zig 的类型系统分类

在深入研究具体的集合类型之前,了解数组、切片和指针在 Zig 类型系统中的位置会很有帮助。Zig 中的每种类型都属于一个类别,每个类别都提供特定的操作:

graph TB subgraph "类型分类" PRIMITIVE["原始类型<br/>bool, u8, i32, f64, void, ..."] POINTER["指针类型<br/>*T, [*]T, []T, [:0]T"] AGGREGATE["聚合类型<br/>struct, array, tuple"] FUNCTION["函数类型<br/>fn(...) ReturnType"] SPECIAL["特殊类型<br/>anytype, type, comptime_int"] end subgraph "常见类型操作" ABISIZE["abiSize()<br/>内存中的字节大小"] ABIALIGN["abiAlignment()<br/>所需对齐"] HASRUNTIME["hasRuntimeBits()<br/>是否有运行时存储?"] ELEMTYPE["elemType()<br/>元素类型(数组/切片)"] end PRIMITIVE --> ABISIZE POINTER --> ABISIZE AGGREGATE --> ABISIZE PRIMITIVE --> ABIALIGN POINTER --> ABIALIGN AGGREGATE --> ABIALIGN POINTER --> ELEMTYPE AGGREGATE --> ELEMTYPE

本章的关键见解:

  • 数组 是编译时长度已知的聚合类型——它们的大小为 element_size * length
  • 切片 是指针类型,存储指针和运行时长度——始终为 2 × 指针大小
  • 指针 有多种形式(单项 *T、多项 [*]T、切片 []T),具有不同的安全保证
  • 所有类型都公开其大小和对齐,这会影响结构体布局和内存分配

这种类型感知的设计使编译器能够对切片强制执行边界检查,同时在你显式选择退出安全性时,允许对多项指针进行指针算术运算。

学习目标

  • 区分数组的值语义与切片的视图,包括用于安全回退的零长度习语。
  • 导航指针形式(*T[*]T?*T)并解包可选类型,而不牺牲安全仪器(参见 #Optionals)。
  • 在与其他 API 互操作时应用哨兵终止字符串和对齐感知转换(@alignCast@bitCast@intCast)(参见 #Sentinel-Terminated-Pointers#Explicit-Casts)。

在内存中构建集合

数组拥有存储,而切片借用存储,因此编译器围绕长度、可变性和生命周期强制执行不同的保证;掌握它们的相互作用可以使迭代可预测,并将大多数边界检查移至调试构建。

数组作为拥有的存储

数组在其类型中携带长度,按值复制,并为你提供一个可变的基线,从中可以划分只读和读写切片。

Zig
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});
}
运行
Shell
$ zig run arrays_and_slices.zig
输出
Shell
array 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 切片

理解数组和切片在内存中的布局方式,可以阐明为什么"数组拥有存储而切片借用存储",以及为什么数组到切片的强制转换是一个廉价操作:

graph TB subgraph "内存中的数组" ARRAY_DECL["const values: [4]i32 = .{1, 2, 3, 4}"] ARRAY_MEM["内存布局(16 字节)\n\n栈帧\n| 1 | 2 | 3 | 4 |"] ARRAY_DECL --> ARRAY_MEM end subgraph "内存中的切片" SLICE_DECL["const slice: []const i32 = &values"] SLICE_MEM["内存布局(64 位上为 16 字节)\n\n栈帧\n| ptr | len=4 |"] POINTS["ptr 指向数组数据"] SLICE_DECL --> SLICE_MEM SLICE_MEM --> POINTS end POINTS -.->|"引用"| ARRAY_MEM subgraph "关键区别" DIFF1["数组:内联存储数据<br/>大小 = elem_size × length"] DIFF2["切片:存储指针 + 长度<br/>大小 = 2 × pointer_size(64 位上为 16 字节)"] DIFF3["强制转换:&array → slice<br/>只是创建 {ptr, len} 对"] end

为什么这很重要:

  • 数组具有 值语义:赋值数组会复制所有元素
  • 切片具有 引用语义:赋值切片只复制指针和长度
  • 数组到切片的强制转换(&array)很廉价——它不复制数据,只创建一个描述符
  • 切片是"胖指针":它们携带运行时长度信息,支持边界检查

这就是为什么函数通常接受切片作为参数——它们可以处理数组、切片以及两者的部分,而无需复制底层数据。

实践中的字符串和哨兵

哨兵终止数组桥接到 C API 而不放弃切片的安全性;你可以使用 std.mem.span 重新解释字节流,并在保留哨兵约定时仍然可以改变底层缓冲区。

Zig
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)});
}
运行
Shell
$ zig run sentinel_strings.zig
输出
Shell
literal 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 提供多种指针类型,每种类型都有不同的安全保证和用例。理解何时使用每种形状对于编写安全、高效的代码至关重要:

graph TB subgraph "指针形状" SINGLE["*T<br/>单项指针"] MANY["[*]T<br/>多项指针"] SLICE["[]T<br/>切片"] OPTIONAL["?*T<br/>可选指针"] SENTINEL_PTR["[*:0]T<br/>哨兵多项指针"] SENTINEL_SLICE["[:0]T<br/>哨兵切片"] end subgraph "特征" SINGLE --> S_BOUNDS["✓ 边界:单个元素<br/>✓ 安全性:解引用检查<br/>📍 用途:函数参数、引用"] MANY --> M_BOUNDS["⚠ 边界:未知长度<br/>✗ 安全性:无边界检查<br/>📍 用途:C 互操作、紧密循环"] SLICE --> SL_BOUNDS["✓ 边界:运行时长度<br/>✓ 安全性:边界检查<br/>📍 用途:大多数 Zig 代码、迭代"] OPTIONAL --> O_BOUNDS["✓ 边界:可能为空<br/>✓ 安全性:必须先解包<br/>📍 用途:可选引用"] SENTINEL_PTR --> SP_BOUNDS["✓ 边界:直到哨兵<br/>~ 安全性:哨兵必须存在<br/>📍 用途:C 字符串、null 终止"] SENTINEL_SLICE --> SS_BOUNDS["✓ 边界:长度 + 哨兵<br/>✓ 安全性:长度和哨兵都有<br/>📍 用途:Zig ↔ C 字符串桥接"] end

比较表:

形状示例长度已知?边界检查?常见用途
*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 边界,内部转换为切片

用于共享可变性的可选指针

可选单项指针暴露可变性而不猜测生命周期——仅在存在时捕获它们,通过解引用进行修改,并在指针不存在时优雅地回退。

Zig
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]});
}
运行
Shell
$ zig run pointers_and_optionals.zig
输出
Shell
alias -> reading 42
optional pointer empty
optional pointer mutated to 52
slice view len=4
samples[2] via many pointer = 42

?*Sensor 门槛将修改保持在模式匹配后面,而多项指针([*]i32)通过放弃边界检查来记录别名风险——这是一种故意的权衡,仅保留给紧密循环和 FFI。

对齐和重新解释数据

当你必须重新解释原始字节时,使用转换内建函数来提升对齐、更改指针元素类型,并保持整数/浮点转换的显式性,以便调试构建可以捕获未定义的假设(参见 #bitCast)。

Zig
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});
}
运行
Shell
$ zig run alignment_and_casts.zig
输出
Shell
32-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 如何依赖调用者提供的范围保证。

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.