这里简单介绍基本常用的语法。
变量
为了保证安全性,声明的变量默认就是不可变的 (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标识是新变量
}
最后一个意味着类型发生了变化,此时会直接报错。另外,需要注意,重定义是不能作用于 static
和 const
变量的。
静态变量
一个静态变量在应用中有唯一的内存地址,通常是可变的,这样就不是线程安全的,所以在读写时需要添加 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
表示,与其它语言类似,只有true
和false
两种。 - 字符类型,通过
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) 声明语句,包括变量、常量、结构体、函数等的声明,以及通过
extern
和use
关键字引入;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
while
和 for
三种。
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);
}
}
while
和 for
的使用方式与其它语言基本类似,比较特殊的稍作说明:if else
可以用在单条语句中;loop
允许设置标签指定 continue
和 break
作用的循环位置,而且可以返回值。
除了上述常规的使用方式,在 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"));
}
上述示例中的 &str
和 String
都可以通过 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
自动推导,可以自动生成,常见的整理如下:
- 比较
<
<=
>
>=
对应PartialOrd
,Eq
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)]
宏简单实现。