Day 12: 数组与切片
欢迎来到第十二天!在之前的学习中,我们已经接触了基本的数据类型。今天,我们将学习如何在Zig中处理一系列的同类型数据。Zig为此提供了两种主要的数据结构:数组(Arrays)和切片(Slices)。理解它们之间的区别与联系,是高效进行Zig编程的关键。数组是固定大小的内存块,而切片则为操作这些内存块(或其他连续内存区域)提供了动态的、灵活的视图。
2. 数组 (Arrays)
Section titled “2. 数组 (Arrays)”数组是一个拥有固定、编译时已知大小的连续内存序列,其中包含相同类型的元素。因为大小是固定的,所以数组的大小是其类型信息的一部分。
声明与初始化:
// 声明一个包含4个i32的数组,所有元素初始化为0var numbers: [4]i32 = .{ 0, 0, 0, 0 };
// 使用类型推断,并初始化值const letters = [_]u8{ 'a', 'b', 'c' }; // 编译器推断大小为3
// 初始化所有元素为同一个值const zeros = [_]u8{0} ** 100; // 一个包含100个0的数组
// 未初始化的数组var uninitialized_array: [10]f32 = undefined;[N]T是数组的类型,N是大小,T是元素类型。[_]T{...}语法让编译器根据初始化列表中的元素数量自动推断数组的大小。{value} ** N是一种方便的语法,用于创建所有元素都相同的数组。
访问元素:
通过索引访问数组元素,索引从0开始。
var arr = [_]i32{ 10, 20, 30 };arr[1] = 25; // 修改第二个元素const value = arr[0]; // 读取第一个元素3. 切片 (Slices)
Section titled “3. 切片 (Slices)”切片是对数组(或任何连续内存区域)的一个“视图”或“引用”。它本身不拥有数据,而是指向一段内存。一个切片包含两个核心部分:一个指向其第一个元素的指针(ptr)和一个长度(len)。
切片的类型写作 []T,其中 T 是元素类型。与数组不同,切片的长度在编译时是未知的。
创建切片:
最常见的创建切片的方式是“切割”一个数组。
const arr = [_]i32{ 10, 20, 30, 40, 50 };
// 创建一个指向整个数组的切片var slice_all: []i32 = &arr;
// 创建一个指向部分数组的切片const slice_partial = arr[1..4]; // 包含索引1, 2, 3的元素,即 { 20, 30, 40 }
// 从头开始的切片const slice_from_start = arr[0..3]; // or arr[..3]
// 直到结尾的切片const slice_to_end = arr[2..]; // { 30, 40, 50 }&arr或arr[0..]或arr[..]都可以创建一个指向整个数组的切片。- 切片操作
[start..end]是半开区间,即包含start索引,但不包含end索引。
4. 多维数组
Section titled “4. 多维数组”Zig支持多维数组,即数组的数组。
// 一个 2x3 的二维数组var matrix: [2][3]i32 = .{ .{ 1, 2, 3 }, .{ 4, 5, 6 },};
matrix[1][1] = 0; // 访问第二行第二列的元素你同样可以创建多维切片,例如 [][]i32。
5. 边界检查
Section titled “5. 边界检查”Zig非常注重安全。在 Debug 和 ReleaseSafe 编译模式下,所有对数组和切片的访问都会进行边界检查。如果索引超出了 len 的范围,程序会触发一个panic,而不是像C语言那样导致未定义行为。
const arr = [_]i32{ 1, 2 };const oops = arr[2]; // Panic! Index 2 out of bounds (len=2)在 ReleaseFast 模式下,为了极致性能,这些检查会被禁用。
6. 示例:切片反转
Section titled “6. 示例:切片反转”下面的函数接受一个切片,并原地反转其元素的顺序。
const std = @import("std");
fn reverse(slice: []anytype) void { var i: usize = 0; var j: usize = slice.len - 1; while (i < j) { const temp = slice[i]; slice[i] = slice[j]; slice[j] = temp; i += 1; j -= 1; }}
pub fn main() !void { var numbers = [_]i32{ 1, 2, 3, 4, 5 }; reverse(&numbers); std.debug.print("{any}\n", .{numbers}); // 输出: { 5, 4, 3, 2, 1 }}这个函数使用了 []anytype,使其可以接受任何类型的切片,展示了Zig泛型的初步应用。
7. 实践练习:实现一个初步的 ArrayList
Section titled “7. 实践练习:实现一个初步的 ArrayList”尝试实现一个简化的 ArrayList 结构体。它需要一个分配器(std.mem.Allocator)来动态管理内存。
你需要实现以下结构和函数:
struct MyArrayList(T: type): 一个泛型结构体,包含items: []T和capacity: usize字段,以及一个指向分配器的指针。init(allocator: *std.mem.Allocator) Self: 初始化一个新的、空的ArrayList。append(self: *Self, item: T) !void: 添加一个元素。如果容量不足,需要重新分配一个更大的内存块(例如,2倍大小),并将旧数据复制过去。deinit(self: *Self): 释放ArrayList占用的所有内存。
这是一个挑战性的练习,会让你对切片、指针、内存分配和泛型有一个更深入的理解。
8. 常见问题
Section titled “8. 常见问题”问:什么是悬垂切片(Dangling Slice)?
答:当一个切片指向的内存已经被释放或已经超出了作用域,这个切片就变成了悬垂切片。访问它会导致未定义行为。例如,一个函数创建了一个局部数组并返回了指向它的切片,这是非常危险的,因为函数返回后,局部数组的内存就会被回收。
fn get_dangling_slice() []u8 { var array = [_]u8{1, 2, 3}; return &array; // 危险!返回了指向局部变量的切片}问:数组和切片作为函数参数有什么区别?
答:数组是按值传递的,这意味着整个数组会被复制一份。对于大数组来说,这会带来巨大的性能开销。切片也是按值传递的,但被复制的只是指针和长度(通常是16个字节),这非常高效。因此,函数参数应优先使用切片,除非你确实需要一个编译时固定大小的数组并且希望复制它。
今天,我们区分了Zig中的数组和切片。数组是固定大小的数据容器,而切片是操作这些数据或其他连续内存区域的动态、轻量级视图。我们学习了如何创建、初始化和操作它们,并了解了Zig在安全方面提供的边界检查。切片是Zig编程中使用得最广泛、最重要的复合类型之一。
明天,我们将专注于处理文本数据:字符串与字面量。