Rust 基本语法

2022-09-19 rust language

这里简单介绍基本常用的语法。

变量

为了保证安全性,声明的变量默认就是不可变的 (immutable),此时编译器就不需要进行跟踪了,这跟常量 (constants) 还是有所区别的。

fn main() {
	let width = 10; // immutable
	println!("The width is {}", width);
	
	const PI = 3.1415926; // constants
	println!("The PI is {}", PI);
}

如果要使用变量,那么就需要通过 let mut width = 10; 类似的方式进行声明了。

在 Rust 中,变量可以有如下的属性:A) Immutable 不可变变量;B) Mutable 可变变量;C) Shadowing 重定义(遮蔽)一个变量;D) Const 常量;E) Static 静态变量。

重定义

使用不可变变量可以重复对某个变量赋值,在其它语言里可能是重复定义,而 Rust 会覆盖之前的定义,并且允许是不同类型,但前提是需要通过 let 声明。

fn main() {
	let spaces = "    ";        // 字符串
	let spaces = spaces.len();  // 空格的数量,是整型

	let mut spaces = "    ";
	//spaces = spaces.len();    // 如果要改变类型,需要使用let标识是新变量
}

最后一个意味着类型发生了变化,此时会直接报错。另外,需要注意,重定义是不能作用于 staticconst 变量的。

静态变量

一个静态变量在应用中有唯一的内存地址,通常是可变的,这样就不是线程安全的,所以在读写时需要添加 unsafe 标识。

static LOG_L_INFO: i32 = 1;
static mut LOG_LEVEL: i32 = 5;

fn main() {
    // println!("Log level {}", LOG_LEVEL); // error: use of mutable static
    println!("Log info {}", LOG_L_INFO); // ok
    unsafe {
        LOG_LEVEL = 6;
        println!("Log level {}", LOG_LEVEL);
    }
}

其它

常量会在编译时复制到其使用地方,所以,对于结构体较大、需要地址访问、可变,可以使用静态变量,其它建议使用常量。

数据类型

如上,Rust 仍然是静态类型语言,定义变量的时候已经确定了具体类型,只是允许覆盖原有的变量。大部分情况下编译器可以推断出具体数据类型,只有少数无法确定类型的场景需要显示声明,最常见的是获取用户的输入。

let guess: u32 = "42".parse().expect("Not a number!");

基础类型

在 Rust 中包含了标量 scalar 和复合 compound 两类数据子集。标量用来代表一个单独的值,包含了如下四种类型:

  • 整型,包括了有符号 i 和无符号 u 两种,可以通过数值指定位数,例如 i8 i128 等,还有与 CPU 平台相关的 isize
  • 浮点数,包括了 f32 f64 两种不同精度,默认采用的是 64 位。
  • 布尔类型,通过 bool 表示,与其它语言类似,只有 truefalse 两种。
  • 字符类型,通过 char 表示,不过编码环境采用的是四字节的 Unicode 编码,而实际使用是 UTF-8 编码。

复合类型可以将多个值组合成一个类型,有两个原生的复合类型:A) 元组 tuple,长度在声明之后不再变化,可以通过解构获取变量,或者使用索引;B) 数组 array,每个元素类型相同,与其它语言不同,其长度是固定的。

fn main() {
	let tup: (i32, f64, u8) = (500, 6.4, 1); // 等价于 let tup = (500, 6.4, 1)
	let (x, y, z) = tup; // 用于解构
	println!("The third value is {}", tup.2);  // 访问序号从0开始
	
	let months = ["January", "February", "March"]; // 字符串类型
	let arr: [i32; 5] = [1, 2, 3, 4, 5]; // 指定数据类型
	let data: [0; 5]; // 使用默认值,等价于 [0, 0, 0, 0, 0]
	let value: [0 as u8; 5]; // 可以指定类型和默认值
	println!("The first month {}", months[0]); // 访问方式与其它语言类似,序号从0开始
}

有个特殊的 () 元组,类似于其它语言中的 void 返回值,也可以搭配标准库中的 Ok(()) 这种方式。另外,有个比较特殊的字符串类型,可以查看 Rust 字符串解析 中的介绍。

在定义某些类型的时候,出于方便查看、指定类型等原因,会有类似如下的声明。

let length = 3_usize; // 3usize 等方式相同,数值后加类型
let length = 3_000;   // 3000 数值可以任意指定下划线,增加可读性,默认i32类型
let length = 0xbc;    // 十六进制
let length = 0o17;    // 八进制
let length = 0b1010;  // 二进制

// 用于字节字面值,等价于 let data: &[u8] = b"Hello";
let data = b"Hello";

数组 Array

在 Rust 中,数组是固定长度的,可以动态增长的是 Vector,相对来说性能略有损耗,后者也被称为集合类型。

如下是几种声明数组的方式。

let a = [1, 2, 3];
let a: [i32; 3] = [1, 2, 3];
let a = [0; 3]; // [0, 0, 0]

访问与其它语言类似,直接通过 a[0] 访问即可,注意,如果越界会导致崩溃。

如果数组中的元素是非基础类型,可以通过如下方式创建。

let a: [String, 3] = std::array::from_fn(|_i| String::from("Hello"));

切片 Slice

切片允许引用集合中部分连续的序列,而不是整个集合,例如内置的 str[u8] 都是切片。

注意,如果直接使用 let hey: str ="Hello"; 会报错,因为字符串切片是动态大小,编译期间无法获取其大小,只有运行时才可以确定,对于强类型、强安全的 Rust 语言是不可接受的。

因为底层切片长度是可以动态变化,编译期间无法获取其具体长度,所以只能分配到堆上,而不能使用栈,也就只能通过引用在栈上使用。上述的数组是定长的,可以保存在栈中,与切片的区别就是显式指定了长度。

可以对上述的数组创建切片。

let a0 = [1, 2, 3, 4, 5];
let s1 = &a0[1..4]; // 1, 2, 3
let s2 = &a0[..4];  // 1, 2, 3
let s3 = &a0[2..];  // 2, 3, 4, 5

遍历切片需要注意引用的处理。

fn main() {
    let arr: &[u8] = &[b'1', b'2', b'3', b'4'];
    let test = &arr[1..3];

    // it's a &u8 value, should dereferece.
    for x in test {
        println!("{}", *x as char);
    }

    for &x in test {
        println!("{}", x as char);
    }

    for (idx, &val) in test.iter().enumerate() {
        println!("{}={}", idx, val as char);
    }
}

枚举

跟很多函数编程语言中的枚举略有区别,可以类似 C/C++ 定义常量,不过此时已经是新类型,如下的 IPAddr::V4|IPAddr::V6 会报错。

enum IPAddr {
    V4,
    V6,
}

impl IPAddr {
    fn value(&self) -> i32 {
        match *self {
            IPAddr::V4 => 1,
            IPAddr::V6 => 2,
        }
    }
}

fn main() {
    println!("IPv4 {}", IPAddr::V4.value());
    println!("IPv6 {}", IPAddr::V6.value());
}

上述是最常规的操作,不过也可以通过如下方式定义。

#[derive(Debug)]
enum IPAddr {
    V4 = 0x01,
    V6 = 0x02,
}

fn main() {
    println!("IPv4 {:?}", IPAddr::V4 as i32); // 1
    println!("IPv6 {:?}", IPAddr::V6); // V6
}

除了可以定义常量之外,还可以添加变量、定义接口等,而且提供了一个极为强大的控制流运算符。

enum IPAddr {
    V4(u8, u8, u8, u8),
    V6(String),
    Vx,
}

impl IPAddr {
    fn dump(&self) -> String {
        match self {
            IPAddr::V4(i1, i2, i3, i4) => {
                format!("{}.{}.{}.{}", i1, i2, i3, i4)
            },
            IPAddr::V6(val) => val.to_string(),
            _ => String::from("Unknown"),
        }
    }
}

fn main() {
    let loopbackx = IPAddr::Vx;
    let loopback4 = IPAddr::V4(127, 0, 0, 1);
    let loopback6 = IPAddr::V6(String::from("::1"));
    println!("IPvx {}", loopbackx.dump());
    println!("IPv4 {}", loopback4.dump());
    println!("IPv6 {}", loopback6.dump());
}

另外,还有一个 if let 的语法糖,用于将 match 只需要单个匹配的语句进行简化。

函数

使用的变量和函数名统一使用 snake case 风格进行命名,也就是所有字母小写并使用下划线分隔;参数需要通过注解显式声明,编译器在运行时会直接使用;返回值的类型可以通过 -> 后指定。

fn sum_int(x: i64, y: i64) -> i64 {
    return x + y;
}

fn main() {
    println!("Sum return {}", sum_int(5, 6));
}

另外,Rust 的函数体是基于表达式的,与其它语言有所不同,而且比较难理解。

表达式 (Expressions) 会计算并产生一个值,而语句 (Statements) 是执行一些操作但不返回值的指令,包括了:A) 声明语句,包括变量、常量、结构体、函数等的声明,以及通过 externuse 关键字引入;B) 表达式语句,专指以分号结尾的表达式,返回单元类型。

对于表达式语句会将求值结果抛弃,总是返回单元类型 (),这一概念来自 OCmal,其唯一的值就是其本身,表示没有特殊价值。如果作为函数返回值,则表示该函数无返回值,通常也就不需要在签名中指定返回类型。

例如,在 Rust 中不允许通过 let x = (let y = 0); 定义多个变量,包括了 let x = y = 0; 这种方式,因为 let x = 0 语句不会返回值,而其它语言则会返回值。

另外,大部分的 Rust 代码都是由表达式组成,例如语句 let y = 6; 中的 6 是一个表达式,它计算出的值是 6;数学运算 5 + 6 也是表达式;还有函数调用、宏调用、大括号等,也都是表达式。

fn sum_int(x: i64, y: i64) -> i64 {
    x + y
}

fn main() {
    let y = {
        let x = 3;
        x + 1
    };
    println!("Brace {}, Sum {}", y, sum_int(5, 6));
}

上述代码展示了大括号的返回,需要注意上述都没有使用分号 ; 结束,现在是表达式,如果使用,那么就是语句了,此时不会返回值,直接报错。

永不返回

当使用 ! 作为返回类型的时候,就表示该函数永不返回,通常用在会导致程序退出的函数或者是一个死循环。

控制流

包含了分支和循环两类,分支也就是 if else 语句,而循环包含了 loop whilefor 三种。

fn main() {
    let num = if true { 3 } else { 9 };
    if num % 4 == 0 {
        println!("number is divisible by 4");
    } else if num % 3 == 0 {
        println!("number is divisible by 3");
    } else {
        println!("number is not divisible by 4 or 3");
    }

    let mut count = 0;
    'counter: loop {
        println!("count {}", count);

        let mut remaining = 10;
        let result = loop {
            println!("remaining {}", remaining);
            if remaining == 9 {
                break remaining; // 退出内层循环并返回值
            }
            if count == 2 {
                break 'counter;
            }
            remaining -= 1;
        };
        count += 1;
        println!("ending result {}", result);
    }
    println!("ending count {}", count);

    let mut number = 3;
    while number != 0 {
        println!("{}", number);
        number -= 1;
    }

	// 反向输出
    for number in (1..4).rev() {
        println!("{}", number);
    }
}

whilefor 的使用方式与其它语言基本类似,比较特殊的稍作说明:if else 可以用在单条语句中;loop 允许设置标签指定 continuebreak 作用的循环位置,而且可以返回值。

除了上述常规的使用方式,在 Rust 中还可以使用 模式匹配

类型转换

对于基础类型来说,可以简单通过 as 关键字完成。

fn times(val: f32) -> f32 {
    val * 10.0
}

fn main() {
    let val: u8 = 10;
    println!("times value {}", times(val as f32));
}

From VS. Into

这是两个常用的类型转换 trait,最常见的是错误类型的转换,只要实现了 From 即可。

struct Number(u32);

impl From<u32> for Number {
    fn from(num: u32) -> Self {
        Number(num)
    }
}

fn main() {
    let val: u32 = 10;
    let num = Number::from(val);
    println!("Number {}", num.0);

    let var: Number = val.into();
    println!("Number {}", var.0)
}

AsMut VS. AsRef

上述的 From 转换相对成本较高,而 AsMut AsRef 分别用于可变以及不可变引用的转换,相比来说成本要低很多。如下的示例中,函数 say_hey() 接受通过转换为 &str 的类型。

fn say_hey<T: AsRef<str>>(name: T) {
    println!("Hey {}", name.as_ref());
}

fn main() {
    say_hey("World");
    say_hey(String::from("Andy"));
}

上述示例中的 &strString 都可以通过 as_ref() 转换为 &str 类型。

其它

属性

可用于模块、crate、函数元数据,类似 Java 中的注解,如果要应用于整个 crate 语法为 #![attribute],否则为 #[attribute] 配置,而且属性可以接受参数,例如 #[attribute="value"] #[attribute(key="value")] #[attribute(value)] 都可以。

如下是常用示例:

  • 禁用 Lint 警告,allow(dead_code, unused)
  • 条件编译代码,cfg(target_os="linux") cfg(not(target_os="linux")) 包括指定平台或者非平台。

实际常用的是 derive 自动推导,可以自动生成,常见的整理如下:

  • 比较 < <= > >= 对应 PartialOrdEq PartialEq 对应 == != 等。

打印

可以通过 std::fmt 中的一系列宏进行格式化,包括了 format! print! eprint! 分别输出到字符串、标准输出、标准错误输出,默认 Rust 编译时会进行类型推断,也可以手动指定。

fn main() {
    // 简单通过fmt::Display格式化显示
    println!("Hello {}, this is {}.", "Bob", "Andy");
    // 可以通过序号、关键字指定格式化显示内容
    println!("Hello {0}, this is {1}. I'm {self}", "Bob", "Andy", self="Alice");

    // 通过:可以在右侧定制格式化方式
    println!("right align {num:>width$}, {num:>0width$}", num=1, width=3);
    println!("0x{num:x} 0o{num:o}", num=0xdeadbeefi64);
}

另外,通过 {:?} {:#?} 可以打印调试信息,对应 fmt::Debug 的实现,后者会通过一些换行美化显示,而且可以通过 #[derive(Debug)] 属性自动推导。如果通过 {} 打印,则需要实现 fmt::Didplay Trait 方法,否则会报错。

use std::fmt;

#[derive(Debug)]
struct Complex {
    real: f64,
    image: f64,
}

impl fmt::Display for Complex {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} + {}i", self.real, self.image)
    }
}

fn main() {
    let num = Complex{real: 10.0, image: 20.0};
    println!("{0:?} {0:#?}", num); // 直接使用属性打印即可
    println!("{}", num); // 需要fmt::Display Trait实现
}

简单来说,通过 {} 打印需要用户指定,而 {:?} 要更详细,而且可以通过 #[derive(Debug)] 宏简单实现。