深入浅出Rust
上QQ阅读APP看书,第一时间看更新

第4章 函数

4.1 简介

Rust的函数使用关键字fn开头。函数可以有一系列的输入参数,还有一个返回类型。函数体包含一系列的语句(或者表达式)。函数返回可以使用return语句,也可以使用表达式。Rust编写的可执行程序的入口就是fn main()函数。以下是一个函数的示例:

    fn add1(t : (i32, i32)) -> i32 {
        t.0 + t.1
    }

这个函数有一个输入参数,其类型是tuple (i32, i32)。它有一个返回值,返回类型是i32。函数的参数列表与let语句一样,也是一个“模式解构”。模式结构的详细解释请参考第7章。上述函数也可以写成下面这样:

    fn add2((x, y) : (i32, i32)) -> i32 {
        x + y
    }

函数体内部是一个表达式,这个表达式的值就是函数的返回值。也可以写return x+y;这样的语句作为返回值,效果是一样的。

函数也可以不写返回类型,在这种情况下,编译器会认为返回类型是unit ()。此处和表达式的规定是一致的。

函数可以当成头等公民(first class value)被复制到一个值中,这个值可以像函数一样被调用。示例如下:

    fn main() {
        let p = (1, 3);
        // func 是一个局部变量
    let func = add2;
    // func 可以被当成普通函数一样被调用
    println!("evaluation output {}", func(p));
}

在Rust中,每一个函数都具有自己单独的类型,但是这个类型可以自动转换到fn类型。示例如下:

    fn main() {
        // 先让 func 指向 add1
        let mut func = add1;
        // 再重新赋值,让 func 指向 add2
        func = add2;
    }

编译,会出现编译错误,如下:

    error[E0308]: mismatched types
      --> test.rs:11:12
        |
    11  |      func = add2;
        |             ^^^^ expected fn item, found a different fn item
        |
        = note: expected type `fn((i32, i32)) -> i32 {add1}`
                   found type `fn((i32, i32)) -> i32 {add2}`

虽然add1和add2有同样的参数类型和同样的返回值类型,但它们是不同类型,所以这里报错了。修复方案是让func的类型为通用的fn类型即可:

    // 写法一,用 as 类型转换
    let mut func = add1 as fn((i32, i32))->i32;
    // 写法二,用显式类型标记
    let mut func : fn((i32, i32))->i32 = add1;

以上两种写法都能修复上面的编译错误。但是,我们不能在参数、返回值类型不同的情况下作类型转换,比如:

    fn add3(x: i32, y: i32) -> i32 {
        x + y
    }
    fn main() {
        let mut func : fn((i32, i32))->i32 = add1;
        func = add2;
        func = add3;
    }

这里再加了一个add3函数,它接受两个i32参数,这就跟add1和add2有了本质区别。add1和add2是一个参数,类型是tuple包含两个i32成员,而add3是两个i32参数。三者完全不一样,它们之间是无法进行类型转换的。

另外需要提示的就是,Rust的函数体内也允许定义其他item,包括静态变量、常量、函数、trait、类型、模块等。比如:

    fn test_inner() {
        static INNER_STATIC: i64 = 42;
        // 函数内部定义的函数
        fn internal_incr(x: i64) -> i64 {
            x + 1
        }
        struct InnerTemp(i64);
        impl InnerTemp {
            fn incr(&mut self) {
                self.0 = internal_incr(self.0);
            }
        }
        // 函数体,执行语句
        let mut t = InnerTemp(INNER_STATIC);
        t.incr();
        println!("{}", t.0);
    }

当你需要一些item仅在此函数内有用的时候,可以把它们直接定义到函数体内,以避免污染外部的命名空间。

4.2 发散函数

Rust支持一种特殊的发散函数(Diverging functions),它的返回类型是感叹号!。如果一个函数根本就不能正常返回,那么它可以这样写:

    fn diverges() -> ! {
        panic! ("This function never returns! ");
    }

因为panic!会直接导致栈展开,所以这个函数调用后面的代码都不会继续执行,它的返回类型就是一个特殊的!符号,这种函数也叫作发散函数。发散类型的最大特点就是,它可以被转换为任意一个类型。比如:

    let x : i32 = diverges();
    let y : String = diverges();

我们为什么需要这样的一种返回类型呢?先看下面的例子:

    let p = if x {
        panic!("error");
    } else {
        100
    };

上面这条语句中包含一个if-else分支结构的表达式。我们知道,对于分支结构的表达式,它的每条分支的类型必须一致。那么这条panic!宏应该生成一个什么类型呢?这就是!类型的作用了。因为它可以与任意类型相容,所以编译器的类型检查才能通过。

在Rust中,有以下这些情况永远不会返回,它们的类型就是!。

❏ panic!以及基于它实现的各种函数/宏,比如unimplemented! 、unreachable!;

❏ 死循环loop {};

❏ 进程退出函数std::process::exit以及类似的libc中的exec一类函数。

关于这个!类型,第8章在对类型系统做更深入分析的时候还会再提到。

4.3 main函数

在大部分主流操作系统上,一个进程开始执行的时候可以接受一系列的参数,退出的时候也可以返回一个错误码。许多编程语言也因此为main函数设计了参数和返回值类型。以C语言为例,主函数的原型一般允许定义成以下几种形式:

    int main(void);
    int main();
    int main(int argc, char **argv);
    int main(int argc, char *argv[]);
    int main(int argc, char **argv, char **env);

Rust的设计稍微有点不一样,传递参数和返回状态码都由单独的API来完成,示例如下:

    fn main() {
        for arg in std::env::args() {
            println!("Arg: {}", arg);
        }
        std::process::exit(0);
    }

编译,执行并携带几个参数,可以看到:

    $ test -opt1 opt2-- opt3
    Arg: test
    Arg: -opt1
    Arg: opt2
    Arg: --
    Arg: opt3

每个被空格分开的字符串都是一个参数。进程可以在任何时候调用exit()直接退出,退出时候的错误码由exit()函数的参数指定。

如果要读取环境变量,可以用std::env::var()以及std::env::vars()函数获得。示例如下:

    fn main() {
    for arg in std::env::args() {
        match std::env::var(&arg) {
            Ok(val) => println!("{}: {:? }", &arg, val),
            Err(e) => println!("couldn't find environment {}, {}", &arg, e),
        }
    }
    println!("All environment varible count {}", std::env::vars().count());
}

var()函数可以接受一个字符串类型参数,用于查找当前环境变量中是否存在这个名字的环境变量,vars()函数不携带参数,可以返回所有的环境变量。

此前,Rust的main函数只支持无参数、无返回值类型的声明方式,即main函数的签名固定为:fn main() -> ()。但是,在引入了?符号作为错误处理语法糖之后,就变得不那么优雅了,因为?符号要求当前所在的函数返回的是Result类型,这样一来,问号就无法直接在main函数中使用了。为了解决这个问题,Rust设计组扩展了main函数的签名,使它变成了一个泛型函数,这个函数的返回类型可以是任何一个满足Terminationtrait约束的类型,其中()、bool、Result都是满足这个约束的,它们都可以作为main函数的返回类型。关于这个问题,可以参见第33章。

4.4 const fn

函数可以用const关键字修饰,这样的函数可以在编译阶段被编译器执行,返回值也被视为编译期常量。示例如下:

    #! [feature(const_fn)]
    const fn cube(num: usize) -> usize {
        num * num * num
    }
    fn main() {
        const DIM : usize = cube(2);
        const ARR : [i32; DIM] = [0; DIM];
        println!("{:? }", ARR);

cube函数接受数字参数,它会返回一个数字,而且这个返回值本身可以用于给一个const常量做初始化,const常量又可以当成一个常量数组的长度使用。

const函数是在编译阶段执行的,因此相比普通函数有许多限制,并非所有的表达式和语句都可以在其中使用。鉴于目前这个功能还没有完全稳定,const函数具体有哪些限制规则,本书就不在此问题上详细展开了,后面也许还会有调整。

4.5 函数递归调用

Rust允许函数递归调用。所谓递归调用,指的是函数直接或者间接调用自己。下面用经典的Fibonacci数列来举例:

    fn fib(index: u32) -> u64 {
        if index == 1 || index == 2 {
            1
        } else {
            fib(index -1) + fib(index -2)
        }
    }
    fn main() {
        let f8 = fib(8);
        println!("{}", f8);
    }

这个fib函数就是典型的递归调用函数,因为在它的函数体内又调用了它自己。

谈到递归调用,许多读者都会自然联想到“尾递归优化”这个概念。可惜的是,当前版本的Rust暂时还不支持尾递归优化,因此如果递归调用层次太多的话,是有可能撑爆栈空间的。不过这个问题已经在设计讨论之中,各位读者可以从最新的RFC项目中了解进度。