Rust 初体验

Rust 初体验

安装

打开官网,下载 rustup-init.exe, 选择缺省模式(1)安装。

国内源设置

.Cargo 目录下新建 config 文件,添加如下内容:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = 'tuna' 

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true

第一个程序

使用任意代码编辑器,编写如下代码,保存为后缀是.rs的源文件,如 hello.rs。

fn main()
{
    println!("Hello World!");
}

了解C语言的同学,会发现这个程序和C语言的hello.c非常相似。不同之处在于,fn 表示函数,main 表示程序的入口,println! 表示输出。! 表示宏,即 println 是个宏,而不是函数。

在源文件文件夹里,运行 rustc hello.rs 进行编译,生成 hello.exe。

运行 hello.exe,输出:

Hello World!

使用 Cargo 管理

cargo new project_name 创建一个新项目

cd project_name 进入项目

cargo build 编译生成Debug版本

cargo build --release 编译并生成可执行文件

cargo run 运行

cargo check 检查错误(不编译)

cargo 常用命令

build, b    Compile the current package
check, c    Analyze the current package and report errors, but don't build object files
clean       Remove the target directory
doc, d      Build this package's and its dependencies' documentation
new         Create a new cargo package
init        Create a new cargo package in an existing directory
add         Add dependencies to a manifest file
remove      Remove dependencies from a manifest file
run, r      Run a binary or example of the local package
test, t     Run the tests
bench       Run the benchmarks
update      Update dependencies listed in Cargo.lock
search      Search registry for crates
publish     Package and upload this package to the registry
install     Install a Rust binary. Default location is $HOME/.cargo/bin
uninstall   Uninstall a Rust binary

变量

Rust 是强类型语言,使用关键字 let 声明变量后,具有自动判断变量类型的能力,如:

let a = 123; 则默认 a 为整型数字,且精度不允许变化,即 a 的值不可改变,可以理解为“只读”。

这与 C 语言中的 const 修饰符起到的效果一样。在 C 语言中 int const a = 10; a 的值也不允许改变。

Rust 中如果要让变量可变,需要用 mut 关键词:

let mut a = 123;
a = 456;

指定数据类型

let a: u64 = 123;  // 后面跟数据类型u64,表示无符号64位整型变量。
let y: f32 = 3.0;   // 后面跟数据类型f32,表示32位浮点型变量。

Shadowing(影射)

变量名可以被重新使用,如:

let x = 5;
let x = x + 1;
let x = x * 2;

最终 x 的值为12。从语法上说, 变量 x 可以作为右值。

注释

可以使用 C/C++, Java 注释。

另外,可用 /// 表示文档开头注释

函数

定义函数,如需要参数,必须声明参数名称、类型。

fn another_function(x: i32, y: i32) { }

可在{}包括的块里,编写较为复杂的表达式,即所谓的函数体表达式。注:函数体表达式,并不能等同于函数体,不能使用 return 关键字。

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

返回值类型声明

在参数声明之后用->来声明函数返回值的类型。 注:Rust 不支持自动返回值类型判断。

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

条件语句

if a > 0 {
    b = 1;
}  
else if a < 0 {
    b = -1;
}  
else {
    b = 0;
}
println!("b is {}", b);

if 后面不需要小括号,{} 必用。右括号 } 后不加

条件表达式必须是 bool 类型,不同于 C/C++ 的非 0 即真。

可以使用 if-else 结构实现类似于三元条件运算表达式 (A ? B : C) 。如:

let number = if a > 0 { 1 } else { -1 };

注:此处编译报错,检测出来其中 0 为 i32,代码未指定数据类型。故将0改为&0

let number = if a > &0 { 1 } else { -1 };

for 循环

最常用的循环结构。

let a = [10, 20, 30, 40, 50];
for i in a.iter() {
    println!("值为 : {}", i);
}

a.iter() 代表 a 的迭代器(iterator)

也可以通过下标来访问数组。如:

let a = [10, 20, 30, 40, 50];
for i in 0..5 {
    println!("a[{}] = {}", i, a[i]);
}

以上代码会报错。正确写法是:

const a: [i32; 5] = [10, 20, 30, 40, 50];
for i in 0..5 {
    println!("a[{}] = {}", i, a[i]);
}

可见,代码检查更严格。

while 循环

let mut number = 1;  // mut 表示number 在循环体内可以改变。
while number != 4 {
    println!("{}", number);
    number += 1;
}

无限循环结构 loop

可以通过 break 关键字,起到类似 return 一样的作用,使整个循环退出并给予外部一个返回值。

let mut i = 0;
let location = loop {
    let ch = s[i];
    if ch == '*' {
        break i;
    }
    i += 1;
};

以上代码会报错,可以改为:

fn main() {
    let s = "hello world";
    let mut i = 0;
    let location = loop {
        let ch = s.chars().nth(i).unwrap();
        if ch == ' ' {
            break i;
        }
        i += 1;
    };
    println!("{}", location);
}

变量与数据交互

方式有移动(Move)和克隆(Clone)两种,前者不保存移动前的变量,后者继续保留移动前的变量。

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 错误!s1 已经失效

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

引用(Reference)

& 运算符可以取变量的"引用"。当一个变量的值被引用时,变量本身不会被认定无效。注:C++中的概念。

引用不会获得值的所有权。引用只能租借(Borrow)值的所有权。

引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权。

&mut 修饰可变的引用类型。

可变引用不允许多重引用,但不可变引用可以。

以上机制设计,主要出于对并发状态下发生数据访问碰撞的考虑,使得编译阶段就避免发生。

垂悬引用(Dangling References)

Rust 语言里不允许出现,如果有,编译器会发现它。

更多内容参看: https://www.runoob.com/rust/rust-ownership.html

字符串切片(String Slice)

let s = String::from("broadcast");
let part1 = &s[0..5];
let part2 = &s[5..9];
println!("{}={}+{}", s, part1, part2);

在 Rust 中有两种常用的字符串类型:str 和 String。

str 是 Rust 核心语言类型,凡用双引号包括的字符串常量整体的类型性质都是 &str。

String 类型是 Rust 标准公共库提供的一种数据类型,其功能更完善。String 和 str 都支持切片,切片的结果是 &str 类型的数据。

注:切片结果必须是引用类型,但开发者必须明示:

let slice = &s[0..3]; 

除了字符串,其他一些线性数据结构也支持切片操作,如:

let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
    println!("{}", i);
}

结构体

定义结构体:

struct Site {
    domain: String,
    name: String,
    nation: String,
    found: u32
}

注:Rust 里 struct 语句仅用来定义,不能声明实例,结尾不需要; 符号,且每个字段定义之后用 , 分隔。

Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义:

let runoob = Site {
    domain: String::from("www.runoob.com"),
    name: String::from("RUNOOB"),
    nation: String::from("China"),
    found: 2013
};

如果正在实例化的结构体,有字段名称与现存变量名称一样,可以简化书写。

let domain = String::from("www.runoob.com");
let name = String::from("RUNOOB");
let runoob = Site {
    domain,  // 等同于 domain : domain,
    name,    // 等同于 name : name,
    nation: String::from("China"),
    traffic: 2013
};

新建一个结构体实例,如果其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中一两个字段的值,可以使用结构体更新语法:

let site = Site {
    domain: String::from("www.runoob.com"),
    name: String::from("RUNOOB"),
    ..runoob
};

注:…runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,至少重新设定一个字段的值才能引用其他实例的值。

元组结构体

struct Color(u8, u8, u8);
struct Point(f64, f64);

let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);

元组结构体对象的使用方式和元组一样,通过 . 和下标来进行访问:

struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
println!("black = ({}, {}, {})", black.0, black.1, lack.2);
println!("origin = ({}, {})", origin.0, origin.1);

结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。

结构体输出

导入调试库 #[derive(Debug)]

在 println 和 print 宏中可以用 {:?} 占位符输出一整个结构体;结构体如果属性较多,可使用另一个占位符 {:#?}

结构体方法

结构体方法的第一个参数必须是 &self,不需声明类型。

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

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1's area is {}", rect1.area());
}

这里与Python中的Class方法有神似。

结构体关联函数

不依赖实例,使用时需要声明是在哪个 impl 块中的。String::from函数就是一个"关联函数"。

# [derive(Debug)]

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

impl Rectangle {
    fn create(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    let rect = Rectangle::create(30, 50);
    println!("{:?}", rect);
}

这里的Rectangle中的create函数,即是结构体关联函数。这个概念与 C++语言里面的类成员函数有些相似。不同之处在于, C++ 类成员函数是在类的内部定义,并且使用 this 指针来访问类的实例。

单元结构体

结构体可以只作为一种象征而无需任何成员:

struct UnitStruct;

枚举类

# [derive(Debug)]

enum Book {
    Papery, Electronic
}

fn main() {
    let book = Book::Papery;
    println!("{:?}", book);
}

为枚举类成员添加元组属性描述

enum Book {
    Papery(u32),
    Electronic(String),
}

let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));

为属性命名,可以用结构体语法:

enum Book {
    Papery { index: u32 },
    Electronic { url: String },
}
let book = Book::Papery{index: 1001};

match 语法

枚举的目的是对某一类事物的分类,分类的目的是为了对不同的情况进行描述。Rust 通过 match 语句来实现分支结构。

fn main() {
    enum Book {
        Papery {index: u32},
        Electronic {url: String},
    }
   
    let book = Book::Papery{index: 1001};
    let ebook = Book::Electronic{url: String::from("url...")};
   
    match book {
        Book::Papery { index } => {
            println!("Papery book {}", index);
        },
        Book::Electronic { url } => {
            println!("E-book {}", url);
        }
    }
}

注意其中的,号与号。

match 枚举类实例 {
    分类1 => 返回值表达式,
    分类2 => 返回值表达式,
    ...
}

对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事 . 例外情况用下划线 _ 表示:

fn main() {
    let t = "abc";
    match t {
        "abc" => println!("Yes"),
        _ => {},
    }
}

Option 枚举类

Rust 标准库中的枚举类,用于填补 Rust 不支持 null 引用的空白。Rust 在语言层面彻底不允许空值 null 的存在,但null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类:

enum Option<T> {
    Some(T),
    None,
}

如果想定义一个可以为空值的类,可以这样:

let opt = Option::Some("Hello");

如果想针对 opt 执行某些操作,必须先判断它是否是 Option::None:

fn main() {
    let opt = Option::Some("Hello");
    match opt {
        Option::Some(something) => {
            println!("{}", something);
        },
        Option::None => {
            println!("opt is nothing");
        }
    }
}

初始值为空的 Option 必须明确类型:

fn main() {
    let opt: Option<&str> = Option::None;
    match opt {
        Option::Some(something) => {
            println!("{}", something);
        },
        Option::None => {
            println!("opt is nothing");
        }
    }
}

这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略 Option:: 直接写 None 或者 Some()。

fn main() {
    let t = Some(64);
    match t {
            Some(64) => println!("Yes"),
            _ => println!("No"),
    }
}

又:

let i = 0;
match i {
    0 => println!("zero"),
    _ => {},
}

用 if let 语法缩短这段代码:

let i = 0;
if let 0 = i {
    println!("zero");
}

如:

fn main() {
    enum Book {
        Papery(u32),
        Electronic(String)
    }
    let book = Book::Electronic(String::from("url"));
    if let Book::Papery(index) = book {
        println!("Papery {}", index);
    } else {
        println!("Not papery book");
    }
}

注意其中 if 语句后的 = 号。

组织管理

Rust 中有三个重要的组织概念:箱、包、模块。

箱(Crate)

"箱"是二进制程序文件或者库文件,存在于"包"中。树状结构,树根是编译器开始运行时编译的源文件所编译的程序。

注意:“二进制程序文件"不一定是"二进制可执行文件”,只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。

包(Package)

当使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。

一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。

当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。

模块(Module)

对于一个软件工程来说,往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。Rust 中的组织单位是模块(Module)。

这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。

路径分为绝对路径和相对路径。绝对路径从 crate 关键字开始描述。相对路径从 self 或 super 关键字或一个标识符开始描述。Rust 中的路径分隔符是 ::。如:

crate::nation::government::govern();

是描述 govern 函数的绝对路径,相对路径可以表示为:

nation::government::govern();

访问权限

Rust 中有两种简单的访问权:公共(public)和私有(private)。

默认情况下,如果不加修饰符,模块中的成员访问权将是私有的。

如果想使用公共权限,需要使用 pub 关键字。

对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。

mod nation {
    pub mod government {
        pub fn govern() {}
    }

    mod congress {
        pub fn legislate() {}
    }
   
    mod court {
        fn judicial() {
            super::congress::legislate();
        }
    }
}

fn main() {
    nation::government::govern();
}

这段程序是能通过编译的。请注意观察 court 模块中 super 的访问方法。更多的信息请参考:
https://www.runoob.com/rust/rust-project-management.html

use 关键字能够将模块标识符引入当前作用域

mod nation {
    pub mod government {
        pub fn govern() {}
    }
}

use crate::nation::government::govern; // 解决局部模块路径过长的问题。

fn main() {
    govern();
}

重命名

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub fn govern() {}
}
   
use crate::nation::government::govern;   
use crate::nation::govern as nation_govern;  // 解决重名问题

fn main() {
    nation_govern();
    govern();
}

还可以与 pub 关键字配合使用:

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub use government::govern;
}

fn main() {
    nation::govern();
}

Rust 官方标准库字典

所有系统库模块都是被默认导入的,所以在使用的时候只需要使用 use 关键字简化路径即可使用。

在 Rust 中没有 Exception。对可恢复错误用 Result 类来处理,对不可恢复错误使用 panic! 宏来处理。

不可恢复错误

fn main() {
    panic!("error occured"); // 程序将在此处中断,不执行后续语句
    println!("Hello, Rust");
}

回溯是不可恢复错误的另一种处理方式,它会展开运行的栈并输出所有的信息,然后程序依然会退出。上面的省略号省略了大量的输出信息,我们可以找到我们编写的 panic! 宏触发的错误。

可恢复的错误

Rust 通过 Result 枚举类作返回值来进行异常表达。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => {
            println!("File opened successfully.");
        },
        Err(err) => {
            println!("Failed to open the file.");
        }
    }
}

可用 if let 语法对简化 match 语法块:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    if let Ok(file) = f {
        println!("File opened successfully.");
    } else {
        println!("Failed to open the file.");
    }
}

Rust 中可以在 Result 对象后添加 ? 操作符将同类的 Err 直接传递出去.

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}

fn g(i: i32) -> Result<i32, bool> {
    let t = f(i)?;
    Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}

fn main() {
    let r = g(10000);
    if let Ok(v) = r {
        println!("Ok: g(10000) = {}", v);
    } else {
        println!("Err");
    }
}

? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去。所以,? 符仅用于返回值类型为 Result 的函数,其中 E 类型必须和 ? 所处理的 Result 的 E 类型一致。

判断 Result 的 Err 类型,用函数 kind()。

use std::io;
use std::io::Read;
use std::fs::File;

fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    let str_file = read_text_from_file("hello.txt");
    match str_file {
        Ok(s) => println!("{}", s),
        Err(e) => {
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!("No such file");
                },
                _ => {
                    println!("Cannot read the file");
                }
            }
        }
    }
}

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