cpp程序员速成rust(更新中)

前言

cpper在学习rust过程中会免不得将rust的语法跟cpp做对比,所以特开此坑,在此记录下来我的rust学习之路,仅供参考。想要从零开始了解rust的语法细节请还是以官方教程为准:

https://doc.rust-lang.org/book/

一、常见编程概念

1.1 基本数据类型

rust cpp
8bit有符号/无符号 i8 / u8 int8_t / uint8_t
16bit有符号/无符号 i16 / u16 int16_t / uint16_t
32bit有符号/无符号 i32 / u32 int32_t / uint32_t
64bit有符号/无符号 i64 / u64 int64_t / uint64_t
128bit有符号/无符号 i128 / u128
与指针大小相同的有符号/无符号 isize / usize intptr_t / uintptr_t
32bit浮点型 f32 float
64bit浮点型 f64 double
字符 char char32_t

上表没什么好解释的,记住就行了。

需要注意的是,rust中的char是4个字节,用于存储32位unicode;cpp中的char是1个字节。rust中的char对应cpp中的char32_t

1.2 声明变量

rust cpp
不带类型声明 let a = 3; auto a = 3;
带类型声明 let a: i32 = 3; int a = 3;

rust中用let来声明一个新的变量,且rust可以自动推导类型。

当显式声明类型时需要在冒号后面加上数据类型。

在本文后面的例子中,类型名称我会在必要时加上。

1.3 复合数据类型

数组array

rust cpp
不带类型声明 let a = [1, 2, 3, 4, 5];
带类型声明

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

array a = {1, 2, 3, 4, 5};

初始化值

let a = [3; 5];  // 5个3

array a;  // 5个元素

a.fill(3);  // 每个都是3

数组的类型是类似于[i32; 5]这个样子,第一个数是元素类型,第二个数是元素个数。

rust的数组类似于cpp中的std::array。

在初始化时可以还可以用[3; 5]这样的语法直接指定初始值。类似于std::array的fill函数。

元组tuple

rust cpp
不带类型声明 let tup = (5, 6.4, 1); auto tup = make_tuple(5, 6.4, 1);
带类型声明 let tup: (i32, f64, u8) = (5, 6.4, 1); tuple tup {5, 6.4, 1};
绑定 let (x, y, z) = tup;  // 解构 auto [x, y, z] = tup; // c++17结构化绑定
按索引访问 let x = tup.0; auto x = get<0>(tup);

1.4 rust中变量的一些特质

变量默认不可变

rust cpp
不可变变量 let x = 5; const int x = 5;
可变变量 let mut x = 5; int x = 5;
常量 const THREE: u32 = 3; constexpr uint32_t THREE = 3;

在rust的世界中,变量默认是不可变的,这是rust语言在设计上的一个考量,它督促你在声明每一个变量时都要先想清楚你到底会不会修改它的值。而cpp中,变量默认是可变的。

如果要声明一个可变的变量,你需要显式加上mut关键字,表示muttable(可变的)。

如果要声明一个编译期常量,则需要加上const,它相当于cpp中的constexpr。

rust中声明常量必须显式带类型。

变量名可以重用

rust cpp

let spaces = "";  // 这里spaces是字符串类型

let spaces = spaces.len();  // 这里又变成了usize类型

string spaces = "";

size_t spacesLen = spaces.length();

在rust中,一个变量名可以先后绑定到不同类型的变量上(前提是要用let,let表示你要声明一个新的变量)。而cpp中没有这种特性,你不得不起另外一个变量名。

1.5 表达式expression

在rust中,语句(statement)和表达式(expression)是两个截然不同的概念。

语句返回值,且结尾必须有分号。

表达式返回值,且结尾不能有分号。 函数调用/宏调用都是表达式。

let y = 6; // 语句
let x = (let y = 6); // 编译失败:语句没有返回值所以不能将它赋值给另一个变量
let y = {
    let x = 3;
    x + 1  // 该代码块的返回值由最后一句决定
};  // 由花括号括起来的一段代码块,它是一个表达式

1.6 函数

rust cpp
无返回值的函数

fn func(x: i32) {

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

}

void func(int x) {

    printf("%d", x);

}

带返回值的函数

fn plus_one(x: i32) -> i32 {

    x + 1

}

int plus_one(int x) {

    return x+1;

}

rust的函数使用fn关键字来声明。

在带返回值的函数中,最后一句不用加分号,因为它要作为表达式的值返回出去。这一点跟cpp很不一样。

1.7 if语句

let cond = true;
if cond {
    println!("true");
} else {
    println!("false");
}

if后面可以不加括号。

rust cpp

let cond = true;

let x = if cond { 5 } else { 6 };

auto cond = true;

auto x = cond ? 5 : 6;

if语句可以当成cpp里的三元运算符来用。

1.8 循环语句loop/while

rust cpp
无限循环loop

let mut x = 1;
loop {
    if x > 5 {
        break;
    }
    x = x + 1;
}

int x = 1;
while (true) {
    if (x > 5) {
        break;
    }
    x = x + 1;
}

条件循环while

let mut x = 1;
while x <= 5 {
    x += 1;
}

int x = 1;
while (x <= 5) {
    x += 1;
}

由于循环体也是个表达式,所以是可以直接返回值的。这一点与cpp不同。

注意下面的语法,break后面可以带上一个返回值,并且loop的右花括号后面需要有分号。

    let mut i = 0;
    let result = loop {
        i += 1;
        if i == 10 {
            break i * 2;
        }
    };

rust支持循环标签,而cpp不支持。

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}");
}

1.9 range for 循环

rust cpp

let a = [1, 2, 3, 4, 5];
for e in a {
    println!("the value is: {e}");
}

array a = {1, 2, 3, 4, 5};
for (int e : a) {
    printf("the value is: %d", e);
}

rust的range for跟python的比较像,都是for ... in ... 的形式。

二、所有权

所有权是rust语言的精髓所在,也是它引以为傲的安全性在语法层面的保障,它是语言设计时对程序员的一种强约束,即你必须时刻清楚每个变量的生命周期由谁掌控。

rust的所有权规则如下:

  1. rust 中的每一个值都有一个owner(一个变量)
  2. 一个值在任一时刻有且只有一个owner
  3. 当owner离开作用域时,该值则被销毁

所有权转移

rust cpp
移动

let s1 = String::from("hello");
let s2 = s1;

// s2接管了s1的内容,s1已经析构了

string s1 = "hello";
string s2 = std::move(s1);

// s2接管了s1的内容,但s1的生命周期还未结束

拷贝 let s1 = String::from("hello");
let s2 = s1.clone();
string s1 = "hello";
string s2 = s1;

来看上表中这个例子,我们声明了一个新的变量s2,并由s1赋值而来。cpp程序员按照经验会认为此时发生了拷贝构造,但在rust的世界中,此时发生的是移动构造。 即类似于cpp中的std::move。

但它们并不完全等价,与cpp不同的是,在本例中,s1将所有权转移给了s2后,s1变量就已经无效了,可以理解它已经被析构了。因此当你再使用s1时,rust编译器会直接报错。而cpp中编译不会报错,因为cpp中s1仍然有效,只不过它里面的字符串是空的而已。

若想进行拷贝的话就需要显式的调用一些函数了,比如.clone()方法。

这样的事情同样发生在函数调用时和函数返回值返回时:

fn func(s: String) -> String { // s 进入作用域
    println!("{}", s);
    s  // s被返回给调用者
}

fn main() {
    let s1 = String::from("hello");  // s1进入作用域
    let s2 = func(s1);             // s1的值被移动到函数里,s1析构
}

另外值得一提的是,若某个类型实现了copy trait,那么这样的写法是不会发生移动的。例如基本数据类型都实现了copy trait:

let x = 5;
let y = x; // 拷贝,而非移动。x不会被销毁掉

引用与借用

rust cpp

不可变变量的不可变引用

let s = String::from("hello");
let s2 : &String = &s;

const string s = "hello";
const string &s2 = s;

可变变量的不可变引用

let mut s = String::from("hello");
let s2 : &String = &s;

string s = "hello";
const string &s2 = s;

可变变量的可变引用

let mut s = String::from("hello");
let s2 : &mut String = &mut s;
s2.push_str("world");

string s = "hello";
string &s2 = s;
s2 += "world";

如果不想转移所有权,就要使用引用。创建一个引用的行为称为借用

rust引用的概念与cpp是一样的,但语法略有差别。

差别1:rust中变量/引用默认不可变,当想创建可变的变量/引用时都需要mut关键字。而cpp中变量/引用都是默认可变的。

差别2:rust中要在被引用的变量前面加&,而cpp不用加。

差别3:rust中引用类型的&是在类型前面(&String),而cpp是在后面(string&)。

引用的特殊规则

1. 变量的作用域到右大括号结束(或者被移动后结束),而引用的作用域到它被最后一次使用为止。 

2. 如果你拥有变量x的一个不可变引用,那么你就不能在相同作用域中拥有x的另外一个可变引用。

3. 如果你拥有变量x的一个可变引用,那么你就不能在相同作用域中拥有x的另外一个引用(无论是否可变)。

总结一下,在一个作用域中,

要么只能有 0个不可变引用 和 1个可变引用;

要么只能有 N个不可变引用 和 0个可变引用。

这些也是rust语言保证安全性的手段。尽管可能会逼疯一些程序员。。

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s; // 编译失败
    println!("{}, {}, and {}", r1, r2, r3);
}

上面这个例子会因为规则2而编译报错,因为你已经拥有了s的不可变引用r1和r2,所以你不能在同一个作用域中拥有可变引用r3。

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}

上面这个例子会因为规则3而编译报错,因为你已经拥有了s的一个可变引用r1,所以你不能在同一个作用域中拥有另外一个可变引用r2。

fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
    let r2 = &mut s;
}

改成上面这样就可以通过编译了,因为r1的作用域被限制在了花括号中。

slice

字符串slice是对字符串中一部分的引用,它的类型是&str,其语法如下:

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

    let hello = &s[0..5]; // 从0~5,前闭后开
    let hello = &s[..5];  // 左边的0可以省略不写
    let world = &s[6..s.len()];  // 从6到末尾,前闭后开
    let world = &s[6..];  // 末尾也可以省略不写
    let whole = &s[..];  // 前后都省略就是对整个字符串的slice
}
fn main() {
    let s = String::from("hello world");
    let hello : &str = &s[0..5];
    s.clear();
}

上面这个例子编译会报错,因为hello是对s片段的不可变引用。而.clear()会尝试获取s的可变引用,因为这违反了规则2。

let s : &str = "Hello, world!";

s的类型跟字符串slice一样,都是&str,因为字符串字面值就是对动态库so中全局区字符串的不可变引用。

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

同样的,数组也可以拥有slice,该例子中它的类型是&[i32]

三、结构体

rust cpp
声明和创建结构体

struct User {
    name: String,  // 字段之间用逗号
    age: u32,
}  // 注意这里没有分号

fn main() {
    let user1 = User {
        name: String::from("tom"),
        age: 18,
    };
}

struct User {
    string name;
    uint32_t age;
};

int main() {
    User user1 {
        .name = "tom",
        .age = 18,
    };
}

为结构体添加静态函数和成员函数

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn get_square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let x = Rectangle::get_square(5);
    println!("area of x is {}", x.area());
}

struct Rectangle {
    uint32_t width;
    uint32_t height;

    static Rectangle get_square(uint32_t size) {
        return Rectangle{
                .width = size,
                .height = size
        };
    }

    uint32_t area() const {
        return width * height;
    }
};

int main() {
    auto x = Rectangle::get_square(5);
    printf("area of x is %u", x.area());
}

rust中结构体方法需要放在impl块中,后面跟结构体类型名。impl块中的方法用Self指代类型本身,用&self指代类型的不可变引用。

你可能感兴趣的:(rust,开发语言)