Rust基础

186 阅读12分钟

初识runst

安装

官网

Hello Word

编译和运行是独立的两步

Cargo

  • cargo new hello_cargo
  • cargo check
  • cargo build
  • cargo build --release 编译时进行优化,但编译更久

语法

变量

可变性

  • 声明变量用let关键字

  • 默认情况下变量不可变

    fn main() {
        println!("Hello, world!!!!!");
        let x = 5;
        println!("x: {}", x);
        x = 6; // 报错 cannot assing twice to immutable varibale
    }
    
    
  • 声明变量时,在变量前加mut关键字,就可使变量可变

常量

  • 常量(constant),常量在绑定值后也是不可变的,但是它与不可变的变量有很多区别:

    1. 不可使用mut,因为常量永远不可变。
    2. 声明常量用const关键字,它的类型必须被标注。
    3. 常量可在任何作用域内进行声明,包括全局作用域
    4. 常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值。
  • 在程序运行期间,常量在其声明的作用域内一直有效。

  • 命名规范,Rust里常量使用全字母大写,每个单词用下划线分隔。例如MAX_POINTS

    const MAX_POINTS: u32 = 100_000;
    

Shadowing(隐藏)

  • 可以使用相同的名字声明新的变量,新的变量就会shadow(隐藏)之前声明的同名变量

    • 在后续的代码中这个变量名就代表着新的变量

      let x = 1;
      let x = x + 1;
      
  • Shadow 和变量标记为mut是不一样的

    • 由于变量的不可变性,不使用let关键字,赋值给非mut编译会报错。
    • 使用let声明的同名新变量也是不可变的。
    • 使用let声明的同名新变量,它的类型可以与之前的不同。

数据类型

在Rust里分为标量类型和复合类型

当编译器无法自己识别应该将变量编译为什么类型的时候,我们需要使用标量类型。

标量类型

  • 一个标量类型代表一个单个的值。
  • Rust有四个主要的标量类型:
    • 整数
    • 浮点
    • 布尔
整数
  • 无符号整数类型以u开头

  • 有符号整数类型以i开头

    LengthSignedUnsigned
    8-biti8u8
    16-biti16u16
    32-biti32u32
    64-biti64u64
    128-biti128u128
    archisizeusize

    isize usize类型的位数由程序运行的计算机的架构所决定,如果计算机是64位的,那么就是64位,以此类推

    使用isize usize的主要场景是对某种集合进行索引操作。

  • 有符号的类型有正有负

  • 有符号取值范围

    (2n1)2n1-(2^n-1) 到 2^n -1
  • 无符号取值范围

    02n10 到 2^n - 1
整数的字面值
  • 除里byte类型外,所有的数值字面值都允许使用类型后缀。
    • 例如 57u8
  • 如果不清楚使用哪种类型,可以使用Rust相应的默认类型
  • 整数的默认类型就是i32
    • 总体上来说速度很快,即使在64位系统中
Number LiteralsExample
Decimal98_222
Hexoxff
Octal0o77
Binaryob1111_0000
Byte(u8 only)b'A'
整数溢出

比如u8范围是0-255,如果把一个u8变量的值设置为256

  • 调试模式下编译:Rust会检查整数溢出,如果发生溢出,程序在运行时会panic
  • 发布模式下(--release)编译:Rust不会检查可能导致panic的整数溢出
    • 如果发生溢出:Rust会执行“环绕”操作:256会变成0,257变成1....
    • 但程序不会panic
浮点类型
  • f32,32位,单精度
  • f64,64位,双精度
  • Rust的浮点类型使用了IEEE-754标准来表述
  • f64是默认类型, 因为在现代CPU上f64和f32的速度差不多,而且精度更高
数值操作

+-*/%

布尔类型

true 和 false,一个字节大小,符号是bool

let a = true;
let b: bool = false;
字符类型
  • Rust语言中char类型被用来描述语言中最基础的单个字符。

  • 字符类型的字面值使用单引号

  • 占用4字节大小

  • 是Unicode标量值,可以表示比ASCII多得多的字符内容:拼音、中日韩文、零长度空白字符、emoji表情等等。

    • u+0000 到 U+D7FF
    • u+E000 到 U+10FFFF
  • 但Unicode中没有“字符”的概念,所以直觉上认为的字符也许与Rust中的概念并不相符。

复合类型

复合类型可以将多个值放在一个类型里,Rust提供了两种基础的复合类型:元祖(Tuple)、数组

Tuple

  • 多个类型的多个值
  • 长度固定,一旦声明就无法改变
创建Tuple
  • 在小括号里,将值用逗号分开Tuple中的每个位置都对应一个类型,Tuple中各元 素的类型不必相同
let tup:(i32, f64, u8) = (500, 5.4, 1);
  • 可以使用模式匹配来解构一个Tuple来获取元素的值
let tup:(i32, f64, u8) = (500, 5.4, 1);
let (x, y, z) = tup;
访问Tuple的元素
  • 在Tuple变量使用点标记法,后接元素的索引号。
let tup:(i32, f64, u8) = (500, 5.4, 1);
println!("{}, {}, {}", tup.0, tup.1, tup.2);

数组

  • 数组可以将多个值放在一个类型里
  • 数组中每个元素的类型必须相同
  • 数组的长度是固定的
声明一个数组
let arr = [1, 2, 3, 4];
数组的用处
  • 如果想让你的数据存放在stack(栈)上而不是heap(堆)上,或者想保证有固定数量的元素,这时使用数组更有好处。
  • 数组没有Vector灵活
    • Vector和数组类似,它由标准库提供。
    • Vector的长度可以改变
数组的类型
  • 数组的类型以这种形式表示:[类型;长度]
    • 例如let a:[i32;5] = [1,2,3,4,5];
另一种声明数组的方法
  • 如果数组的每个元素值都相同,那么可以在

    • 在中括号里指定初始值
    • 然后是一个分号
    • 最后是数组的长度
    let a = [3;5];
    // 相当于
    let a = [3,3,3,3,3];
    
访问数组的元素

首先数组是Stack上分配的单个块的内存,可以使用索引来访问数组的元素

如果访问的索引值超出了数组的范围,编译会通过,运行会报错。Rust不允许其继续访问相应地址的内存

let a = [1,2,3,4];
a[0]; //1

函数

创建函数

  • 声明函数使用fn关键字

  • 针对函数和变量名,Rust使用snake case 命名规范

    • 所有的字母都是小写的,单词之间使用下划线分开
fn main() {
  println!("hello world")
  another_function();
}
fn another_function() {
  println!("Another function")
}

函数的参数

  • Parameters, arguments
  • 函数签名里必须声明每个参数的类型。
fn main() {
  another_function(5); // argument
}
fn another_function(x: i32) { // parameters
  println!("the value of x is: {}", x);
}

函数体中的语句与表达式

  • 函数体由一系列语句组成,可选的由一个表达式结束
  • Rust是一个基于表达式的语言
  • 语句是执行一些动作的指令
  • 表达式会计算产生一个值
  • 函数的定义也是语句
  • 语句没有返回值,所以不可以使用let将一个语句赋给一个变量
fn main() {
  let x= 5;
  let y = {
    let x = 1;
    x + 3 // 这里注意,没加分号是表达式,表达式会产生一个值,x + 3 作为这个{}(块)的值,若加了分号,y的类型会变为一个()元祖
    // 语句没有返回值
  }
}

函数的返回值

  • 在 -> 符号后边声明函数的返回值的类型,但是不可以为返回值命名
  • 在Rust里面,返回值就是函数体里面最后一个表达式的值
  • 如果你想提前返回,需使用return关键字,并指定一个值
fn five() -> i32 {
  5 // 加了分号就是语句了
}
fn main() {
  let x = five();
  println!("{}", x); // 5
}

控制流

if表达式

  • if表达式允许根据条件来执行不同的代码分支,这个条件必须是bool类型
  • if表达式中,与条件相关联的代码块就叫做分支(arm)
  • 可选的,在后边可以加上一个else表达式
let number = 3;
if number < 5 {
  println!("true")
} else {
  println!("false")
}

if number % 4 == 0 {
  println!("4")
} else if number % 3 == 0 {
  println!("4")
}
// 如果else if很多,建议用match重构

由于if else是表达式,所以代码块中的值可以赋值给变量

let a = 1;
let b = if a == 1 {5} else {6};

但在if控制流中,所有块的值必须一致

Rust 的循环

loop循环
  • loop关键字告诉Rust反复的执行一块代码,直到你喊停
  • 可以在loop中使用break停止循环
while条件循环
  • 每次执行循环体之前都判断一次条件
let mut number = 3;
while number != 0 {
  println!("{}!", number);
 	number = number - 1;
}
println!("lift off")
for循环遍历集合

当然也可以用while,效率较低,易出错

let a = [10, 20, 30, 40, 50];
for element in a.iter() {
  println!("value, {}", element);
}

Range

  • 标准库提供
  • 指定一个开始数字和一个结束数字,Range可以生产它们之间的数字(不含结束)
  • rev方法可以反转Range
// 倒计时
for number in (1..4).rev() {
  println!("{}!", number);
}
println!("LIFTOFF!")

所有权

Rust的核心特性就是所有权

  • 所有程序在运行时都必须管理它们的计算机内存的方式
    • 有些语言有垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存
    • 有些语言,必须在代码中显示地分配和释放内存
  • Rust采用了第三种方式
    • 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
    • 当程序运行时,所有权特性不会减慢程序运行的速度

Stack vs Heap (栈内存 vs 堆内存)

在像Rust这样的系统级编程语言里,一个值在stack上还是在heap上对语言的行为和你为什么要做某些决定是由更大影响的。

存储数据

  • Stack按值的接受顺序来存储,按相反的顺序将它们移除(后进先出,LIFO)
    • 添加数据叫压入栈
    • 移除数据叫弹出栈
  • 所有存储在Stack伤的数据必须拥有已知的固定大小
    • 编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上
  • Heap内存组织性差一些
    • 当你把数据放入heap时,你会请求一定数量的空间
    • 操作系统在heap里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
    • 这个过程叫做在heap上进行分配,有时仅仅成为“分配”
  • 把值压倒stack上不叫分配
  • 指针是已知固定大小的,可以把指针存放在stack上
    • 但如果想要实际数据,你必须用指针来定位
  • 把数据压到stack上要比heap上分配快得多
    • 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
  • 而在heap上分配空间需要做更多的工作
    • 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配

访问数据

  • 访问heap中的数据访问stack中的数据,因为需要通过指针才能找到heap中的数据
    • 对于现代处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快
  • 在stack上,如果数据存放的距离比较近,那么处理器的处理速度就会更快一些
  • 在heap上,如果数据之间的距离比较远,那么处理速度就会慢一些
    • 在heap上分配大量的空间也是需要时间的

函数调用

当你的代码调用函数时值被传入到函数(也包括指向heap的指针)。函数本地的变量被压到stack上。当函数结束后,这些值会从stack上弹出

所有权存在的原因

所有权解决的问题:

  1. 跟踪代码的哪些部分正在使用heap的哪些数据
  2. 最小化heap上的重复数量
  3. 清理heap上未使用的数据,避免空间不足

你懂了所有权,就不需要经常取想stack或heap了

管理heap数据是所有权存在的原因,这有助于解释它为什么会这样工作。

所有权规则

  1. 每个值都有一个变量,这个变量是该值的所有者
  2. 每个值同时只能有一个所有者
  3. 当所有者超出作用域(scope)时,该值将被删除

变量的作用域

  • scope {}

    fn main() {
      // s 不可用
      let s = "hello"; // s 可用
      // 可以对s进行相关操作
    } // s 作用域到此结束, s 不再可用
    

String类型(所有权相关的部分)

String比那些基础标量数据类型更复杂一些。

  • 字符串字面值:程序例手写的那些字符串值,它们是不可变的。
  • Rust还有第二种字符串类型:String类型。
    • heap上分配。能够存储在编译时未知数量的文本。
创建String类型的值
  • 可以使用from函数从字符串字面值创建出String类型

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

    • :: 表示from是String类型下的函数
    • 这类字符串是可以被修改的
    fn main() {
    	let s = String::from()
    }
    

    todo..