Rust-环境配置以及一些基础知识

rust的下载

rust的下载地址是: https://www.rust-lang.org/learn/get-started

其中, 如果用的是Unix系统, 用这一行命令就可以完成下载:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

之后, 如果你想更新rust, 可以用rustup update这个命令.

Cargo

Cargo相当于是rust工程的管理工具, 类似于Python的pip.

Cargo的安装目录在~/.cargo中, 用cargo下载的包全都在这里.

rust的Hello World

rust的Hello World如下:

fn main() {
  println!("Hello World");
}

使用rustc编译:

rustc test.rs

然后运行:

./test

其中, println!()是换行打印, print!()是不换行打印.

rust注释

rust的注释和C/C++一样, 单行是//, 多行是/**/

rust还有一种特殊的多行注释///, 在这种多行注释中可以使用markdown进行渲染:

/// Adds one to the number given.
///
/// # Examples
///
/// 
/// let x = add(1, 2);
///
///

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

fn main() {
    println!("{}",add(2,3));
}

在这种注释下, 开发者可以使用cargo doc功能自动生成具有可读性的文档.

rust格式化打印

rust中使用{}来进行格式化打印, 例如:

println!("a is {}, b is {}, c is {}", a, b, c);

rust转义字符

rust中转义字符和C/C++一样, 都是\.

但是对于两个字符特殊, 这两个字符是{}.

如果要打印{:

println!("{{");
```

如果要打印`}`:

```rust
println!("}}");

rust变量的定义

首先, rust是一种强类型语言.

如果要定义变量, 可以使用let关键字, 例如:

let a = 123;

其中, rust会自动判别变量的类型.

首先, 由于rust是强类型语言, 这种写法会被报错:

a = "abc"

其次, rust不允许==有精度损失的类型转换==, 因此, 下面的写法会被报错:

a = 4.56;

再次, 如果只是用let定义变量, 那么变量默认为不可变, 因此, 下面的写法会被报错:

a = 456;

但是, 不可变的变量可以被重新绑定, 因此, 下面的写法是正确的:

let a = 123;
let b = 456;

这种重新绑定在rust中叫做重影(Shadowing).

如果要定义一个可变的变量, 那么可以用mut关键字:

let mut a = 1;
a = 2;

但是, 由于rust是强类型的语言, 可变的变量只有值可以改变, 类型不能变化, 因此, 下面的代码会报错:

let mut s = "123";
s = s.len()

rust数据类型

如果要在变量定义的时候要定义上数据类型, 可以这样写:

let mut x : f32 = 3.0;
let y : f64 = 1.0;

rust整数类型

几位 无符号 有符号
8位 u8 i8
16位 u16 i16
32位 u32 i32
64位 u64 i64
128位 u128 i128

rust浮点数

浮点数有f32f64两种.

rust布尔类型

布尔类型的关键字是bool, 值只能是true/false.

rust字符类型

字符类型的关键字是char, 注意, ==在rust中, 字符一共占用4个字节==.

rust数组类型

rust数组下标也是从0开始.

rust使用[]来定义一个数组, 数组中的元素必须是同一类型:

let a = [ "January", "February", "March" ];

如果要声明数组类型:

// 类型是i32, 长度是5的数组
let b: [i32; 5] = [1, 2, 3, 4, 5]

如果要定义一个数组元素相同的元素, 可以:

// 元素是3, 长度是5的数组
let c = [3; 5]

rust的tuple类型

rust中的tuple可以包含不同的元素, 使用()定义一个tuple:

let tup: (i32, f64, u8) = (500, 6.4, 1)

可以把元组中的变量拆开赋给不同的变量:

let (x, y, z) = tup;

rust运算符

加法: +

减法: -

乘法: *

除法: /

  • 和C/C++的用法一行, 整数除法是向下取整

取模: %

rust中没有++--, 只有+=-=.

rust函数

rust的函数定义如下:

fn main() {
  println!("Hello World");
}

如果要带参数和返回值:

fn add(a: i32, b: i32) -> i32 {
  return a + b;
}

函数体表达式

rust中, 可以使用{}定义一个比较复杂的表达式, 定义的文法如下:

{
  // Statement也可以没有
  Statement
  Expression
}

其中, Statement是末尾带;的那种语句, 而Expression就是单独的表达式, 例如:

let y = {
  let x = 3;
  x + 1
};

此时, y的值就是4.

rust条件语句

rust条件语句的格式如下:

fn main() {

  let a = 12;
  let b;

  if a > 0 {
      b = 1;  
  }
  else if a < 0 {
    b = 2;
  }
  else {
    b = 0;
  }
}

需要说明几点:

  • 按照rust的要求, if或者else if后面必须是bool类型.
  • if/else if后面不需要加(), 但是不是不可以.
  • 无论后面是不是单个语句, 都必须用{}.

结合函数体表达式, rust中的三目运算符可以定义为:

let number = if a > 0 {1} else {-1};

rust循环语句

while循环的格式:

while number != 4 {
  //...
}

For循环遍历数组中的值, 可以使用iter()迭代器:

let a = [1, 2, 3, 4, 5];

for item in a.iter() {
  // item去除的是数组的值
  println!("{}", item)
}

如果要数组下标, 可以用a..b, 表示$[a, b)$:

for i in 0..5 {
  println!("a[{}] = {}", i, a[i]);
}

其中:

  • ..y: 表示$[0, y)$
  • x..: 表示$[x, end)$
  • ..: 表示从0到结束.

rust中还支持一种无限循环loop, 相当于while (true)

loop {
  //...
}

退出循环可以使用break.

其中, rust有一种新东西, 可以在loop循环中使用break关键字, 实现return类似的功能, 可以给循环一个返回值, 这样就不用使用额外的变量记录循环中的值了, 例如:

let a = [1, 2, 3, 4, 5];
let mut i = 0;

// 查找数组中元素2的下标
let location = loop {

  let ch = a[i];
  if a[i] == 2 {
    break i;
  }

  i += 1;
}

注意, 这种写法只针对loop循环, 其他循环不能使用.

rust的所有权

在rust中, 每一个内存中的每一个值就相当于一个房子, 它都有一个所有者(owner), 这个所有者就是指向这个值的变量.

在进行各种各样的操作时, 所有权会发生各种各样的变化.

赋值操作中所有权的变化

基本类型

rust中的基本类型有如下几类:

  • 所有整数类型.
  • 所有浮点数类型.
  • 布尔类型bool.
  • 字符类型char.
  • 仅包含以上类型的tuple.

基本类型存储在栈上, 如果执行赋值操作会发生如下变化:

  • 栈上的值会被复制一份.
  • 被赋值的变量会指向刚出炉的被复制的值.

例如:

let x = 5;
let y = x;

当执行完let y = x;后, yx是毫不相干的变量, 有点类似于深拷贝.

非基本类型

非基本类型的值基本存储在堆(heap)中.

如果执行赋值操作, 会发生所有权的交换, 例如:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 错误!s1 已经失效

在执行完let s2 = s1;之后, String::from("hello")这个数据的所有权已经从s1转移到了s2, s1已经不能再使用了.

函数参数传递所有权的变化

基本类型

如果基本类型变量被当作函数参数, 那么函数参数是原来的变量在栈上复制得到的, 不会发生所有权的变化.

fn main() {
  let x = 5;
  function(x);
  // 在这里x还是有效的.
}

非基本类型

如果非基本类型被当作函数参数, 那么值的所有权会从原变量转移到函数参数, 原变量的引用就会失效, 例如:

fn main() {
  let s1 = String::from("hello");
  function(s1);
  // 此处, s1会失效, 不能再次使用.
}

因此, 为了不让变量因为函数调用而失效, 一般非基本类型作为函数参数, 都传递引用.

函数返回值所有权的变化

基本类型

非基本类型

克隆

对于非基本类型的变量可以使用clone(), 这个方法干了如下几件事情:

  • 将堆中的数据复制一份.
  • 创建一个新的引用, 指向这个数据.

也就相当于深拷贝.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
  // s1与s2是独立的
    println!("s1 = {}, s2 = {}", s1, s2);
}

引用

不可变引用

引用可以让两个变量共享一个值的所有权, 例如:

fn main() {
  let s1 = String::from("hello");
  let s2 = &s1;
  // s1, s2同时指向堆中的同一个地址
}

在函数参数传递时, 也可以传递引用:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

注意, 考虑这种情况:

let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;

s2s1共享一个值之后, 所有权又从s1转移到了s3, 此时s1失效, s2也会失效, 需要重新建立引用:

let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s1;
s2 = &s3;

可变引用

注意, 建立引用后, 不能通过引用修改数据, 例如:

let s1 = String::from("hello");
let s2 = &s1;
// 无法修改, 只能使用
s2.push_str("haha");

如果要修改, 可以使用mut修饰的引用类型 (可变引用), 例如:

let s1 = String::from("hello");
let s2 = &mut s1;
s2.push_str("haha");

多重引用

注意, 不可变引用可以多重引用, 可变引用不能进行多重引用, 例如:

let s = String::from("hello")

// 不可变引用, 可以多重引用
let r1 = &s;
let r2 = &s;

// 可变引用, 不能多重引用
let R1 = &mut s;
let R2 = &mut s; // 报错

垂悬引用 (Dangling Reference)

本质上就是一个引用, 指向了被释放的变量值, 相当于空指针.

例如如下例子:

fn main() {
  let a = function();
}
fn function() -> &String {
  let s = String::from("hello");
  // 函数结束后, s会被释放, 返回的引用就是空指针, Dangling Reference.
  return &s;
}

rust中切片类型(Slice)

字符串切片

字符串切片的定义如下:

let s = String::from("hello")
let a = &s[0..5]

如果一个字符串被切片, 那么原字符串不能修改:

fn main() {
  let mut s = String::from("hello");
  let slice = &s[0..3];
  s.push_str("haha"); // 会报错
}

数组切片

数组切片的定义如下:

fn main() {
  let arr = [1, 3, 4, 5, 5];
  let part = &arr[0..3];
}

rust结构体

结构体定义与访问

rust结构体定义如下:

struct Person {
  name: String,
  age: u32,
  home: String
}

注意结构体定义最后没有;.

创建结构体变量:

let p = Person {
  name: String::from("haha"),
  age: 18,
  home: String::from("fandouhuayuan")
}

访问结构体变量可以使用.:

println!("{}, {}, {}", p.name, p.age, p.home);

假设你想创建一个结构体变量, 这个变量大部分属性和现有的结构体变量一致, 只有少部分属性不一样, 可以用这种写法:

let p = Person {
  name: String::from("haha"),
  age: 18,
  home: String::from("fandouhuayuan")
}

let another_p = Person {
  name: String::from("niuniu"),
  // ..关键字
  ..p
}

tuple结构体

tuple结构体主要用来处理那些经常定义, 但是又不太复杂的数据:

定义方式如下:

struct Point(f64, f64);

创建实例:

let origin = Point(0.0, 0.0);

访问成员变量:

println!("{}, {}", origin.0, origin.1);

打印结构体

如果要打印结构体, 首先要引入调试库, 在最开头加上这一句:

#[derive(Debug)]

然后, 用{:?}这个占位符打印结构体:

let rect1 = Rectangle {
  width: 30,
  height: 50
};

println!("rect1 is {:?}", rect1);

如果结构体的成员变量太多了, 可以用{:#?}进行打印, 这样每个成员变量就会单独占一行.

结构体方法

成员方法

结构体方法需要用implself关键字来定义, 例如:

/* 定义成员变量 */
struct Rectangle {
  width: u32,
  height: u32
}

/* 定义成员方法 */
impl Rectangle {

  /* 第一个参数需要是&self */
  fn area(&self) -> u32 {
    /* 访问成员变量用self.xxx */
    return self.width * self.height;
  }

  fn wider(&self, rect: &Rectangle) -> bool {
    return self.width > rect.width;
  }

}

注意, impl一个结构体可以写很多个, 最终效果相当于他们拼接.

结构体关联函数

结构体关联函数有点类似于静态方法, 不依赖于某一个实例, 只要在impl块中不加&self就是静态方法.

例如:

struct Rectangle {
  width: u32,
  height: u32
}

impl Rectangle {
  /* 结构体关联函数 */
  fn create(width: u32, height: u32) -> Rectangle {
    return Rectangle { width, height };
  }

}

调用时需要使用:::

let rect = Rectangle::create(30, 50);

可以使用结构体关联函数定义构造方法/静态方法.

结构体所有权问题

考虑如下代码:

struct Person {
  name: String,
  age: u32
}

fn main() {
  let p = Person {
    name: String::from("haha"),
    age: 18
  };

  /* 此时, 结构体p中的name所有权已经交给了str */
  let str = p.name;

  /* 再次访问p.name会报错 */
  println!("{}", p.name);
}

应该改成: let str = p.name.clone().

rust枚举

枚举可以这样定义:

enum Book {
  Papery, Electronic
}

创建枚举变量:

let book = Book::Papery;

打印枚举变量:

  • 首先需要在开头引入调试库: #[derive(Debug)]
  • 然后使用{:?}占位符打印:
println!("{:?}", book);

rust工程结构

首先, 可以使用cargo创建一个标准的rust项目:

cargo new "myproject"

这个myproject就是package的名字.

然后, 你可以在src下面创建很多文件夹, 文件夹中写很多的rs文件.

组织方式如下:

  • 一个rust文件相当于一个module, 模块名字就是文件名, 如果要暴露出来, 需要在函数/类前面用pub.

  • 写完之后, 文件夹里需要创建一个mod.rs, 把文件夹变成module, 然后在里面引入这个文件夹中的所有模块:

    pub mod module1;
    pub mod module2;
    ///
    
  • 然后在最顶层的lib.rs中导入所有顶层的模块, 例如:

    .
    ├── lib.rs
    ├── main.rs
    ├── net
    │  ├── mod.rs
    │  ├── netutils.rs
    │  └── test.rs
    └── utils.rs
    
    • lib.rs就只需要导入pub mod net;pub mod utils即可, 然后net中的mod.rs再递归导入.
  • main.rs中, 如果要引用一个子模块中的函数, 需要用以下路径:

    • myproject::net::test::xxx, 注意开头是myproject.
  • 如果要跨文件引用, 例如要在test.rs中用utils.rs的东西, 需要用到以下路径:

    • crate::utils.rs, 注意开头是crate.

rust泛型

函数泛型

函数泛型定义如下:

fn max<T>(array: &[T]) -> T {
  // ....
}

结构体泛型

结构体泛型的定义格式如下:

struct Point<T> {
  x : T,
  y : T
}

// 注意impl后面也要有<T>
impl<T> Point<T> {

  // 函数名后面也要有<T>
  fn getX<T>(&self) -> &T {
    return &self.x;
  }
}

使用时:

let p = Point { x : 1, y : 2 };
println!("{}", p.getX());

rust的trait

trait的基本用法

rust中也提供了和Java接口类似的东西, 叫做trait, trait中可以包含方法的定义, 也可以实现一个方法.

  • 如果包含了方法的定义, 那么这个trait可以绑定到一个具体的类上, 然后根据这个类的具体情况实现其中的方法.
  • 如果trait中本身就实现了方法, 那么这些方法就是default trait, 如果将来这个trait绑定到了某个类, 这个类可以选择重写这个default trait, 也可以不重写, 直接作为默认的方法.

例如我定义了一个类:

struct Person {
  name: String,
  age: u8
}

我规定一个trait, 要求重写打印这个类的方法:

trait Descriptive {
  fn describe(&self) -> String;
}

然后, 我可以将这个trait绑定到这个类, 然后这个类负责重写:

impl Descriptive for Person {

  fn describe(&self) -> String {
    /* ... */
  }
}

trait作为函数参数

假设我定义了一个trait叫做Descriptive, 那么, 有一种类型叫做impl Descriptive, 这个类型的含义叫做:

实现了Descriptive这个trait的对象.

如果作为函数参数:

fn output(object: impl Descriptive) {
  println!("{}", object.describe());
}

那么只有实现了Descriptive trait的对象才能被放到这个函数中.

还有一种写法是结合泛型:

fn output<T: Descriptive>(object: T) {
  println("{}", object.describe());
}

如果涉及到了多个trait, 可以用+连接:

fn output<T: Descriptive + ABABA>(object: T) {
  println("{}", object.describe());
}

例如一个取数组中最大值的例子, 我首先需要实现一个比较大小的trait:

trait Comparable {
  /* self是变量, 相当于this, &Self是类型, 相当于this的类型 */
  fn compare(&self, object: &Self);
}

/* 对浮点数实现 */
impl Comparable for f64 {
  fn compare(&self, object: &f64) {

    if &self > &object { 1 }
    else if &self == &object { 0 }
    else { -1 }
  }
}

然后定义一个泛型函数, 要求函数参数必须实现Comparable接口:

fn max<T: Comparable>(array: &[T]) {
  // ...
}

trait作为函数返回值

trait如果作为函数返回值, 表示如下几个意思:

  • 要求这个函数返回的对象必须实现指定的trait.

  • 函数内所有可能的返回值类型必须完全相同:

    • 假设有A, B两个类实现了Descriptive trait, 下面的代码还是错误的:

      fn function(bool b) -> impl Descriptive {
        if b { A{} }
        else { B{} }
      }
      

      因为, AB两个类型不同.

用trait约束泛型类的impl

假设有一个泛型类, 他有很多impl实现的成员方法, 我可以实现: 如果这个泛型类有实现某些trait, 那么它就可以拥有某些成员方法, 例如下面这个例子:

// 泛型类A
struct A<T> {

}

// 如果A实现了B和C这两个trait, 那么A就具有下面的成员方法
impl<T: B + C> A<T> {
  // ...
}