当前位置:
首页
文章
前端
详情

Rust学习笔记#4:语句和表达式

Rust学习笔记#4:语句和表达式

严格的说,Rust中的所有东西只分为两类:表达式(Expression)和语句(Statement)。

  • 表达式:在维基百科的定义中,表达式是指由变量、常量和操作符等组合成的可求值的语法实体。
  • 语句:语句分为声明语句和表达式语句两种。声明语句用于声明变量、结构体、函数等各种项以及通过use等关键字引入包等。表达式语句,由一个表达式和一个分号组成,即在表达式后面加一个分号就将一个表达式转变为了一个语句。

可见,表达式在Rust中扮演了至关重要的角色,Rust基本上就是一个表达式语言。在Rust程序中,表达式可以是语句的一部分,反过来,语句也可以是表达式的一部分。一个表达式总是会产生一个值,因此它必然有类型;语句不产生值,它的类型永远是()。如果把一个表达式加上分号,那么它就变成了一个语句;如果把语句放到一个语句块中包起来,那么它就可以被当成一个表达式使用。

Rust有很多种表达式类型,具体可参见《Rust Reference》,点击这里。下面介绍几种常见的表达式。

表达式的副作用

从传统意义上讲,表达式的作用就是求值,它除了产生一个计算结果外,不应该改变参与计算过程的任何变量的值,这样的表达式称为无副作用的表达式。若一个表达式在求值过程中,改变了所使用的变量的值,则这样的表达式称为有副作用的表达式。例如:

// 没有改变任何变量的值,其为无副作用的表达式
5 + x 
// 改变了变量y的值,其为有副作用的表达式
y = x + 1 

表达式语句就是表达式副作用的重要应用。任何表达式加一个分号都可以作为一个语句来使用,但无副作用的表达式语句没有任何意义,例如5+x;这个表达式语句就没有意义。

运算表达式

运算表达式是含有运算符的表达式。运算符可分为以下几类:

  • 算术运算符:加(+),减(-),乘(*),除(/),求余(%
  • 比较运算符:等于(==),不等于(!=),小于(<),大于(>),小于等于(<=),大于等于(>=)。比较表达式的类型是bool
  • 逻辑运算符:逻辑与(&&),逻辑或(||),逻辑取反(
  • 位运算符:按位取反(),按位与(&),按位或(|),按位异或(^),左移(<<),右移(>>
  • 复合运算符: +, -, *, /, %, &, |, ^, <<>>都可以和=组成复合运算符

这些运算符的用法和C/C++中基本一致,需要注意的是以下两点:

  • Rust禁止连续比较,例如a == b == c这是错误的,必须要加上括号才行。这样设计对减少歧义有很大好处。
  • 按位取反和逻辑取反都是运算符!:如果被操作数是bool类型,则是逻辑取反,其他情况是按位取反。

关于运算符之间优先级的细微知识这里就不展开讲了,因为不论在哪种编程语言中,我都建议,如果碰到复杂的表达式,要使用小括号来明确表达计算顺序。

赋值表达式

一个左值表达式、赋值运算符(=)和右值表达式,可以构成一个赋值表达式。关于左值右值的知识可以参考《Rust学习笔记#2》。需要注意的是,赋值表达式的类型为(),这和C语言是不同的,C语言中赋值表达式的类型是左值表达式的类型。这样设计的好处有两点:

  • 防止连续赋值:x = y = z会产生编译错误,因为赋值运算符=要求两边的表达式同类型,而y = z()类型所以会产生编译错误。
  • 防止把==写成=:Rust中要求条件表达式的类型必须为bool,而赋值表达式的类型是(),会产生编译错误。

语句块表达式

语句块由{}构成,其类型是语句块中最后一个表达式的类型。也就是说,如果最后一个表达式带了分号,那么语句块的类型就是语句的类型();如果没带,就是表达式的类型。例如:

let x: () = {5;}; // x的类型为()
let x: i32 = {5}; // x的类型为i32

这样设计的好处是在写函数返回值时可以直接去掉最后一个语句的分号作为返回值,而不必写return。例如:

fn fun() -> i32 {
    100
}
// 等价于
fn fun() -> i32 {
    return 100;
}

条件表达式

if-else表达式在C语言中也有,我们这里讲一些不一样的地方:

  • if-else后必须要有大括号,不得省略,这样可以避免悬空else所导致的bug。

  • 条件表达式不需要用小括号括起来,如果加上小括号,编译器会提示这是一个多余的小括号。

  • if-else表达式的所有分支必须返回同一个类型的值,if-else的求值策略和语句块表达式相同。如果else分支省略,则默认else分支的类型为()。见下面的例子:

    let a = 1;
    let b = if a > 10 {
        a * 2
    } else {
        a * 3
    };
    println!("{}", b); // 输出:3
    

循环表达式

Rust中包括三种循环表达式:whileloopfor...in,其用法和其他编程语言相应的表达式基本类似,也可以使用continuebreak控制循环流程。注意,Rust中没有C语言中那种三段式for循环,这里的for...in本质上就是一个迭代器。

loop表示一个无限死循环,while是带条件判断的循环语句。注意,loopwhile true是不同的。相对于其他语言,Rust要做更多的静态分析,Rust认为while循环的条件可真可假,所以循环体里的表达式也会忽略不会进行分析,看下面的示例:

let x;
while true {
    x = 1;
    break;
}
println!("{}", x); // error[E0381]: borrow of possibly-uninitialized variable: `x`

Rust无法得知变量x的值在while循环块里被初始化过,从而会报使用未初始化变量的错。

match表达式

Rust提供了match表达式用于匹配各种情况,有点类似于C语言中的switch...case语句,但功能更加强大。先看一个简单的match示例:

enum Direction {
    East,
    West,
    South,
    North,
}

let x = Direction::East;
match x {
    Direction::East => println!("East"),
    Direction::West => println!("West"),
    Direction::South => println!("South"),
    Direction::North => println!("North"),
}
// 输出:East

上面的用法和switch...case语句很类似,下面讲讲不一样的地方:

  • exhaustive特性:exhaustive的意思是无遗漏的,也就是说,Rust要求match必须对所有的情况做完整的、无遗漏的匹配,如果漏掉了某些情况,是不能通过编译的。这样做的好处是可以强迫程序员对所有的情况进行考虑,从而减少bug的发生。

    // 同上面的代码
    match x { 
        Direction::East => println!("East"),
        Direction::West => println!("West"),
        Direction::South => println!("South"),
    }
    // error[E0004]: non-exhaustive patterns: `North` not covered
    
  • 下划线:当不想把每种情况一一列出时,可以用一个下划线来表达“除了列出来的那些之外的其他情况”。下划线存在的另外一个意义是,如果我们引用了他人的库中的某个enum类,但该类添加了新成员,这就会导致我们的代码编译失败。因此,不论何时,都推荐使用下划线作为容错措施。

    // 同上面的代码
    match x {
        Direction::East => println!("East"),
        _ => println!("Else"),
    }
    // 输出:East
    
  • match可以作为表达式,但要求其每一个分支都返回相同的类型。

    let x = Direction::East;
    let y = match x {
        Direction::East => 1,
        _ => 0,
    };
    println!("{}", y); // 输出:1
    
  • 可以使用范围作为匹配条件:

    let x = 'X';
    match x {
        'a'..='z' => println!("lowercase"),
        'A'..='Z' => println!("uppercase"),
        _ => println!("something else"),
    }
    // 输出:uppercase
    
  • 可以使用if作为匹配条件,当匹配成功且符合if条件时,才执行后面的语句:

    let x = Some(5);
    match x {
        Some(i) if i > 10 => println!("{}", i),
        _ => println!("Nothing!"),
    }
    // 输出:Nothing!
    

另外,Rust提供了if let语法糖来简化某些情况下的match表达式。如果我们有一个Option<T>类型的变量opt_val,如果我们需要取出里面的值,可以这样做:

match opt_val {
    Some(x) => {
        // handle x
    },
    _ => (),
}

但这样写比较冗长,而使用if let语法,可以这样做:

if let Some(x) = opt_val {
    // handle x
}

if let的语法为:if let PATTERN = EXPRESSION {BODY},它和match的区别是:它不需要完整匹配,只匹配感兴趣的某个特定分支即可。

参考文献

  • 《Rust编程之道》张汉东
  • 《深入浅出Rust》范长春

免责申明:本站发布的内容(图片、视频和文字)以转载和分享为主,文章观点不代表本站立场,如涉及侵权请联系站长邮箱:xbc-online@qq.com进行反馈,一经查实,将立刻删除涉嫌侵权内容。