Rust语言学习笔记

这是个人阅读官方文档整理的笔记,大部分是从文档里复制过来的,也有一些自己的思考。
阅读建议:至少掌握一门编程语言,C/C++,python更好(因为可以类比理解)。

变量与常量

可变变量与不可变变量

可变变量let mut a = 1;
跟其他语言中的变量一样,可以随便赋值(相同数据类型)

不可变变量let a = 1; 在程序运行过程中不可随便赋值。
特殊的语言机制:隐藏
1.在新作用域内,用let隐藏之前的变量值,退出作用域后,值自动恢复为以前的

fn main() {
    let x = 5;

    let x = x + 1;
	//大括号内是个新的作用域
    {
        let x = x * 2; //必须借助let 否则会报错
        println!("The value of x in the inner scope is: {x}");
    }
	//x = 999;  错误,仍然是个不可变变量
    println!("The value of x is: {x}"); //x恢复为6
}

输出:
The value of x in the inner scope is: 12
The value of x is: 6

2.隐藏复用变量名,可以改变值的类型;但可变变量不行

	//可行
    let spaces = "   ";
    let spaces = spaces.len();
    
	//报错
	let mut spaces = "   ";
    spaces = spaces.len();

个人思考:使用let mut时,就像C一样使用变量;使用let 在加上隐藏机制,就可以像Python一样使用变量
不可变变量出现原因:如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。

常量

类似 C/C++中#define 宏定义的用法。
全局变量 在声明它的作用域中,在整个程序生命周期都有效。
声明 const PI:f32 = 3.1415

数据类型

标量类型

整型、浮点型、布尔型、字符型 参考
数值运算
除法:/ 除不尽则向0取整
取余:%
特殊的:
整型的 isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的,32 位架构上它们是 32 位的。
布尔型只有true/false,像Python,不像C, C语言标准(C89)没有为布尔值单独设置一个类型,所以在判断真假时,使用整数 0 表示假,所有非0表示真。
字符型:单引号''声明char字面量,双引号 ""声明字符串字面量。有字面量应该就有指针、引用什么的,后面会说到

复合类型

元组

1、元组内,多个值的类型可以不同
2、长度固定,声明后不能变

//声明
	let tup = (500, 6.4, 1);
	let tup: (i32, f64, u8) = (500, 6.4, 1);
//解构
	let (x, y, z) = tup; //x=500,y=6.4,z=1
//访问
	let five_hundred = x.0;

数组

1、数组内,多个值类型必须相同,这点与元组不同
2、长度固定,声明后不能变,这点与元组相同

//声明
	let a = [1, 2, 3, 4, 5];
	let a: [i32; 5] = [1, 2, 3, 4, 5];
	let a = [3; 5];
//访问
	let first = a[0];

函数

语句与表达式(Rust特有)

语句(Statements)是执行一些操作但不返回值的指令。

函数定义 fn main() { let y = 6; }
变量定义 let y = 6; 不返回值,所以不能x = y = 6,

表达式(Expressions)计算并产生一个值。大部分 Rust 代码是由表达式组成的.

1.数学运算,比如 5 + 6,这是一个表达式并计算出值 11
2.表达式可以是语句的一部分,语句 let y = 6; 中的 6 是一个表达式
3.宏调用是一个表达式。
4.函数调用是一个表达式。(可以把一个函数直接赋值给变量)
5.用大括号创建的一个新的块作用域也是一个表达式(可以把一个大括号直接赋值给变量)
6.if语句也是表达式,具体见下面介绍if的板块

fn main() {
    let y = {
        let x = 3;
        x + 1  //最后是个表达式,不能有分号,否则就变成了语句,导致报错
    }; //这里也暗示了,在使用函数返回值的时候,可以直接使用表达式
    println!("The value of y is: {y}");
}

参数与返回值

参数没什么很特别的,就是声明时要指明类型,像C。
在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return加粗样式 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。

fn main() {
    let x = 5;
    let y = 1;
	let x = plus_y(x,y);
    println!("The value of x is: {x}");
}
// 看一下参数怎么定义,
// 注意需指明返回值类型,用->符号
fn plus_y(x: i32, y: i32) -> i32 {
    x + y //没有;  是一个表达式,隐式返回
}

控制流

分支 —— if

if是一个表达式,可以用来给变量赋值,注意,if和else分支赋值的类型要一致
Rust编译时就确切的知道变量类型。

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

基本使用语法,没什么特殊的。像其他语言一样,只会执行第一个条件为 true 的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。

fn main() {
    let number = 6;
    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else {
    println!("number is not divisible by 4 or 3");
    }
}

分支 —— match

强大的 Rust 分支结构 match 还没学到

循环 —— loop

第一次遇见这个(除了汇编),还没想明白这么设计的原因
1、基本语义
一直执行,除非遇到break,或者控制台手动Ctrl+C
2、循环返回值
break后面可以跟返回值

这个挺特殊的,有返回值传参,可以发挥一些作用,但我还没想清楚
官方举例说:比如检查线程是否完成了任务。然而你可能会需要将操作的结果传递给其它的代码。

3、循环标签与多重循环
一般来说,最内层循环不用定义标签,(我测试了一下,写上标签也是可以的,但是没啥用)。因为标签的作用是,在最内层循环里,break加上标签来指明跳出外层循环,否则默认是跳出当前循环。
注意标签的定义,'counting_up: loop{},最前面一个',且没有闭合,最后有:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;
        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }
        count += 1;
    }
    println!("End count = {count}");
}

//输出是
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

循环 —— while

一点特殊的都没有,像Python一样用就行

循环 —— for

像Python一样,可以用直接用for遍历集合元素,
要循环指定次数,像下面一样用,也和Python一样属于[a,b),取不到b。
这里的..,可以看作一个符号,不能在中间加个数用来指定步长,语法不允许。
要制定步长可以采用 for i in (0..3).rev().step_by(1),.rev()是反转,.step_by()是指定步长,步长不能为负值。

fn main() {
    for number in (1..4).rev() {//反转range
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
    //指定步长
    for i in (0..3).step_by(1){
    	  print!("{}", i);
    }
}

所有权(最与众不同的特性)

首先明确3条规则

Rust 中的每一个值都有一个 所有者(owner)。
值在任一时刻有且只有一个所有者。
当所有者(变量)离开作用域,这个值将被丢弃。(个人理解:一个作用域包括用{}括起来的;函数等)

    {                      // s 在这里无效,它尚未声明
        let s = "hello";   // 从此处起,s 是有效的

        // 使用 s
    }                      // 此作用域已结束,s 不再有效

变量与内存的交互方式

堆上分配的数据:移动

对于String类型这种在堆上分配的数据,离开作用域后,Rust会自动调用drop()函数释放堆内存。(类似C++自动调用析构函数)
在采用正常的等号赋值时,如下:

	let s1 = String::from("hello");
    let s2 = s1;
    println!("{}, world!", s1);//报错

实际上是将s2指向hello的实际内存位置,没有另外分配内存,此时s1和s2都指向同一位置,而为了保证不出现二次释放的错误,Rust机制使得s1无效,等价于将s1移动到s2。
如果一定要重新分配堆内存的话,需要用到.clone()方法。

    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2);

栈上分配的数据:克隆

对于在栈上分配的数据(就是整型、浮点型、字符型、布尔型这种内存大小确定的),因为给变量分配一个内存的开销很小,只是再执行压栈操作,所以不采用像上文String类型那种移动的方式,而是相当于直接克隆的。

在函数中使用:

根据作用域的知识,我们可以知道,存在堆上的数据,如果直接作为参数传入函数,在函数调用时发生作用域切换,在函数返回时,会被drop掉。
而这个参数很有可能在函数返回后还会被用到,我们不希望它被drop。以及我们在函数中创建的一些变量,我们也希望能回到主函数后继续使用。有一个方法,通过return将值传回去,使得可以继续使用,但这样未免太麻烦。因此我们要介绍引用机制。

引用

引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确保指向某个特定类型的有效值。指针指向一个实际内存地址,引用只是一个内存单元的别名。(对概念的解释可能不太完整,目前知道怎么用就行了,暂不深究)
对于Rust里的引用,它指向变量,但不拥有所有权,释放引用对变量不产生影响。
创建引用:&s1语法让我们创建一个指向值s1的引用

fn calculate_length(s: &String) -> usize { // s 是 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

变量 s 有效的作用域与函数参数的作用域一样,不过当 s 停止使用时并不丢弃引用指向的数据,因为 s 并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
可变引用
定义变量和传引用时,都加上mut修饰。

fn main(){
	let mut s = String::from("hello");
    change(&mut s);
}
 
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

出现了新的限制
1.不能同时拥有,两个可变引用,以免出现数据竞争,例如用一个引用修改了原变量,但数据没有及时同步到内存中。(参考体系结构缓存一致性问题)
这里同时的意思是:可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用。因为大括号内的引用在退出大括号时会被销毁。
2.不能出现可变引用和不可变引用同时存在,同样是因为缓存一致性问题
3.可以同时有多个不可变引用,不涉及到修改,有几个都没问题

在任意给定时间,要么 只能有一个可变引用,要么只能有多个不可变引用。
引用必须总是有效的。

悬垂指针——如何返回一个在函数内创建的变量?
悬垂指针是其指向的内存可能已经被分配给其它持有者。在编译时会报错。

fn dangle() -> &String { // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!返回的引用s 指向的内存被释放了!

//正确使用,直接返回 String
fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

slice—切片

slice 是一种引用,所以它没有所有权
String slice
1.基本使用:返回一个引用,可以是部分的引用,也可以是全部的引用

    let s = String::from("hello world");
	//部分引用
    let hello = &s[0..5];
    let world = &s[6..11];
	let len = s.len();
	//全部引用
	let slice = &s[0..len];
	let slice = &s[..];

2.从一个例子体会slice是不可变引用&str
返回一个slice 解决index与内容不匹配导致内存问题,本质上因为slice是一个&str,不可变引用,由于引用间的限制,使得内存不会出错。

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // 错误!

    println!("the first word is: {}", word);
}

错误原因:clear 需要清空 String,它尝试获取一个可变引用。
在调用 clear 之后的 println! 使用了 word 中的引用,所以这个不可变的引用在此时必须仍然有效。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,因此编译失败。
3.写函数签名时,参数用&str,因为String可以轻松转换为&str
传参要传一个String时,可以用slice传一个&str,这样函数参数就兼容String类型和字符串字面量

fn main() {
    let my_string = String::from("hello world");
    // `first_word` 适用于 `String`(的 slice),部分或全部
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` 也适用于 `String` 的引用,
    // 这等价于整个 `String` 的 slice
    let word = first_word(&my_string);

    let my_string_literal = "hello world";
    // `first_word` 适用于字符串字面值,部分或全部
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);
    // 因为字符串字面值已经字符串 slice 了,这也是适用的,无需 slice 语法!
    let word = first_word(my_string_literal);
}

4.要想在函数里修改string,不能用&str
传入&mut String可变引用,类似上文可变引用部分

fn main(){
    let mut my_string = String::from("Hello, world!");
    my_string = myreplace(&mut my_string);
    println!("{my_string}");
}
//这里之所以有返回值,是因为replace()方法会重新分配一段内存
//如果用push_str()方法,可以去掉返回值
fn myreplace(s:&mut String) -> String{
    s.replace(",",":")
}

字符串字面量本质上是slice
基本数据类型里提到的字符串字面量,就是一个slice,类型&str,它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;因为&str 是一个不可变引用。
怎么在函数里修改字符串字面量(&str,不可变引用)

//来自copilot,测试有效
fn main() {
    let original_string = "Hello, world!";
    let modified_string = replace_char_at(original_string, 5, 'X');
    println!("{}", modified_string); // prints "HelloX world!"
}
//就是在函数里手动复制这个字符串,并在需要的地方修改
fn replace_char_at(s: &str, idx: usize, c: char) -> String {
    let mut result = String::with_capacity(s.len());
    for (i, d) in s.char_indices() {//s.char_indices()返回迭代器(index, char)
        result.push(if i == idx { c } else { d });
    }
    result
}

数组的slice

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

结构体

整个结构体都是可变的。
没什么特别的,就是当结构体包含引用时,要指明生命周期。还没学到
定义:

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

例化:

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
    user1.email = String::from("anotheremail@example.com");
}
//通过函数返回一个结构体的实例,以及初始化简写(字段名和值名一样,可以简写,顺序无关)
fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}
//用其他实例的值,新建一个实例
 let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
}; 
//用其他实例的值,新建一个实例,简写语法  
let user2 = User {
        email: String::from("another@example.com"),
        ..user1
};

省略元组结构体、没有字段的类单元结构体
方法:

#[derive(Debug)]  //为了便于调试,一般都会写上
struct Rectangle {
    width: u32,
    height: u32,
}
//用impl关键字,定义在Rectangle上下文中,
//方法的第一个参数一定是&self,可用来访问结构体定义的字段
//如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}
//使用函数
let rec = Rectangle{width:1,height:2};
let area = rec.area();

关联函数
self不作为第一参数
经常被用作返回一个结构体新实例的构造函数。

//例如square关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle
impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}
//使用关联函数
let sq = Rectangle::square(3);

打印结构体的内容
在结构体定义之前加上外部属性 #[derive(Debug)]

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
	//方式1 println!宏接收一个引用,打印到标准输出控制台流(stdout)
    println!("rect1 is {:?}", rect1);
    //方式2 dbg! 宏接收一个表达式的所有权,打印到标准错误控制台流(stderr),还会打印出所在行号
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),//打印过程值,便于调试
        height: 50,
    };
     dbg!(&rect1);
 }

枚举

枚举类似结构体,成员可以任意类型

每一个枚举的成员,都可以是不同的类型,可以将数据附加到枚举的每个成员上

enum Message {
    Quit, //没有关联任何数据
    Move { x: i32, y: i32 }, //类似结构体。
    Write(String),//包含单独一个 String
    ChangeColor((i32, i32, i32),(i8,i8)),//包含两个元组
}

枚举的方法,适用于所有成员

可以定义枚举的方法,这样有一个好处,就是写一个方法适用于这个枚举里的所有成员

    impl Message {
        fn call(&self) {
            // 在这里定义方法体
        }
    }
    //通过 :: 例化一个枚举成员
    let m = Message::Write(String::from("hello"));
    m.call();

Option 枚举和其相对于空值的优势

使用Option枚举可以避免由空值引发的错误。
只要一个值不是 Option 类型,你就 可以安全的认定它的值不为空。
必须显式的将一个可能为空的值,放入对应类型的 Option 中。接着,当使用这个值时,必须明确的处理值为空的情况
如何使用,待补充

match 与 if let

match用法很类似switch to,但是match是穷尽的,所以分支里要考虑所有的情况,否则会编译失败。
如果不是所有的情况都有不同的处理,可以用占位符集中处理。
可以匹配枚举成员中的值

#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {//匹配成员的值
            println!("State quarter from {:?}!", state);
            25
        }
        _ => println("illegal input");
    }
}

if let与match机制类似,用在仅有一个需要处理的情况,可以减少代码量

    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {:?}!", state),
        _ => count += 1,
    }
    
    //等价的 if let else 写法
	if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }

参考资料

Rust官方文档——简体中文版
Rust官方文档——通过例子学习Rust(简体中文版)
log4rs简明使用教程

  • 23
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值