当前位置: 首页 > 工具软件 > Rust > 使用案例 >

Rust的基础概念

东门令
2023-12-01

数据类型

Rust是静态类型的语言,在编译时,必须明确数据的类型。当然,编译器可以根据其值和使用方式,自动推导出数据的类型,不一定需要人工的指定。只有当多种类型都有可能时,需要显式的指定类型。
rust使用let关键字绑定(不是声明)变量,语法为:

let x = 5;      // 绑定x变量为5,不指定x的类型
let y:i64 = 10; // 绑定y变量为10,指定y的类型为64位整型

rust的数据类型为分两大类:标量类型与复合类型。标量类型是一个单独的值,而复合类型则是由多个标量类型组合在一起的类型。

标量类型

标量( scalar) 类型代表一个单独的值。 Rust 有四种基本的标量类型:

  • 整型
  • 浮点型
  • 布尔类型
  • 字符类型

整型

rust的整型,根据长度的不同,分为6种,每种又有有符号和无符号之分,如下表:

有符号无符号长度
i8u88bit
i16u1616bit
i32u3232bit
i64u6464bit
i128u128128bit
isizeusize视CPU架构而定,32位CPU为32bit,64位CPU为64bit

有符号的整型,根据其位数,值域范围为-(2n-1)2<sup>n-1</sup>-1。无符号的整型,根据其位数,值域范围为02n-1。

rust绑定整型变量可以使用下面的多种方式:

let a = 123;            // 使用十进制
let b = 123_456;        // 为增加可读性,可以使用下划线分隔
let c = 0xff;           // 使用16进制
let d = 0o123;          // 使用8进制
let e = 0b1101_0011;    // 使用二进制
let f = 57u8;           // 增加u8后缀,指定其为8位无符号整型

因为现在的CPU架构,32位的整型运算速度要高于64位,所以rust编译器会默认将上面除了f以外其他的都推导为i32,如有特殊需要,可人为指定其他的整数类型。

浮点型

Rust中的浮点型就不像整型那么多了,它只有f32和f64两种,分别占用32位和64位。现代的CPU中,这两种类型的计算速度几乎一样,并且f64精度更高,所以Rust编译器会默认将类型推导为f64。

let x = 2.0;        // f64
let y: f32 = 3.0;   // f32

布尔类型

Rust的bool型,只能为true和false。与C/C++不同,其bool类型不能与整型混用。

let b = true;

如果将整型赋值给bool型,编译后报错,如:

let b:bool = 1;

编译时会产生错误:

error[E0308]: mismatched types
 --> src\main.rs:2:18
  |
2 |     let t:bool = 1;
  |                  ^ expected bool, found integer
  |
  = note: expected type `bool`
             found type `{integer}`

error: aborting due to previous error

字符类型

Rust的字符类型关键字为char,但它与C/C++的char截然不同。Rust的char占用4个字节,可以表示任意Unicode字符,包括英文、中文、日文、韩文,甚至emoji。

let en = 'z';
let ch1 = '中';
let ch2 = '發';

复合类型

Rust的复合类型是由多个标量类型组成的复杂类型,原生的复合类型有数组的元组两种,在其基础上,又有了切片和字符串。

数组

Rust的数组是由多个同一类型的值组成的复合类型。它与C/C++的数组差不多,一旦声明后就是定长的,不能改变其长度。

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

当需要显式的指定元素类型和长度时,可以使用下面的方式:

let a: [i32; 5] = [1, 2, 3, 4, 5];

要访问数组的元素,Rust也是采用下标的方式,如:

fn main() {
    let a = [1, 2, 3, 4, 5];
    println!("{}", a[3]);
}

元组

Rust的元组可以由多个不同类型的值组成,其长度也不能改变。

let tup: (i32, f64, u8) = (500, 6.4, 1);

元组的访问与数组不同,它不是采用下标的方式,而是采用如下方式:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    println!("{}", tup.1);
}

这种方式不能动态的计算。
通过下面的方式,可以将元组嵌套入数组中,如:

fn main() {
    let a:[(i32, f32);2] = [
        (100, 1.1),
        (200, 2.2)
    ];
    println!("{}", a[0].1);
}

字符串

在Rust中,字符串被分为两种,&str和String。

fn main() {
    let a = "Hello";  // 使用""推导出来的类型为&str
    let mut b = a.to_string(); // b的类型为String
    b += " world.";     // b是可变的
    println!("a = {:?}, b = {:?}", a, b);
}

&str为字符串字面值,它是不可变的,要对其操作时,首先要转成String类型进行操作。要构造String类型,可以通过如下的方式:

fn main() {
    let a = "Hello world".to_string();
    let b = String::from("Hello world");
    println!("a = {:?}, b = {:?}", a, b);
}

Rust中的字符串,不像其他语言那样是字符型的数组。它不能通过下标访问,如下面的代码:

fn main() {
    let a = "Hello world";
    println!("{}", a[0]);
}

编译时会报错:

error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> src\main.rs:3:20
  |
3 |     println!("{}", a[0]);
  |                    ^^^^ string indices are ranges of `usize`
  |
  = help: the trait `std::slice::SliceIndex<str>` is not implemented for `{integer}`
  = note: you can use `.chars().nth()` or `.bytes().nth()`
          see chapter in The Book <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
  = note: required because of the requirements on the impl of `std::ops::Index<{integer}>` for `str`

这个例子还充分体现出Rust的编译器的人性化,它不但告诉我们出现了什么错误,同时还告诉了解决方法,通过chars()或bytes()转换成数组,就可以了:

fn main() {
    let a = "Hello world";
    println!("{:?}", a.chars().nth(0));
}

还有一种方式可以将字符串转化为数组:

fn main() {
    let a = "Hello world.".to_string();
    let b = a.as_bytes();
    println!("b = {:?}", b[0]);
}

这种方式是将字符串转化为byte类型的数组,如果是英文的字符串,这没有什么问题,如果字符串里包含中文,会引发问题。

切片

Rust在数组的基础上,构造了切片,从根本上来讲,切片是对数组的引用。

fn main() {
    let a = [1,2,3,4,5,6,7,8,9,0];
    let s = &a[2..8];
    println!("{}", s[0]);
}

s为a数组的切片,切片引用了a数组,并指定引用为从a数组的第2个元素到第8个。
Rust有两种切片方式:

  • start…end:切片结果中不包含end
  • start…=end:切片结果中包含end

注:start和end都可以省略,如果省略start,则从0开始切片,如果省略end,则一直切到最后一位。

fn main() {
    let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    let s1 = &a[2..8];
    println!("{}", s1.len());       // 输出结果为6
    let s2 = &a[2..=8];
    println!("{}", s2.len());       // 输出结果为7
}

对于字符串,同样可以使用切片操作,如:

fn main() {
    let a = "hello";
    let s = &a[..3];
    println!("{}", s);
}

元类型

Rust中的元类型为(),相当于c、java中的void。

变量与常量

变量绑定

在学习Rust的过程中,我们看到所有的教程中都在使用绑定,而不是声明,我没有查询到绑定与声明的区别到底在哪里,但从语法上看,绑定比起声明还是高级一些的。

变量绑定使用let关键字,其实,上面的代码中我们已经接触到了很多种绑定的方式,如:

fn main() 
{
    let a = 1;      // 自动推导变量类型
    let b:i64 = 1;  // 使用类型注解指定变量类型
    let c = 1i64;   // 使用后缀指定变量类型
    let d = 1_i64;  // 使用后缀指定变量类型的另一种方式
}

变量绑定时,还可以使用一种高级的方式,叫模式匹配,模式匹配是Rust的另一精华所在。有很多种语言也支持模式匹配,但Rust将其发挥到的极致。
复杂的模式匹配后面会拿出一篇单独学习,这里看一下简单的:

fn main() 
{
    let (a, b):(i32, i64) = (1, 3);
}

通过这种方式可以同时绑定多个变量,并分别指定其类型。

和C/C++一样,Rust的变量在使用前必须要进行初始化,如果没有初始化,C/C++会出现core dump错误,而Rust的编译器会为我们进行检查,直接不能通过编译。
如果我们绑定一个没有初始化的变量,但并不使用它,如:

fn main() 
{
    let a:i32;
}

编译时会有警告:

warning: unused variable: `a`
 --> src\main.rs:3:9
  |
3 |     let a:i32;
  |         ^ help: consider prefixing with an underscore: `_a`
  |
  = note: `#[warn(unused_variables)]` on by default

而一旦使用它,编译器就会严厉的拒绝,如:

fn main() 
{
    let a:i32;
    println!("{}", a);
}

编译时直接出错,使用了未初始化的变量:

error[E0381]: borrow of possibly-uninitialized variable: `a`
 --> src\main.rs:4:20
  |
4 |     println!("{}", a);
  |                    ^ use of possibly-uninitialized `a`

error: aborting due to previous error

变量的可变性

Rust与众不同之处,是其变量也具有可变性。let绑定变量,默认为不可变变量,如:

fn main() 
{
    let a:i32 = 1;
    a = 2;
    println!("{}", a);
}

编译会报错:

error[E0384]: cannot assign twice to immutable variable `a`
 --> src\main.rs:4:5
  |
3 |     let a:i32 = 1;
  |         -
  |         |
  |         first assignment to `a`
  |         help: make this binding mutable: `mut a`
4 |     a = 2;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

编译器告诉我们要使用mut关键字,于是我们可以修改代码为:

fn main() 
{
    let mut a:i32 = 1;
    a = 2;
    println!("{}", a);
}

可以通过编译。

常量

相信很多人会问:“不可变的变量,不就是常量吗?”,答案是否定的。Rust可以用const关键字定义常量,常量与变量存在如下区别:

  1. 变量可以使用mut关键字使其可变,而常量总是不能改变。
    fn main() 
    {
        const mut a = 1;
    }
    
    编译会出错:
    error: const globals cannot be mutable
    --> src\main.rs:3:11
    |
    3 |     const mut a = 1;
    |     ----- ^^^ cannot be mutable
    |     |
    |     help: you might want to declare a static instead: `static`
    
  2. 变量可以自动推导出类型,而常量必须指定类型:
    fn main() 
    {
        const a = 1;
    }
    
    还是会出错:
    error: missing type for `const` item
    --> src\main.rs:3:11
    |
    3 |     const a = 1;
    |           ^ help: provide a type for the item: `a: i32`
    
    error: aborting due to previous error
    
  3. Rust的变量的概念等同于其他语言的局部变量,不能在全局使用,而常量可以在全局使用:
    let A:i32 = 1;
    
    fn main() 
    {
        println!("{}", A);
    }
    
    编译出错:
    error: expected item, found keyword `let`
    --> src\main.rs:1:1
    |
    1 | let A:i32 = 1;
    | ^^^ expected item
    
    error: aborting due to previous error
    
    将上面代码中的let替换成const,则可以编译通过。

静态变量

尽管全局变量不是很安全,但在很多场合下还是需要全局变量的。Rust特别注重代码的安全性,所以Rust没有全局变量。想要达到这个效果,可以使用静态变量代替,如:

static a:i32 = 1;

fn main() 
{
    println!("{}", a);
}

static同样默认为不可变的,不可变的静态变量不会引发安全问题,但要让静态常量可变,编译器就不同意了:

static mut a:i32 = 1;

fn main() 
{
    a = 10;
    println!("{}", a);
}

编译器立马亮起红灯:

error[E0133]: use of mutable static is unsafe and requires unsafe function or block
 --> src\main.rs:5:5
  |
5 |     a = 10;
  |     ^^^^^^ use of mutable static
  |
  = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

error[E0133]: use of mutable static is unsafe and requires unsafe function or block

意思是可变的静态变量在多线程中是不安全的,要想使用可变的静态变量,你必须使用unsafe函数或代码块。
所以把上面的代码修改为:

static mut a:i32 = 1;

fn main() 
{
    unsafe
    {
        a = 10;
        println!("{}", a);
    }
}

终于可以通过编译了。

函数与宏

前面的代码中,虽然没有特别指出,但我们不可避免的用到了Rust的函数与宏。相信很多初学者都会提出疑问,为什么println后面还会有个感叹号。是的,我们的main函数是函数,而println就是宏。

函数

Rust使用fn关键字定义函数,其语法为:

fn 函数名(参数列表)-> 返回值类型

如:

fn main() 
{
    let sum = add(1, 2);
    println!("{}", sum);
}

fn add(a:i32, b:i32) ->i32
{
    return a + b;
}

Rust的函数不需要像C语言可样,被使用的函数必须要在使用之前声明,它可以在代码的任何地方定义。
好了,Rust的函数就讲到这里。
等,等下,这就完了吗?按照惯例,这里还要讲解默认参数和可变参数啊。
抱歉,我没有找到在Rust中使用默认参数和可变参数的方法,如果你知道了,麻烦您告诉我,谢谢。

Rust中宏的功能非常强大,从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的元编程( metaprogramming)。用白话说,Rust的宏与C/C++的宏是一样的,在编译前,编译器会将宏进行展开,生成新的代码,然后对新的代码进行编译。
Rust宏的应用,除了刚才见到的println之外,还有非常实用的动态数组。如:

fn main() 
{
    let mut a = vec![0, 1, 2];
    a.push(3);
    println!("{}", a.len());
}

我们可以自定义宏,但那不再属于基础知识,我目前还看不太懂,只好放到后面学习了。

语句与表达式

Rust 是一门基于表达式( expression-based)的语言,这是一个不同于其他语言重要区别。只有两种语句(“声明语句”和“表达式语句”),其它的一切都是表达式。

语句

语句( Statements)是执行一些操作但不返回值的指令。Rust只有两种语句:

  • 声明语句
    顾名思义,声明语句一是声明变量、常量、函数等的语句。
  • 表达式语句
    说到表达式语句,就不得不先说一个分号。Rust在形式上,和C语言一样,每行的末尾都有个分号,但其含义却大不相同。Rust使用分号将表达式变成表达式语句。换个专业的说法,Rust的表达式语句,是由表达式+分号组成。所以有多少种表达式,就有多少种表达式语句。

表达式

表达式( Expressions)可以计算并产生一个值,概念上类似于C/C++的右值。如声明语句是语句而不是表达式,它不能作为右值。所以let a = (let b = 5);是不合法的。而下面的代码则是合法的:

let y = {
    let x = 3;
    x + 1
};

x + 1,因为它后面没有加分号,所以它是表达式,而不是表达式语句。它可以作为右值被绑定到变量。
回到刚才自定义函数的例子,我们也可以省略return和分号,写成下面这样:

fn main() 
{
    let sum = add(1, 2);
    println!("{}", sum);
}

fn add(a:i32, b:i32) ->i32
{
    a + b
}

a + b是一个表达式,可以直接被add函数返回,作为右值绑定到sum。这也充分说明,Rust不是以分号作为行的结束。相反,如果这一行加上分号,表达式变成了表达式语句,这种写法是不合法的。

fn main() 
{
    let sum = add(1, 2);
    println!("{}", sum);
}

fn add(a:i32, b:i32) ->i32
{
    a + b;
}

此时编译出错:

error[E0308]: mismatched types
 --> src\main.rs:7:24
  |
7 | fn add(a:i32, b:i32) ->i32
  |    ---                 ^^^ expected i32, found ()
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 | {
9 |     a + b;
  |          - help: consider removing this semicolon
  |
  = note: expected type `i32`
             found type `()`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

因为add函数没有返回值,编译器认为它返回了元类型(),与定义中指定的返回i32不一致。

 类似资料: