rust primer 要点总结

类型,运算符和字符串

原生类型

char

Rust中我们要用’来表示一个char,如果用"的话你得到的实际上是一个&'static str。

slice

Slice从直观上讲,是对一个Array的切片,通过Slice,你能获取到一个Array的部分或者全部的访问权限。和Array不同,Slice是可以动态的,但是呢,其范围是不能超过Array的大小,这点和Golang是不一样的。

let arr = [1, 2, 3, 4, 5, 6];
let slice_complete = &arr[..]; // 获取全部元素
let slice_middle = &arr[1..4]; // 获取中间元素,最后取得的Slice为 [2, 3, 4] 。切片遵循左闭右开原则。
let slice_right = &arr[1..]; // 最后获得的元素为[2, 3, 4, 5, 6],长度为5。
let slice_left = &arr[..3]; // 最后获得的元素为[1, 2, 3],长度为3。

Vec

let mut v1: Vec<i32> = vec![1, 2, 3]; // 通过vec!宏来声明let v2 = vec![0; 10]; // 声明一个初始长度为10的值全为0的动态数组
println!("{}", v1[0]); // 通过下标来访问数组元素for i in &v1 {
    print!("{}", i); // &Vec<i32> 可以通过 Deref 转换成 &[i32]
}

println!("");for i in &mut v1 {
    *i = *i+1;
    print!("{}", i); // 可变访问
}

原生字符串

你可以用str来声明一个字符串,事实上,Rust中,所有用""包裹起来的都可以称为&str

函数

复合类型

元祖Tuples

let y = (2, "hello world");let x: (i32, &str) = (3, "world hello");

结构体struct

struct A {
    attr1: i32,
    atrr2: String,
}
struct B(i32, u16, bool);
struct D;

各种ref的讨论

struct RefBoy<'a> {
    loc: &'a i32,
}

这里解释一下这个符号<>,它表示的是一个属于的关系,无论其中描述的是生命周期还是泛型。即: RefBoy in 'a。最终我们可以得出个结论,RefBoy这个结构体,其生命周期一定不能比’a更长才行。
结构体里的引用字段必须要有显式的生命周期
一个被显式写出生命周期的结构体,其自身的生命周期一定小于等于其显式写出的任意一个生命周期
注:生命周期和泛型都写在<>里,先生命周期后泛型,用;分隔。

被move的self

struct A {
    a: i32,
}
impl A {
    pub fn show(self) {
        println!("{}", self.a);
    }
}

fn main() {
    let ast = A{a: 12i32};
    ast.show();
    println!("{}", ast.a);
}

错误

13:25 error: use of moved value: `ast.a` [E0382]
<anon>:13     println!("{}", ast.a);
#[derive(Copy, Clone)]struct A {
    a: i32,
}

这么写的话,会使编译通过。但是这么写实际上也是有其缺陷的。其缺陷就是:你不能在一个被copy的impl函数里改变它!事实上,被move的self其实是相对少用的一种情况,更多的时候,我们需要的是ref和ref mut。

需要注意的是,一旦你的结构体持有一个可变引用,你,只能在&mut self的实现里去改变他!

枚举类型enum

rust的枚举能做到的,比C语言的多很多。 比如,枚举里面居然能包含一些你需要的,特定的数据信息! 这是常规的枚举所无法做到的,更像枚举类,不是么?

enum SpecialPoint {
    Point(i32, i32),Special(String),
}
enum SpecialPoint {
    Point {
        x: i32,
        y: i32,
    },Special(String),
}

和struct的成员访问符号.不同的是,枚举类型要想访问其成员,几乎无一例外的必须要用到模式匹配。并且, 你可以写一个Direction::West,但是你绝对不能写成Direction.West。

enum SpecialPoint {
    Point(i32, i32),Special(String),
}

fn main() {
    let sp = SpecialPoint::Point(0, 0);match sp {
        SpecialPoint::Point(x, y) => {
            println!("I'am SpecialPoint(x={}, y={})", x, y);
        }
        SpecialPoint::Special(why) => {
            println!("I'am Special because I am {}", why);
        }
    }
}

Strings

str,String, OsStr, CStr,CString
Rust中的字符串实际上是被编码成UTF-8的一个Unicode字符数组。这么说比较拗口,简单来说,Rust字符串内部存储的是一个UTF-8数组,但是这个数组是Unicode字符经过编码得来的。因此,可以看成Rust原生就支持Unicode字符集

str

所有的用""包裹起来的字符串,都被声明成了一个不可变,静态的字符串。

let x = "Hello";let x:&'static str = "Hello";

String

一种在堆上声明的字符串String被设计了出来。 它能动态的去增长或者缩减
从str中转换:

let x:&'static str = "hello";let mut y:String = x.to_string();
println!("{}", y);
y.push_str(", world");
println!("{}", y);

一个String重新变成&str呢,用 &* 符号

fn use_str(s: &str) {
    println!("I am: {}", s);
}

fn main() {
    let s = "Hello".to_string();use_str(&*s);
}

首先呢, &是两个符号&和的集合,按照Rust的运算顺序,先对String进行Deref,也就是操作。 由于String实现了impl Deref<Target=str> for String,这相当于一个运算符重载,所以你就能通过获得一个str类型。但是我们知道,单独的str是不能在Rust里直接存在的,因此,我们需要先给他进行&操作取得&str这个结果。 有人说了,我发现只要用&一个操作符就能将使上面的编译通过。 这其实是一个编译器的锅,因为Rust的编译器会在&后面插入足够多的*来尽可能满足Deref这个特性。这个特性会在某些情况下失效,因此,为了不给自己找麻烦,还是将操作符写全就好。

需要知道的是,将String转换成&str是非常轻松的几乎没有任何开销的。但是反过来,将&str转换成String是需要在堆上请求内存的,因此,要慎重。

索引访问


let x = "hello".to_string();
x[1]; //编译错误!

Rust的字符串实际上是不支持通过下标访问的,但是呢,我们可以通过将其转变成数组的方式访问


let x = "哎哟我去".to_string();for i in x.as_bytes() {
    print!("{} ", i);
}

println!("");for i in x.chars() {
    print!("{}", i);
}

x.chars().nth(2);

字符串切片

对字符串切片是一件非常危险的事,虽然Rust支持,但是我并不推荐。因为Rust的字符串Slice实际上是切的bytes。这也就造成了一个严重后果,如果你切片的位置正好是一个Unicode字符的内部,Rust会发生Runtime的panic,导致整个程序崩溃。 因为这个操作是如此的危险,所以我就不演示了…

操作符和格式化字符


fn main() {
    let s = format!("{1}是个有着{0:>0width$}KG重,{height:?}cm高的大胖子",81, "wayslog", width=4, height=178);// 我被逼的牺牲了自己了……
    print!("{}", s);
}

函数

函数参数

函数参数声明不能省略类型

函数作为参数


fn main() {
  let xm = "xiaoming";let xh = "xiaohong";say_what(xm, hi);say_what(xh, hello);
}

fn hi(name: &str) {
  println!("Hi, {}.", name);
}

fn hello(name: &str) {
  println!("Hello, {}.", name);
}

fn say_what(name: &str, func: fn(&str)) {
  func(name)
}

函数返回值

main函数的返回值是(),在rust中,当一个函数返回()时,可以省略。

main函数的返回值类型是(),它是一个特殊的元组——没有元素的元组,称为unit,它表示一个函数没有任何信息需要返回。

retrun关键字用来提前返回

rust不支持返回多个值,但是可以利用元组来返回多个值

发散函数不返回,它使用感叹号!作为返回类型表示:


fn main() {
  println!("hello");diverging();
  println!("world");
}

fn diverging() -> ! {
  panic!("This function will never return");
}

语句和表达式

rust是一个基于表达式的语言,不过它也有语句。rust只有两种语句:声明语句和表达式语句,其他的都是表达式。基于表达式是函数式语言的一个重要特征,表达式总是返回值。

rust的声明语句可以分为两种,一种为变量声明语句,另一种为Item声明语句。变量声明语句。主要是指let语句,Item声明。是指函数(function)、结构体(structure)、类型别名(type)、静态变量(static)、特质(trait)、实现(implementation)或模块(module)的声明。这些声明可以嵌套在任意块(block)中。

表达式语句,由一个表达式和一个分号组成,即在表达式后面加一个分号就将一个表达式转变为了一个语句。所以,有多少种表达式,就有多少种表达式语句。

通常不使用一个元素的元组,不过如果你坚持的话,rust也是允许的,不过需要在元素后加一个逗号:


(0,); // single-element tuple
(0); // zero in parentheses

结构体表达式一般用于构造一个结构体对象,它除了以上从零构建的形式外,还可以在另一个对象的基础上进行构建:


let base = Point3d {x: 1, y: 2, z: 3};Point3d {y: 0, z: 10, .. base};

如果以语句结尾,则块表达式的值为():


let x: () = { println!("Hello."); };

范围表达式(range expression): 可以使用范围操作符…来构建范围对象(variant of std::ops::Range):


1..2;   // std::ops::Range3..;    // std::ops::RangeFrom..4;    // std::ops::RangeTo..;     // std::ops::RangeFull

高阶函数

关键字fn可以用来定义函数。除此以外,它还用来构造函数类型。与函数定义主要的不同是,构造函数类型不需要函数名、参数名和函数体。

在rust中,函数的所有权是不能转移的,我们给函数类型的变量赋值时,赋给的一般是函数的指针,所以rust中的函数类型,就像是C/C++中的函数指针,当然,rust的函数类型更安全。可见,rust的函数类型,其实应该是属于指针类型(Pointer Type)。rust的Pointer Type有两种,一种为引用(Reference&),另一种为原始指针(Raw pointer *),详细内容请看Rust Reference 8.18 Pointer Types。而rust的函数类型应是引用类型,因为它是安全的,而原始指针则是不安全的,要使用原始指针,必须使用unsafe关键字声明。

在函数内定义函数在很多其他语言中是不支持的,不过rust支持,这也是rust灵活和强大的一个体现。不过,在函数中定义的函数,不能包含函数中(环境中)的变量,若要包含,应该闭包

模式匹配

match关键字

match所罗列的匹配,必须穷举出其所有可能。当然,你也可以用 _ 这个符号来代表其余的所有可能性情况,就类似于switch中的default语句。
match的每一个分支都必须是一个表达式,并且,除非一个分支一定会触发panic,这些分支的所有表达式的最终返回值类型必须相同。
match还有一个非常重要的作用就是对现有的数据结构进行解构,轻易的可以拿出其中的数据部分来。

模式

一个模式中如果出现了和前面的同名变量,则这个变量会在当前作用域里被模式中的值覆盖掉。

let x = 1;let c = 'c';match c {
    x => println!("x: {} c: {}", x, c),
}

println!("x: {}", x);

有的时候我们其实只对某些字段感兴趣,就可以用…来省略其他字段。

struct Point {
    x: i64,
    y: i64,
}

let point = Point { x: 0, y: 0 };match point {
    Point { y, .. } => println!("y is {}", y),
}

我们遇到了两种不同的模式忽略的情况——_和…。这里要注意,模式匹配中被忽略的字段是不会被move的,而且实现Copy的也会优先被Copy而不是被move。

我们遇到了两种不同的模式忽略的情况——_和…。这里要注意,模式匹配中被忽略的字段是不会被move的,而且实现Copy的也会优先被Copy而不是被move。

let tuple: (u32, String) = (5, String::from("five"));let (x, s) = tuple;// 以下行将导致编译错误,因为String类型并未实现Copy, 所以tuple被整体move掉了。// println!("Tuple is: {:?}", tuple);let tuple = (5, String::from("five"));// 忽略String类型,而u32实现了Copy,则tuple不会被movelet (x, _) = tuple;

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

在模式匹配中,当我想要匹配一个数字(字符)范围的时候,我们可以用…来表示:

let x = 1;match x {
    1 ... 10 => println!("一到十"),_ => println!("其它"),
}

let c = 'w';match c {
    'a' ... 'z' => println!("小写字母"),'A' ... 'Z' => println!("大写字母"),_ => println!("其他字符"),
}

当我们只是单纯的想要匹配多种情况的时候,可以使用 | 来分隔多个匹配条件

let x = 1;match x {
    1 | 2 => println!("一或二"),_ => println!("其他"),
}

前面我们了解到,当被模式匹配命中的时候,未实现Copy的类型会被默认的move掉,因此,原owner就不再持有其所有权。但是有些时候,我们只想要从拿到一个变量的(可变)引用,而不想将其move出作用域,怎么做呢?答:用ref或者ref mut。

let mut x = 5;match x {
    ref mut mr => println!("mut ref :{}", mr),
}
// 当然了……在let表达式里也能用let ref mut mrx = x;

在模式匹配的过程内部,我们可以用@来绑定一个变量名,这在复杂的模式匹配中是再方便不过的

#[derive(Debug)]struct Person {
    name: Option<String>,
}

let name = "Steve".to_string();let x: Option<Person> = Some(Person { name: Some(name) });match x {
    Some(Person { name: ref a @ Some(_), .. }) => println!("{:?}", a),_ => {}
}

一个后置的if表达式可以被放在match的模式之后,被称为match guards。例如如下代码:

let x = 4;let y = false;match x {
    4 | 5 if y => println!("yes"),_ => println!("no"),
}

trait

trait 关键字

泛型的trait约束
多trait约束
where关键字
trait的默认方法
trait的默认方法
derive属性

trait对象

trait对象在Rust中是指使用指针封装了的 trait,比如 &SomeTrait 和 Box。

并不是所有的trait都能作为trait对象使用的


let v = vec![1, 2, 3];let o = &v as &Clone;

虽然Clone本身集成了Sized这个trait,但是它的方法fn clone(&self) -> Self和fn clone_from(&mut self, source: &Self) { … }含有Self类型,而在使用trait对象方法的时候Rust是动态派发的,我们根本不知道这个trait对象的实际类型,它可以是任何一个实现了该trait的类型的值,所以Self在这里的大小不是Self: Sized的,这样的情况在Rust中被称为object-unsafe或者not object-safe,这样的trait是不能成为trait对象的。

总结:

如果一个trait方法是object safe的,它需要满足:

方法有Self: Sized约束, 或者
同时满足以下所有条件:
没有泛型参数
不是静态函数
除了self之外的其它参数和返回值不能使用Self类型
如果一个trait是object-safe的,它需要满足:

所有的方法都是object-safe的,并且
trait 不要求 Self: Sized 约束

泛型

可变性,所有权,租借和生命周期

所有权

Rust并不会像其他语言一样可以为变量默认初始化值,Rust明确规定变量的初始值必须由程序员自己决定。
通俗的讲,let关键字可以把一个标识符和一段内存区域做“绑定”,绑定后,这段内存就被这个标识符所拥有,这个标识符也成为这段内存的唯一所有者。

{
    let a: String = String::from("xyz");let b = a;
    println!("{}", a);
}
  let a: i32 = 100;let b = a;
    println!("{}", a);

上面的代码无法编译通过,因为所有权发生了move,而下面的代码正常,是因为i32实现了Copy属性,所以在move的时候发生了copy,并绑定了新变量
在Rust中,基本数据类型(Primitive Types)均实现了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。

let mut a: &str = "abc";  //可变绑定, a <=> 内存区域A("abc")
a = "xyz";    //绑定到另一内存区域, a <=> 内存区域B("xyz")
println!("{:?}", a);  //打印:"xyz"

Rust中也有const常量,常量不存在“绑定”之说,和其他语言的常量含义相同:

const PI:f32 = 3.14;

哪些情况下我们自定义的类型(如某个Struct等)可以实现Copy特性? 只要这种类型的属性类型都实现了Copy特性,那么这个类型就可以实现Copy特性。 例如:

struct Foo {  //可实现Copy特性
    a: i32,
    b: bool,
}

struct Bar {  //不可实现Copy特性
    l: Vec<i32>,
}

通过derive让Rust编译器自动实现
手动实现Clone和Copy trait
去掉move后,包体内就会对x进行了可变借用,而不是“剥夺”x的所有权

引用和借用

借用与引用是一种相辅相成的关系,若B是对A的引用,也可称之为B借用了A。 很相近对吧,但是借用一词本意为要归还。所以在Rust用引用时,一定要注意应该在何处何时正确的“归回”借用/引用。
规则

同一时刻,最多只有一个可变借用(&mut T),或者2。
同一时刻,可有0个或多个不可变借用(&T)但不能有任何可变借用。
借用在离开作用域后释放。
在可变借用释放前不可访问源变量。

总结

借用不改变内存的所有者(Owner),借用只是对源内存的临时引用。
在借用周期内,借用方可以读写这块内存,所有者被禁止读写内存;且所有者保证在有“借用”存在的情况下,不会释放或转移内存。
失去所有权的变量不可以被借用(访问)。
在租借期内,内存所有者保证不会释放/转移/可变租借这块内存,但如果是在非可变租借的情况下,所有者是允许继续非可变租借出去的。
借用周期满后,所有者收回读写权限
借用周期小于被借用者(所有者)的生命周期。

生命周期

除非编译器无法自动推导出Lifetime,否则不建议显示指定Lifetime标识符,会降低程序的可读性。
显式Lifetime

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

Lifetime推导

要推导Lifetime是否合法,先明确两点:

输出值(也称为返回值)依赖哪些输入值
输入值的Lifetime大于或等于输出值的Lifetime (准确来说:子集,而不是大于或等于)
Lifetime推导公式:
当输出值R依赖输入值X Y Z …,当且仅当输出值的Lifetime为所有输入值的Lifetime交集的子集时,生命周期合法。

 Lifetime(R) ⊆ ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )
fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

推导总结

通过上面的学习相信大家可以很轻松完成Lifetime的推导,总之,记住两点: 1. 输出值依赖哪些输入值。 2. 推导公式。

Lifetime in struct

struct Person<'a> {
    age: &'a u8,
}
impl<'a> Person<'a> {
    fn print_age(&self) {
        println!("Person.age = {}", self.age);
    }
}

这样加上<'a>后就可以了。读者可能会疑问为什么print_age中不需要加上’a,这是个好问题,因为print_age的输出参数为(),也就是可以不依赖任何输入参数, 所以编译器此时可以不必关心和推导Lifetime。即使是fn print_age(&self, other_age: &i32) {…}也可以编译通过。

impl<'a, 'b> Person<'a> {
    fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {
        if self.age > p.age {
            self.age
        } else {
            p.age
        }
    }
}

类似之前的Lifetime推导章节,当返回值(借用)依赖多个输入值时,需显示声明Lifetime。和函数Lifetime同理。

闭包

可以看到,第一句就已经说明了什么是闭包:闭包是引用了自由变量的函数。所以,闭包是一种特殊的函数。

在rust中,函数和闭包都是实现了Fn、FnMut或FnOnce特质(trait)的类型。任何实现了这三种特质其中一种的类型的对象,都是 可调用对象 ,都能像函数和闭包一样通过这样name()的形式调用,()在rust中是一个操作符,操作符在rust中是可以重载的。rust的操作符重载是通过实现相应的trait来实现,而()操作符的相应trait就是Fn、FnMut和FnOnce,所以,任何实现了这三个trait中的一种的类型,其实就是重载了()操作符。

闭包的语法

let plus_one = |x: i32| x + 1;

assert_eq!(2, plus_one(1));

捕获变量

let num = 5;let plus_num = |x: i32| x + num;

assert_eq!(10, plus_num(5));

这个闭包,plus_num,引用了它作用域中的let绑定:num。更明确的说,它借用了绑定。
如果你的闭包需要它,Rust会取得所有权并移动环境:

let nums = vec![1, 2, 3];let takes_nums = || nums;

println!("{:?}", nums);

Vec拥有它内容的所有权,而且由于这个原因,当我们在闭包中引用它时,我们必须取得nums的所有权。这与我们传递nums给一个取得它所有权的函数一样。

闭包的实现

闭包作为参数和返回值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值