Rust初步入门
变量与可变性
-
声明变量使用let的关键字
-
默认情况下,变量是不可变得(明明叫变量却不可变,,醉了) 通过mut关键字声明可变
-
虽说Rust变量是不可变类型,但是毕竟是变量。在Rust中是有常量的概念的:
-
使用const声明的的量是常量,不可被mut关键词修饰
-
常量的类型必须被标注清楚
-
常量可以在任何作用域内声明, 包括全局作用域
//一个例子的demo fn main(){ let x:u8 = 5; let mut x:&str= "Hello boy"; const A_CONST_VARIABLE:u16 = 99999; println!("num is {}",x); println!("What you input is {}",s); }
-
常量只能绑定到常量表达式,无法绑定到函数的调用结果或只能运行时才能计算出的值
-
在程序运行期间,常量在其声明的作用域中一直有效
-
命名规范 RUST中常量使用全大写字母,每个单词采用下滑线分开)
-
-
Shadowing(隐藏) 可以使用相同的名字声明新变量,新的变量会shadow之前的同名变量
- 变量支持隐藏,常量不支持隐藏
//隐藏还能这么用哦 fn main() { let x:u8 = 5; let x:u8 = x + 10; let s:&str = "I'm your father"; let s:usize = s.len(); println!("Num is {}", x); println!("Some words be like {}", s); } Num is 15 Some words be like 15
数据类型
在我们定义变量的时候一般我们会隐式的定义变量类型。实际上我个人觉得还是显示的定义类型比较好。变量类型使用变量名: type这样的形式指定
我们可以根据是否人为定义将数据类型基本的分为标量类型与符合类型。Rust是静态编译语言(python解释性语言)需要知道所有变量的类型
-
一般来说Rust可以根据赋值自动推断出类型,但是对于比较复杂的情况Rust的解释器也比较物理,所以推荐显示定义变量类型
// 比如直接这样定义一个没有任何类型的变量就会报错 let mut p; 4 | let mut p; | ^^^^^ consider giving `p` a type
-
Rust中的一个标量类型代表一个单个的值,Rust中主要有四个主要的标量类型:
-
整数类型:
整数类型没有小数部分,可以分为有符号(int, 写作i) 或 (uint, 写作u)整数类型, 大小范围为 2^ n - 1 eg. i32: -2^31 ~ 2^31 - 1 大小范围为 8,16,32,64,128
整数支持以下几种进制的表示
Number Literature Example Decimal (十进制) 98——222 Hex (十六进制) 0xff Octal (八进制) 0o77 Binary (二进制) 0b1111_0000 Byte (u8 only) 二进制字符 b’A’ (Ascii码) 整数有范围限制当然有可能发生溢出,我们前面说过Rust有两种编译模式,一种是不带 releasr参数的我们叫做调试模式, 一种是带release参数的我们叫做发布模式,有意思的是两种模式下对编译时候发生溢出的做法还不一样:
- 调试模式下如果发生溢出就会panic,终端
- 发布模式下如果发生溢出就会把溢出的数字对上限取模赋值, 程序不会panic
-
浮点类型、小数类型
浮点类型表示为f , 可以采用32精度与64位精度,即f32与f64
Rust的浮点类型使用了 IEEE754标准描述 其中f64是默认类型,操作速度与f32持平。
fn main(){ // 两种数据类型(整形与浮点型)都支持 +,-,*,/,% let m2:i16 = 40; let m3:i16 = 7; // 很明显的向下取整 let p:i16 = m2 / m3; let q:i16 = m2 - m3; let a:i16 = m2 + m3; let s:i16= m2 % m3; println!("{},{},{},{}",p,q,a,s); } 5,33,47,5
- bool 类型,与其他语言都基本类似,一个True, 一个False, 占用一个字节的大小(不用1bit是会产生内存空间碎片不好利用,,)
let a:bool = true; let b:bool = false;
-
字符类型:
char类型, 最基本的单个字符, RUST中占用4个字节大小,编码格式是Unicode/utf8bm4,使用单引号作为定义字符的规则
//因为是unicode所以连emoji也可以复制哦 let a_char:char = '😀'
-
-
复合类型:可以将多个值放在一个类型中。Rust提供了两种基础的符合类型:元组(Tuple), 数组(Array)。
-
Tuple:
-
可以将多个类型的的多个值放在一个类型里
-
长度固定, 一旦声明就无法改变
//声明一个tuple,需要用小括号表示 let new_tuple: (u8, u16, i32, f32, &str) = (10, 135, 65538, 10.47, "I'm your father"); // 使用点标记法访问元素 println!("{},{},{}", new_tuple.1, new_tuple.0, new_tuple.4); 135,10,I'm your father
-
除了上述的方法调用tuple中的值,我们还可以使用destructrue 模式匹配解构Tuple获取元素值
let (a,b,c,d,e) = new_tuple; println!("{},{},{}", a, d, e); 10,10.47,I'm your father
-
-
数组Array
-
数组也可以将多个值放在同一个类型中,同时长度固定
-
数组的每个元素的类型必须相同
// 声明方法,使用[] // 类型为[标量类型;数量] let new_array:[i8;8] = [5,10,15,20,25,30,35,40]; //调用方法为中括号法 println!("{},{},{}", new_array[1],new_array[7]); 10,40
-
当我们想快速声明定长初识数组的时候我们还可以这样:
// 设值初始值254, 重复10次 let array_init: [u8; 10] = [254; 10]; println!("{},{}", array_init[9], array_init[0]); 254,254
-
数组是栈(Stack)上分配的 单个块的内存,如果索引超出数组的范围,编译并不会报错,但是运行会出错:Rust不允许访问相应地址的内存。简单来说就是溢出不会继续访问,不允许这种访问的行为,但是C中我们用指针的越界是可以访问的,,,这也是内存安全的原因之一
-
-
Vector
一种与数组类似的数据结构,由标准库提供,但是长度可以自由改变
let new_vec:Vec<i8> = vec![10, 20, 30]; println!("{}", new_vec[2]) 30
-
函数
-
声明函数使用fn关键字,
-
针对函数和变量名,Rust使用snake case命名规范:
// 所有的字母都是小写,单词之间使用下划线分靠 fn main() { println!("Hello, world!"); fuc1(); } fn fuc1(){ println!("I'm your first fuction"); } Hello, world! I'm your first fuction
-
函数的参数:
parametes: 形参,在函数内部定义的参数。在调用的时候才会被赋值
arguement: 实参, 传入函数内部的参数。我们在传参的时候必须指明参数类型
fn main() { fuc1(1, 2); } fn fuc1(x: i8, y: i8) { let z:&str = "哭笑😀"; println!("I got two arguements {} and {}, and I define a variable {}", x, y, z); } I got two arguements 1 and 2, and I define a variable 哭笑😀
-
函数由一系列语句组成,可选的由一个表达式结束。表达式本身不返回值,如下
let x = let y = 5; // 报错,因为let y = 5本身这个表达式无返回值 //我们可以把它写为下面的样子: let x = { let y = 10; y //这里的y不要加分号。加上分号之后x的类型就会变成(),因为a;本身就是一个表达式,还是返回空,用a 返回a的值 };
-
函数的返回。既然函数有输入,那我们肯定希望有输出。我们在函数命名的同行加上-> 后面跟上我们返回的数据类型:
fn main() { let (a, b, c) = fuc1(1, 2); print!("The fuction return {}, {} , and {}", a, b, c); } fn fuc1(x: i8, y: i8) -> (i8, i8, &str) { let z: &str = "哭笑😀"; println!("I got two arguements {} and {}, and I define a variable {}", x, y, z); (x, y, z) } // 报错 expected named lifetime parameter。不是引用的错,是跟Rust的生命周期有关的一个错,或者说语言特性我们后面在详细解释
控制流
-
判断 if else: 接受布尔类型的传参,语法和C几乎没什么区别,非常简单。**值得一提的是rust中的if只接受布尔类型,无法像其他语言营养什么0:False, 1:True,或不为空:True。这些判断都不可以。**if在rust中支持用match重构。(我个人是更喜欢if哒)
fn main() { let a = 5; let b = 10; if (a < b){ print!("Nice"); }else (a > b){ println!("奶思"); } // 或者我们可以这么写: let result_:bool = a < b; let return_:&str = if result_ {"Nice"} else {"奶思"}; println!("{}", return_); } Nice // 或者我们可以这么写
-
循环。rust提供了三种循环,分别是Loop, for, while。
-
loop, 最简单的循环,直接把需要循环的部分嵌套。除非break; 或者程序崩溃否则无法退出,可以理解为定时器
-
while 条件循环,每次执行循环体之前都判断一次条件
let mut judge = 10; while (judge > 5){ judge -= 1; println!("we do another loop"); } we do another loop we do another loop we do another loop we do another loop we do another loop
-
使用for遍历集合,主要适合写类迭代器吧属于
// for只能应用于s数组和Vector, 元素的数据结构一致的集合。我用元组试过了,因为数据结构不统一所以报错。 let a:[&str;4] = ["5","87.9","4444444","I have a pen"]; for element in a.iter(){ println!("This value is {}", element); }
-
出乎意料的是,for支持关键字range的使用,(for num in range(1…10).rev(), rev代表逆向)
-
所有权
所有权是Rust中最独特的特性,也是让Rust无需GC(垃圾处理器) 就可以保证内存安全的关键。可以说所有权是Rust的核心特性。
因为程序的运行都必须要使用内存。很多语言都支持GC, 就是在运行时不断寻找不再使用的内存,将他们收集并释放,比如Python, Java; 或者不采用Gc 像C一样显式地分配和释放内存。
而RUST采用了第三种方式:内存通过一个所有权系统管理,其中包括**一组编译器在编译时检查的规则。**意思就是编译就会确认规则是否有效可以使用,运行时所有权特性不会减慢程序的运行速度
对于RUST这样的系统级编程语言,一个值是在stack还是在heap上对语言的行为和你为什么要做默写决定有更大的影响。估计是我以后接触的唯一一个连栈和堆都要分开考虑的存在 。虽然在运行的时候他俩都表现为你可用的内存,但是他们的结构很不同!
我们来对比一下Stack 与 Heap https://blog.youkuaiyun.com/u012836896/article/details/89973820。
- Stack栈, 后进先出LIFO。所有存储在Stack上的数据必须有已知的固定的大小,(不固定的请选择Heap
- Heap 堆,内存组织性弱于Stack。 当你把数据放入Heap的时候,会请求一定数量的空间。操作系统在Heap里找到一块足够大的空间,把它标记为再用,并返回这个空间的地址指针。
- 对比:
- 把数据放在Stack上比放在Heap上快(操作系统不需要寻找大空间,从栈顶压入)
- 访问数据同样是Stack比Heap快,因为需要通过指针的地址访问heap数据。指针本身也是放在Stack中的(因为类型确定,占用空间确定嘛)
- 在heap上分配空间的时候还要进行标记,甚至对于过大的空间他还会拆除你不需要的空间在放回列表中。
- 当代码调用函数时,值被传到函数(包括Heap指针),函数本地的变量被压在Stack上,函数结束后,这些值从stack上弹出
- 所有权的存在解决了: 一句话,管理heap数据,就是所有权的目的
- 跟踪代码的那些部分在使用heap的那些数据
- 最小化Heap上的重复数据量
- 清理heap上未使用的数据避免空间不足
所有权的三条规则:
- 每个值都有一个变量,这个变量时该值的所有者
- 每个值同时只能由一个所有者
- 当所有者超出作用域(scope) 的时候该值将被删除
很明显,所有权跟作用域脱不了关系,就像上面我函数传参的时候哪个报错就是作用域不同,这点我们稍后再说。
我们以String类型为例:
-
String类型时存储在堆上的一个数据类型(长度可变),以String为例
-
字符串字面值 (&str):程序中手写的字符串值不可变,而且不能代表没有定义的字符串值。这也是为什么我们使用io.stdin的时候选用String::new选用而不是str
-
Rust还有第二种字符串类型:String: 在Heap上分配,能够存储编译时未知数量的文本。额String 和&str的区别我搞懂了会补充的.
// 用String创建一个!!可以被修改!!的字符串 fn main(){ let mut p:String = String::from("Hello"); let s:char ='😀'; let o:&str = " My name is Lihua"; p.push(s); p.push_str("My name is Lihua"); p = p.add(" I'm Fime"); println!("{}",p); } Hello😀My name is Lihua I'm Fime
-
所以说String是可以修改的字符串类型,与str采用了不同的内存分配方式
-
str字面值在编译的时候就知道他的内容了,其文本内容直接被写到执行文件中。速度快,高效,是因为其不可变放在了Stack中
-
String 为了支持可变性在Heap上分配内存保存编译时的位置文本内容:
操作系统必须在运行是请求内存(通过调用String::from实现)
-
用完String之后,会用某种方式将内存返回给操作系统。在Rust中我们不必手动释放。在变量走出作用域的时候内存会自动交还给OS,(调用drop函数)
// 一个例子 fn main() { let acc: u8 = { let b: u8 = 10; b }; print!("{}", b); } // 两个例子 use std::cmp::Ordering; fn main() { let mut a:u8 = 5; loop{ let m:u8 = a; a += 1; match a.cmp(&10){ Ordering::Less => { continue } Ordering::Equal => { println!("a finally bigger than b"); continue } Ordering::Greater => { println!("we gonna break"); break; } } } println!("{}", m); } // 最后很明显最后这两个都会在最后一句println出错,为什么,因为越界已经被回收掉了,不存在这个变量了。
-
变量与数据交互的方式:
//移动:Move let x = 5; let y = x; //问题来了,此时有几个5被压入栈了? //答案是两个,虽然这种写法应该是引用,但是整数时已知固定大小的简单值,所以有两个5被压入栈中 let s1 = String::from("Hello"); let s2 = s1; //提问,有几个"hello”被压入栈了? //答案1个。首先String具体数据,但是String数据结构是定长的放在栈上,但是我们二次引用之后在不考虑Rust的前提下我们很容易想到了s2把指针复制给了S1.不过这样我们删除的时候,我们先删除s1,栓除了数据,再删除s2,此时s1已经把指针保存位置的数据删了,第二次根本不知道删掉了啥,也就是二次废弃。 //Rust面对这种情况的废弃方案很神奇。一般来讲如果我们s1 和 s2都进行废弃会引起二次废弃的问题。但是Rust在s1赋值给s2的时候为s1增加废弃位。换句话说在s1赋值给s2的时候就已经被废弃了。。。 //一个例子 fn main() { let s1 = String::from("Hello"); let s2 = s1; println!("{}", s1); } 6 | println!("{}", s1); | ^^ value borrowed here after move // 对此,我只能说,不愧是你,,当然对于变量类型的数据结构不会出现这样的情况
关于Rust字符型,我建议这篇Rust字符串胖指针到底是胖在栈上还是堆上了?
就上面的Move模式还能看出,rust不会进行深拷贝,任何自动复制的操作都是廉价的。不过上面的Rust操作有让我想到,既然所有的String变量都是有点像(不是)浅拷贝的话,那我们在函数中更改实参会发生什么:
fn main() { let mut s1 = String::from("Hello"); change(s1); println!("{}", s1); } fn change(mut m1:String) { m1 = m1.add(" Google"); } // 没想到Rust早有防备,即使是传参也相当于Move,所以再调用s1是不可能了,, 8 | change(s1); | -- value moved here 9 | println!("{}", s1); | ^^ value borrowed here after move
当然我们也不是不可以进行深拷贝/Clone, 就是连同栈上的字符型数据一并复制,比较消耗资源。
fn main() { let mut s1 = String::from("Hello"); change(s1.clone()); println!("{}", s1); } fn change(mut m1: String) { m1 = m1.add(" Google"); println!("{}", m1); } Hello Google Hello
从代码的角度来说,深度复制是通过Copy trait实现的,本身与我们所有权控制的Drop (std::ops::Drop) 互斥, 理由也很简单,我们都在Stack上存了两条数据了,直接删了就完了,不需要考虑二次删除,Drop自然与Copy互斥。你Copy完了肯定不能Drop,但你写了Drop类就会帮你Drop(应该是钩子触发), 这不是自相矛盾么
一些拥有Copy trait的类型:
- 所有简单标量的组合类型(bool,char, int, tuple长度小于12的)
- 任何需要分配内存或资源的都不是Copy的
-
-
所有权与函数:
-
传参予=与作用域在语义上,把实参传给函数与赋予变量类似,都会发生移动Move或克隆上面我已经举了一个例子了。
-
返回值与作用域:函数在返回值的过程中同样会发生所有权的转移
fn main() { let mut s1 = String::from("Hello"); let s2:String = change(s1); println!("{}", s2); } fn change(mut m1: String) -> String { m1 = m1.add(" Google"); println!("{}", m1); m1 } Hello Google
-
-
引用:
我们想在不交换s1所有权的情况下进行传参,虽然上面我用s1.clone()的方式,但其实本质是又创建了一个变量,这样做至少实参在函数中的变换没法继承且耗费资源更多。Rust给我们提供了引用(&)的方式解决这种问题。作用是允许你使用某些值而不取得其所有权:
一个example
fn main() { let mut s1: String = String::from("Hello"); change(&mut s1); println!("{}", s1); } fn change(mut m1: &mut String) { println!("{}", m1); } Hello Hello // 所有权所有权,字面意思如果你尝试在change中更改m1的参数,很抱歉那是不可能的,因为你没有所有权,比如说 fn change(mut m1: &String) { println!("{}", m1); //如果加上这一句又能成功运行,因为你在stack新建额一个数据,堆中也新建了一个数据 // let m1 = m1.clone() let m2 = m1.add("Google"); } 12 | let m2 = m1.add("Google"); | ^^^^^^^^^^^^^^^^ move occurs because `*m1` has type `String`, which does not implement the `Copy` trait
可变引用存在如下限制:在特定作用域内,对一块数据,只能由一个可变引用,且不可以同时拥有一个可变引用和一个不可变引用,可以拥有多个不可变引用(不更改数据不发生数据冲突)
悬空引用:在Rust永远不会出现(指针指向的内存被释放或分配给其他人)
-
切片(Slice):Rust另外一种不持有所有权的数据类型
字符串切片是指向字符串中一部分内容的引用:(和python切片很像)
fn main(){ let s:String = String::from("Hello World"); let a= &s[0..5]; let b = &s[6..]; pr(a,b); } fn pr(a:&str,b:&str){ println!("{}",a); print!("{}",b); }
fn main() { let s: String = String::from("Hello World"); let a = &s[0..6]; let b = &s[6..]; pr(a, b); } fn pr(a: &str, b: &str) -> usize{ println!("{}", a); let words = a.as_bytes(); for (i, &letter) in words.iter().enumerate() { if (letter == b' ') { println!("Yes there is a space"); return i; } } print!("{}", b); return { let um:usize = 10; um } } Hello Yes there is a space //另一个简单的deemo, 组要演示字符串的遍历与匹配,从这个过程也能看到字符串字面值也是切片的格式。