RUST快速入门
由于博主是Java开发,会通过Java与Rust语言进行对比学习,有问题希望大家指出,互相沟通学习。
一. RUST语言的基本构成
- 语言规范
- 编译器
官方编译器为rustc,负责将RUST源代码编译为可执行文件或其他库文件。 - 核心库
核心库为标准库的基础,不依赖于操作系统和网络,比如编写一个操作系统或者嵌入式,就无法使用到标准库的信息。
如一些基础的trait,Copy,Debug,Option,assert!,env!,bool,i32/u32,slice,tuple等。 - 标准库
标准库比较依赖于平台,比如并发,IO,消息传递,以及TCP,UDP等。 - 包管理器
按照一定规则组织的多个rs文件编译后就得到一个包(crate),一般常用的包管理器为Cargo。
二. 变量的绑定
在Rust中,变量通过let关键字来创建。
fn main() {
let first = 1;
println!("first number is {}", first); // first number is 1。
}
上述表明了标识符first与值1的绑定关系。
1.位置表达式和值表达式
位置表达式是表示内存位置的表达式,比如:
- 本地变量
- 静态变量
- 解引用
- 数组索引
- 字段引用
- 位置表达式
通过位置表达式可以对某个数据单元的内存进行读写。
值表达式一般只是引用了某个存储单元地址中的数据,只能进行读取操作。
位置表达式一般代表了持久性数据,值表达式代表了临时数据。值表达式要么是字面量,要么是求值过程中创建的临时值。
2.不可变绑定与可变绑定
前面所说的let关键字默认声明的位置表达式为不可变的。
fn main() {
let a = 1;
//a = 2; //无法改变a的值,因为默认为不可变绑定
let mut b = 2;
b = 3 //加入mut关键字,声明为可变位置表达式。
}
let默认声明的不可变绑定只能对相应的存储单元进行读取,加入mut关键字后可进行写入操作。
3.所有权和借用
先看代码,在下面代码中
fn main() {
let str1 = "hello";
let str2 = "hello".to_string();
let other = str1;
println!("{:?}", str1);
let other = str2;
println!({"?}", str2);
}
每个变量绑定实际上都是拥有该存储单元的所有权,这种转移内存地址的行为就是所有权转移,在RUST中被称为移动,那种不转移的情况被称为复制。
在日常开发中,有时候无需转移所有权,可以通过借用操作符&,直接取内存位置,可以通过该内存位置对存储进行读取。
fn main() {
let a = [1,2,3];
let b = &a; //借用a,a的所有权不转移。
println("{:?}", b); //打印a的地址。
let mut c = vec![1,2,3];
let d = &mut c; //变量c的可变借用
d.push(4);
println("{:?}", d); //打印结果为[1,2,3,4];
let e = &42;
assert_eq!(42, *e); //*为解引用符号, 可直接获取到e的具体值。
}
上述代码使用&借用符后,将赋值表达式右侧变成了位置上下文,只是共享内存地址。
三.函数与闭包
1.函数的定义
函数通过fn关键字定义。如下列代码:
pub fn fizz_buzz(num: i32) -> String {
if num % 15 == 0 {
return "fizzbuzz".to_string();
} else if num % 3 == 0 {
return "fizz".to_string();
} else if num % 5 == 0 {
return "buzz".to_string();
} else {
return num.to_string();
}
}
上述代码中,通过关键字fn定义一个fizz_buzz的函数,函数中的括号为入参类型,约定入参类型为i32,符号->后面的String为该函数的返回类型,该函数定义的返回类型为String。
2.作用域与生命周期
简单来讲,一个大括号就是一个生命周期,Rust语言的作用域是静态作用域。
fn main() {
let v = "hello world";
let v = "hello rust";
{
let v = "hello handsome boy";
println!("{?}", v);//新的作用域,这里输出的是hello handsome boy
}
println!("{"?}", v); //由于在同一个作用域中,第二个覆盖了第一个,所以这里输出的是hello rust。这种情况被称为变量屏蔽。
}
上述代码中,虽然是同样的代码变量,但是在不同的作用域,所以拥有不同的生命周期。
3.函数指针
万物皆与函数,函数在Rust语言中分量很大,所以函数本身可以作为其他函数的参数和返回值使用。
下面是函数作为参数的情况:
//op为一个定义的函数
pub fn math(op: fn(i32,i32) ->i32, a: i32, b: i32) -> i32 {
op(a,b)
}
fn sum(a:i32, b:i32) -> i32{
a+b
}
fn product(a:i32, b:i32) -> i32 {
a*b
}
fn main() {
let a = 2;
let b = 3;
let sum_res = math(sum, a, b);
let product_res = math(product, a, b);
println!("the sum result is {:?}", sum_res);//5
println!("the product result is {:?}", product_res);//6
}
下面是函数作为返回值的情况:
fn is_true() -> bool {
true
}
fn true_maker() ->fn() -> bool {
is_true//这里不能加上(),如果加上()后会直接调用is_true这个函数,不加则是返回同名函数的指针。
}
fn main() {
(true_maker())();//由于true_maker返回的是is_true的指针,加上()后则是直接调用该函数
}
4.闭包
闭包的特点:
- 类似于Java中的匿名函数
- 可以捕获到上下文环境中的自由变量
- 可以推断出输入和返回的类型
下面是闭包的演示代码:
fn main() {
let out = 42;
fn add(i: i32, j: i32) -> i32 {
i+j
}
let closure_annotated = |i: i32, j: i32| -> i32 {
i+j+out
}; //由于闭包理解成一个变量形式,所以需要用;结尾
let closure_inferred = |i, j| i+j+out;
let i = 1;
let j = 2;
println!("{:?}",add(i,j)); //3
println!("{:?}",closure_annotated(i, j)); //45
println!("{:?}",closure_inferred(i, j)); //45
}
闭包和函数的重要区别在于,闭包可以捕获到外部变量,但是函数不可以。
比如上述方法中的add改为fn add(i:i32,j:i32)->i32{ i+j+out}则会出错。但是闭包可以。
闭包也可以作为函数的参数和返回体,下面是闭包作为参数:
fn math<F: Fn() -> i32>(op: F) ->i32 {
op()
}
fn main() {
let a = 2;
let b = 3;
println!("result is {:?}", math(|| a + b)); //5
println!("result is {:?}", math(|| a*b)); //6
}
上述定义了函数math,其参数是一个泛型F,并且这个F受到一个Fn() -> i32的trait限定,(trait理解成Java中的interface接口),代表该函数只允许实现Fn()->i32的trait的类型作为参数。
所以Rust中的闭包实际上就是一个匿名结构体和trait来组合实现的。
闭包作为返回值的情况:
fn two_times_impl() -> impl Fn(i32) -> i32 {
let i = 2;
move |j| j * i//move关键字将i的所有权转移到闭包中,因为闭包默认是引用,这里是为了防止悬垂指针,后续更新。
}
fn main() {
let result = two_times_impl();
println!("result is {:?}", result(2));
}
上述代码中,定义了impl Fn(i32)->i32作为返回值类型,但是在函数定义的时候并不知道具体的返回类型,在函数调用时,编译器会推断出来。
四.流程控制
主要讲条件语句和循环语句
1.条件表达式
之所以叫表达式,就代表其一定有值,而且if表达式的各个分支必须返回同一个类型的值。
fn main() {
let n = 13;
let big_n = if (n < 10 && n > -10 ){
10*n
} else {
n/2
};
println!("big_n is {:?}", big_n); //6
}
上述代码中,根据逻辑推断出走的else分支,但是13/2应该是6.5,由于变量n默认推断为i32类型,且big_n变量也已经被Rust编译器根据上下文默认推断为i32类型,所以在计算n/2的时候,Rust编译器会将结果截取来符合所推断类型i32。
2.循环表达式
在Rust中,存在三种循环表达式:while,loop,for…in。
下面用for实现FizzBuzz:
fn main() {
for n in 1..101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
prinln!("buzz");
} else {
prinln!("{}", n);
}
}
}
当使用无限循环的时候Rust可使用loop,避免使用while true
3.match表达式
fn main() {
let number = 42;
match number {
0 => println!("zero"),
1...3 => println!("one to three"),
5 | 7 | 13 => println!("five or seven or thire"),
n @ 42 => println!("you can use n in this match,n is {}", n),
_ => println!("others"),
}
}
这种格式分为左右两个分支,左侧为number所匹配的具体结果,右侧为执行代码,上述代码,当number为0的时候,输出zero,当为数字1-3之间的数字时,执行右侧代码,依次,下面是单独匹配,当是5,7,13之间某一位数字的时候执行右侧代码,@符号为match中的赋值符号,可以把当前值赋值给变量n,供右侧代码使用。最后的下划线_为其余情况统一执行右侧代码。
match表达式左侧为模式,右侧为执行的代码。
5.if let和 while let表达式
这两个是在某些情况来代替match表达式的。
fn main() {
let boolean = true;
let mut binary = 0;
if let true = boolean {
binary = 1;
}
assert_eq!(binary, 1);
}
if let表达式左侧为模式,右侧为具体的值,上述代码及当boolean变量为true的时候,将binary的值从0改为1。
在某些循环场合下,while let可简化代码,循环代码如下:
fn main() {
let mut v = vec![1,3,5];
loop {
match v.pop() {
Some(x) => println!("{}",x),
None => break,
}
}
}
上述代码中,None => break只负责跳出代码,可简化,改为while let及为:
fn main() {
let mut v = vec![1,3,5];
while let Some(x) = v.pop() {
println!("{}", x);
}
}
五.基本数据类型
1. 布尔类型
Rust内置了布尔类型,类型名为bool,只有两个值,true和false。
fn main() {
let x = true;
assert_eq!(x as i32, 1);// 可以通过as把bool类型转为数字类型,但是不能数字转bool
let y: bool = false;
let x = 5;
if x > 1 {
println!("x is bigger than 1 ")
};
assert_eq!(y as i32, 0);
}
2.基本数字类型
Rust的基本数字类型可为三类:固定取值范围的类型,动态取值范围的类型,浮点类型
- 固定取值范围的为无符号整数和符号整数,
无符号:u8,u16,u32,u64,u128,后面的数字代表占有多少位,比如u8占用8位及一个字节。
有符号:i8,i16,i32,i64,i128,与无符号一样的含义。 - 动态取值范围类型:
usize,占用4个或者8个字节,取决于机器的字长。
isize,与上诉usize一样。 - 浮点类型:
f32,单精度32位浮点数,至少6位有效数字。
f64,双精度64位浮点数,至少15位有效数字。
3.字符类型
在Rust中,用单引号来定义字符,每个字符占4个字节。
fn main() {
let x = 'r';
}
4.数组类型
数组类型的特点:
- 数组大小是固定的
- 元素均为同一类型
- 默认是不可变的
数组类型的声明方式位[T; N],T为泛型,就是指定为某一特定类型,后续不可变,N为数组的长度。
fn main() {
let arr: [i32, 3] = [1,2,3];
let mut mut_arr = [1,2,3];// 绑定为一个可变数组
assert_eq!(1, arr[0]);
mut_arr[0] = 3; // 可修改可变数组中的数据
assert_eq!(3, mut_arr[0]);
let init_arr = [0; 10]; //创建一个长度为10,初始值为0的数组
//println!("out index is error :{}", arr[5])编译失败,数组越界。
}
对于越界的数组,Rust会编译报错。
对于原始的数组,只有实现了Copy trait的类型才能作为其元素。
5.范围类型
范围类型包括了左闭右开和全闭合两种。
fn main() {
assert_eq!((1..5), std::ops::Range{start: 1, end: 5});//左闭右开
assert_eq!((1..=5), std::ops::RangeInclusive::new(1, 5));//全闭合
assert_eq!(3+4+5, (3..=5).sum());
assert_eq!(3+4+5, (3..6).sum());
for i in (1..5) {
println!("{}", i);// 1,2,3,4
}
for i in (1..=5) {
println!("{}", i);// 1,2,3,4,5
}
}
6.切片类型
切片类型是对一个数组(包括固定大小的数组和动态数组)的引用片段,有利于安全有效的访问数组的一部分,而不需要拷贝。理论上切片的引用是已经存在的变量,在底层,切片代表一个指向数组起始位置的指针和数组长度。用[T]类型表示连续序列,那么切片类型就是&[T]和&mut [T]。
fn main() {
let arr: [i32, 5] = [1,2,3,4,5];
assert_eq!(&arr, &[1,2,3,4,5]);
assert_eq!(&arr[1..], [2,3,4,5]); //表示获取索引1以后的所有元素,索引是从0开始计算的。
assert_eq!((&arr).len(), 5);
let arr = &mut [1,2,3];// 定义一个可变切片,可以直接通过索引修改对应的值。
arr[1] = 7;
assert_eq!(arr, &[1,7,3]);
let vec = vec![1,2,3];
assert_eq!(&vec[..], [1,2,3]);
}
7. str字符串类型
Rust存在原始的字符串类型str,通常都是以不可变借用的形式存在,既&str(字符串切片),处于内存安全考虑,Rust将字符串分为两类,一种是固定长度字符串,&str,另一种是可增长字符串,可以随意改变其长度,String。
fn main() {
// 定义一个静态生命周期的str字符串,则truth的生命周期和该程序代码的生命周期是同步的。
let truth: &'static str = "Rust是一门优雅的语言";
let ptr = truth.as_ptr();
let len = truth.len();
assert_eq!(28, len);
}
- &str字符串类型由两个部分组成:指向字符串序列的指针和记录长度的值。分别对应的方法为as_ptr和len方法。
8.原生指针
可以表示内存地址的类型被称为指针。Rust中包括了多种指针,引用(reference),原生指针(Raw Pointer),函数指针(fn pointer)和智能指针(Smart Pointer)。