乐趣区

关于后端:Rust-笔记-2

构造体

初始化

Rust 的构造体相似于 C,应用关键字 struct 申明。

struct User {
  active: bool,
  sign_in_count: u32,
  username: String,
  email: String
}

构造体中的每个元素称为“域”(field),域是可批改的(mutable),应用 . 来拜访域的值。

创立实例

为了应用构造体,须要依据构造体创立一个实例(instance),并给该构造体的域成员赋值,赋值的程序能够 不同于 构造体定义的程序。

使得域可批改,必须给 实例 增加 mut 关键字,Rust 不容许 给某一个或几个域增加 mut 关键字。

struct User {
  active: bool,
  sign_in_count: u32,
  username: String,
  email: String
}

fn main() {
  let mut user1 = User {
    active: false,
    sign_in_count: 1,
    username: String::from("someusername"),
    email: String::from("someuseremail"),
  };
  
  user1.email = "anotheremail";
}

能够应用 构造体更新语法 .. 来从其余实例来创立新实例:

struct User {
  active: bool,
  sign_in_count: u32,
  username: String,
  email: String
}

fn main() {
  let user1 = User {
    active: false,
    sign_in_count: 1,
    username: String::from("someusername"),
    email: String::from("someuseremail"),
  };
  
  /*
  // regular 
  let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
    
   */
  
  let user_2 = User {
    active: true,
    ..user1
  }
}

下面的代码示意,除了域 active 之外,user_2 的其余域值和 user1 相等。

注:..user1 后没有 ,,而且必须放在最初一行。

元组构造体

元组构造体(tuple struct) 相似于元组。能够了解为给元组调配了有意义的名称,然而并没有确切的域成员,只有域成员的类型。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {let red = Color(0, 0, 0);
      let origin = Point(0, 0, 0);
}

下面的两个实例尽管有着雷同的域成员类型以及域成员值,但仍然是不同的实例。

每个构造体实例的类型就是其定义的类型,即使它们有完全相同的域成员且域成员的类型统一。

类单元构造体

类单元构造体(unit-like struct)指的是不含任何数据的构造体。相似于不含成员的元组 –单元(unit) ()

struct ULS;

fn main() {let subject = ULS;}

函数 VS 办法

关联和区别

在一些编程语言中,函数(function)和办法(method)通常有着雷同的含意。在 Rust 中,两者的关联和区别如下:

  • 关联

    • 都应用 fn 关键字申明
    • 都有参数和返回值
    • 都能够被调用
  • 区别

    • 办法的第一个参数永远是 self,示意被调用的办法作用的实例(instance)
    • 办法通常被定义在一个构造体、枚举或者 trait 对象的上下文(context)下,而函数通常没有具体的上下文

Rust 应用办法的起因是进步代码的组织性。impl 紧紧关联着作用的构造体。

定义方法

办法应用 fn 关键字申明,通常写在 impl(implementation) 块(block)中。

struct Rectangle {
        width: u32,
      height: u32,
}

impl Rectangle {fn area(&self) -> u32 {self.width * self.height}
}

fn main() {
    let rect1 = Rectangle {
      width: 30,
      height: 50,
    };
    println!("The are of rectangle {}", rect1.area());
}

办法的第一个参数是 self,其实是 self: Self 的简洁示意。如果不心愿办法带走 ownership,应该应用 &self,如果心愿更改数据,应用 &mut self

相似函数,办法同样应用 . 运算符调用。与 C、C++ 等语言不同,Rust 不反对应用 -> 运算符来调用办法,而是通过被称为 主动援用和解援用 的形式来调用办法。大抵原理为:当调用 object.method() 时,Rust 会主动增加 &&mut*,因而 object 匹配了办法的签名

以下两行代码作用雷同:

p1.distance(&p2);
(&p1).distance(&p2);

getters

如果对构造体实现了同名的域成员和办法,那么 object.field 示意拜访域成员,object.method() 示意调用办法。

通常,调用同名的办法示意心愿获取其同名的域成员的值,这类办法被称为 getters。一些编程语言会主动实现 getters,然而 Rust 并非如此。

联合函数

定义在 impl 块下的函数都被称为 联合函数(Associated Function),因为它们作用于 impl 后的构造体。

也能够定义第一个参数不为 self 的联合函数,这类函数通过 :: 作用,例如:String::from()

impl Rectangle {fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

let sq = Rectangle::square(3);

所以 :: 语法同时用于联合函数和模块(module)的命名空间。

多个参数的办法

impl Rectangle {fn area(&self) -> u32 {self.width * self.height}
      fn can_hold(&self, other: &Rectangle) -> bool {self.width > other.width && self.height > other.height}
}

Rust 容许应用多个 impl 块申明办法,然而在本例中,两个办法放在一个 impl 中可读性更好。


枚举

枚举类型(enumerations / enums)定义穷举所有可能的值的数据类型。

定义枚举

例如上面的代码:

use std::cmp::Ordering;
use std:io;

fn main() {println!("This is a guessing game");
      const SECRET_NUMBER: u32 = 12;
      let mut guess: String = String::new();
      println!("Enter your guess:");
      io::stdin.read_line(&mut guess).expect("Failed to read line");
      let mut guess = match guess.parse().wrap() {Ok(num) => num,
      Err(_) => {println!("You should enter a number!");
      }
      }
      match guess.cmp::Ordering {Less => println!("Too small!"),
      Greater => println!("Too big!"),
      Equal => println!("You are the winner!");
  }
}

match 关键字用于开始匹配枚举。

这段代码中用到了两个 枚举 类型,别离是:

  1. guess 的类型判断会返回枚举类型 Result,它有 OkErr 两个枚举值,别离示意胜利和谬误两种状况,
  2. guessSECRET_NUMBER 两个值之间的比拟会返回枚举类型 Ordering,它有 LessGreaterEqual 三种状况,别离示意小于,大于和等于。

模式匹配

在 Rust 中,应用 match 来对某个值和一系列模式进行匹配。模式能够是字面量、变量以及其余类型。

每个匹配(arm)都由 模式 代码 组成,每个 arm 之间用 , 分隔。模式和代码之间用 => 相连。

如果代码为多行,须要放入括号 {}。代码由表达式组成,如果匹配胜利,该表达式的值作为整个 match 的返回值。

例如:

enum Coin {
      Penny,
      Nickel,
      Dime,
      Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
      match coin {
          /*
          Coin::Penny => {println!("That's an penny!");
                  1
          },
          */
              Coin::Penny => 1,
          Coin::Nickel => 5,
          Coin::Dime => 10,
          Coin::Quarter => 25,
      }
}

matchif 不同 在于:if 的条件后果必须是 bool,而 match 能够是任意类型。

if let

if let 语法提供了一种更简洁的形式来解决某种模式匹配胜利并疏忽其余选项的状况。

let config_max = Some(3u8);
match config_max {Some(max) => println!("The maximum is configured to be {}", max),
      _ => (),}

// same as 
let config_max = Some(3u8);
if let Some(max) = config_max {println!("The maximum is configured to be {}", max);
}

模块零碎

Rust 的 模块零碎(Module System)包含:

  1. 包(package):构建、测试、共享 crates。
  2. crates:可生成库(library)或者可执行程序的模块树。
  3. 模块(module):管制门路的组织形式、作用域以及公有性。
  4. 门路(path):命名一个实体的形式,例如:构造体、函数、模块。

是一系列 crates 的汇合。包中有名为 Cargo.toml 的文件定义了如何构建这些 crates。

应用 cargo new 命令后,Rust 会在当前目录创立一个包。

包中至多要蕴含一个 crate。包能够蕴含多个二进制 crates,然而最多只能蕴含一个库 crate。

Cargo 是最罕用的包,其默认把 src/main.rssrc/lib.rs 作为二进制 crate 和库 crate,并把两者作为 crate root。当应用 rustc 时,Rust 把这两个文件(如果存在)编译。

Crate

在 Rust 中,crate 指的是编译器所编译的源文件,是编译器一次编译时的最小单位。

crate 蕴含多个模块。

crate 分为二进制 crate(binary crate)和库(library crate)两种:

  • 二进制 crate 是由 Rustaceans 所编写的代码,每个二进制 crate 必须蕴含一个 main 函数。
  • 库 crate 不含 main 函数,不能被编译为可执行程序,而是作为一种共享形式在我的项目中。

两种 crate 别离在 src 门路下以 main.rslib.rs 两种文件名称存在。

crate root 指的是 Rust 编译器编译的源文件,以及 crate 的根模块。

模块

总览

假如有以下文件构造:

backyard
|--Cargo.lock
|--Cargo.toml
|--src
        |--garden
        |        |--vegetable.rs
        |--garden.rs
        |--main.rs
// src/main.rs

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {let plant = Asparagus {};
    println!("I'm growing {:?}!", plant);
}
// garden.rs

pub mod vegetables;
// vegetables.rs

#[derive(Debug)]
pub struct Asparagus {}
  • backyard 是 crate 目录
  • src/main.rs 是 crate root
  • pub mod garden 示意 garden 是一个模块,可见性为 pub。这行代码示意把 garden.rs 里的内容引入
  • pub mod vegetables 作用同上

所以,模块的工作原理:

  • 从 crate root 开始:当编译 crate 时,编译器首先找到 crate root 文件(通常是 main.rs 或者 lib.rs)来编译
  • 定义模块:在 crate root 文件中,能够用 mod 关键字申明新的模块,例如:mod garden,编译器会在以下目录寻找该模块的代码:

    • 行内
    • src/garden.rs
    • src/garden/mod.rs
  • 定义子模块 :在 crate root 的文件里还能够定义子模块,例如:mod vegetables,编译器会在其父模块的目录下寻找子模块的代码:

    • 行内
    • src/garden/vegetables.rs
    • src/garden/vegetables/mod.rs
  • 模块中的门路:一旦申明模块后,能够通过门路引入模块。例如:在 vegetables 模块内申明了 Asparagus,引入门路为:crate::garden::vegetables::Asparagus
  • 公有 vs 共有 :默认状况下,子模块的内容对父模块是 公有的,应用 pub 关键字使其公有化
  • use 关键字:应用 use 来简化援用。

劣势

模块的劣势:

  • 进步代码的可读性和可重用性
  • 隐衷性

门路

相似于文件系统,有 绝对路径 相对路径 两种形式来示意层级关系:

  • 绝对路径:指的是从 crate root 开始的残缺门路。对于内部 crate 来说,绝对路径以 crate 的名称为开始;对外部 crate 来说,绝对路径以字面量 crate 开始
  • 相对路径:从以后模块开始,通常蕴含 selfsuper 等关键字

绝对路径和相对路径 应用 :: 示意层级间的分隔符。

相对和相对路径各有优劣,能够依据集体偏好进行抉择。在 Rust 中,个别应用 相对 门路,起因是这样使得依赖绝对独立。

mod front_of_house {
    mod hosting {fn add_to_waitlist() {}}
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();}

公有、私有

在 Rust 乃至整个计算机领域都有 外部实现对外部不可见 的准则。

公有的概念通常和作用域(scope)相干。例如:定义在函数 A 外部定义了子函数 B。对该子函数 B 来说,函数 A 对其是可见的。但对函数 A 的其余局部来说,子函数 B 是不可见的,

通常来讲,Rust 把对象(items)设置为 公有(privacy),或者称为不可见的。如果调用了不可见的对象,编译器会弹出谬误。

在 Rust 中,默认对父模块公有的对象(items)包含 模块、函数、办法、构造体、枚举、常量

能够应用 pub 关键字使对象变为 可见、私有 的。

注:把某个 内部 对象标识为 pub 并不意味着 其外部 对象也被标识为 pub枚举 类型除外,如果枚举类型应用了 pub,那么枚举的所有后果默认也为 pub),例如:

fn main() {pub fn outer_function() {fn inner_function() {// --snip--}
      }
      outer_function(); // OK
      inner_function(); // Error, because the function sign has no **pub**}

pub enum IPAddr {V4(String), // also **pub**
      V6(String), // also **pub**
}

最佳实际

一般来说,一个包同时蕴含二进制 crate src/main.rs以及库 crate src/lib.rs。两者默认都含有包名。

罕用的范式是:在 src/lib.rs 中定义模块树,这样,在二进制 crate 中调用任何私有的对象(items)都能够以该包名为开始作为门路。

super

应用 super 关键字来援用父级门路,这相似于文件系统中的 ..

fn deliver_order() {}

mod back_of_house {fn fix_incorrect_order() {cook_order();
        super::deliver_order();}

    fn cook_order() {}
}

use、as

use

应用 use 关键字来引入门路。

Rust 的常规是:在调用某个函数时,其门路应该引入到 父级 。尽管引入到 以后级 成果雷同,然而前者使得函数定义更加清晰。例如:

use crate::galaxy::solar_system::earth;
earth();

use crate::galaxy::solar_system;
solar_system.earth();              // same thing, but this one is better. 
再导出

再导出(re-exporting) 使得以后作用域引入的对象也能够用于其余作用域。因为默认状况下,应用 use 要害把某个名称引入以后作用域后,该名称对其余作用域是公有的。

应用 pub use 实现再导出:

mod front_of_house {
      pub mod hosting {pub fn add_to_waitlist() {}}
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {hosting::add_to_waitlist();
}
  • 如果 没有 再导出,内部代码想调用 add_to_waitlist() 函数必须应用门路:restaurant::front_of_house::hosting::add_to_waitlist()
  • 再导出后,应用 restaurant::hosting::add_to_waitlist() 即可
嵌套门路

为了防止应用多个 use 的多行引入导致代码可读性变差,能够把同一父对象下的子对象用花括号在同一行中。

use std::{io, fmt};
// same as
use std::io;
use std::fmt;

如果同时引入了父对象和其子对象,应用 self 关键字示意该父对象。

use std::io::{self, Result};
// same as
use std::io;
use std::io::Result;
Glob 运算符

如果要引入全副对象,应用 全局 glob 运算符 *

use std::io::*;

as

如果引入的对象名称过长,能够应用 as 关键字来通过别名来引入。

use this_is_a_very_long_function_name as lfn;

lfn(); // much simpler

汇合

Rust 的规范库中蕴含了一系列罕用的数据结构被称为 汇合(collection)。最罕用的是:

  • 向量 Vector
  • 字符串 String
  • 哈希表 HashMap

这些构造的特点是:存储在 中,可 变长 ,应用 泛型 实现。这意味着在编译时,编译器并不知道这些构造的大小。

初始化汇合的通用办法是 ::new()

向量

向量中的元素在内存中紧挨着彼此存储。

向量只能存储同种类型的数据,然而能够借助枚举来存储不同类型的数据。

初始化

向量 Vec<T> 的初始化,能够用 ::new() 来初始化一个空向量,也能够应用 macro vec![] 显式把向量的成员列出来初始化向量:

fn main() {let v = vec![1, 2, 3, 4, 5];
    let ano_v: Vec<i32> = Vec::new();}

注:在第二种申明中指明了存储元素的类型,否则 Rust 并不知道 Vector 要存储什么类型的数据。

读写

应用 push 办法给向量增加元素:

let mut v = Vec::new();
v.push(1);
v.push(2);

应用 .get() 或者括号索引 [] 的形式来拜访向量元素:

let third: &i32 = &v[2]; // 3
let two: Option<&i32> = v.get(2); // Some(2)

如下面的代码所示,应用 .get() 办法失去的是 Option<T> 数据类型,而不是向量元素 <T> 的类型。

因为 .get() 办法失去的是 Option<T> 类型,因而能够应用 match 来对获得的值做判断。

let two = v.get(2);
match two {Some(two) => println!("The element is {}", two),
    None => println!("No such element"),
}

越界

两种拜访形式对于向量越界有着不同的解决形式:

let third = &v[100]; // index out of bound
let two = v.get(100); // None

应用 [] 索引拜访能够通过编译,但在运行时会呈现 index out of bound 索引越界的谬误;应用 .get() 办法会失去 None

上面的例子阐明了向量的工作形式:

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("{}", first); // Error

下面的代码不能通过编译,起因是:因为向量的元素在内存中是紧挨着彼此存储的。因而,给向量中增加新元素时,如果以后的内存空间地位不能容下新退出的元素,就须要调配一块新的内存空间并拷贝旧的元素到该空间。而援用拜访向量元素兴许会导致拜访到曾经被解除调配的空间。

遍历

应用 for in 来遍历向量元素:

for i in &v {println!("{}", i); 
}

应用 mut 援用来遍历并批改向量元素:

for i in &mut v {*i += 50;}

字符串

两类字符串的比拟

Rust 语言的外围中只有一种字符串:字符串切片 str,通常以 &str 模式呈现。

String 类型由 Rust 规范库实现的。

两者都是 UTF-8 编码的。

初始化

字符串 String 的初始化,能够用 ::new() 来初始化一个空字符串,也能够用 ::from()显式初始化字符串,或者先申明字符串字面量,而后转化为 String 类型的字符串:

fn main() {let mut s = String::from("Hello");
  
    let mut ano_s = String::new();
  
      let s_in_stack: &str = "Hello World";
        let s_in_heap: String = s_in_stack.to_string();}

读写

应用 push_str() 把字符串拼接至另一字符串尾:

s.push_str(", World");
println!("{}", s) // Hello, World

应用 push() 拼接一个字符到字符串尾:

let mut s = String::from("lo");
s.push('l');
println!("{}", s) // "lol"

应用 + 来拼接已有字符串:

let s1 = String::from("Hello,");
let s2 = String::from("World");
let s = s1 + &s2;
println!("{}", s1); // Error
println!("{}", s2); // "World"
println!("{}", s); // "Hello, World"

注:拼接之后,s1 的 ownership 被转移给 s,所以 s1 不能再被应用。这是因为 add 函数的签名:

fn add(self, s: &str) -> String {

决定了 self 地位的变量的 ownership 被篡夺。第二个变量须要应用 & 援用模式,而不是间接把两个字符串的值相加。

这里编译器应用 coerceString 类型转化为 &str,当调用 add 时,Rust 会应用 deref coercion&s2 转化为 &s2[..]

如果不心愿 s1 的 ownership 发生变化,能够应用 format! macro 来拼接字符串:

let s1 = String::from("Hello,");
let s2 = String::from("World");
let s = format!("{s1}{s2}");
println!("{}", s1); // "Hello,"
println!("{}", s2); // "World"
println!("{}", s); // "Hello, World"

Rust 不反对 索引拜访字符串中的字符。

let s1 = String::from("Code");
println!("{}", s1[0]); // Error

下面的代码将不能通过编译,起因和 String 类型的外部实现无关:

String 类型是对 Vec<u8> 的包装,所以:

let hello = String::from("Hola");
let ano_hello = String::from("Здравствуйте");

hellolen4,因为在 UTF-8 编码中每个字符占用 1 个字节,而 ano_hellolen24,而非 12,因为在 UTF-8 编码中,每个 Unicode scalar 值占用 2 个字节。因而,如果应用索引拜访,将返回无意义的值。

能够应用 [..] 创立字符串切片:

let ano_hello = "Здравствуйте";
// let ano_hello = String::from("Здравствуйте"); // also Ok
let s = &ano_hello[0..4]; // Зд

sano_hello 的前 4 个字节,而非字符。

类型转换

应用 to_string 把其余类型转化为字符串:

let i: i32 = 2;
let i_s: String = i.to_string(); // "2";

遍历

应用 .chars() 取得字符串的序列,并用 for in 来遍历以输入字符串的字符:

let s = String::from("Hello");
for c in s.chars() {println!("{}", c);
}

// H
// e
// l
// l
// o

相似地,应用 .bytes() 或者字符对应的字节序列,并用 for in 来遍历以输入字符串的字符:

let s = String::from("Hello");
for b in s.bytes() {println!("{}", b);
}

// 72
// 101
// 108
// 108
// 111

哈希表

应用哈希表 HashMap<K, V> 前须要用 use 关键字引入:

use std::collections::HashMap;

哈希表的键类型为 String i32,键和值必须为雷同类型。

初始化

能够用 ::new() 来初始化一个空哈希表:

fn main() {let hm = HashMap::new();
}

读写

应用 .insert() 增加键值对到哈希表(留神:mut 关键字):

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 30);
scores.insert(String::from("Black"), 20);

应用 .get() 依据键获取值:

println!("the blue: {:?}", scores.get("Blue").unwrap()); // 10
println!("the blue: {:?}", scores.get("Yellow")); // Some(30)
println!("the blue: {:?}", scores.get("Red").copied().unwrap_or(0)); // None

和向量相似,获取的值是 Option<&V> 类型,能够应用 unwrap() 获取 <T> 类型。

遍历

应用元组遍历哈希表:

for (key, value) in &scores {println!("{} {}", key, value);
}

// One Possible Outcome:
    // Blue 10
    // Yellow 30
    // Black 20

注:遍历的后果是 无序 的。

更新

哈希表的更新有几种不同的形式:

如果给同一个键增加多个值,后果是只保留最初一个值:

use std::collections::HashMap;

fn main() {let mut map = HashMap::new();
      map.insert(String::from("One"), 1);
        map.insert(String::from("One"), 2);
      println!("{:?}", map); // {"One": 2};
}

如果不存在键,应用 entry()or_insert() 增加键值对:

use std::collections::HashMap;

fn main() {let mut map = HashMap::new();
      map.insert(String::from("One"), 1);
      map.entry(String::from("One")).or_insert(2);
      map.entry(String::from("Two")).or_insert(2);
      println!("{:?}", map); // {"One": 1, "Two": 2};
}

entry() API 返回 Entry 枚举类型,该枚举类型返回指定的键是否存在,or_insert() 构建在 Entry 之上,如果键存在就不做批改,如果不存在就增加该键值对。

退出移动版