Skip to content

Day 10: 函数声明与参数

欢迎来到第十天!函数是组织代码、实现复用和抽象的核心单元。在Zig中,函数是一等公民,这意味着它们可以像任何其他值一样被传递、赋值给变量和作为其他函数的返回值。今天,我们将深入学习如何声明函数、理解其参数传递机制,并探索Zig中一些强大的函数特性,如多返回值和可变参数。

一个基本的Zig函数声明包含可见性修饰符(可选)、fn关键字、函数名、参数列表、返回类型和函数体。

// 语法: [pub] fn functionName(param1: Type1, ...) ReturnType { ... }
/// 一个简单的加法函数
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
  • pub: 使函数在当前文件外部可见。如果省略,函数默认为文件私有。
  • fn: 声明这是一个函数。
  • add: 函数的名称。
  • (a: i32, b: i32): 参数列表。每个参数都必须有明确的类型。
  • i32: 返回类型。如果函数不返回值,则为 void。如果可能返回错误,则为 !ReturnType。
  • { ... }: 函数体,包含要执行的代码。
  • return: 关键字,用于从函数返回值。

在Zig中,参数默认按值传递(Pass-by-Value)。这意味着函数接收的是原始值的一个副本。在函数内部修改参数,不会影响到函数外部的原始变量。

fn increment(mut val: i32) void {
val += 1; // 修改的是副本
}
var x: i32 = 10;
increment(x);
// x 的值仍然是 10

如果你想让函数能够修改外部的变量,你必须传递一个指针。

fn incrementPtr(ptr: *i32) void {
ptr.* += 1; // 解引用指针,修改原始值
}
var y: i32 = 10;
incrementPtr(&y);
// y 的值现在是 11

与其他许多语言不同,Zig的函数可以方便地返回多个值。这是通过返回一个匿名的结构体(或元组)来实现的。

fn divRem(numerator: i32, denominator: i32) struct { div: i32, rem: i32 } {
return .{
.div = numerator / denominator,
.rem = denominator,
};
}
const result = divRem(10, 3);
// result.div 是 3
// result.rem 是 1

接收返回值时,可以使用 const { .div, .rem } = divRem(10, 3); 这样的解构语法,但这在Zig 0.12.0中已被移除,推荐直接访问返回的结构体字段。

Zig支持类型安全的可变参数(variadic functions),这与C语言的 ... 非常不同。在Zig中,你必须使用 ... 语法,并且所有可变参数必须是同一种类型。

fn sum(numbers: []const i32) i32 {
var total: i32 = 0;
for (numbers) |n| {
total += n;
}
return total;
}
// 调用时,使用 .{} 语法创建一个匿名切片
const total = sum(&.{ 1, 2, 3, 4, 5 });

在函数定义中使用 ... 语法是另一种方式,它在底层会被转换成一个切片。

fn format(comptime fmt_str: []const u8, args: anytype) []const u8 {
// ...
}
// `anytype` 允许 `args` 是一个包含多种类型的匿名结构体
// `std.fmt.bufPrint` 就是这样实现的

6. 示例:递归计算斐波那契数列

Section titled “6. 示例:递归计算斐波那契数列”

递归是函数调用自身的一种技术,非常适合解决斐波那契数列这类问题。

const std = @import("std");
fn fib(n: u32) u32 {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
pub fn main() !void {
const result = fib(10);
std.debug.print("Fib(10) = {d}\n", .{result}); // 输出: Fib(10) = 55
}

注意: 这种朴素的递归实现效率很低,仅用于演示函数递归调用。

创建一个名为 math.zig 的文件,在其中实现两个函数:

  1. gcd(a: u32, b: u32) u32: 使用欧几里得算法计算两个数的最大公约数(Greatest Common Divisor)。
  2. lcm(a: u32, b: u32) u32: 使用公式 lcm(a, b) = (|a * b|) / gcd(a, b) 计算最小公倍数(Least Common Multiple)。

将这两个函数都声明为 pub。然后,在 main.zig 中使用 @import 导入你的 math.zig 模块,并调用这两个函数进行测试。

问:为什么我的函数参数类型与传入的实参类型完全一样,编译器还是报“类型不匹配”?

答:最常见的原因是指针的可变性问题。一个期望 *T (可变指针) 的函数不能接受 *const T (常量指针) 类型的参数。请仔细检查你的指针类型,确保可变性匹配。另一个可能的原因是数字类型不完全匹配,例如函数期望 i32,但你传入了一个 u32 或 i64。记住,Zig没有隐式类型转换。

问:函数可以嵌套定义吗?

答:不可以。Zig不支持在函数内部定义另一个函数。所有的 fn 都必须在顶层作用域或 struct/union/enum 的内部定义。

今天,我们掌握了Zig函数的核心知识。我们学习了如何定义函数、按值传递和按指针传递的区别、强大的多返回值能力以及类型安全的可变参数。函数是构建任何程序的基本模块,理解其工作原理至关重要。

明天,我们将正式深入Zig最受赞誉的特性之一:显式错误处理。我们将学习 error 类型和 try/catch 表达式。