函数的结构
Zero 函数的一般形式:
fun name(param1: Type1, param2: Type2) -> ReturnType {
// body
return value
}
各个部分:
fun:引入函数的关键字。name:函数名。(param1: Type1, ...):参数列表。每个参数都有显式的类型。-> ReturnType:返回类型。{ ... }:函数体,一个由语句组成的代码块。return value:带着value退出函数。
一个具体的小例子:
fun double(value: i32) -> i32 {
return value * 2
}
这就是整个函数。它接收一个 i32,返回另一个 i32,不做任何 I/O。在函数体内部,value 是一个 let 式的绑定,可以像其他局部变量一样使用。
调用函数
调用方式和你想的完全一样:
let result = double(21)
实参必须与参数类型一致。结果绑定到 result,因为 double 返回 i32,编译器会推断 result 也是 i32。
下面这个完整示例把一个辅助函数和 main 组合在一起,点击“运行”看看效果:
你应该会在 stdout 上看到 math works\n。
pub 与可见性
默认情况下,在文件中声明的函数对这个文件是私有的(或者说对模块私有,项目变大后可见性规则会更严格)。要让函数在模块之外可用,就在前面加上 pub:
pub fun greet() -> String {
return "hello\n"
}
没有 pub,其他模块中的代码就无法调用 greet。运行时要从任何用户定义的模块之外调用 main,所以 main 总是 pub 的。
默认私有是一条值得遵循的规则。只把你打算作为接口的部分标记出来,其余的都留在模块内部。
返回类型
每个函数都在 -> 之后声明返回类型。常见的返回类型:
fun answer() -> i32 { return 42 }
fun ok() -> bool { return true }
fun label() -> String { return "ready\n" }
fun nothing() -> Void { }
Void 是通过副作用完成工作、而不产生值的函数的返回类型。Void 函数不需要显式的 return,执行到函数体末尾就够了。
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
丢弃返回值的函数调用
如果函数返回了一个值,而你并不关心它,仍然需要明确承认这个返回值。惯用写法是用 let 绑定它:
ignored 是一个函数后面不会再读取的绑定。用 ignored(或 _)这个名字是一种约定,表示丢弃是有意为之。这比悄悄丢掉返回值多了一点麻烦,而这正是目的所在:在一门由智能体阅读和生成代码的语言里,一个没人读取的值往往就是一个值得暴露出来的 bug。
raises 的作用
可能失败的函数要在签名上声明这一点。我们在 main 上已经见过:
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
raises 子句可以单独出现(任何错误),也可以写得很具体:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raises { InvalidInput } 表示“这个函数可能因 InvalidInput 而失败,而且只会因为它失败”。调用方必须用 check(或更复杂的处理形式)来传递或处理这个错误。
Raises 和 Check 会深入讲解这个话题,包括有多种错误类型时会怎样,以及 check 如何与调用方的 raises 子句配合。
泛型函数
如果想让一个函数适用于多种类型,就在尖括号中声明类型参数:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T 和 U 是类型参数,由调用方决定它们是什么。调用 makePair(40, 2_u8) 会得到一个 Pair<i32, u8>。完整内容,包括泛型 shape 和约束,请看 泛型。
函数放在哪里
在小程序中,你直接在 .0 文件里写函数。在 包 中,函数分布在 src/ 下的多个文件里,编译器会替你解析跨文件的引用。无论函数实际放在哪里,基本要素都一样:fun、参数、返回类型和函数体。
风格说明
官方示例中常见的几个约定:
- 函数名以小写开头,单词连写(
makePair),也就是驼峰命名。标准库倾向于使用驼峰命名。 - 每个函数只返回一个值。如果需要返回多个东西,就为它们定义一个小的
shape,这比返回元组套元组清楚得多。 Void函数只做check调用;计算值的函数尽量避免 I/O。这种分工一部分是社区习惯,一部分是语言强制的:纯计算函数不接收world,所以根本无法做 I/O。
最后一点值得多说几句。因为 I/O 藏在 World 能力对象后面,而 World 是显式传入的,所以函数签名就能告诉你它是否可能做 I/O。签名中没有提到 World 的函数,对外部世界而言就是纯函数。这是智能体(和人类)不读函数体也能依赖的性质。
下一步:If/Else
你已经顺带见过 if 了。下一篇文档会详细介绍 if/else 表达式,包括它们如何与绑定配合,以及刻意缺失的功能(没有真假值隐式转换,也没有三元运算符)。
常见问题
Zero 中怎么声明函数?
用 fun:fun name(param: Type) -> ReturnType { body }。在前面加上 pub,函数就能在所属模块之外使用。如果函数可能失败,在返回类型后面加上 raises。例如:pub fun double(value: i32) -> i32 { return value * 2 }。
pub 关键字有什么作用?
pub 把声明标记为公开的,当前模块以外的代码也能看到它。没有 pub 的函数只在声明它的文件(或包)内可见。约定的入口 pub fun main 必须是公开的,这样运行时才能找到并调用它。
Zero 函数怎么返回值?
在函数体里写 return value。这个表达式必须与声明的返回类型一致。返回类型为 Void 的函数不返回任何值,也不需要显式的 return 语句,执行到函数体末尾就可以了。
Zero 函数可以有多个参数吗?
可以。把参数写在括号里,用逗号分隔,每个参数都要写名字和类型:fun add(a: i32, b: i32) -> i32 { return a + b }。每个参数在函数体中都相当于一个 let 式的绑定。Zero 要求参数显式写出类型,声明函数时不会推断参数类型。
函数签名上的 raises 是什么意思?
raises 声明这个函数可能失败。单独的 raises 允许任何错误类型;raises { InvalidInput } 则把错误限制为某个具名错误。调用方必须用 check(或其他处理失败的结构)明确承认失败的可能,不能悄悄忽略。